بحث GitHub
في هذا الدليل، سنبني تطبيق بحث GitHub باستخدام Flutter وAngularDart لشرح كيف يمكننا مشاركة طبقة البيانات (data layer) وطبقة منطق الأعمال (business logic layer) بين المشروعين.


المواضيع الرئيسية
Section titled “المواضيع الرئيسية”- BlocProvider، وهو
widgetفي Flutter يوفّر bloc للأبناء. - BlocBuilder، وهو
widgetفي Flutter يتولى بناء الواجهة استجابةً للحالات الجديدة. - استخدام Cubit بدلًا من Bloc. ما الفرق؟
- تجنب إعادة البناء غير الضرورية باستخدام Equatable.
- استخدام
EventTransformerمخصص معbloc_concurrency. - تنفيذ طلبات الشبكة عبر حزمة
http.
مكتبة GitHub Search المشتركة
Section titled “مكتبة GitHub Search المشتركة”ستتضمن مكتبة GitHub Search المشتركة النماذج (models)، ومزوّد البيانات (data provider)، وRepository، بالإضافة إلى bloc الذي سنعيد استخدامه بين AngularDart وFlutter.
الإعداد
Section titled “الإعداد”سنبدأ بإنشاء مجلد جديد لتطبيقنا.
mkdir -p github_search/common_github_searchنحتاج إلى إنشاء pubspec.yaml يتضمن dependencies المطلوبة.
name: common_github_searchdescription: Shared Code between AngularDart and Flutterversion: 1.0.0+1publish_to: none
environment: sdk: ">=3.10.0 <4.0.0"
dependencies: bloc: ^9.0.0 equatable: ^2.0.0 http: ^1.0.0 stream_transform: ^2.0.0dev_dependencies: bloc_lint: ^0.3.0أخيرًا، نحتاج إلى تثبيت dependencies.
dart pub getانتهى إعداد المشروع. الآن يمكننا البدء في بناء حزمة common_github_search.
Github Client
Section titled “Github Client”GithubClient سيكون مسؤولًا عن توفير البيانات الخام من
GitHub API.
لننشئ github_client.dart.
import 'dart:async';import 'dart:convert';
import 'package:common_github_search/common_github_search.dart';import 'package:http/http.dart' as http;
class GithubClient { GithubClient({ http.Client? httpClient, this.baseUrl = 'https://api.github.com/search/repositories?q=', }) : _httpClient = httpClient ?? http.Client();
final String baseUrl; final http.Client _httpClient;
Future<SearchResult> search(String term) async { final response = await _httpClient.get(Uri.parse('$baseUrl$term')); final results = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode == 200) { return SearchResult.fromJson(results); } else { throw SearchResultError.fromJson(results); } }
void close() { _httpClient.close(); }}الخطوة التالية هي تعريف نموذجي SearchResult وSearchResultError.
نموذج نتيجة البحث
Section titled “نموذج نتيجة البحث”أنشئ search_result.dart، وهو يمثل قائمة SearchResultItems بناءً على استعلام
المستخدم:
import 'package:common_github_search/common_github_search.dart';
class SearchResult { const SearchResult({required this.items});
factory SearchResult.fromJson(Map<String, dynamic> json) { final items = (json['items'] as List<dynamic>) .map( (dynamic item) => SearchResultItem.fromJson(item as Map<String, dynamic>), ) .toList(); return SearchResult(items: items); }
final List<SearchResultItem> items;}نموذج عنصر نتيجة البحث
Section titled “نموذج عنصر نتيجة البحث”الآن سننشئ search_result_item.dart.
import 'package:common_github_search/common_github_search.dart';
class SearchResultItem { const SearchResultItem({ required this.fullName, required this.htmlUrl, required this.owner, });
factory SearchResultItem.fromJson(Map<String, dynamic> json) { return SearchResultItem( fullName: json['full_name'] as String, htmlUrl: json['html_url'] as String, owner: GithubUser.fromJson(json['owner'] as Map<String, dynamic>), ); }
final String fullName; final String htmlUrl; final GithubUser owner;}نموذج مستخدم GitHub
Section titled “نموذج مستخدم GitHub”الآن سننشئ github_user.dart.
class GithubUser { const GithubUser({ required this.login, required this.avatarUrl, });
factory GithubUser.fromJson(Map<String, dynamic> json) { return GithubUser( login: json['login'] as String, avatarUrl: json['avatar_url'] as String, ); }
final String login; final String avatarUrl;}بهذا نكون انتهينا من تنفيذ SearchResult واعتمادياته، وننتقل الآن إلى
SearchResultError.
نموذج خطأ نتيجة البحث
Section titled “نموذج خطأ نتيجة البحث”أنشئ search_result_error.dart.
class SearchResultError implements Exception { SearchResultError({required this.message});
factory SearchResultError.fromJson(Map<String, dynamic> json) { return SearchResultError( message: json['message'] as String, ); }
final String message;}انتهينا من GithubClient، والخطوة التالية هي GithubCache، والذي سيكون مسؤولًا
عن memoization كتحسين للأداء.
GitHub Cache
Section titled “GitHub Cache”سيكون GithubCache مسؤولًا عن تذكّر جميع الاستعلامات السابقة لتجنب تنفيذ طلبات
شبكة غير ضرورية إلى GitHub API. هذا يساعد أيضًا في تحسين أداء التطبيق.
أنشئ github_cache.dart.
import 'package:common_github_search/common_github_search.dart';
class GithubCache { final _cache = <String, SearchResult>{};
SearchResult? get(String term) => _cache[term];
void set(String term, SearchResult result) => _cache[term] = result;
bool contains(String term) => _cache.containsKey(term);
void remove(String term) => _cache.remove(term);
void close() { _cache.clear(); }}الآن أصبحنا جاهزين لإنشاء GithubRepository.
GitHub Repository
Section titled “GitHub Repository”GithubRepository مسؤول عن إنشاء طبقة تجريد (abstraction) بين طبقة البيانات
(GithubClient) وطبقة منطق الأعمال (Bloc). وهنا أيضًا سنستخدم GithubCache.
أنشئ github_repository.dart.
import 'dart:async';
import 'package:common_github_search/common_github_search.dart';
class GithubRepository { GithubRepository({GithubCache? cache, GithubClient? client}) : _cache = cache ?? GithubCache(), _client = client ?? GithubClient();
final GithubCache _cache; final GithubClient _client;
Future<SearchResult> search(String term) async { final cachedResult = _cache.get(term); if (cachedResult != null) { return cachedResult; } final result = await _client.search(term); _cache.set(term, result); return result; }
void dispose() { _cache.close(); _client.close(); }}بهذا نكون أكملنا طبقة مزوّد البيانات وطبقة الـ repository، وأصبحنا جاهزين للانتقال إلى طبقة منطق الأعمال.
حدث GitHub Search
Section titled “حدث GitHub Search”سيتم إشعار Bloc عندما يكتب المستخدم اسم repository، وسنمثل ذلك عبر حدث
TextChanged من نوع GithubSearchEvent.
أنشئ github_search_event.dart.
import 'package:equatable/equatable.dart';
sealed class GithubSearchEvent extends Equatable { const GithubSearchEvent();}
final class TextChanged extends GithubSearchEvent { const TextChanged({required this.text});
final String text;
@override List<Object> get props => [text];
@override String toString() => 'TextChanged { text: $text }';}حالة GitHub Search
Section titled “حالة GitHub Search”طبقة العرض تحتاج عدة حالات لتتمكن من رسم الواجهة بشكل صحيح:
-
SearchStateEmptyلإبلاغ طبقة العرض بعدم وجود إدخال من المستخدم. -
SearchStateLoadingلإبلاغ طبقة العرض بضرورة إظهار مؤشر تحميل. -
SearchStateSuccessلإبلاغ طبقة العرض بوجود بيانات جاهزة للعرض.itemsستكونList<SearchResultItem>التي ستُعرض.
-
SearchStateErrorلإبلاغ طبقة العرض بحدوث خطأ أثناء جلب repositories.errorيمثل الخطأ الفعلي الذي حدث.
يمكننا الآن إنشاء github_search_state.dart وتنفيذه كالتالي.
import 'package:common_github_search/common_github_search.dart';import 'package:equatable/equatable.dart';
sealed class GithubSearchState extends Equatable { const GithubSearchState();
@override List<Object> get props => [];}
final class SearchStateEmpty extends GithubSearchState {}
final class SearchStateLoading extends GithubSearchState {}
final class SearchStateSuccess extends GithubSearchState { const SearchStateSuccess(this.items);
final List<SearchResultItem> items;
@override List<Object> get props => [items];
@override String toString() => 'SearchStateSuccess { items: ${items.length} }';}
final class SearchStateError extends GithubSearchState { const SearchStateError(this.error);
final String error;
@override List<Object> get props => [error];}بعد تنفيذ Events وStates، يمكننا إنشاء GithubSearchBloc.
GitHub Search Bloc
Section titled “GitHub Search Bloc”أنشئ github_search_bloc.dart:
import 'package:bloc/bloc.dart';import 'package:common_github_search/common_github_search.dart';import 'package:stream_transform/stream_transform.dart';
const _duration = Duration(milliseconds: 300);
EventTransformer<Event> debounce<Event>(Duration duration) { return (events, mapper) => events.debounce(duration).switchMap(mapper);}
class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> { GithubSearchBloc({required GithubRepository githubRepository}) : _githubRepository = githubRepository, super(SearchStateEmpty()) { on<TextChanged>(_onTextChanged, transformer: debounce(_duration)); }
final GithubRepository _githubRepository;
Future<void> _onTextChanged( TextChanged event, Emitter<GithubSearchState> emit, ) async { final searchTerm = event.text;
if (searchTerm.isEmpty) return emit(SearchStateEmpty());
emit(SearchStateLoading());
try { final results = await _githubRepository.search(searchTerm); emit(SearchStateSuccess(results.items)); } catch (error) { emit( error is SearchResultError ? SearchStateError(error.message) : const SearchStateError('something went wrong'), ); } }}ممتاز. انتهينا من حزمة common_github_search. الناتج النهائي يجب أن يبدو مثل
هذا.
الخطوة التالية: تنفيذ Flutter.
بحث GitHub باستخدام Flutter
Section titled “بحث GitHub باستخدام Flutter”Flutter GitHub Search سيكون تطبيق Flutter يعيد استخدام النماذج، ومزوّدي
البيانات، والـ repositories، والـ blocs من common_github_search لتنفيذ ميزة
البحث في GitHub.
الإعداد
Section titled “الإعداد”نبدأ بإنشاء مشروع Flutter جديد داخل مجلد github_search في نفس مستوى
common_github_search.
flutter create flutter_github_searchبعد ذلك نحدّث pubspec.yaml ليتضمن كل dependencies المطلوبة.
name: flutter_github_searchdescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ">=3.10.0 <4.0.0"
dependencies: bloc: ^9.0.0 common_github_search: path: ../common_github_search flutter: sdk: flutter flutter_bloc: ^9.0.1 url_launcher: ^6.0.0
flutter: uses-material-design: truedev_dependencies: bloc_lint: ^0.3.0الآن نثبت dependencies.
flutter pub getانتهى إعداد المشروع. بما أن common_github_search تحتوي طبقة البيانات وطبقة
منطق الأعمال، فكل ما نحتاج لبنائه هو طبقة العرض.
نموذج البحث
Section titled “نموذج البحث”سنحتاج إلى إنشاء نموذج يحتوي widget باسم _SearchBar وwidget باسم
_SearchBody.
_SearchBarسيكون مسؤولًا عن استقبال إدخال المستخدم._SearchBodyسيكون مسؤولًا عن عرض نتائج البحث ومؤشرات التحميل والأخطاء.
لننشئ search_form.dart.
SearchForm سيكون StatelessWidget يعرض _SearchBar و_SearchBody.
_SearchBar سيكون أيضًا StatefulWidget لأنه يحتاج لإدارة
TextEditingController خاص به حتى نتتبع مدخلات المستخدم.
_SearchBody هو StatelessWidget مسؤول عن عرض نتائج البحث والأخطاء ومؤشرات
التحميل. وهو المستهلك لـ GithubSearchBloc.
إذا كانت الحالة SearchStateSuccess، سنعرض _SearchResults الذي سننفذه لاحقًا.
_SearchResults هو StatelessWidget يستقبل List<SearchResultItem> ويعرضها
كقائمة من _SearchResultItems.
_SearchResultItem هو StatelessWidget مسؤول عن عرض بيانات نتيجة بحث واحدة.
وهو أيضًا مسؤول عن التعامل مع تفاعل المستخدم والتنقل إلى رابط repository عند
النقر.
import 'package:common_github_search/common_github_search.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:url_launcher/url_launcher.dart';
class SearchForm extends StatelessWidget { const SearchForm({super.key});
@override Widget build(BuildContext context) { return Column( children: <Widget>[ _SearchBar(), _SearchBody(), ], ); }}
class _SearchBar extends StatefulWidget { @override State<_SearchBar> createState() => _SearchBarState();}
class _SearchBarState extends State<_SearchBar> { final _textController = TextEditingController(); late GithubSearchBloc _githubSearchBloc;
@override void initState() { super.initState(); _githubSearchBloc = context.read<GithubSearchBloc>(); }
@override void dispose() { _textController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return TextField( controller: _textController, autocorrect: false, onChanged: (text) { _githubSearchBloc.add( TextChanged(text: text), ); }, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: GestureDetector( onTap: _onClearTapped, child: const Icon(Icons.clear), ), border: InputBorder.none, hintText: 'Enter a search term', ), ); }
void _onClearTapped() { _textController.text = ''; _githubSearchBloc.add(const TextChanged(text: '')); }}
class _SearchBody extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder<GithubSearchBloc, GithubSearchState>( builder: (context, state) { return switch (state) { SearchStateEmpty() => const Text('Please enter a term to begin'), SearchStateLoading() => const CircularProgressIndicator.adaptive(), SearchStateError() => Text(state.error), SearchStateSuccess() => state.items.isEmpty ? const Text('No Results') : Expanded(child: _SearchResults(items: state.items)), }; }, ); }}
class _SearchResults extends StatelessWidget { const _SearchResults({required this.items});
final List<SearchResultItem> items;
@override Widget build(BuildContext context) { return ListView.builder( itemCount: items.length, itemBuilder: (BuildContext context, int index) { return _SearchResultItem(item: items[index]); }, ); }}
class _SearchResultItem extends StatelessWidget { const _SearchResultItem({required this.item});
final SearchResultItem item;
@override Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( child: Image.network(item.owner.avatarUrl), ), title: Text(item.fullName), onTap: () => launchUrl(Uri.parse(item.htmlUrl)), ); }}تجميع كل شيء
Section titled “تجميع كل شيء”كل ما تبقى هو تنفيذ التطبيق الرئيسي في main.dart.
import 'package:common_github_search/common_github_search.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_github_search/search_form.dart';
void main() => runApp(const App());
class App extends StatelessWidget { const App({super.key});
@override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => GithubRepository(), dispose: (repository) => repository.dispose(), child: MaterialApp( title: 'GitHub Search', home: Scaffold( appBar: AppBar(title: const Text('GitHub Search')), body: BlocProvider( create: (context) => GithubSearchBloc( githubRepository: context.read<GithubRepository>(), ), child: const SearchForm(), ), ), ), ); }}بهذا نكون نفذنا تطبيق بحث GitHub في Flutter بنجاح باستخدام حزمتَي bloc و flutter_bloc، وتمكنا من فصل طبقة العرض عن طبقة منطق الأعمال.
يمكنك العثور على المصدر الكامل هنا.
الآن سننتقل إلى بناء تطبيق GitHub Search باستخدام AngularDart.
بحث GitHub باستخدام AngularDart
Section titled “بحث GitHub باستخدام AngularDart”AngularDart GitHub Search سيكون تطبيق AngularDart يعيد استخدام النماذج، ومزوّدي
البيانات، والـ repositories، والـ blocs من common_github_search لتنفيذ ميزة
البحث في GitHub.
الإعداد
Section titled “الإعداد”نبدأ بإنشاء مشروع AngularDart جديد داخل مجلد github_search في نفس مستوى
common_github_search.
stagehand web-angularبعدها يمكننا استبدال محتوى pubspec.yaml بما يلي:
name: angular_github_searchdescription: A web app that uses AngularDart Components
environment: sdk: ">=3.10.0 <4.0.0"
dependencies: angular_bloc: ^10.0.0-dev.5 bloc: ^9.0.0 common_github_search: path: ../common_github_search ngdart: ^8.0.0-dev.4
dev_dependencies: build_daemon: ^4.0.0 build_runner: ^2.0.0 build_web_compilers: ^4.0.0نموذج البحث
Section titled “نموذج البحث”كما في تطبيق Flutter، نحتاج إلى SearchForm يحتوي على مكوّني SearchBar و
SearchBody.
مكوّن SearchForm سيطبّق OnInit وOnDestroy لأنه يحتاج لإنشاء
GithubSearchBloc ثم إغلاقه.
SearchBarمسؤول عن استقبال إدخال المستخدم.SearchBodyمسؤول عن عرض نتائج البحث ومؤشرات التحميل والأخطاء.
لننشئ search_form_component.dart.
import 'package:angular_bloc/angular_bloc.dart';import 'package:angular_github_search/src/github_search.dart';import 'package:common_github_search/common_github_search.dart';import 'package:ngdart/angular.dart';
@Component( selector: 'search-form', templateUrl: 'search_form_component.html', directives: [ SearchBarComponent, SearchBodyComponent, ], pipes: [BlocPipe],)class SearchFormComponent implements OnInit, OnDestroy { @Input() late GithubRepository githubRepository;
late GithubSearchBloc githubSearchBloc;
@override void ngOnInit() { githubSearchBloc = GithubSearchBloc( githubRepository: githubRepository, ); }
@override void ngOnDestroy() { githubSearchBloc.close(); }}وسيكون القالب (search_form_component.html) كالتالي:
<div> <h1>GitHub Search</h1> <search-bar [githubSearchBloc]="githubSearchBloc"></search-bar> <search-body [state]="$pipe.bloc(githubSearchBloc)"></search-body></div>الآن سننفذ مكوّن SearchBar.
Search Bar
Section titled “Search Bar”SearchBar هو مكوّن مسؤول عن استقبال إدخال المستخدم، وإشعار GithubSearchBloc
بتغيّر النص.
أنشئ search_bar_component.dart.
import 'package:common_github_search/common_github_search.dart';import 'package:ngdart/angular.dart';
@Component( selector: 'search-bar', templateUrl: 'search_bar_component.html',)class SearchBarComponent { @Input() late GithubSearchBloc githubSearchBloc;
void onTextChanged(String text) { githubSearchBloc.add(TextChanged(text: text)); }}بعدها ننشئ search_bar_component.html.
<label for="term" class="clip">Enter a search term</label><input id="term" placeholder="Enter a search term" class="input-reset outline-transparent glow o-50 bg-near-black near-white w-100 pv2 border-box b--white-50 br-0 bl-0 bt-0 bb-ridge mb3" autofocus (keyup)="onTextChanged($event.target.value)"/>انتهينا من SearchBar، والآن ننتقل إلى SearchBody.
Search Body
Section titled “Search Body”SearchBody هو مكوّن مسؤول عن عرض نتائج البحث والأخطاء ومؤشرات التحميل. وهو
المستهلك لـ GithubSearchBloc.
أنشئ search_body_component.dart.
import 'package:angular_github_search/src/github_search.dart';import 'package:common_github_search/common_github_search.dart';import 'package:ngdart/angular.dart';
@Component( selector: 'search-body', templateUrl: 'search_body_component.html', directives: [ coreDirectives, SearchResultsComponent, ],)class SearchBodyComponent { @Input() late GithubSearchState state;
bool get isEmpty => state is SearchStateEmpty; bool get isLoading => state is SearchStateLoading; bool get isSuccess => state is SearchStateSuccess; bool get isError => state is SearchStateError;
List<SearchResultItem> get items => isSuccess ? (state as SearchStateSuccess).items : [];
String get error => isError ? (state as SearchStateError).error : '';}أنشئ search_body_component.html.
<div *ngIf="state != null" class="mw10"> <div *ngIf="isEmpty" class="tc"> <span>🔍</span> <p>Please enter a term to begin</p> </div> <div *ngIf="isLoading"> <div class="sk-chase center"> <div class="sk-chase-dot"></div> <div class="sk-chase-dot"></div> <div class="sk-chase-dot"></div> <div class="sk-chase-dot"></div> <div class="sk-chase-dot"></div> <div class="sk-chase-dot"></div> </div> </div> <div *ngIf="isError" class="tc"> <span>‼️</span> <p>{{ error }}</p> </div> <div *ngIf="isSuccess"> <div *ngIf="items.length == 0" class="tc"> <span>⚠️</span> <p>No Results</p> </div> <search-results [items]="items"></search-results> </div></div>إذا كانت الحالة isSuccess، سنعرض SearchResults، وسننفيذه الآن.
Search Results
Section titled “Search Results”SearchResults هو مكوّن يستقبل List<SearchResultItem> ويعرضها كقائمة من
SearchResultItems.
أنشئ search_results_component.dart.
import 'package:angular_github_search/src/github_search.dart';import 'package:common_github_search/common_github_search.dart';import 'package:ngdart/angular.dart';
@Component( selector: 'search-results', templateUrl: 'search_results_component.html', directives: [coreDirectives, SearchResultItemComponent],)class SearchResultsComponent { @Input() late List<SearchResultItem> items;}بعد ذلك ننشئ search_results_component.html.
<ul class="list pa0 ma0"> <li *ngFor="let item of items" class="pa2 cf"> <search-result-item [item]="item"></search-result-item> </li></ul>حان الوقت لتنفيذ SearchResultItem.
Search Result Item
Section titled “Search Result Item”SearchResultItem هو مكوّن مسؤول عن عرض معلومات نتيجة بحث واحدة، كما يتعامل مع
تفاعل المستخدم وينتقل إلى رابط repository عند النقر.
أنشئ search_result_item_component.dart.
import 'package:common_github_search/common_github_search.dart';import 'package:ngdart/angular.dart';
@Component( selector: 'search-result-item', templateUrl: 'search_result_item_component.html',)class SearchResultItemComponent { @Input() late SearchResultItem item;}ثم أنشئ القالب المقابل search_result_item_component.html.
<div class="fl w-10 h-auto"> <img class="br-100" src="{{ item.owner.avatarUrl }}" /></div><div class="fl w-90 ph3"> <h1 class="f5 ma0">{{ item.fullName }}</h1> <p> <a href="{{ item.htmlUrl }}" class="light-blue" target="_blank">{{ item.htmlUrl }}</a> </p></div>تجميع كل شيء
Section titled “تجميع كل شيء”لدينا الآن جميع المكونات، وحان وقت تجميعها داخل app_component.dart.
import 'package:angular_github_search/src/github_search.dart';import 'package:common_github_search/common_github_search.dart';import 'package:ngdart/angular.dart';
@Component( selector: 'my-app', template: '<search-form [githubRepository]="githubRepository"></search-form>', directives: [SearchFormComponent],)class AppComponent { final githubRepository = GithubRepository();}بهذا نكون نفذنا تطبيق بحث GitHub في AngularDart بنجاح باستخدام حزمتَي bloc و
angular_bloc، وتمكّنا من فصل طبقة العرض عن طبقة منطق الأعمال.
يمكنك العثور على المصدر الكامل هنا.
الملخص
Section titled “الملخص”في هذا الدليل، أنشأنا تطبيق Flutter وتطبيق AngularDart مع مشاركة جميع النماذج، ومزوّدي البيانات، والـ blocs بين المشروعين.
الجزء الوحيد الذي احتجنا لكتابته مرتين فعليًا هو طبقة العرض (UI)، وهذا ممتاز من ناحية الكفاءة وسرعة التطوير. وبما أن تطبيقات الويب وتطبيقات الجوال غالبًا ما تملك تجارب استخدام وأنماط تصميم مختلفة، فهذا النهج يوضح مدى سهولة بناء تطبيقين بواجهتين مختلفتين تمامًا مع مشاركة نفس طبقة البيانات وطبقة منطق الأعمال.
يمكنك العثور على المصدر الكامل هنا.