اذهب إلى المحتوى

كل الأنشطة

تحدث تلقائيًا

  1. الساعة الماضية
  2. اليوم
  3. افطل نسخ مستقره في لينكس مستودعاته كثير معى البرنمج الحرق usb
  4. كما تم التوضيح في التعليقات السابقة فتحديد المدة الزمنية اللازمة لإنشاء تطبيق يعتمد على العديد من العوامل، ولكن هناك بعض الخطوات والنصائح التي يمكن أن تفيدك كمايلي أولاً يجب عليك تحديد ومعرفة جميع المتطلبات والوظائف التي يجب أن يحتويها التطبيق. بعد ذلك ستقوم بتقسيم المشروع إلى مهام صغيرة مع تقدير الزمن اللازم لكل مهمة على حدة. ومع خبرتك السابقة في مشاريع مشابهة ستجد أنه لا يوجد مشكلة هنا. كما أنه دائمًا ما تكون هناك مهام غير متوقعة أو تأخيرات في المشاريع. لذلك، يفضل إضافة وقت احتياطي إلى تقديرك الكلي. ومع ذلك يجب معرفة المدة الزمنية التي يتوقع العميل أن تقوم بإنجاز العمل بها فإذا كان لا يهمه كثيراً الوقت يمكنك تنفيذه في مدة زمنيه مناسبة بالنسبة لك ولكن إذا كان مستعجلاً فيفضل إنجاز العمل سريعاً
  5. تُعد فكرة الوعود Promises أساسًا للغة جافا سكريبت غير المتزامنة. والوعد هو كائن يُعاد من الدالة غير المتزامنة ويمثّل الوضع الراهن للعملية. ولا تكون العملية قد انتهت بعد في الوقت الذي تعيد في الدالة الوعد إلى مستدعيها، لكن كائن الوعد المُعَاد يمتلك توابع لمعالجة التنفيذ الناجح أو المخفق للعملية. تحدثنا في المقال السابق عن استخدام الاستدعاءات لإنجاز الدوال غير المتزامنة. إذ نستدعي وفق هذا اﻷسلوب الدالة غير المتزامنة ممررين إليها دالة استدعاء أخرى تسمى دالة رد النداء callback، عندها تُعيد هذه الدالة قيمتها مباشرة، ثم تستدعي بعد ذلك دالة رد النداء التي مررناها عندما تنتهي العملية. وما يحدث في الواجهات البرمجية المبنية على الوعود، أن الدالة غير المتزامنة تبدأ عملية ما وتعيد كائن وعد Promise، وبإمكانك حينها ربط هذا الكائن بمعالجات أحداث ستُنفَّذ عند نجاح أو إخفاق هذه العملية. استخدام الواجهة البرمجية fetch ملاحظة: سنتعلم مفهوم الوعود في هذا المقال بنسخ عينات من الكود البرمجي من الصفحة إلى طرفية جافا سكريبت في المتصفح. وﻹعداد هذا اﻷمر: انتقل إلى الموقع: https://example.org افتح طرفية جافا سكريبت الموجودة ضمن أدوات مطوري الويب في نفس النافذة الفرعية. عندما نعرض مثالًا ما، انسخه إلى الطرفية، وعليك حينها إعادة تحميل الصفحة في كل مرة تُلصق فيها مثالًا جديدًا، وإلا تعترض الطرفية لأنك أعدت تصريح المتغير fetchPromise. سننزل في هذا المثال ملف JSON ونسجّل بعض المعلومات المتعلقة به، ولتنفيذ اﻷمر نرسل طلب HTTP إلى الخادم يتضمن رسالة مرسلة إليه وننتظر الاستجابة. ففي مثالنا سنطلب ملف JSON من الخادم. وكما أشرنا في المقال السابق، نستخدم الواجهة البرمجية XMLHttpRequest لتنفيذ طلبات HTTP، لكننا سنستخدم في هذا المقال الواجهة البرمجية ()fetch وهي بديل أحدث عن كائن XMLHttpRequest ومبنية على الوعود. انسخ اﻵن الشيفرة التالية إلى طرفية جافا سكريبت في متصفحك: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); console.log(fetchPromise); fetchPromise.then((response) => { console.log(`Received response: ${response.status}`); }); console.log("Started request…"); ما فعلناه في هذه الشيفرة هو التالي: استدعينا الواجهة()fetchوأسندنا القيمة التي تعيدها إلى المتغير fetchPromise. سجلنا بعد ذلك مباشرة قيمة المتغير، ومن المفترض أن تشبه النتيجة ما يلي: Promise { <state>: "pending" }، لتخبرنا أنه لدينا كائن وعد Promise له حالة قيمتها "pending" ويعني ذلك أن عملية إحضار الملف لا تزال قيد التنفيذ. مررنا دالة معالجة إلى التابع ()then العائد لكائن الوعد، فإن نجحت العملية وعندما تنتهي، يستدعي الوعد دالة المعالجة التي نمرر إليها كائن الاستجابة Response الذي يضم استجابة الخادم. طبعنا الرسالة "…Started request" لتدل على أننا بدأنا تنفيذ الطلب. من المفترض أن يكون خرج الشيفرة السابقة كالتالي: Promise { <state>: "pending" } Started request… Received response: 200 لاحظ كيف ظهرت العبارة "Started request" على الشاشة قبل تلقي الاستجابة. فعلى خلاف الدوال المتزامنة، تعيد()fetch قيمتها قبل أن يكتمل الطلب، مما يسمح للبرنامج بمتابعة التنفيذ. ثم يعيد البرنامج بعد ذلك رمز الحالة 200 ويعني أن الطلب قد نُفِّذ بنجاح. قد يبدو هذا المثال مشابهًا للمثال في المقال السابق الذي استخدمنا فيه معالجات أحداث على الكائن XMLHttpRequest، لكننا مررنا هذه المرة دالة معالجة إلى التابع ()then العائد لكائن الوعد الذي تعيده الدالة()fetch. سلسلة من الوعود بمجرد حصولك على كائن استجابة Response باستخدام الواجهة()fetch، عليك استدعاء دالة أخرى للحصول على بيانات الاستجابة، ونريدها في هذه الحالة على هيئة بيانات JSON لهذا نستدعي التابع ()json العائد للكائن Respond. وكذلك الأمر، التابع ()json غير متزامن، هنا سنكون أمام حالة نستدعي فيها دالتين غير متزامنتين على التوالي. جرّب اﻵن ما يلي: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise.then((response) => { const jsonPromise = response.json(); jsonPromise.then((data) => { console.log(data[0].name); }); }); أضفنا في هذا المثال أيضًا التابع ()then إلى الوعد الذي تعيده ()fetch. لكن المعالج سيستدعي هذه المرة()response.json ومن ثم يمرر معالج ()then جديد إلى الوعد الذي يعيده التابع ()response.json. يُفترض في هذه الحالة طباعة العبارة في الطرفية (وهو اسم أول منتج في القائمة التي يضمها الملف "products.json"). لكن مقارنة مع الاستدعاءات التي شرحناها في مقال سابق، سنجد أننا نستدعي التابع ()then ضمن تابع ()then آخر وهذا مشابهة لفكرة استدعاء دالة استدعاء ضمن دالة استدعاء بشكل متعاقب، وكنا قد قلنا بأن هذا اﻷمر سيزيد من صعوبة قراءة الشيفرة وفهمها، وأطلقنا عليه اسم "جحيم الاستدعاء callback hell"، وما يحدث هنا أمر مشابه لكن مع الدالة ()then! نعم، اﻷمر نفسه تمامًا، لكن الوعد يقدم ميزة خاصة وهي أن التابع ()then يعيد هو أيضًا وعدًا يكتمل بنتيجة الدالة التي مُرر إليها. لهذا من اﻷفضل إعادة كتابة المثال السابق كالتالي: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => response.json()) .then((data) => { console.log(data[0].name); }); فبدلًا من استدعاء تابع ()then آخر ضمن معالج التابع ()then الأول، يمكننا إعادة الوعد الذي يعيده التابع ()json ثم نستدعي التابع ()then الثاني على القيمة المُعادة. يُدعى هذا اﻷمر بسلسلة الوعود Promise chaining، ونستطيع من خلال هذه الميزة تفادي زيادة مستوى التداخل عندما نضطر إلى استدعاء الدوال غير المتزامنة بشكل متتالٍ. قبل الانتقال إلى الخطوة التالية، علينا إضافة شيء آخر، وهو التأكد من قبول الخادم للطلب وقدرته على معالجته قبل أن نحاول قراءة الاستجابة، ولتنفيذ اﻷمر نتحقق من رمز حالة الطلب ونعرض خطأ إن لم يكن رمز الحالة 200 (أو OK) const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }); التقاط اﻷخطاء وصلنا حاليًا إلى الجزء اﻷخير المتعلق بمعالجة الأخطاء. إذ يمكن للواجهة البرمجية ()fetch رمي أخطاء لأسباب عديدة مثل عدم وجود اتصال بالشبكة أو تمرير عنوان URL غير صالح وغيرها من الأسباب، وبإمكاننا أيضًا رمي أخطاء بأنفسنا إن أعاد الخادم حالة خطأ. ورأينا في المقال السابق أن معالجة اﻷخطاء قد يغدو صعبًا عند تداخل الاستدعاءات ويدفعنا لمعالجة اﻷخطاء في كل مستوى على حدة. ولمعالجة اﻷخطاء، يقدّم الكائن Promise التابع ()catch الذي يشبه كثيرًا التابع ()then من حيث استدعاؤه وتمريره إلى دالة المعالجة. لكن ما يحدث أن استدعاء التابع ()catch يكون عند إخفاق العملية غير المتزامنة وليس نجاحها. فلو أضفت ()catch إلى نهاية سلسلة الوعود، سيُستدعى هذا التابع إن أخفقت أية دالة غير متزامنة في السلسلة. أي بإمكانك إنجاز أي عملية غير متزامنة على شكل سلسلة من الدوال غير المتزامنة المتتابعة التي تنتهي بتابع يعالج جميع اﻷخطاء. جرّب هذه النسخة التي تستخدم ()catch وتُعدّل عنوان URL حتى تُخفق العملية: const fetchPromise = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }) .catch((error) => { console.error(`Could not get products: ${error}`); }); نفّذ الشيفرة السابقة، ومن المفترض أن ترى الخطأ الذي يعرضه التابع ()catch. المصطلحات الخاصة بالوعود تستخدم الوعود مجموعة مخصصة من المصطلحات التي لا بد من توضيحها. بداية للوعد حالة من ثلاث حالات وهي كالتالي: قيد التنفيذ pending: يُنشأ كائن الوعد في هذه الحالة، لكن الدالة غير المتزامنة المرتبطة به لم تنجح أو تخفق بعد. وتوافق هذه الحالة إعادة الوعد إلى الدالة بعد استدعاء()fetch بينما لا يزال الطلب قيد التنفيذ. منجز fulfilled: وهي حالة نجاح العملية غير المتزامنة، ويُستدعى حينها التابع ()then. مرفوض rejected: وهي حالة إخفاق العملية غير المتزامنة، ويُستدعى عندها التابع ()catch. أما معنى "النجاح" أو "اﻹخفاق" فيعود للواجهة البرمجية المستخدمة. فالواجهة()fetch مثلًا ترفض الوعد المعاد لأسباب منها خطأ في الشبكة يمنع إرسال الطلب، وتنجزه عندما يعيد الخادم الاستجابة حتى لو كانت اﻹستجابة حالة خطأ مثل 404 Not Found. كما نستخدم أيضًا مصطلح "مسوّىً settled" ليشير إلى حالتي الرفض أو اﻹنجاز. ونقول عن الوعد أنه "مقضي resolved" إن جرت "تسويته settled" أو كان "مقفلًا locked in" بانتظار حالة وعد آخر. الجمع بين عدة وعود إن تكوّنت العملية غير المتزامنة من عدة دوال نستخدم حينها سلسلة من الوعود، ولا بد حينها من تسوية كل وعد قبل الانتقال إلى اﻵخر. لكنك قد تحتاج أحيانًا إلى الجمع بين عدة استدعاءات لدوال غير المتزامنة، لهذا تزوّدك الواجهة البرمجية ()Promis ببعض الدوال المساعدة. قد يتطلب الأمر في بعض التطبيقات إنجاز عدة وعود لا تتعلق ببعضها البعض. والطريقة اﻷكثر فعالية لتنفيذ هذا اﻷمر هي استدعاء جميع الدوال في نفس الوقت، ثم الحصول على تنبيه عندما تنجز جميعها، وهذا ما يقدمه التابع ()Promise.all فهو يُعيد مصفوفة من الوعود كما يعيد وعدًا واحدًا، ويكون هذا الوعد: منجزَا: عندما تُنجز كل الوعود في المصفوفة. ويُستدعى عند ذلك معالج التابع ()then وتُمرّر له مصفوفة من الاستجابات وبنفس ترتيب الوعود التي مُررت إلى التابع ()all. مرفوضًا: إن رٌفض أي من وعود المصفوفة، ويُستدعى عند ذلك معالج التابع ()catch ويُمرر له الخطأ الناتج عن الوعد الذي رُفض. إليك مثالًا: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); نفّذنا في الشيفرة السابقة ثلاثة طلبات()fetchإلى ثلاثة عناوين URL مختلفة، فإن نجحت هذه الطلبات، نعرض حالة الاستجابة لكل طلب، وإن أخفقت إحداها، نطبع الخطأ. ينبغي أن تُنجز الطلبات جميعها لأن العناوين صحيحة، مع ملاحظة أن رقم الحالة للطلب الثاني سيكون 404 بدلًا من 200 لأن الملف الذي نطلبه غير موجود حقيقة. لهذا سيكون خرج الكود السابق على النحو التالي: https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404 https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200 جرّب أن تنفّذ الشيفرة السابقة بعد كتابة العناوين بشكل خاطئ: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); نتوقع اﻵن أن تُنفَّذ دالة المعالجة الخاصة بالتابع ()catch، ومن المفترض حينها أن يكون الخرج مشابهًا للتالي: Failed to fetch: TypeError: Failed to fetch وقد تريد في بعض الحالات أن يُنجز أحد الوعود ولا يهم أيها، عندها يمكنك الاستفادة من التابع ()Promise.any الذي يشابه ()Promise.all لكنه يُنجز بمجرد إنجاز أي وعد في مصفوفة الوعود، ويُرفض إن رُفضت جميع الوعود: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.any([fetchPromise1, fetchPromise2, fetchPromise3]) .then((response) => { console.log(`${response.url}: ${response.status}`); }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); ملاحظة: لا يُمكن في هذه الحالة توقّع أي وعد سيُنجز أولًا. للتعرف على بقية التوابع التي يمكن استخدامها للجمع بين الوعود راجع توثيق ()Promis. استخدام التعليمتين async و await تُسهّل التعليمة async عمل الشيفرة غير المتزامنة التي تعتمد على الوعود، فإضافة هذه التعليمة إلى بداية الدالة يجعلها دالة غير متزامنة: async function myFunction() { // This is an async function } وباﻹمكان استخدام التعليمة await قبل استدعاء الدالة التي تعيد وعدًا. وهذا ما يجعل الشيفرة تنتظر عند هذه النقطة حتى يسوّى الوعد وعندها تُعد قيمة الوعد المنجز هي القيمة المعادة من قبل الدالة أو ترمي الدالة قيمة الوعد المرفوض كخطأ. وهكذا ستتمكن من كتابة شيفرة غير متزامنة مع أنها تبدو كذلك. لهذا سنحاول كتابة مثال()fetch كالتالي: async function fetchProducts() { try { //`()fetch` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء // الذي سيُعسد استجابة أو يرمي خطأ const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } //`response.json()` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء //أو يرمي خطأ JSON الذي يعيد كائن const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } } fetchProducts(); نستدعي هنا الدالة ()await fetch وسنحصل على كائن استجابة Response مكتمل بدلًا من الوعد ()Promisوكأن()fetchدالة متزامنة. ونستطيع أيضًا استخدام الكتلة try...catch لمعالجة اﻷخطاء كما لو كنا نكتب شيفرة متزامنة. وتذكر أن الدوال غير المتزامنة تُعيد وعدًا دائمًا، لهذا لا يمكن أن نكتب شيفرة كهذه: async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); console.log(promise[0].name); //هو كائن وعد فلن تعمل هذه الشيفرة "promise" ويجب عليك تصحيح الكود السابق على النحو التالي: async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); promise.then((data) => console.log(data[0].name)); وتذكر أن استخدام await يكون ضمن دالة، إلا في الحالة التي تكون فيها الشيفرة ضمن وحدة JavaScript module وليس ضمن سكريبت نمطي: try { // using await outside an async function is only allowed in a module const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } قد تستخدم دوال async كثيرًا مقارنة باستخدام سلسلة الوعود لكونها تجعل العمل مع الوعود أكثر وضوحًا. وتذكر أن await -كما هو حال سلسلة الوعود- تجبر العمليات المتزامنة على التنفيذ المتسلسل، وهذا أمر ضروري إن اعتمدت نتيجة العملية الثانية على سابقتها، أما إن لم يكن الوضع كذلك، ففكرّ في هذه الحالة باستخدام الدالة ()Promise.all. الخلاصة تُعرفنا في مقال اليوم على مفهوم الوعود التي تعد أساسًا للبرمجة غير المتزامنة في جافا سكريبت الحديثة. إذ تجعل تسلسل العمليات غير المتزامنة أكثر وضوحًا وإدراكًا بدلًا من التداخل المفرط للاستدعاءات، كما تدعم نمطًا من معالجة الأخطاء مشابهًا ﻵلية try...catch. وتسّهل التعليمتان async و await بناء عمليات باستدعاء سلسلة من الدوال غير المتزامنة المتتابعة دون الحاجة إلى سلاسل صريحة من الوعود وكتابة شيفرة شبيهة بالشيفرة المتزامنة. تعمل الوعود في النسخ اﻷخيرة من معظم المتصفحات الحديثة، وستكون المشكلة فقط مع متصفحي أوبرا ميني Opera mini وإنترنت إكسبلورر 11 والنسخ اﻷقدم. لم نناقش بالتأكيد كل ميزات الوعود في مقالنا الحالي، بل سلطنا الضوء على الميزات اﻷكثر أهمية واستخدامًا، وستتعلم خلال مسيرتك في تعلم جافا سكريبت الكثير من الميزات والتقنيات المفيدة الأخرى، ويجدر بالذكر أن الكثير من واجهات الويب البرمجية مبنية أساسًا على الوعود مثل WebRTC و Web Audio API Media Capture and Streams API وغيرها الكثير. ترجمة -وبتصرف- للمقال: How to use promises اقرأ أيضًا: المقال السابق: مدخل إلى جافا سكريبت غير المتزامنة اللاتزامن والانتظار async/await في جافاسكربت الوعود Promise في جافاسكربت البرمجة غير المتزامنة في جافاسكريبت
  6. هذي التقرير بي الخطى abud@abud-B460MDS3HV2:~$ sudo mv /etc/apt/preferences.d/nosnap.pref ~/Documents/nosnap.backup [sudo] password for abud: mv: cannot move '/etc/apt/preferences.d/nosnap.pref' to '/home/abud/Documents/nosnap.backup': No such file or directory abud@abud-B460MDS3HV2:~$ sudo apt update تجاهل:1 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy InRelease خطأ:2 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release الرجاء استخدام apt-cdrom لتعريف APT بهذا القرص المدمج. لا يمكن استخدام apt-get update لإضافة أقراص مدمجة جديدة. تجاهل:3 http://packages.linuxmint.com victoria InRelease Hit:4 https://mirrors.isu.net.sa/apt-mirror jammy InRelease جلب:5 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB] Hit:6 http://packages.linuxmint.com victoria Release جلب:7 https://mirrors.isu.net.sa/apt-mirror jammy-updates InRelease [128 kB] جلب:9 http://dl.google.com/linux/earth/deb stable InRelease [1821 B] خطأ:9 http://dl.google.com/linux/earth/deb stable InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 Hit:10 https://mirrors.isu.net.sa/apt-mirror jammy-backports InRelease Hit:11 https://debrepo.freedownloadmanager.org jammy InRelease قراءة قوائم الحزم... تمّ% E: The repository 'cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release' does not have a Release file. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. W: GPG error: http://dl.google.com/linux/earth/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 E: The repository 'http://dl.google.com/linux/earth/deb stable InRelease' is not signed. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. abud@abud-B460MDS3HV2:~$ sudo apt install snapd قراءة قوائم الحزم... تمّ0% Building dependency tree... تمّ% Reading state information... تمّ Package snapd is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source E: Package 'snapd' has no installation candidate abud@abud-B460MDS3HV2:~$ sudo snap install gabutdm sudo: snap: command not found abud@abud-B460MDS3HV2:~$ ^[[5~https://github.com/gabutakut/gabutdm.git~~ bash: https://github.com/gabutakut/gabutdm.git~~: No such file or directory abud@abud-B460MDS3HV2:~$ abud@abud-B460MDS3HV2:~$ sudo apt update [sudo] password for abud: Sorry, try again. [sudo] password for abud: تجاهل:1 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy InRelease خطأ:2 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release الرجاء استخدام apt-cdrom لتعريف APT بهذا القرص المدمج. لا يمكن استخدام apt-get update لإضافة أقراص مدمجة جديدة. Hit:3 https://mirrors.isu.net.sa/apt-mirror jammy InRelease تجاهل:4 http://packages.linuxmint.com victoria InRelease Hit:5 https://mirrors.isu.net.sa/apt-mirror jammy-updates InRelease Hit:6 https://mirrors.isu.net.sa/apt-mirror jammy-backports InRelease Hit:7 http://packages.linuxmint.com victoria Release جلب:9 http://dl.google.com/linux/earth/deb stable InRelease [1821 B] خطأ:9 http://dl.google.com/linux/earth/deb stable InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 Hit:10 https://debrepo.freedownloadmanager.org jammy InRelease Hit:11 http://security.ubuntu.com/ubuntu jammy-security InRelease قراءة قوائم الحزم... تمّ% E: The repository 'cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release' does not have a Release file. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. W: GPG error: http://dl.google.com/linux/earth/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 E: The repository 'http://dl.google.com/linux/earth/deb stable InRelease' is not signed. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. abud@abud-B460MDS3HV2:~$ sudo apt install build-essential قراءة قوائم الحزم... تمّ0% Building dependency tree... تمّ% Reading state information... تمّ The following additional packages will be installed: g++ g++-11 libc-dev-bin libc-devtools libc6-dev libcrypt-dev libnsl-dev libstdc++-11-dev libtirpc-dev rpcsvc-proto الحزم المقترحة: g++-multilib g++-11-multilib gcc-11-doc glibc-doc libstdc++-11-doc سيتم تثبيت الحزم الجديدة التالية: build-essential g++ g++-11 libc-dev-bin libc-devtools libc6-dev libcrypt-dev libnsl-dev libstdc++-11-dev libtirpc-dev rpcsvc-proto 0 سيتم ترقيتها، 11 مثبتة حديثاً، 0 لإزالتها و 1 لم يتم ترقيتها. بحاجة إلى جلب 16.1 Mب من الأرشيف. After this operation, 64.5 MB of additional disk space will be used. هل تريد الاستمرار؟ [Y/n] y جلب:1 https://mirrors.isu.net.sa/apt-mirror jammy-updates/main amd64 libc-dev-bin amd64 2.35-0ubuntu3.8 [20.3 kB] جلب:2 https://mirrors.isu.net.sa/apt-mirror jammy/main amd64 libcrypt-dev amd64 1:4.4.27-1 [112 kB] جلب:3 https://mirrors.isu.net.sa/apt-mirror jammy/main amd64 rpcsvc-proto amd64 1.4.2-0ubuntu6 [68.5 kB] جلب:4 https://mirrors.isu.net.sa/apt-mirror jammy-updates/main amd64 libtirpc-dev amd64 1.3.2-2ubuntu0.1 [192 kB] جلب:5 https://mirrors.isu.net.sa/apt-mirror jammy/main amd64 libnsl-dev amd64 1.3.0-2build2 [71.3 kB] جلب:6 https://mirrors.isu.net.sa/apt-mirror jammy-updates/main amd64 libc6-dev amd64 2.35-0ubuntu3.8 [2100 kB] جلب:7 https://mirrors.isu.net.sa/apt-mirror jammy-updates/main amd64 libstdc++-11-dev amd64 11.4.0-1ubuntu1~22.04 [2101 kB] جلب:8 https://mirrors.isu.net.sa/apt-mirror jammy-updates/main amd64 g++-11 amd64 11.4.0-1ubuntu1~22.04 [11.4 MB] جلب:9 https://mirrors.isu.net.sa/apt-mirror jammy/main amd64 g++ amd64 4:11.2.0-1ubuntu1 [1412 B] جلب:10 https://mirrors.isu.net.sa/apt-mirror jammy/main amd64 build-essential amd64 12.9ubuntu3 [4744 B] جلب:11 https://mirrors.isu.net.sa/apt-mirror jammy-updates/main amd64 libc-devtools amd64 2.35-0ubuntu3.8 [28.9 kB] جلب 16.1 Mب في 6s (2534 kب/ث) Selecting previously unselected package libc-dev-bin. (Reading database ... 584439 files and directories currently installed.) Preparing to unpack .../00-libc-dev-bin_2.35-0ubuntu3.8_amd64.deb ... Unpacking libc-dev-bin (2.35-0ubuntu3.8) ... Selecting previously unselected package libcrypt-dev:amd64. Preparing to unpack .../01-libcrypt-dev_1%3a4.4.27-1_amd64.deb ... Unpacking libcrypt-dev:amd64 (1:4.4.27-1) ... Selecting previously unselected package rpcsvc-proto. Preparing to unpack .../02-rpcsvc-proto_1.4.2-0ubuntu6_amd64.deb ... Unpacking rpcsvc-proto (1.4.2-0ubuntu6) ... Selecting previously unselected package libtirpc-dev:amd64. Preparing to unpack .../03-libtirpc-dev_1.3.2-2ubuntu0.1_amd64.deb ... Unpacking libtirpc-dev:amd64 (1.3.2-2ubuntu0.1) ... Selecting previously unselected package libnsl-dev:amd64. Preparing to unpack .../04-libnsl-dev_1.3.0-2build2_amd64.deb ... Unpacking libnsl-dev:amd64 (1.3.0-2build2) ... Selecting previously unselected package libc6-dev:amd64. Preparing to unpack .../05-libc6-dev_2.35-0ubuntu3.8_amd64.deb ... Unpacking libc6-dev:amd64 (2.35-0ubuntu3.8) ... Selecting previously unselected package libstdc++-11-dev:amd64. Preparing to unpack .../06-libstdc++-11-dev_11.4.0-1ubuntu1~22.04_amd64.deb ... Unpacking libstdc++-11-dev:amd64 (11.4.0-1ubuntu1~22.04) ... Selecting previously unselected package g++-11. Preparing to unpack .../07-g++-11_11.4.0-1ubuntu1~22.04_amd64.deb ... Unpacking g++-11 (11.4.0-1ubuntu1~22.04) ... Selecting previously unselected package g++. Preparing to unpack .../08-g++_4%3a11.2.0-1ubuntu1_amd64.deb ... Unpacking g++ (4:11.2.0-1ubuntu1) ... Selecting previously unselected package build-essential. Preparing to unpack .../09-build-essential_12.9ubuntu3_amd64.deb ... Unpacking build-essential (12.9ubuntu3) ... Selecting previously unselected package libc-devtools. Preparing to unpack .../10-libc-devtools_2.35-0ubuntu3.8_amd64.deb ... Unpacking libc-devtools (2.35-0ubuntu3.8) ... Setting up libtirpc-dev:amd64 (1.3.2-2ubuntu0.1) ... Setting up rpcsvc-proto (1.4.2-0ubuntu6) ... Setting up libnsl-dev:amd64 (1.3.0-2build2) ... Setting up libcrypt-dev:amd64 (1:4.4.27-1) ... Setting up libc-dev-bin (2.35-0ubuntu3.8) ... Setting up libc-devtools (2.35-0ubuntu3.8) ... Setting up libc6-dev:amd64 (2.35-0ubuntu3.8) ... Setting up libstdc++-11-dev:amd64 (11.4.0-1ubuntu1~22.04) ... Setting up g++-11 (11.4.0-1ubuntu1~22.04) ... Setting up g++ (4:11.2.0-1ubuntu1) ... update-alternatives: using /usr/bin/g++ to provide /usr/bin/c++ (c++) in auto mo de Setting up build-essential (12.9ubuntu3) ... Processing triggers for man-db (2.10.2-1) ... abud@abud-B460MDS3HV2:~$ sudo apt install linux-headers-$(uname -r) قراءة قوائم الحزم... تمّ0% Building dependency tree... تمّ% Reading state information... تمّ linux-headers-5.15.0-76-generic is already the newest version (5.15.0-76.83). 0 سيتم ترقيتها، 0 مثبتة حديثاً، 0 لإزالتها و 1 لم يتم ترقيتها. abud@abud-B460MDS3HV2:~$ cd /path/to/your/file bash: cd: /path/to/your/file: No such file or directory abud@abud-B460MDS3HV2:~$ chmod +x VMware-Workstation-Full-17.5.2-23775571.x86_64.bundle chmod: cannot access 'VMware-Workstation-Full-17.5.2-23775571.x86_64.bundle': No such file or directory abud@abud-B460MDS3HV2:~$ sudo ./VMware-Workstation-Full-17.5.2-23775571.x86_64.bundle sudo: ./VMware-Workstation-Full-17.5.2-23775571.x86_64.bundle: command not found abud@abud-B460MDS3HV2:~$ sudo apt update تجاهل:1 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy InRelease خطأ:2 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release الرجاء استخدام apt-cdrom لتعريف APT بهذا القرص المدمج. لا يمكن استخدام apt-get update لإضافة أقراص مدمجة جديدة. Hit:3 http://security.ubuntu.com/ubuntu jammy-security InRelease جلب:4 http://dl.google.com/linux/earth/deb stable InRelease [1821 B] خطأ:4 http://dl.google.com/linux/earth/deb stable InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 تجاهل:5 http://packages.linuxmint.com victoria InRelease Hit:6 https://mirrors.isu.net.sa/apt-mirror jammy InRelease Hit:7 https://mirrors.isu.net.sa/apt-mirror jammy-updates InRelease Hit:8 https://mirrors.isu.net.sa/apt-mirror jammy-backports InRelease Hit:9 http://packages.linuxmint.com victoria Release Hit:11 https://debrepo.freedownloadmanager.org jammy InRelease قراءة قوائم الحزم... تمّ% E: The repository 'cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release' does not have a Release file. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. W: GPG error: http://dl.google.com/linux/earth/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 E: The repository 'http://dl.google.com/linux/earth/deb stable InRelease' is not signed. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. abud@abud-B460MDS3HV2:~$ sudo apt install p7zip-full قراءة قوائم الحزم... تمّ0% Building dependency tree... تمّ% Reading state information... تمّ p7zip-full is already the newest version (16.02+dfsg-8). 0 سيتم ترقيتها، 0 مثبتة حديثاً، 0 لإزالتها و 1 لم يتم ترقيتها. abud@abud-B460MDS3HV2:~$ sudo apt install unetbootin قراءة قوائم الحزم... تمّ0% Building dependency tree... تمّ% Reading state information... تمّ E: تعذر العثور على الحزمة unetbootin
  7. هذي التقرير بي الخطى abud@abud-B460MDS3HV2:~$ sudo mv /etc/apt/preferences.d/nosnap.pref ~/Documents/nosnap.backup [sudo] password for abud: mv: cannot move '/etc/apt/preferences.d/nosnap.pref' to '/home/abud/Documents/nosnap.backup': No such file or directory abud@abud-B460MDS3HV2:~$ sudo apt update تجاهل:1 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy InRelease خطأ:2 cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release الرجاء استخدام apt-cdrom لتعريف APT بهذا القرص المدمج. لا يمكن استخدام apt-get update لإضافة أقراص مدمجة جديدة. تجاهل:3 http://packages.linuxmint.com victoria InRelease Hit:4 https://mirrors.isu.net.sa/apt-mirror jammy InRelease جلب:5 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB] Hit:6 http://packages.linuxmint.com victoria Release جلب:7 https://mirrors.isu.net.sa/apt-mirror jammy-updates InRelease [128 kB] جلب:9 http://dl.google.com/linux/earth/deb stable InRelease [1821 B] خطأ:9 http://dl.google.com/linux/earth/deb stable InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 Hit:10 https://mirrors.isu.net.sa/apt-mirror jammy-backports InRelease Hit:11 https://debrepo.freedownloadmanager.org jammy InRelease قراءة قوائم الحزم... تمّ% E: The repository 'cdrom://Linux Mint 21.2 _Victoria_ - Release amd64 20230711 jammy Release' does not have a Release file. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. W: GPG error: http://dl.google.com/linux/earth/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2 E: The repository 'http://dl.google.com/linux/earth/deb stable InRelease' is not signed. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details. abud@abud-B460MDS3HV2:~$ sudo apt install snapd قراءة قوائم الحزم... تمّ0% Building dependency tree... تمّ% Reading state information... تمّ Package snapd is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or is only available from another source E: Package 'snapd' has no installation candidate abud@abud-B460MDS3HV2:~$ sudo snap install gabutdm sudo: snap: command not found abud@abud-B460MDS3HV2:~$ اطفه لي المتصفح Motrix تحمل من اليوتب مثلن كيف تثبيت JDownloader2Setup_unix_nojre.sh افطل نسخ مستقره في لينكس مستودعاته كثير
  8. لا يوجد جواب دقيق ولا يمكن أن يوجد. السبب أن الصفحة في تطبيق ويب ليست صفحة نصية مثل صفحات ملف PDF. الصفحة تتفاعل مع كود الواجهة الخلفية، وقد يكون فيها وظائف معقدة، وقد يكون فيها تحريكات (Animations) مميزة، وهذا كله يزيد في تعقيد الصفحة، كما أن الصفحات في كثير من الأحيان تكون مرتبطة ببعضها البعض، وتغيير في واحدة يؤثر على الصفحات الأخرى. لذلك هناك كثير من المتغيرات كما ترى. فلا يمكن أن نحسب "الزمن المتوسط للمبرمجين" في إنشاء الصفحات. إذا كنت تتوقع أنه بإمكانك إنجاز المطلوب بين يومين أو 7 أيام، فربما من الأحسن أن تقول 5 أيام، أو حتى 7 أيام، لكي تعطي لنفسك الوقت الكافي. وهذا في حالة ما لم يكن العميل مستعجلا. وإلا فحاول إنجاز العمل في أقرب الآجال.
  9. نعم إجابة واضحة مشكور. لكن لدي سؤال آخر، مثلا صفحة من تطبيق علي إنجازها في كم مدة؟ يعني ماهو الزمن المتوسط للمبرمجين؟ فبإمكاني إنشاءها في يومين كما يمكنني إنشاءها في أسبوع.
  10. ستجد أسفل فيديو الدرس في نهاية الصفحة صندوق تعليقات كما هنا، أرجو طرح الأسئلة أسفل الدرس وليس هنا في قسم الأسئلة البرمجة حيث نطرح الأسئلة العامة الغير متعلقة بمحتوى الدورة أو الدرس، وذلك لمساعدتك بشكل أفضل.
  11. بعد ما اكتب الأوامر في أدوات المطور كيف أحفظ الأكواد دون تخريب ما كتبت
  12. لا داعي لذكر مدة زمنية محددة وإلزام نفسك بها، بل اطرح ما تستطيع فعله وذكر المهارات التي تمتلكها وما الفائدة التي ستعود على العميل من تلك المهارات فهو لا يدري ما معنى Full-Stack. ويجب أن تظهر بمظهر إحترافي ومنظم وعدم الإنجراف وراء ما يطرحه الآخرين. مع ذكر شرط أنه يتم تحديد مدة المشروع بناءًا على المتطلبات ومدى تعقيد المشروع، وعند إعطاء مدة زمنية للعميل قم بمضاعفتها ففي البداية لن تكون لديك خبرة بتحديد المدة اللازمة، وفي حال قمت بإنجاز المشروع في أقل من ذلك فستكون تلك نقطة جيدة لصالحك وليس العكس. يمكنك ذكر مثلاً أن تطبيق بسيط مكون من 5 صفحات يستغرق مدة زمنية تساوي كذا
  13. السلام عليكم، في موقع خمسات مثلا، حين أود أن أقدم خدمة، كإنشاء تطبيق واب أو هاتف من الصفر، بإستعمال React و React Nativeو Node، أي أن التطبيق Fullstack. كيف يمكنني تحديد المدة اللازمة لإنشاء التطبيق؟ أعني بذلك كيف يمكنني تحديد المجال الزمني المناسب و اللازم لإنشاء التطبيق؟ أعلم أنه ليست هناك إجابة محددة، كون تواجد العديد من العوامل المتحكمة و المتغيرات، كالخبرة و درجة تعقيد التطبيق وماشابه. ولكن على الأقل هناك ما يساعدني على تحديد مجال زمني مناسب ؟ شكرا مسبقا.
  14. قم بتثبيت مديز الحزم snap من خلال تنفيذ الأوامر التالية بالترتيب: sudo mv /etc/apt/preferences.d/nosnap.pref ~/Documents/nosnap.backup sudo apt update sudo apt install snapd ثم قم بتسجيل الخروج من حسابك وتسجيل الدخول مجددًا في النظام أو أعد تشغيل النظام. ثم تثبيت البرنامج من خلال: sudo snap install gabutdm
  15. كيف اثبت Gabut Sownload manager كيف تثبيته ومنه على لينكس منت صيغا التثييتbad
  16. لا يوجد بديل مماثل تمامًا، لكن لتحميل الفيديوهات من اليوتيوب يوجد أداة stacher توجه إلى موقع stacher.io لتحميلها. أما بالنسبة لبرامج تحميل ملفات مشابهة لبرنامج IDM، فيوجد: Motrix Gabut Download Manager jdownloader2
  17. لا حاجة إلى ذلك، في حال لديك البيانات الخاصة بالحساب وتتذكريها جيدًا، تستطيعي استرجاع الحساب من خلال مراسلة الدعم، توجهي للصفحة التالية من خلال هاتفك: https://www.instagram.com/hacked/ ثم اختاري my account was hacked ثم اضغطي على next، بعد ذلك عليك إدخال اسم المستخدم أو رقم الهاتف أو البريد للبحث عن حسابك ثم اضغطي على next. وسيتم سؤالك عن رفع صور للتعرف عليك وإثبات أنك صاحبة الحساب عليك الضغط على yes i have photos ثم سيطلب منك بريد إلكتروني أدخليه للتواصل معك وإرسال رابط إليه. بعد ذلك سيطلب منك تصوير نفسك بالكاميرا الأمامية للتعرف عليك وإتباع تعليمات التصوير مثل تدوير الوجه وخلافه. والآن انتظري يوم إلى يومين وسيتم إرسال بريد على الإيميل الذي أدخلتيه به رابط لاسترجاع الحساب.
  18. مرحبا احتاج خبير استعداد حساب مهكر في الانستقرام. الهكر غير الايميل و رقم التلفون و حط F2A. احتاج استراجعه باسرع وقت، مستعدة ادفع المبلغ المطلوب، بس بدي يرجعلي حسابي ضروري. ارجو الرد بسرعة
  19. البارحة
  20. أولاً لديك خطأ في حساب متوسط التقييمات في السطر الذي يحسب فيه المتوسط النهائي finalRate. فالمتغير rate ربما يتم إرساله كقيمة غير صحيحة مثل قيمة نصية من الواجهة الأمامية، لذا لنتأكد من أن rate يتم تحويله إلى رقم صحيح قبل استخدامه في الحساب، وأيضًا التأكد من صحة قيم التقييم المرسلة من الواجهة الأمامية. أيضًا بعد إضافة المراجعة إلى قاعدة البيانات، عليك إعادة بيانات الكتاب المحدثة إلى الواجهة الأمامية، بما في ذلك التقييم الجديد، وبعد تلقي بيانات الكتاب المحدثة من الخادم، يجب تحديث حالة الكتاب في الواجهة الأمامية لتعكس التقييم الجديد. ثم استخدام البيانات المحدثة للكتاب في المكون لعرض التقييم الجديد. إليك التعديلات مع تعليقات لتوضيح أماكن التعديل. تعديل في bookController.js: const addReview = asyncHandler(async(req, res) => { const { id } = req.params const { comment, rate } = req.body const book = await Book.findById(id) const user = await User.findById(req.userId) // التحقق من وجود الكتاب if (!book) { return res.status(404).json({ message: "Book Not Found" }) } // التحقق من أن المستخدم لم يقيم الكتاب مسبقاً const isRated = book.reviews.findIndex(m => m.user == req.userId) if (isRated > -1){ return res.status(403).send({ message: "Review Is Already Added" }); } // تحويل تقييم المستخدم إلى رقم صحيح والتحقق من صحته const numericRate = parseInt(rate, 10); if (isNaN(numericRate) || numericRate < 1 || numericRate > 5) { return res.status(400).send({ message: "Invalid rating value" }); } // حساب التقييم النهائي const totalRate = book.reviews.reduce((sum, review) => sum + review.rate ,0) const finalRate = (totalRate + numericRate) / (book.reviews.length + 1) // تحديث بيانات الكتاب بإضافة المراجعة الجديدة وتحديث التقييم await Book.updateOne( { _id: id }, { $push: { reviews: { user: req.userId, username: user.name, comment, rate: numericRate } }, $set: { rate: finalRate } } ) // إعادة بيانات الكتاب المحدثة بعد الإضافة const updatedBook = await Book.findById(id); res.status(201).json(updatedBook) }) تحديث الدالة postReview في apiCall: export function postReview(bookId, review) { return async (dispatch, getState) => { try { dispatch(bookActions.setLoading()) const {data} = await axios.post(`${BOOK_URL}/${bookId}/reviews`, review, { headers: { "authorization": getState().auth.user.accessToken } }); // عرض رسالة نجاح باستخدام toast toast.success(data?.message) // تحديث بيانات الكتاب المحدثة في الحالة dispatch(bookActions.updateBook(data)); dispatch(bookActions.clearLoading()); } catch (error) { // عرض رسالة خطأ باستخدام toast toast.error(error?.response?.data.message); dispatch(bookActions.clearLoading()); } }; } تعديل في الواجهة الأمامية Modal.js const Modal = ({ showModal, handleClose, book }) => { const [rate, setRating] = useState(0); const [comment, setComment] = useState(""); const dispatch = useDispatch(); const navigate = useNavigate(); const submitReview = (e) => { e.preventDefault(); if (comment === "") { // التحقق من وجود تعليق وعرض رسالة خطأ في حال عدم وجوده return toast.error("Comment is required") } // تحويل تقييم المستخدم إلى رقم صحيح والتحقق من صحته const numericRate = parseInt(rate, 10); if (isNaN(numericRate) || numericRate < 1 || numericRate > 5) { return toast.error("Invalid rating value"); } // إرسال المراجعة باستخدام الدالة postReview dispatch(postReview(book._id, { rate: numericRate, comment })) }; return ( <div className={`modal ${showModal ? "show" : ""}`}> <div className="modal-content"> <span className="close" onClick={handleClose}>&times;</span> <h2>Submit Your Review</h2> <form onSubmit={submitReview}> <div className="rating-input"> <label>Rating:</label> <select value={rate} onChange={(e) => setRating(e.target.value)} required> <option value="" disabled>Select a rating</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> <div className="comment"> <label>Comment:</label> <textarea value={comment} onChange={(e) => setComment(e.target.value)} required /> </div> <button className='modal-btn' type="submit">Submit</button> </form> </div> </div> ); }; export default Modal; تحديث bookSlice لتضمين الدالة updateBook: const bookSlice = createSlice({ name: "book", initialState: { books: [], error: false, loading: false, }, reducers: { getBooks(state, action) { state.books = action.payload; }, findBook(state, action) { state.books = action.payload; }, addReviews(state, action) { state.books = action.payload; }, updateBook(state, action) { // البحث عن الكتاب المحدث وتحديثه const updatedBook = action.payload; const index = state.books.findIndex(book => book._id === updatedBook._id); if (index !== -1) { state.books[index] = updatedBook; } }, setLoading(state) { state.loading = true; }, clearLoading(state) { state.loading = false; }, setError(state) { state.error = true; }, clearError(state) { state.error = false; }, } }); export const bookActions = bookSlice.actions; export default bookSlice.reducer;
  21. السلام عليكم. في المثال التالي، أواجه الأخطاء التالية: عندما أضيف Review معين (بين 1 و 5) فإنه يتم إرسال عدد أكبر من 5 إلى قاعدة البيانات لايتم إضافة التقييم إلى واجهة الصفحة إلا بعد التحديث الكود bookController // method POST // route api/books/:id/reviews // desc Add a book review // access Private | auth const addReview = asyncHandler(async(req, res) => { const { id } = req.params const { comment, rate } = req.body const book = await Book.findById(id) const user = await User.findById(req.userId) if (!book) { return res.status(404).json({ message: "Book Not Found" }) } const isRated = book.reviews.findIndex(m => m.user == req.userId) if (isRated > -1){ return res.status(403).send({ message: "Review Is Already Added" }); } const totalRate = book.reviews.reduce((sum, review) => sum + review.rate ,0) const finalRate = (totalRate + rate) / (book.reviews.length + 1) await Book.updateOne( { _id: id } , { $push: { reviews: { user: req.userId, username: user.name, comment, rate } }, $set: { rate: finalRate } } ) res.status(201).json({ message: "Review added successfully" }) }) Frontend apiCall // Post Review export function postReview(bookId, review) { return async (dispatch, getState) => { try { dispatch(bookActions.setLoading()) const {data} = await axios.post(`${BOOK_URL}/${bookId}/reviews`, review, { headers: { "authorization": getState().auth.user.accessToken } }); toast.success(data?.message) dispatch(bookActions.addReviews()); dispatch(bookActions.clearLoading()); } catch (error) { toast.error(error?.response?.data.message); dispatch(bookActions.clearLoading()); } }; } bookSlice const bookSlice = createSlice({ name: "book", initialState: { books: [], error: false, loading: false, }, reducers: { getBooks(state, action) { state.books = action.payload; }, findBook(state, action) { state.books = action.payload; }, addReviews(state, action) { state.books = action.payload; }, setLoading(state) { state.loading = true; }, clearLoading(state) { state.loading = false; }, setError(state) { state.error = true; }, clearError(state) { state.error = false; }, } } Component const Modal = ({ showModal, handleClose, book }) => { const [rate, setRating] = useState(0); const [comment, setComment] = useState(""); const dispatch = useDispatch(); const navigate = useNavigate(); const submitReview = (e) => { e.preventDefault(); if (comment === "") { return toast.error("Comment is required") } dispatch(postReview(book, { rate, comment })) navigate(`/${book}`) }; return ( <div className={`modal ${showModal ? "show" : ""}`}> <div className="modal-content"> <span className="close" onClick={handleClose}>&times;</span> <h2>Submit Your Review</h2> <form onSubmit={submitReview}> <div className="rating-input"> <label>Rating:</label> <select value={rate} onChange={(e) => setRating(e.target.value)} required> <option value="" disabled>Select a rating</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> </div> <div className="comment"> <label>Comment:</label> <textarea value={comment} onChange={(e) => setComment(e.target.value)}/> </div> <button className='modal-btn' type="submit">Submit</button> </form> </div> </div> ); }; export default Modal; شكرا على المساعدة
  22. سنشرح في هذا المقال نوع البيانات المسمى بالقاموس dictionary، الذي يوفر طريقة مرنة للوصول إلى البيانات وتنظيمها، ثم سنتعلم كيفية كتابة لعبة إكس-أو عبر دمج ما تعلمناه في المقالات السابقة من هذه السلسلة مع القواميس. نوع البيانات dictionary القواميس تشبه القوائم في أنها مجموعة قابلة للتعديل mutable من القيم، لكن بدلًا من وجود الفهارس الرقمية للعناصر كما في القوائم، يمكن استخدام مختلف أنواع البيانات في القواميس. نسمي المفاتيح في القواميس بالمفاتيح keys، وكل مفتاح مرتبط بقيمة، ونسمي ذلك زوجًا من المفاتيح والقيم key-value pair. نكتب القواميس في بايثون بإحاطها بقوسين مجعدين أو معقوصين {}: >>> myCat = {'size': 'fat', 'color': 'gray', 'disposition': 'loud'} السطر السابق يسند قاموسًا إلى المتغير myCat، ومفاتيح هذا القاموس هي 'size' و 'color' و 'disposition'، والقيم المرتبطة بتلك المفاتيح هي 'fat' و 'gray' و 'loud' على التوالي وبالترتيب. يمكنك الوصول إلى هذه القيم عبر مفاتيحها: >>> myCat['size'] 'fat' >>> 'My cat has ' + myCat['color'] + ' fur.' 'My cat has gray fur.' ما يزال بالإمكان استخدام الأعداد الصحيحة مفاتيحًا لقيم القواميس، لكن ليس من الضروري أن تبدأ من الصفر 0 ويمكنك استخدام أي رقم: >>> spam = {12345: 'Luggage Combination', 42: 'The Answer'} مقارنة القواميس والقوائم على خلاف القوائم، لا تكون القواميس مرتبةً، فأول عنصر في قائمة اسمها spam سيكون spam[0]‎، لكن لا يوجد «أول» عنصر في القاموس. سيكون ترتيب العناصر مهمًا في حال أردنا تحديد إن كانت قائمتان متساويتين، بينما لا يفرق ترتيب كتابة أزواج المفاتيح-القيم في القواميس: >>> spam = ['cats', 'dogs', 'moose'] >>> olive = ['dogs', 'moose', 'cats'] >>> spam == olive False >>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'} >>> steak = {'species': 'cat', 'age': '8', 'name': 'Zophie'} >>> eggs == steak True ولأن القواميس غير مرتبة، فلا يمكن تقسيمها كما في القوائم. محاولة الوصول إلى مفتاح غير موجود في قاموس ما سيؤدي إلى حدوث الخطأ KeyError، وهي تشبه رسالة الخطأ IndexError في القوائم حين محاولة الوصول إلى قيمة خارج مجال فهارس القائمة. لاحظ رسالة الخطأ الآتية التي تظهر لعدم وجود المفتاح 'color': >>> spam = {'name': 'Zophie', 'age': 7} >>> spam['color'] Traceback (most recent call last): File "<pyshell#1>", line 1, in <module> spam['color'] KeyError: 'color' وصحيحٌ أن القواميس غير مرتبة، لكن إمكانية استخدام أي قيمة تريدها للمفاتيح يسمح لنا بترتيب البيانات بطرائق رائعة! لنقل أننا نريد كتابة برنامج يخزن معلومات حول أعياد ميلاد أصدقائك، يمكنك أن تكتب الشيفرة الآتية birthdays.py التي تستخدم القواميس وتجعل أسماء أصدقائك مفاتيحًا للقيم: ➊ birthdays = {'Alice': 'Apr 1', 'Bob': 'Dec 12', 'Carol': 'Mar 4'} while True: print('Enter a name: (blank to quit)') name = input() if name == '': break ➋ if name in birthdays: ➌ print(birthdays[name] + ' is the birthday of ' + name) else: print('I do not have birthday information for ' + name) print('What is their birthday?') bday = input() ➍ birthdays[name] = bday print('Birthday database updated.') أنشأنا قاموسًا وخزناه في المتغير birthdays ➊، ثم تحققنا إن كان الاسم المدخل موجودًا في القاموس عبر الكلمة المحجوزة in ➋ كما كنا نفعل مع القوائم. إذا كان الاسم موجودًا في القاموس فيمكنك الوصول إلى القيمة المرتبطة به عبر استخدام الأقواس المربعة ➌، وإلّا فيمكنك إضافتها باستخدام صيغة الأقواس المربعة مع عامل الإسناد ➍: Enter a name: (blank to quit) Alice Apr 1 is the birthday of Alice Enter a name: (blank to quit) Eve I do not have birthday information for Eve What is their birthday? Dec 5 Birthday database updated. Enter a name: (blank to quit) Eve Dec 5 is the birthday of Eve Enter a name: (blank to quit) للأسف ستحذف كل المعلومات التي أدخلتها في هذا البرنامج حين انتهاء تنفيذه. ستتعلم كيفية حفظ الملفات على القرص في مقال لاحق من هذه السلسلة. القواميس المرتبة في بايثون 3.7 صحيح أن القواميس غير مرتبة ولا يوجد فيها عنصر «أول»، لكن القواميس في الإصدار 3.7 من بايثون وما يليه ستتذكر ترتيب أزواج القيم الموجودة فيها حين إنشاء متسلسل sequence منها. فمثلًا لاحظ أن ترتيب العناصر في القائمتين المنشأتين من القاموسين eggs و steak يطابق ترتيب إدخالها: >>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'} >>> list(eggs) ['name', 'species', 'age'] >>> steak = {'species': 'cat', 'age': '8', 'name': 'Zophie'} >>> list(steak) ['species', 'age', 'name'] ستبقى القواميس غير مرتبة، ولا يمكنك أن تصل إلى العناصر فيها عبر فهرس رقمي مثل eggs[0]‎ أو steak[2]‎، لا يجدر بك الاعتماد على هذا السلوك لأن بايثون لا تتذكر ترتيب إدخال العناصر في الإصدارات القيمة منها، فلاحظ المثال الآتي الذي لا يطابق ترتيب عناصر القاموس الناتج النهائي الترتيبَ الذي أدخلتها به، ناتج التنفيذ الآتي على إصدار بايثون 3.5: >>> spam = {} >>> spam['first key'] = 'value' >>> spam['second key'] = 'value' >>> spam['third key'] = 'value' >>> list(spam) ['first key', 'third key', 'second key'] التوابع keys()‎ و values()‎ و items()‎ هنالك ثلاثة توابع خاصة بالقواميس التي تعيد قيمًا شبيهة بالقوائم list-like من مفاتيح القواميس أو قيمها أو كلًا من المفاتيح والقيم معًا وهي التوابع keys()‎ و values()‎ و items()‎ بالترتيب. القيم المعادة من هذه التوابع ليست قوائم حقيقيةً، فلا يمكننا تعديلها وليس لها التابع append()‎، لكن أنواع البيانات المعادة (وهي dictkeys و dictvalues و dict_items بالترتيب) يمكن أن تستخدم في حلقات التكرار for: >>> spam = {'color': 'red', 'age': 42} >>> for v in spam.values(): ... print(v) red 42 ستمر حلقة for هنا على كل قيمة في القاموس spam، يمكن لحلقة for المرور على المفاتيح فقط، وعلى المفاتيح والقيم معًا: >>> for k in spam.keys(): ... print(k) color age >>> for i in spam.items(): ... print(i) ('color', 'red') ('age', 42) حين استخدامنا للتوابع keys()‎ و values()‎ و items()‎ فيمكن للحلقة for المرور على قيم المفاتيح أو قيم العناصر أو قيم أزواج المفتاح-القيمة على التوالي. لاحظ أن القيم المعادة من items()‎ هي صفوف tuples تحتوي على المفتاح ثم قيمته. إذا أردتَ قائمةً حقيقية من ناتج أحد تلك التوابع، فيمكننا تمرير القيمة الشبيهة بالقوائم إلى الدالة list()‎ كما يلي: >>> spam = {'color': 'red', 'age': 42} >>> spam.keys() dict_keys(['color', 'age']) >>> list(spam.keys()) ['color', 'age'] يأخذ السطر list(spam.keys())‎ القيمة ذات النوع dict_keys المعادة من التابع keys()‎ ويمررها إلى الدالة list()‎، والتي تعيد بدورها قائمةً فيها ['color', 'age']. يمكنك استخدام الإسناد المتعدد مع حلقة for لإسناد المفتاح والقيمة إلى متغيرات منفصلة: >>> spam = {'color': 'red', 'age': 42} >>> for k, v in spam.items(): ... print('Key: ' + k + ' Value: ' + str(v)) Key: age Value: 42 Key: color Value: red التحقق من وجود مفتاح أو قيمة في قاموس نتذكر من المقال السابق أن العاملين in و not in يمكن أن يستخدما للتحقق من وجود قيمة في قائمة. ويمكننا استخدام نفس العاملين للتحقق من وجود قيمة ما في مفتاح أو قيمة في قاموس: >>> spam = {'name': 'Zophie', 'age': 7} >>> 'name' in spam.keys() True >>> 'Zophie' in spam.values() True >>> 'color' in spam.keys() False >>> 'color' not in spam.keys() True >>> 'color' in spam False لاحظ أن كتابة ‎'color' in spam هي مطابقة لكتابة ‎'color' in spam.keys()‎. فإذا أردت التحقق من وجود (أو عدم وجود) مفتاح ما في قاموس فاستعمل الكلمة المحجوزة in (أو not in) مع القاموس نفسه. التابع get()‎ من الرتيب أن نتحقق من وجود مفتاح ما في القاموس قبل الوصول إلى القيمة المرتبطة به، لكن لحسن الحظ هنالك تابع باسم get()‎ يأخذ وسيطين: الأول هو المفتاح الذي نريد الوصول إلى قيمته، والثاني هو قيمة افتراضية ستعاد إن لم يكن المفتاح موجودًا: >>> picnicItems = {'apples': 5, 'cups': 2} >>> 'I am bringing ' + str(picnicItems.get('cups', 0)) + ' cups.' 'I am bringing 2 cups.' >>> 'I am bringing ' + str(picnicItems.get('eggs', 0)) + ' eggs.' 'I am bringing 0 eggs.' ولعدم وجود المفتاح 'eggs' في القاموس picnicItems فستعاد القيمة 0 من التابع get()‎، وإن لم نستعمل التابع get()‎ في المثال السابق فستظهر رسالة خطأ كما يلي: >>> picnicItems = {'apples': 5, 'cups': 2} >>> 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.' Traceback (most recent call last): File "<pyshell#34>", line 1, in <module> 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.' KeyError: 'eggs' التابع setdefault()‎ الحاجة إلى ضبط قيمة في القاموس مرتبطة بمفتاح معين إن لم يكن ذاك المفتاح موجودًا مسبقًا هو أمرٌ شائع، وتكون الشيفرة بالشكل الآتي: spam = {'name': 'Pooka', 'age': 5} if 'color' not in spam: spam['color'] = 'black' يوفر التابع setdefault()‎ طريقة أسهل لفعل ذلك بسطر برمجي وحيد، فأول وسيط يمرر إلى التابع هو المفتاح الذي سنتحقق من وجوده، والوسيط الثاني هو القيمة التي ستُضبَط إن لم يكن المفتاح موجودًا. إذا كان المفتاح موجودًا فسيعيد التابع setdefault()‎ قيمة ذاك المفتاح: >>> spam = {'name': 'Pooka', 'age': 5} >>> spam.setdefault('color', 'black') 'black' >>> spam {'color': 'black', 'age': 5, 'name': 'Pooka'} >>> spam.setdefault('color', 'white') 'black' >>> spam {'color': 'black', 'age': 5, 'name': 'Pooka'} حينما استدعينا التابع setdefault()‎ أول مرة، فتغيرت قيمة القاموس spam إلى ‎{'color': 'black', 'age': 5, 'name': 'Pooka'}‎، وسيعيد التابع setdefault()‎ القيمة 'black' لأنها القيمة المضبوطة للمفتاح 'color' حاليًا. لكن حين استدعاء spam.setdefault('color', 'white')‎ فإن القيمة لن تتغير إلى 'white' لأن القاموس spam يحتوي على مفتاح باسم 'color'. التابع setdefault()‎ هو اختصار جميل للتأكد من وجود مفتاح معين وضبط قيمته. هذا مثال عن برنامج يعدّ عدد مرات وجود كل حرف في سلسلة نصية. احفظ المثال الآتي باسم characterCount.py: message = 'It was a bright cold day in April, and the clocks were striking thirteen.' count = {} for character in message: ➊ count.setdefault(character, 0) ➋ count[character] = count[character] + 1 print(count) سيمر البرنامج على كل محرف في السلسلة النصية المخزنة في المتغير message، ويعد كم مرة يظهر فيها كل محرف. استدعاء الدالة setdefault()‎ ➊ سيضمن وجود المفتاح في القاموس count ويضبط قيمته الافتراضية إلى 0، وبالتالي لا يرمي البرنامج الخطأ KeyError حين تنفيذ التعبير البرمجي count[character] = count[character] + 1 ➋. سيبدو الناتج كما يلي: {' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, 'a': 4, 'c': 3, 'b': 1, 'e': 5, 'd': 3, 'g': 2, 'i': 6, 'h': 3, 'k': 2, 'l': 3, 'o': 2, 'n': 4, 'p': 1, 's': 3, 'r': 5, 't': 6, 'w': 2, 'y': 1} سترى من الناتج السابق أن الحرف c الصغير مكرر 3 مرات، بينما الفراغ مكرر 13 مرة، والحرف A الكبير يظهر مرة واحدة. سيعمل البرنامج السابق على جميع السلاسل النصية بغض النظر عن محتويات المتغير message حتى لو كان يحتوي على مليون حرف! تجميل الطباعة إذا استوردت الوحدة pprint في برامجك، فيمكنك الوصول إلى الدالتين pprint()‎ و pformat()‎ التي «تجمل طباعة» pretty print قيم القواميس، ستستفيد من هذه الدوال إن أردت عرض قيم القواميس عرضًا أجمل من طريقة عرض الدالة print()‎، لنعدل المثال السابق: import pprint message = 'It was a bright cold day in April, and the clocks were striking thirteen.' count = {} for character in message: count.setdefault(character, 0) count[character] = count[character] + 1 pprint.pprint(count) سيظهر لنا الناتج الجميل الآتي: {' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, --snip-- 't': 6, 'w': 2, 'y': 1} سنستفيد فعليًا من الدالة pprint.pprint()‎ عندما يحتوي القاموس على قوائم أو قواميس متشعبة داخله nested. إذا أردت الحصول على قيمة النص المجمّل بدلًا من طباعته على الشاشة مباشرةً، فاستدعِ الدالة pprint.pformat()‎. السطران الآتيان متكافئان تمامًا: pprint.pprint(someDictionaryValue) print(pprint.pformat(someDictionaryValue)) استخدام بنى المعطيات لنمذجة عناصر حقيقية كان بإمكاننا لعب الشطرنج مع شخص آخر عن بعد قليل ظهور الإنترنت، فكان يعد كل لاعب رقعة الشطرنج في منزله، ثم يبادلان الرسائل البريدية يصف كل منهما خطوته، ولكي يستطيعوا فعل ذلك كان لاعبو الشطرنج بحاجة إلى وصف حالة رقعة الشطرنج والحركات التي يجريها وصفًا لا لبس فيه. تمثل الفراغات في رقعة الشطرنج في التأشير الجبري Algebraic chess notation بإحداثيات تتألف من حرف ورقم كما في الشكل 5-1. الشكل 5-1: إحداثيات رقعة الشطرنج في التأشير الجبري تُعرّف قطع الشطرنج بالأحرف: K للملك king، و Q للملكة queen (يسميها البعض بالوزير)، و R للقلعة rook، و B للفيل bishop، و N للحصان knight. أما الجنود فلا رمز لهم. يكون وصف الحركات متألفًا من الحرف الذي يمثل القطعة، وإحداثيات الوجهة. وزوج من تلك الحركات يصف ما يحدث في دورٍ واحد (بفرض أن صاحب اللون الأبيض يبدأ أولًا)؛ فمثلًا التأشير 2‎. Nf3 Nc6 يعني أن الأبيض حرك الحصان إلى f3 والأسود حرك الحصان إلى c6 في الدور الثاني من اللعبة. هنالك المزيد من القواعد للتأشير الجبري للشطرنج، لكن الفكرة التي أحاول إيصالها هي أننا نستطيع وصف لعبة الشطرنج دون الحاجة إلى أن نكون أمام رقعة، ويمكن أن يكون خصمك في الطرف الثاني من الكوكب، وإذا كانت لديك ذاكرة ومخيلة جيدة فلا تحتاج إلى رقعة شطرنج حقيقية من الأساس: فيمكنك أن تقرأ الحركات من الرسائل البريدية وتحدِّث الرقعة الموجودة في مخيلتك! تمتلك الحواسيب ذواكر رائعة، فيمكن أن يخزن الحاسوب مليارات السلاسل النصية من الشكل ‎'2. Nf3 Nc6'‎، وبهذا يمكن أن تلعب الحواسيب الشطرنج دون لوحة حقيقية؛ فما تفعله الحواسيب هو نمذجة البيانات لتمثيل رقعة الشطرنج، ويمكنك كتابة شيفرة تفعل ذلك بنفسك. هنا تلعب القوائم والقواميس دورها، فمثلًا القاموس ‎{'1h': 'bking', '6c': 'wqueen', '2g': 'bbishop', '5h': 'bqueen', '3e': 'wking'}‎ يمثل الرقعة في الشكل 5-2: الشكل 5-2: رقعة شطرنج منمذجة وفق قيمة قاموس لكن لمثالنا القادمة سنستعمل لعبة أسهل وأبسط من الشطرنج وهي لعبة إكس-أو. لعبة إكس-أو لعبة إكس-أو (تسمى بالإنكليزية tic-tac-toe) تشبه رمز # كبير فيه 9 خانات يمكن أن تكون قيمها X أو O أو أن تكون فارغة. لتمثيل هذه الرقعة بقاموس، فيجب أن نسند لكل خانة زوجًا من المفتاح-القيمة كما في الشكل 5-3. الشكل 5-3: خانات رقعة إكس-أو مع المفاتيح الموافقة لها يمكنك استخدام القيم النصية لتمثيل ما هو موجود في كل خانة في الرقعة: 'x' أو 'o' أو ' ' (فراغ)، وبالتالي نحتاج إلى تسع سلاسل نصية، يمكنك استخدام قاموس من القيم لهذا الأمر، فالقيمة النصية المرتبطة مع المفتاح 'top-R' تمثل القيمة في الركن العلوي الأيمن، والسلسل النصية المرتبطة مع المفتاح 'low-L' تمثل الركن السفلي الأيسر، والسلسلة النصية المرتبطة مع المفتاح 'mid-m' تمثل المنتصف، وهلم جرًا للبقية. هذا القاموس هو بنية معطيات تمثل رقعة لعبة إكس-أو، ولنخزن هذا القاموس في متغير باسم theBoard، ولنحفظ الشيفرة الآتية في ملف باسم ticTacToe.py: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} بنية المعطيات المخزنة في المتغير theBoard تمثل رقعة إكس-أو الموضحة في الشكل 5-4. الشكل 5-4: لوحة إكس-أو فارغة ولما كانت قيمة كل مفتاح في القاموس theBoard هي فراغ واحد فيمثل ذاك القاموس رقعةً فارغةً تمامًا. وإذا بدأت اللاعب X واختار الخانة في المنتصف تمامًا فسيصبح القاموس الذي يمثل الرقعة على الشكل: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} أصبحت بنية المعطيات theBoard تمثل الرقعة الموضحة في الشكل 5-5. الشكل 5-5: الحركة الأولى وتكون رقعة ربح فيها اللاعب O بوضع الشكل O في الصف العلوي كما يلي: theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} وهي ممثلة في الشكل 5-6. الشكل 5-6: ربح اللاعب O وبالتأكيد لا يستطيع أن يرى اللاعب إلا ما يطبع على الشاشة أمامه، ولا يعرف محتويات المتغيرات، فلننشئ دالةً تطبع القاموس الذي يحتوي على الرقعة على الشاشة. أضف ما يلي إلى ملف ticTacToe.py: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) printBoard(theBoard) حينما تشغل هذا البرنامج فستطبع رقعة إكس-أو فارغة: | | -+-+- | | -+-+- | | يمكن أن تتوالى الدالة printBoard()‎ أي بنية معطيات تمثل رقعة إكس-أو تمررها إليها، جرب تغيير الشيفرة إلى ما يلي: theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) printBoard(theBoard) ستعرض الرقعة الآتية على الشاشة: O|O|O -+-+- X|X| -+-+- | |X ولأنك أنشأت بنية معطيات تمثل لوحة إكس-أو وكتبت الشيفرة في printBoard()‎ التي تفسر بنية المعطيات وتظهر الرقعة، فأنت كتبت برنامجًا «ينمذج» models رقعة إكس-أو. كان بإمكانك تنظيم البيانات في بنية المعطيات بطريقة مختلفة، فمثلًا يمكنك استخدام المفتاح 'TOP-LEFT' بدلًا من 'top-L'، لكن طالما كانت شيفرتك تعمل مع بنية المعطيات التي لديك، فأنت كتبت عملية النمذجة بشكل صحيح. فمثلًا تتوقع الدالة printBoard()‎ أن بنية المعطيات التي تمثل الرقعة هي قاموس فيه مفاتيح لجميع الخانات التسع، لكن إن كان في قاموسك مفتاحٌ ناقص وليكن 'mid-L' فلن يعمل برنامجك: O|O|O -+-+- Traceback (most recent call last): File "ticTacToe.py", line 10, in <module> printBoard(theBoard) File "ticTacToe.py", line 6, in printBoard print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) KeyError: 'mid-L' لنضف الآن الشيفرة التي تسمح للاعبين بإدخال حركاتهم. لنعدل برنامجنا ليبدو كما يلي: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) turn = 'X' for i in range(9): ➊ printBoard(theBoard) print('Turn for ' + turn + '. Move on which space?') ➋ move = input() ➌ theBoard[move] = turn ➍ if turn == 'X': turn = 'O' else: turn = 'X' printBoard(theBoard) الشيفرة الجديدة تطبع اللوحة في بداية كل دور ➊ ثم تطلب المدخلات من اللاعب الحالي ➋ ثم تحدث الرقعة وفقًا لذلك ➌ ثم تبدل اللاعب الحالي ➍ قبل الانتقال إلى الدور القادم. | | -+-+- | | -+-+- | | Turn for X. Move on which space? mid-M | | -+-+- |X| -+-+- | | --snip-- O|O|X -+-+- X|X|O -+-+- O| |X Turn for X. Move on which space? low-M O|O|X -+-+- X|X|O -+-+- O|X|X صحيحٌ أن البرنامج ليس لعبة إكس-أو كاملة، فلن يتحقق إن ربح أحد اللاعبين مثلًا؛ لكنه كافٍ لمعرفة كيفية استخدام بنى المعطيات في برامج حقيقية. القواميس والقوائم المتشعبة نمذجة لوحة إكس-أو هو أمر سهل: تحتاج اللوحة إلى قاموس فيه 9 مفاتيح تمثل خاناتها. لكن إن أردت نموذج أمور أكثر تعقيدًا فستجد أنك تحتاج إلى القواميس والقوائم التي تحتوي على قواميس وقوائم أخرى داخلها. تناسب القوائم تخزين سلسلة مرتبة من القيم، بينما تفيد القواميس بتخزين قواميس التي ترتبط فيها المفاتيح مع القيم. هذا مثال يستعمل قاموسًا يحتوي على قواميس داخله فيها الأغراض التي أتى بها الضيوف إلى الرحلة. يمكن أن تقرأ الدالة totalBrought()‎ بنية المعطيات وتحسب العدد الكلي للعناصر المجلوبة من كل الضيوف: allGuests = {'Alice': {'apples': 5, 'pretzels': 12}, 'Bob': {'steak sandwiches': 3, 'apples': 2}, 'Carol': {'cups': 3, 'apple pies': 1}} def totalBrought(guests, item): numBrought = 0 ➊ for k, v in guests.items(): ➋ numBrought = numBrought + v.get(item, 0) return numBrought print('Number of things being brought:') print(' - Apples ' + str(totalBrought(allGuests, 'apples'))) print(' - Cups ' + str(totalBrought(allGuests, 'cups'))) print(' - Cakes ' + str(totalBrought(allGuests, 'cakes'))) print(' - Steak Sandwiches ' + str(totalBrought(allGuests, 'steak sandwiches'))) print(' - Apple Pies ' + str(totalBrought(allGuests, 'apple pies'))) داخل الدالة totalBrought()‎ هنالك حلقة for تدور على أزواج مفتاح-قيمة في المتغير guests ➊، وسنسند داخل الحلقة اسم كل ضيف إلى المتغير k، وسنسند القاموس الذي يحتوي على قائمة الأغراض التي سيجلبها معه إلى الرحلة إلى المتغير v. إذا كان أحد المعامل item موجودة في القاموس، فستضاف قيمته (كمية الأغراض المجلوبة) إلى المتغير numBrought ➋، لكن إذا لم يكن المفتاح موجودًا فيسعيد التابع get()‎ القيمة 0 لإضافتها إلى numBrought. سيكون ناتج تنفيذ البرنامج كما يلي: Number of things being brought: - Apples 7 - Cups 3 - Cakes 0 - Steak Sandwiches 3 - Apple Pies 1 قد يبدو لك أن حالة الاستخدام السابقة بسيطة ولا حاجة إلى نمذجتها، لكن فكر أن الدالة totalBrought()‎ يمكن أن تتعامل بسهولة مع قاموس يحتوي على آلاف الضيوف، وكل ضيف يجلب آلاف العناصر، وتنظيم هذه المعلومات في بنية معطيات واضحة ووجود الدالة totalBrought()‎ سيوفر عليك وقتًا كثيرًا. يمكنك أن تنمذج العناصر في بنى المعطيات بالطريقة التي تراها مناسبة، لطالما كانت بقية الشيفرة في برنامج قادرةً على التعامل مع بنية المعطيات المنشأة. حينما تبدأ البرمجة فلا تقلق أن تنمذج البيانات بالطريقة «الصحيحة»، فكلما ازدادت خبرتك أصبحت تخطر ببالك طرائق أكثر فاعلية وكفاءة للنمذجة، لكن أهم ما في الأمر أن تعمل بنية المعطيات مع احتياجات برنامجك. الخلاصة يمكن أن تحتوي القوائم والقواميس على عدّة قيم، بما فيها قوائم وقواميس أخرى. القواميس مفيدة لأنك تستطيع ربط مفتاح معين مع قيمة، على عكس القوائم التي هي سلسلة من القيم المرتبة. يمكن الوصول إلى القيم داخل القاموس عبر استخدام الأقواس المربعة كما في القوائم، لكن بدلًا من استخدام فهرس رقمي فيمكن أن تكون المفاتيح في القواميس من مختلف القيم سواءً كانت أعدادًا صحيحةً أو أعدادًا عشريةً أو سلاسل نصية أو صفوف tuples. يمكنك تمثيل الكائنات الحقيقية بتنظيم قيم البرنامج في بنى المعطيات، ورأينا مثال ذلك عمليًا على لعبة إكس-أو. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. مدقق لقواميس الشطرنج استعملنا في هذا المقال قيمةً مثل ‎{'1h': 'bking', '6c': 'wqueen', '2g': 'bbishop', '5h': 'bqueen', '3e': 'wking'}‎ لتمثيل رقعة الشطرنج. اكتب دالةً باسم isValidChessBoard()‎ التي تقبل وسيطًا هو قاموس وتعيد القيمة True أو False اعتمادًا إن كان القاموس صالحًا لتمثيل رقعة الشطرنج. تحتوي الرقعة السليمة على ملك أسود واحد وملك أبيض واحد. ويمكن لأيٍ من اللاعبين امتلاك 16 قطعة كحد أقصى، و 8 جنود كحد أقصى، ويجب أن تكون جميع القطع في المجال بين 1a و 8h، أي لا يمكن أن تكون القطعة في المكان 9z. يجب أن تبدأ أسماء القطع بحرف w أو b لتمثيل اللونين الأبيض أو الأسود، متبوعًا بإحدى الكلمات pawn أو knight أو bishop أو rook أو queen أو king. قائمة الأدوات في لعبة نحن نعمل على لعبة فيها قائمة أدوات يمكن أن يمتلكها اللاعب، والتي سننمذجها باستخدام بنية معطيات تتألف من قاموس تكون فيه مفاتيحه هي سلاسل نصية تصف القيمة الموجودة في قائمة الأدوات، وقيمتها هي عدد نصي يمثل عدد الأدوات التي يمتلكها اللاعب مثلًا القاموس ‎{'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}‎ تعني أن اللاعب يملك حبلًا واحدًا، و 6 شعلات، و 42 قطعة ذهبية …إلخ. اكتب دالةً باسم displayInventory()‎ التي تأخذ أي قاموس يمثل قائمة أدوات ويعرضه بالشكل: Inventory: 12 arrow 42 gold coin 1 rope 6 torch 1 dagger Total number of items: 62 تلميح: يمكنك استخدام حلقة for للمرور على جميع المفاتيح في القاموس: # inventory.py stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} def displayInventory(inventory): print("Inventory:") item_total = 0 for k, v in inventory.items(): # أكمل الشيفرة هنا print("Total number of items: " + str(item_total)) displayInventory(stuff) دالة تحويل قائمة إلى قاموس في لعبة لنفترض أن لاعبنا في اللعبة السابقة قد حصل على غنيمة ممثلة في القائمة الآتية: playerLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] اكتب دالةً باسم addToInventory(inventory, addedItems)‎ حيث أن المعامل inventory هو قاموس يمثل قائمة أدوات اللاعب (كما في المثال السابق) والمعامل addedItems يشبه المتغير playerLoot. يجب أن تعيد الدالة addToInventory()‎ قاموسًا يمثل قائمة الأدوات المحدثة. لاحظ أن القائمة الموجودة في addedItems قد تحتوي على عدة نسخ من نفس الأداة. يفترض أن تكون شيفرتك شبيهة بما يلي: def addToInventory(inventory, addedItems): # اكتب الدالة هنا inv = {'gold coin': 42, 'rope': 1} dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] inv = addToInventory(inv, dragonLoot) displayInventory(inv) يجب أن يظهر البرنامج السابق (مع الدالة displayInventory()‎ من المثال السابق) الناتج الآتي: Inventory: 45 gold coin 1 rope 1 ruby 1 dagger Total number of items: 48 ترجمة -بتصرف- للفصل Dictionaries And Structuring DATA من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: القوائم Lists في لغة بايثون أنواع البيانات والعمليات الأساسية في لغة بايثون تعلم لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
  23. في البداية بدون دراية برمجية لن تتمكن من التعديل والتطوير، يجب الإلمام بلغة PHP لفعل ذلك بجانب لغات الويب HTML, CSS, JS. أولاً، عليك تثبيت Smarty: composer require smarty/smarty ثم قم بتهيئة Smarty في ملف PHP الرئيسي، ولنفترض أن ملفك الرئيسي هو index.php. <?php require_once 'vendor/autoload.php'; $smarty = new Smarty(); $smarty->setTemplateDir(__DIR__ . '/template'); $smarty->setCompileDir(__DIR__ . '/template_c'); $smarty->setCacheDir(__DIR__ . '/cache'); $smarty->setConfigDir(__DIR__ . '/configs'); ثم لنقم بكتابة كود PHP لقراءة القوالب المتاحة في مجلد القوالب. <?php // تابع للخطوة 2 $templateDir = __DIR__ . '/template'; $templateFolders = array_filter(glob($templateDir . '/*'), 'is_dir'); $templates = []; foreach ($templateFolders as $folder) { $templateName = basename($folder); $templates[] = $templateName; } $smarty->assign('templates', $templates); $smarty->display('admin.tpl'); بعد ذلك إنشاء ملف قالب Smarty يسمى admin.tpl لعرض القوالب في جدول. <!-- admin.tpl --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Admin Panel - Templates</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> </head> <body> <div class="container"> <h1>Available Templates</h1> <table class="table table-bordered"> <thead> <tr> <th>Template Name</th> <th>Action</th> </tr> </thead> <tbody> {foreach from=$templates item=template} <tr> <td>{$template}</td> <td> <form method="post" action="install_template.php"> <input type="hidden" name="template_name" value="{$template}"> <button type="submit" class="btn btn-primary">Install Template</button> </form> </td> </tr> {/foreach} </tbody> </table> </div> </body> </html> ثم إنشاء ملف PHP يسمى install_template.php لتنفيذ عملية تركيب القالب. <?php if ($_SERVER['REQUEST_METHOD'] === 'POST') { $templateName = $_POST['template_name']; // نفذ عملية تركيب القالب هنا // تستطيع نسخ الملفات إلى مجلد آخر أو تحديث إعدادات قاعدة البيانات echo "Template '{$templateName}' has been installed!"; } else { echo "Invalid request."; } بذلك ستتمكن من عرض القوالب المتاحة في مجلد template على شكل جدول في صفحة لوحة التحكم، مع زر لتركيب كل قالب، وعند النقر على زر "تركيب القالب"، سيتم إرسال طلب POST إلى install_template.php لتنفيذ عملية التركيب، تستطيع تعديل عملية التركيب حسب متطلباتك.
  24. في البداية ستحتاج بالطبع إلى إنشاء حساب مطور على Google Cloud Vision API أو Amazon Rekognition. ثم ضبط إعدادات متغيرات البيئة في المشروع في ملف .env وأهما مفاتيح الـ API، عليك إضافة إعدادات Google Cloud Vision API أو Amazon Rekognition فقط، وإليك كلاهما: GOOGLE_CLOUD_VISION_API_KEY=google_api_key AWS_ACCESS_KEY_ID=aws_access_key AWS_SECRET_ACCESS_KEY=aws_secret_key AWS_REGION=aws_region ثم أنشئ نموذج وقاعدة بيانات لتخزين بيانات الصور: php artisan make:model Image -m في ملف التهجير: Schema::create('images', function (Blueprint $table) { $table->id(); $table->string('image_path'); $table->json('features'); // لتخزين بيانات الميزات (features) $table->timestamps(); }); أيضًا اعتمد على مكتبة مثل intervention/image لمعالجة الصور وضغط حجمها قبل إرسالها. ثم استخدام مكتبة google/cloud-vision أو aws/aws-sdk-php للتواصل مع المنصة. وإليك مثال لكلاهما اختر ما تريد: // ImageController.php use Illuminate\Http\Request; use Intervention\Image\Facades\Image; use Google\Cloud\Vision\V1\ImageAnnotatorClient; // or Aws\Rekognition\RekognitionClient; public function uploadImage(Request $request) { $image = $request->file('image'); // Process and resize the image $image->resize(300, 300); // Analyze the image using Google Cloud Vision API or Amazon Rekognition $visionClient = new ImageAnnotatorClient(); // or $rekognitionClient = new RekognitionClient(); $response = $visionClient->annotateImage(file_get_contents($image->getPathname()), ['LABEL_DETECTION']); // or $rekognitionClient->detectLabels(['Image' => ['Bytes' => file_get_contents($image->getPathname())]]); $features = $response->getLabelAnnotations(); // or $response->getLabels(); // Store the features in the database $imageFeatures = new ImageFeatures(); $imageFeatures->features = json_encode($features); $imageFeatures->save(); return response()->json(['message' => 'Image uploaded and analyzed successfully']); } بعد ذلك اعتمد على مكتبة doctrine/dbal للتفاعل مع قاعدة البيانات، حيث تتوفر cosine similarity أو Euclidean distance لقياس التشابه بين ميزات الصور. use App\ImageFeatures; use Illuminate\Http\Request; public function searchByImage(Request $request) { $searchImage = $request->file('image'); $searchFeatures = $this->analyzeImage($searchImage); // Query the database to find similar images $similarImages = ImageFeatures::all()->filter(function ($imageFeature) use ($searchFeatures) { $storedFeatures = json_decode($imageFeature->features, true); return $this->cosineSimilarity($searchFeatures, $storedFeatures) > 0.5; }); return response()->json(['similar_images' => $similarImages]); } private function analyzeImage($image) { // هنا يجب أن تضع الكود الخاص بتحليل الصورة والحصول على الميزات // سأضع مثال بسيط لتحليل الصورة return [/* array of features */]; } private function cosineSimilarity($features1, $features2) { $dotProduct = 0; $magnitude1 = 0; $magnitude2 = 0; foreach ($features1 as $index => $feature1) { $dotProduct += $feature1 * $features2[$index]; $magnitude1 += $feature1 * $feature1; $magnitude2 += $features2[$index] * $features2[$index]; } $magnitude1 = sqrt($magnitude1); $magnitude2 = sqrt($magnitude2); if ($magnitude1 * $magnitude2 == 0) { return 0; } return $dotProduct / ($magnitude1 * $magnitude2); } لتحسين الأداء، باستطاعتك استخدام تقنيات مثل الفهرسة المكانية أو مكتبات متخصصة في البحث عن الصور مثل Elasticsearch.
  25. افتحي مشروع Unity الذي تريدين تصديره كمكتبة، وانتقلي إلى File > Build Settings، وفي نافذة Build Settings، اختاري المنصة المستهدفة (Android أو iOS). وفي حال تستهدفي Android، اختاري Android من القائمة. ثم اضغطي على Player Settings في الزاوية السفلية اليسرى من نافذة Build Settings، وضبط الإعدادات اللازمة مثل اسم الحزمة (Package Name) وغيرها. وفي نافذة Build Settings، تأكدي من اختيار المنصة الصحيحة ثم اضغطي على Export في الزاوية اليمنى السفلى. وعندما تظهر نافذة التصدير، عليك تحديد خيار Export as a Library (أو خيار مشابه يعبر عن تصدير كمكتبة)، ثم اختيار مكان حفظ المجلد الذي سيحتوي على مكتبة Unity والضغط على Export لبدء عملية التصدير. بعد اكتمال التصدير، ستجدي مجلد يحتوي على مكتبة Unity في المسار الذي اخترتيه، باستطاعتك الآن دمج تلك المكتبة في مشروع Flutter كما هو موضح في الخطوات التي شرحها سابقًا.
  26. يمكنك ربط Python بلغة JavaScript .و هناك عدة طرق لتحقيق ذلك، ولكن الطريقة الأكثر شيوعًا هي استخدام واجهات برمجة التطبيقات (APIs). يمكنك إنشاء واجهة برمجة تطبيقات (API) باستخدام إطار عمل مثل Flask أو Django في Python كما بالعليق السابق . حيث ستقوم هذه الواجهة بتعريف نقاط نهاية تتيح لك الوصول إلى الدوال التي كتبتها في Python. بعد ذلك، يمكنك استدعاء هذه النقاط النهائية من جانب العميل (Client-side) باستخدام JavaScript وإجراء طلبات HTTP مثل GET أو POST لتمرير البيانات والحصول على النتائج. على سبيل المثال، إذا كتبت دالة في Python لحساب المتوسط الحسابي لمجموعة من الأرقام، يمكنك إنشاء نقطة نهائية في واجهة برمجة التطبيقات تسمح لك بإرسال الأرقام كبيانات JSON وتستدعي دالة Python لحساب المتوسط وإرجاع النتيجة. هناك أيضًا طرق أخرى مثل استخدام WebAssembly أو إطارات عمل مثل Transcrypt أو Brython التي تتيح لك كتابة كود Python يمكن تشغيله مباشرة في المتصفح. ومع ذلك، فإن استخدام واجهات برمجة التطبيقات هو الأكثر شيوعًا وفعالية.
  27. ملحوظة: يرجى كتابة عنوان واضح حتى تعم الفائدة على الجميع. بخصوص استخدام لغة البايثون من خلال الجافاسكريبت، فيمكن ذلك عن طريق إنشاء واجهات برمجية التطبيقات APIs ثم استدعاؤها من خلال الجافاسكريبت، وفي الحقيقة هذا يتم مع كل لغات البرمجة وليس مع البايثون فقط. ووجهة برمجة التطيبقات API هي عبارة عن وظائف يتم كتابتها بلغة برمجة معينة على الخادم، ثم تهيئتها لتكون متاحة لاستدعائها بواسطة أي لغة أخرى. بالنسبة لبايثون، فإن أطر عمل مثل الفلاسك والجانجو Flask & Django يوفران بيئة تطوير جاهزة تُمكن المبرمج من البدء في كتابة الواجهات البرمجية بكل سهولة ويسر.
  1. عرض المزيد
×
×
  • أضف...