Skip to content

Work - #1

Open
SalehGNUTUX wants to merge 32 commits into
mainfrom
work
Open

Work#1
SalehGNUTUX wants to merge 32 commits into
mainfrom
work

Conversation

@SalehGNUTUX

Copy link
Copy Markdown
Owner

No description provided.

كان مجلَّد المصدر يحمل رقم الإصدار (GMD-26.5) فيتقادم مع كلّ نشر،
وصار اسمه app/ — اسمٌ ثابت لا يحتاج تحديثاً.

وأُخرِجت من الشجرة ملفّات لا محلّ لها في مستودع مصدر:
- أدوات البناء linuxdeploy و appimagetool و AppImage قديمة (28 م.ب مجتمعة)،
  وهي تُنزَّل وقت البناء ولا تُودَع.
- مُخرَجات البناء في app/dist/ التي كانت تكرّر الخطوط ثلاث مرّات.
- ملفّ test الفارغ، بقيّةُ تجربة.

أُبقيَ gmd-icon.png في مكانه داخل «GMD APPIAMGE BIULD» عمداً: سكربت
gmd v1.92 المثبَّت عند المستخدمين يجلب أيقونته من ذلك المسار بالضبط،
وحذفُه كان يكسر تثبيت الأيقونة في كلّ نسخة قائمة.

وأُضيف .gitignore يمنع عودة node_modules ومُخرَجات البناء.
كانت كلّ عمليّة تُبنى نصّاً ثمّ تُنفَّذ بـ spawn('bash', ['-c', cmd])،
ويُوضَع الرابط ومسار الملفّ بين علامتَي اقتباس مزدوجتَين. لكنّ الصدفة
تُوسِّع $(...) والعلامات الخلفيّة داخل الاقتباس المزدوج، فرابطٌ ملصوق
فيه $(...) كان يُنفَّذ بصلاحيّات المستخدم. والمسارات كذلك، وهي تأتي من
نافذة الملفّات فقد تحمل محارف خاصّة.

صار عقد run-command يستقبل { jobs: [{ bin, args, outFile? }], title }
بدل { cmd, title }، وتُمرَّر الوُسَطاء مصفوفةً إلى spawn(bin, argv) بلا
صدفة أصلاً، فالرابط والمسار بيانات لا شيفرة. وتسبق «--» كلَّ رابط حتّى
لا يُقرأ رابطٌ يبدأ بشَرطة كأنّه خيار.

شمل التحويل:
- المكوّنات الستّة التي تبني أوامر yt-dlp و ffmpeg
- ffprobe في get-file-info و --dump-json و --flat-playlist
- which في check-tool و check-ffmpeg و detect-package-manager
- curl في install-ytdlp، و yt-dlp -U، و update-desktop-database
- install-tool: قائمة أدوات مسموحة ووُسَطاء صريحة بدل split(' ')

وقُيِّد بروتوكول media:// إلى الملفّات التي اختارها المستخدم فعلاً عبر
نوافذ البرنامج، بعد أن كان يخدم أيّ مسار في نظام الملفّات.

وصار التحويل الدُّفعيّ طابوراً حقيقيّاً: كانت الملفّات تُوصَل بـ && في أمر
واحد فيسقط الباقي كلّه عند فشل ملفّ واحد.
عطبان كانا يظهران للمستخدم بلا سبب ظاهر:

١. كان handleRunCommand يسجّل مستمعَي command-output و command-done في
   كلّ عمليّة، ويُرجِع دالّة تنظيف لا يستدعيها أحد — فكلّ المكوّنات تكتب
   await handleRunCommand(...) وتُهمِل المُرجَع. فالعمليّة رقم N كانت
   تُعالِج كلّ سطر خرج N مرّةً وتفتح N صندوق نتيجة، ويضطرب شريط التقدُّم
   لتضاعُف النصّ المتراكم، ويزداد الأمر سوءاً كلّما طالت الجلسة.
   صارت المستمعات تُسجَّل مرّةً واحدةً في تأثير التركيب، وتُوجَّه عبر
   مرجعَين تُبدّلهما كلُّ عمليّة.

٢. كان النجاح يُقاس بـ !output.match(/error|ERROR|failed|فشل/i)، و yt-dlp
   يطبع ERROR عند محاولة فاشلة ثمّ ينجح بالتالية، و ffmpeg يطبع تحذيرات
   تحوي الكلمة، بل يكفي اسم ملفّ فيه «error». صار الحكم برمز الخروج
   وحده، يُضاف إليه التحقُّق من وجود الملفّ الناتج فعلاً حيث يُعرَّف.

ويعرض صندوق التقدُّم الآن عدّاد «المهمّة / المجموع» في العمليّات الدُّفعيّة.
كان الرقم مكرَّراً في خمسة مواضع — package.json و main.js و index.html
وملفَّي الترجمة — وبثلاث صيغ متضاربة: 26.05.0 في المصدر، و 26.5.0 في
أسماء الحزم المنشورة لأنّ electron-builder يُطبِّع الصفر البادئ، و
GMD-26.05 في الوسم.

صار package.json المرجع الوحيد، يقرأه main.js بـ app.getVersion() ويصل
إلى الواجهة عبر get-app-version. وحُذف مفتاح app.version من ملفَّي
الترجمة لأنّه لم يكن نصّاً يُترجَم أصلاً.

والصيغة الآن semver صحيحة (26.5.1 لا 26.05.1) فلا يعيد أحد تطبيعها،
ويتطابق ما في المصدر مع ما في أسماء الحزم مع الوسم.
يستقبل GMD الآن التحديثات عند صدورها ويُنزّلها ويثبّتها دون أن يغادر
المستخدم البرنامج.

التحقُّق: يسأل واجهةَ إصدارات GitHub ويقارن بـ app.getVersion(). ولأنّ
صيغة الوسم تبدّلت عبر عمر المشروع (GMD-26.05 و GMD_1.92_... و v1.1)
يستخرج المحلّل أوّل رقم منقَّط من الوسم بدل افتراض شكلٍ بعينه.
يجري تحقُّق صامت عند التشغيل، مُقيَّد بمرّة كلّ ستّ ساعات حتّى لا يُستنفَد
حدُّ طلبات GitHub، ويظهر شريط تنبيه في أعلى النافذة عند توفُّر إصدار.

التنزيل: قابل للاستئناف عبر ترويسة Range، فانقطاع الاتصال في منتصف
حزمةٍ حجمها مئة ميغابايت لا يُلزِم البدء من الصفر. ويُتحقَّق من الحجم
مقابل ما تعلنه واجهة GitHub قبل قبول الملفّ، مع شريط تقدُّم وسرعة وإلغاء.

التثبيت: يسلك المسار الذي يناسب طريقة تثبيت النسخة العاملة فعلاً —
تُكتَشَف من متغيّر APPIMAGE ومن dpkg -S و rpm -qf:
- AppImage: تستبدل نفسها باستبدالٍ ذرّيّ ثمّ تعرض إعادة التشغيل
- deb / rpm: تُسلَّم الحزمة لمدير حزم النظام عبر pkexec
- شجرة تطوير أو تثبيت يدويّ: يقول ذلك صراحةً ويعرض الأمر ومكان الملفّ

وكلّ زرّ هنا مسنودٌ بمنطق فعليّ: لا يظهر زرّ التنزيل إن لم توجد حزمة
تطابق طريقة التثبيت، بل يُعرَض رابط صفحة الإصدار بدلاً منه.

وأُضيف خيار قبول الإصدارات التجريبيّة، معطَّلاً افتراضيّاً، وخيار تعطيل
التحقُّق التلقائيّ. الترجمة كاملة في اللغتين (24 مفتاحاً).
كان المستودع بلا اختبار ولا مُدقِّق ولا بناء آليّ، ورفعُ الحزم يدويّاً في
كلّ إصدار.

الاختبارات (npm test):
- test/updater.js يُحمِّل وحدة التحديث خلف وحدة electron مُزيَّفة فيختبر
  منطقها دون إقلاع متصفّح، ويسأل واجهة GitHub الحقيقيّة عمداً: صيغة
  الوسم تبدّلت عبر عمر المشروع، والمحلّل يجب أن يجاري ما هو منشور فعلاً
  لا ما تقوله عيّنة ثابتة.
- test/smoke.js يقود الواجهة الحقيقيّة عبر بروتوكول أدوات المطوّر، فلا
  تدخل شيفرةُ اختبار إلى src أصلاً. يحرس ما أرساه هذا الإصدار: مصدرٌ
  واحدٌ للنسخة، ومستمعاتٌ تُسجَّل مرّةً واحدة، ونجاحٌ يُقاس برمز الخروج
  والملفّ الناتج لا بكلمة في الخرج، ودفعةٌ تقف عند أوّل فشل، و media://
  يرفض مساراً لم يختره المستخدم.
- test/run.sh يرفض العمل إن كان منفذ التنقيح مشغولاً، لأنّ نسخةً قديمةً
  عالقةً كانت تجعل المِرقاب يتّصل ببناءِ الأمس ويُبلِّغ عن أعطاب لا وجود
  لها في الشيفرة الحاليّة.

البناء (.github/workflows/build.yml):
- يبني AppImage و deb و rpm لمعماريّتَي x64 و arm64 عند دفع وسم، ويرفعها
  إلى الإصدار مع مجاميع sha256 وملاحظات مستخرَجة من CHANGELOG.
  و arm64 كانت معلَنةً في package.json منذ 26.05 ولم تُبنَ قطّ.
- وقبل البناء: فحص وحدة التحديث، وتطابق مفاتيح الترجمة، وخلوّ الملفّات
  من الأرقام المشرقيّة، ومنعُ عودة أوامر الصدفة النصّيّة.
- كلّ إصدار يُنشَر تجريبيّاً حتّى يُعلَن استقراره يدويّاً.
- CHANGELOG.md بالعربيّة الفصحى، يوثّق 26.5.1 و 26.05.0 و 1.92. لم يكن
  في المستودع ما يقول ما تغيّر بين 1.92 و 26.05.
- README: تحديث الإصدار والمسارات إلى app/، وقسمٌ جديد للتحديث الذاتيّ،
  وأمر npm test.
- app/CLAUDE.md: توثيق عقد run-command الجديد صراحةً — لا بناء أوامر
  نصّاً ولا bash -c، وكلّ قيمة من المستخدم في خانتها، والنجاح برمز
  الخروج لا بالبحث عن كلمة «error» في الخرج — حتّى لا يعود العطب من
  حيث لا يُحتسَب.
المصدر المنشور في GMD-26.5 لم يكن يُبنى أصلاً: مكوّن UrlInput.jsx
تستورده خمسة مكوّنات — تنزيل فيديو وتنزيل صوت وتنزيل وتحويل وقصّ
وسائط وخيارات إضافيّة — ولم يُرفَع قطّ إلى المستودع. من يستنسخ
المستودع وينفّذ npm run build يفشل عنده البناء بخطأ استيراد.

سببه أنّ الرفع كان يدويّاً عبر واجهة المتصفّح ملفّاً ملفّاً، وهو ما
يُسقِط ملفّاً بلا أثر. سير عمل البناء الآليّ المُضاف قبل قليل يمنع
تكرار هذا: أيّ نقص في المصدر يُسقِط البناء قبل النشر.

ومعه نسختان أحدث كانتا محلّيّتَين ولم تصلا:
- MediaInfo.jsx يستعمل UrlInput بدل حقل مكرَّر.
- after-install.sh يبحث عن الأيقونة في مسارات أوسع، منها مسار
  app.asar.unpacked الذي تحتاجه حزم deb و rpm.
الإصدار القادم — لسطح المكتب والهاتف معاً — تحت سلسلة 26.09.

ويُكتَب الرقم في semver بلا صفر بادئ: 26.9.0 لا 26.09.0. هذا ليس
اختصاراً بل ضرورة: electron-builder يحذف الصفر البادئ من أسماء الحزم،
وهو ما جعل 26.05.0 في المصدر تصير 26.5.0 في الحزم بينما الوسم
GMD-26.05 — ثلاث صيغ للرقم الواحد. فالسلسلة 26.09 والرقم 26.9.0
والوسم v26.9.0 متطابقة كلّها.

وصارت الاختبارات تقرأ الرقم من package.json بدل تثبيته في خمسة مواضع،
فرفع الإصدار لم يعد يستلزم تعديلها.
req.destroy() في Node يُطلِق حدث close ولا يُطلِق error. وكان الحسم
معلَّقاً على معالج error وحده، فالضغط على «إلغاء» أثناء تنزيل تحديثٍ
يترك وعد updateDownload بلا حسم: نافذة التقدُّم تبقى مفتوحةً إلى الأبد
والبرنامج لا يستجيب لعمليّة تحديثٍ جديدة.

صار الحسم يجري على close أيضاً حين يكون الإلغاء مطلوباً، ويُغلَق مجرى
الكتابة صراحةً عند الإلغاء ليُفرَغ الملفّ الجزئيّ إلى القرص بحجمٍ متّسق
يصلح للاستئناف.

وأُضيف test/download.js — اختياريّ عبر npm run test:download لأنّه يُنزّل
الحزمة المنشورة كاملةً (نحو 105 م.ب). هو الذي كشف العطب أصلاً، ويغطّي
المسار الذي لا تنفع فيه المحاكاة: تنزيلٌ حقيقيّ، فقطعٌ للاتّصال، فاستئنافٌ
من الملفّ الجزئيّ، فتحقُّقٌ من الحجم، فاستبدالٌ ذرّيّ للحزمة مع التحقُّق من
صلاحيّات التنفيذ وعدم ترك ملفّ .new معلَّق.
كانت `app/node_modules` وصلةً رمزيّةً مودَعةً في المستودع تشير إلى
مسار محلّيّ مطلق على جهاز المطوِّر، فتكسر أيّ استنساخ للمستودع.
أُزيلت من التتبُّع مع بقائها على القرص.

وصُحِّح النمط في `.gitignore` من `node_modules/` إلى `node_modules`،
لأنّ النمط بشَرطة مائلة ختاميّة يطابق المجلَّدات فقط ولا يمنع إيداع
وصلةٍ رمزيّة بالاسم نفسه.
إصداراتُ المشروع كلُّها — من أوّلها حتى GMD-26.05 — منسوبةٌ إلى
SalehGNUTUX. ولو وُسِم إصدارٌ الآن لنشره حسابُ github-actions[bot]،
لأنّ `action-gh-release` ينسب الإصدارَ إلى مالكِ الرمز، والافتراضيُّ
هو `GITHUB_TOKEN` أي حسابُ الآليّة. والنسبةُ تظهر للزائر في صفحة
الإصدارات ولا تتغيّر بعد النشر.

صار النشرُ برمزٍ شخصيٍّ في السرّ `RELEASE_TOKEN`، وأُضيفت خطوةٌ تُسقط
سيرَ العمل إن غاب الرمز: السقوطُ الصريحُ أهونُ من نشرٍ صامتٍ باسمٍ
خاطئ لا سبيل إلى تصحيحه.
بلا `artifactName` تختلف صيغة الاسم بين AppImage وdeb وrpm وبين
المعماريّات، فيتعذّر على الموقع وعلى وحدة التحديث الذاتيّ توقُّع اسم
الحزمة. صارت كلّها `GMD-${version}-${arch}.${ext}`.

يوازي ما ضُبط في المصدر المنشور على فرع main.
كان اختيار الصيغة `bv*[height<=720]+ba/best[height<=720]`، وهو صحيح في
الفيديو الأفقيّ حيث الارتفاع هو الضلع الأصغر، وخاطئ في العموديّ حيث
ينقلب الأمر: مقطع «720p» عموديّاً مقاسه 720×1280، فارتفاعه 1280 يتجاوز
كلّ سقف يطلبه المستخدم — حتى سقف 1080 — فلا تطابق الشرطَ صيغة واحدة
ويموت التنزيل بـ`Requested format is not available`.

لم يُبلَّغ عن العطب في نسخة الحاسوب قطُّ لأنّ التجارب جرت على مقاطع
أفقيّة. وقد تُحقِّق من وقوعه فيها بأحدث yt-dlp على مقطع عموديّ فعليّ:
الترشيح القديم يفشل، والجديد ينزّل 720×1280 لطلب 720p و1080×1920 لطلب
1080p، ولا يغيّر شيئاً في الأفقيّ (854×480 و1280×720 و1920×1080).

صار الترشيح `-S res:N`. وحقل `res` عند yt-dlp هو الضلع الأصغر نفسه
فيصحّ في الاتّجاهين، وهو ترتيب لا شرط قاطع: يقدّم الأقرب عند السقف أو
دونه، فإن لم يكن في المصدر إلّا ما فوقه اختار أصغر ما فوقه بدل أن يفشل.
و`[height>0]` يُبعد مجهول المقاس — `sd`/`hd` في فيسبوك — عن التفضيل
الأوّل لأنّ المجهول يتصدّر ترتيب `res` زوراً، ويُبقيه احتياطاً أخيراً.

وكانت قائمة الجودات مكرّرة حرفيّاً في شاشتَي التنزيل والتنزيل مع
التحويل، فوُحِّدت في `src/quality.js` مع `videoFormatArgs` تُعيد مصفوفة
وُسَطاء لا نصّاً، حفاظاً على قاعدة «لا صدفة ولا حقن أوامر». وشمل
الإصلاح `gmd` و`gmd-gui`، ودالّة الجودة في الثاني تُنادى داخل `$( )`
فأيّ متغيّر عامّ فيها يضيع في الصدفة الفرعيّة — فصارت تُعيد الصيغة
والترتيب مفصولين بـ«|» ويُفصَلان عند الاستدعاء.
عطبان كلّفانا وقتاً في إصدار 26.9.0، وكلاهما يتكرّر بلا أثر في الشيفرة:
فشل rpm محلّيّاً برسالة لا تدلّ على سببها (fpm 1.9.3 مقابل rpm 4.20)،
وترجمة ${arch} بعرف كلّ صيغة فتختلف أسماء الحزم عن قالبها.
كان الملفّ إنجليزيّاً كلّه، ثمّ أُضيف إليه قسم المزالق بالعربيّة فصار
مختلطاً. وقاعدة المشروع أنّ الملاحظات داخل المستودع بالعربيّة الفصحى.

عُرّب النصّ كلّه وبقيت المعرّفات والمسارات وأسماء الأوامر بالإنجليزيّة
كما هي في الشيفرة، فترجمتها تُفسد البحث عنها. وأُضيف إلى جدول أسماء
الحزم عمود `--arm64` بعد أن أظهر البناء الفعليّ أنّ الصيغ الثلاث تختلف
في المعماريّتين ولا تطّرد إلّا deb. وذُكر في قسم الترجمة أنّ الآليّة
تفحص تطابق مفاتيح ar.json و en.json وتُفشل البناء إن اختلفا.
نقل ما استُحدث في نسخة الهاتف إلى الحاسوب، مع احترام خصوصيّة كلّ منهما:
نُقل ما يخدم الحاسوب وتُرك ما أُنشئ لعلّة أندرويد وحدها. فالمعرض الداخليّ
لم يُنقَل لأنّ سببه أنّ أندرويد يحبس الملفّات خلف MediaStore ولا مدير
ملفّات في متناول اليد، وعلى غنو/لينكس كلاهما موجود. وتنزيل التحديث في
الخلفيّة لم يُنقَل لأنّ علّته أنّ أندرويد يجمّد العمليّة، وElectron لا
يُجمَّد — والاستئناف بترويسة Range موجود هنا سلفاً ومتحقَّق منه.

**سجلّ التنزيلات** في localStorage بسقف 500 مدخل، بمُرشِّح ثلاثيّ وحذف
فرديّ ومنتقىً وجماعيّ وإعادة محاولة. وقائمة التشغيل مدخل واحد بمعلوماتها
لا مدخل لكلّ ملفّ. والعمليّات المحلّيّة تمرّ بلا تسجيل: السجلّ للروابط لا
لكلّ أمر يُنفَّذ.

**الاقتصاص عند التنزيل** بـ`--download-sections`، فلا يُنزَّل ما لا يُراد.
ولا يُمرَّر `--force-keyframes-at-cuts`: يُدقّق الحدّ لكنّه يعيد الترميز
كاملاً، دقائقُ معالجةٍ لأجل جزء من ثانية. وكشف اختبارُ محلِّل الوقت عطباً
في شيفرتي قبل أن تُستعمل: نصٌّ غير صالح كان يُقرأ صفراً صامتاً فينزّل من
كتب «abc» من أوّل المقطع وهو يظنّ أنّه حدّد موضعاً. صار الفراغ وحده يعني
البداية وما سواه يُرفَض.

**ومجلّد لكلّ قائمة تشغيل** بدل رمي مقاطعها في المجلّد المختار مختلطةً
بغيرها، ويكتب yt-dlp اسم القائمة بنفسه فلا يُبنى نصّاً هنا.

**والمحدِّث الذاتيّ كان يُهمل لاحقة ما قبل الإصدار** — العطب نفسه الذي
ظهر في الهاتف. والأثر هنا أنّ إصداراً تجريبيّاً لا يُرى أبداً ما دام رقمه
رقم المستقرّ. صارت المقارنة بترتيب semver وتُحقِّق من ثمانِ حالات حدّيّة.

**وملاحظات الإصدار كانت تُعرَض ترميزاً خاماً**، و**إغلاق النافذة أثناء
تنزيل جارٍ كان يقتله صامتاً** وما نزل يُهمَل — فصار يُسأل أوّلاً.
الجولة إصلاحات في سلسلة منشورة وميزتان لا تكسران توافقاً، فموضعها رقم
التصحيح لا رقم الميزة. وسلسلة 26.09 تبقى كما هي فلا يتبدّل مجلَّد المصدر
المنشور ولا تتفرّق حزم السلسلة الواحدة.
سجلُّ التغييرات كان يقول «غيرُ منشورٍ بعد» وهو ما يقرؤه سيرُ العمل نفسُه
ليكتب ملاحظات الإصدار، فيؤرَّخ قبل الوسم لا بعده.

وروابطُ README كانت تدلّ على 26.05 وحدها، وأسماءُ الحزم فيها مبنيّةً على
قالب لا على الواقع: لاحقة المعماريّة تختلف بين الصيغ الثلاث — x86_64 في
AppImage، وamd64 في deb، وx86_64/aarch64 في rpm — وقد نُشرت مرّةً بلاحقة
واحدة فكانت روابطَ ميّتة. فكُتبت هنا من أسماء الملفّات الفعليّة، والتحقّق
يُحال إلى ملفّ SHA256SUMS بدل نسخ بصمة قد تتقادم.
الصفّ فيه أيقونة ثمّ عنوان بـ`flex-1` ثمّ سهم الطيّ. وانعكاس الصفّ في وضع
اليمين يضع الأيقونة يميناً كما ينبغي، لكنّ `text-left` يُلصق النصّ بالحافّة
اليسرى من حاويته العريضة — فيقع العنوان في الطرف المقابل لأيقونته ملاصقاً
للسهم. و`text-start` يحلّها في الاتّجاهين معاً بلا شرط.

وسائر المشروع كان يستعمل `text-start` أصلاً — في القائمة الرئيسة ومدير
التحديث والقصّ ومعلومات الوسائط — فهذه المواضع السبعة كانت الشاذّة لا
القاعدة. وقُيِّدت القاعدة في دليل المشروع كي لا تعود.

وما مضمونه لاتينيّ دائماً — الروابط ومسارات الملفّات — يبقى `dir="ltr"`
صريحاً كما هو، فليس هذا من محاذاة اللغة في شيء.
خطوة «Fetch all refs» تفشل في كلّ دفعة إلى فرع — `refusing to fetch into
branch 'refs/heads/work' checked out` — و`|| true` يبتلع الفشل. فلا يبقى
تحت refs/heads إلّا الفرع المدفوع وحده، ثمّ يقول `git push --mirror`
للوجهة: اجعل نفسك مثلي، أي احذف كلّ فرعٍ سواه.

فكان الأثر مقلوباً: الدفعة إلى work تفشل كلّها لأنّ Forgejo يأبى حذف
الفرع الافتراضيّ main، والدفعة إلى main **تنجح** فتحذف work من Codeberg
صامتةً. والنجاح ههنا أخطر من الفشل، ولذلك بقي العطب شهوراً.

وأُزيلت الخطوة الفاشلة أصلاً: الفروع كلّها موجودة تحت refs/remotes/origin/*
بفضل fetch-depth: 0، فتُدفَع منها مباشرةً بمُواصفتَي مرجعٍ صريحتَين. ومعهما
--prune فيبقى معنى المرآة: ما حُذف في المصدر يُحذَف في الوجهة، ولا يُحذَف
ما لم يُحذَف. ويُسقَط refs/remotes/origin/HEAD لئلّا يصير فرعاً اسمه HEAD.
أساسٌ لِما طلبه المستخدم في نسخة الهاتف ثمّ في الحاسوب: قسمان مستقلّان
يعملان معاً. ولا يُبنى عليه شيءٌ قبل أن تحتمل النواةُ أكثر من مهمّة.

كان في `main.js` مقبضُ عمليّةٍ واحدٌ (`activeChild`) وعَلَمُ إلغاءٍ واحدٌ على
مستوى الوحدة. فطلبٌ ثانٍ أثناء الأوّل يدهسه من ثلاثة أوجه: يُصفّر عَلَم
إلغائه فيُبطل أمراً سابقاً، ويكتب فوق مقبضه فيقتل `cancel-command` آخر ما
بدأ لا ما قصده المستخدم، ويبثّ خرجه على القناة نفسها بلا ما يُميّزه —
والأحداث لا تحمل إلّا العنوان. وفي المُصيِّر مرجعا معالجةٍ مفردان
(`onOutputRef` و`onDoneRef`) يكتب فوقهما كلّ استدعاء، فمهمّةٌ ثانية تسرق
مستمعَ الأولى فيضيع خرجها ولا يُغلَق صندوق تقدُّمها.

فصار لكلّ مهمّةٍ معرِّف. ويُولَّد في المُصيِّر لا في main: الخرج يبدأ بالوصول
قبل أن يعود ردُّ النداء، فلو وَلَّده main لما عرف المُصيِّر لمن يُنسَب أوّل
سطر. وفي العمليّة الرئيسة خريطةٌ فيها لكلّ مهمّةٍ مقبضُها وعَلَمُها، وفي
المُصيِّر خريطةُ معالِجاتٍ بالمفتاح نفسه. و`cancel-command` يأخذ المعرِّف
فيُلغي مهمّةً بعينها، وبلا معرِّفٍ يُلغي الكلّ — وهو ما يحتاجه زرُّ «أوقف
الكلّ» لاحقاً. ومعرِّفٌ مستعمَلٌ يُرَدُّ بدل أن يدهس صاحبه. وحوار الإغلاق
يَعُدّ المهامّ الجارية بدل أن يسأل عن واحدة.

والواجهة لم تتغيّر: ما تزال تُشغّل مهمّةً واحدة، والحارس عليها أربعة فحوصٍ
جديدة — مهمّتان معاً بلا اختلاط خرج، ومعرِّفٌ في كلّ ردّ، وإلغاء إحداهما لا
يمسّ الأخرى، ومعرِّفٌ مكرّرٌ مرفوض. 27 فحصاً تمرّ.
كان الرقم يُرفَع بين التجارب — 26.9.0 ثمّ 26.9.1 ثمّ 26.9.2 — فتبدو ثلاثة
إصدارات مستقرّة وهي ثلاث تجارب لسلسلةٍ واحدة. صار ما يسبق الاستقرار لاحقةً على
الرقم نفسه: `26.9.0-beta.1` ثمّ `-beta.2` وهكذا حتّى `26.9.0` المستقرّ، وهو
ترقيم نسخة الهاتف نفسه فلا يفترق المشروعان في شيءٍ يراه المستخدم.

وأُعيدت تسمية الوسمين المنشورين — `v26.9.0` صار `v26.9.0-beta.1` و`v26.9.1`
صار `v26.9.0-beta.2` — ولم يكن ذلك تجميلاً: المحدِّث الذاتيّ يقرأ الرقم من
الوسم، فلو بقيا لعرض على من يحمل beta.3 «تحديثاً» إلى 26.9.1 وهو أقدم منه
بترتيب semver. وروابط الموقع والتوثيق تتبع الوسم الجديد؛ وأسماء الأصول تبقى
كما بُنيَت فالوسم وحده تغيّر.

**وأثرٌ لا مفرّ منه:** من ثبّت 26.9.0 أو 26.9.1 يحمل تطبيقه ذلك الرقم، وكلّ
26.9.0-beta.N أقدم منه بترتيب semver — فلن يعرض عليه المحدِّث شيئاً حتّى
المستقرّ. يلزمه تثبيتٌ يدويّ مرّةً واحدة، وهما إصداران عمرهما يومان.
مركز البرامج كان يعرض «Unknown author» و«Licenses: Unknown» — لا لخللٍ في
حقول الحزمة بل لأنّه يقرأ هذين من بطاقة AppStream وحدها: حزمة deb لا حقل
رخصةٍ فيها أصلاً، والمطوّر لا يُقرأ من Maintainer.

فصار يُثبَّت في /usr/share/metainfo ملفٌّ فيه الرخصة والمطوّر — بالوسم الحديث
والمهجور معاً لأنّ مراكز البرامج الأقدم لا تقرأ إلّا الثاني — ووصفٌ ومزايا
بالعربيّة والإنجليزيّة وروابط المشروع. ومعه --license و--vendor و--url في حقول
الحزمة، وتحديثُ مخزن AppStream بعد التثبيت فلا ينتظر المستخدم دورةً يوميّة.

والرقم فيها مولَّدٌ من package.json وحده ضمن `npm run build`، فلا يُكتَب رقمٌ
في موضعين — وهي قاعدة هذا المشروع.

وكشف التحزيم خطأً كان سيُبطل الأثر كلَّه: البطاقة تشير إلى `GMD.desktop`
والمثبَّت فعلاً `gmd-gui.desktop` — electron-builder يشتقّه من حقل `name` لا من
`productName` — وبغير التطابق لا يربط AppStream البطاقة بالتطبيق فتبقى معلَّقة.
وتُحقِّق من الحزمة نفسها بـ`dpkg -c` بعد البناء، لا من الإعداد وحده.
الدفعة الثانية من نقل ما جُرِّب في الهاتف إلى الحاسوب. والنواة أُصلحت قبلها،
وهذه الواجهة التي كانت تمنع الفائدة منها بثلاثة حواجز.

**الأوّل: حالة كلّ شاشة فيها لا فوقها.** والمكوّن يُفكَّك عند الخروج منها، فالرابط
والجودة والقائمة ومسار الحفظ تضيع بمجرَّد النظر في شاشة أخرى ثمّ العودة —
وهو أسوأ ممّا في الهاتف حيث كانت الحالة مشتركةً على الأقلّ. ارتفعت إلى `App`
في `sections`، ولكلّ قسمٍ حالته: رابط الصوت لا يظهر في الفيديو، وتنزيلٌ هنا لا
يمنع تنزيلاً هناك.

**والثاني: صندوق التقدُّم غشاءٌ يملأ النافذة** (`fixed inset-0`) فلا يُوصَل إلى شيءٍ
ما دام يعمل. صار التقدُّم داخل شاشته، ومعه شريطٌ سفليٌّ يُظهر ما يجري من أيّ
شاشة وينقل إليه بنقرة. والعمليّات المحلّيّة — تحويلٌ وقصٌّ ومعلومات — تبقى في
صندوقها: هي هدف الشاشة نفسها ولا شيء يُنتظَر بعدها، بخلاف تنزيلٍ يطول.

**والثالث: النسبة وحدها لا تكفي.** تقف عند 100٪ حين ينتهي التنزيل ويبدأ ما
بعده — استخراج صوتٍ أو تجميع تيّارين — فيظنّ المستخدم أنّ البرنامج تجمّد. صارت
المرحلة مكتوبةً والشريط متحرّكاً بلا رقمٍ كاذب. وتُقرأ من الخرج لأنّ yt-dlp لا
يُعلن مرحلته إلّا فيه.

**وقائمة التشغيل بطاقةٌ في الشاشة لا نافذة**: كانت تُغلَق لحظة بدء التنزيل فلا
يرى صاحبها أيّ عنصرٍ تمّ وأيّه يجري. صارت أمامه بعلامات الحال، وزرّ التنزيل
يحمل عدد المختار.

**وصيغة الفيديو تُختار** — MP4 وWEBM وMKV والأصل — إعادةَ تغليفٍ لا ترميز،
وتُفضَّل في الاختيار أيضاً فيُنتقى تيّارٌ لا يحتاج تغليفاً. والقيدان في `-S` واحدٍ
مفصولٍ بفواصل: تكرار الخيار يُلغي أوّله فتضيع الجودة لأجل الحاوية.

وأربعة فحوصٍ جديدة تحرس هذا كلَّه: فتح الشاشة من القائمة، وبقاء الحالة بعد
التنقُّل، واستقلال القسمين، وعرض الحاويات. 31 فحصاً تمرّ.
مركزُ البرامجِ كانَ يعرضُ «Unknown author» و«License: Unknown» والبطاقةُ مثبَّتةٌ
صحيحةٌ في `/usr/share/metainfo`: الحزمةُ تُثبَّتُ من ملفٍّ لا من مستودَع، فلا
فهرسَ توزيعةٍ يقولُ لـDiscover إنّ هذا المكوّنَ يخصُّ حزمةَ `gmd-gui`، فيبقى
المكوّنُ معلَّقاً بلا حزمةٍ يُنسَبُ إليها. أُضيفَ `<pkgname>` فصارَ الربطُ صريحاً.

وأُضيفت لقطتانِ إلى البطاقةِ فلا تبقى صفحةُ البرنامجِ بلا صورة، وهما عنوانانِ من
الموقعِ لا صورتانِ في الحزمة.

وفي السجلّ: قسمُ beta.3 كانَ ما يزالُ «غيرُ منشورٍ بعد» وقد نُشِر، فأُخِذَ
تاريخُه.
نقلٌ لآخرِ ما في نسخةِ الهاتف، ولأصلِه هنا وجهٌ آخر: أندرويد يفرضُ مجلَّدَي
`Movies/GMD` و`Music/GMD`، وهنا المسارُ يختارُه المستخدم — فالمعرضُ يقرأُ مجلَّدَي
الحفظِ الافتراضيَّين ومستوىً واحداً تحتَهما، إذ كلُّ قائمةِ تشغيلٍ تُنزَّلُ في
مجلَّدٍ باسمِها.

- **معلوماتُ الرابطِ تُجلَبُ من تلقائِها** بعدَ مهلةِ هدوء، فيُرى ما سيُنزَّلُ —
  أو عناصرُ قائمتِه — قبلَ النقر. وكلُّ تغييرٍ يُلغي سابقَه فلا تصلُ إلّا نتيجةُ
  الرابطِ الأخير، ولا يُسأَلُ عن رابطٍ والتنزيلُ جارٍ.

- **المعرض**: تبويبانِ وثلاثةُ أقسام (قوائمي · قوائمُ التنزيل · المفردات)، ولكلِّ
  مقطعٍ صورتُه تُستخرَجُ مرّةً وتُخزَّن. ومنه القصُّ والإظهارُ في المجلَّدِ والحذفُ
  **إلى سلّةِ المهملاتِ لا محواً**.

- **مشغّلٌ صوتيٌّ داخليّ**: عنصرُ صوتٍ واحدٌ في قشرةِ البرنامجِ لا في شاشتِه، فلا
  ينقطعُ الصوتُ بالتنقُّل، ومعه شريطٌ سفليٌّ في كلِّ شاشةٍ واستئنافٌ من الموضعِ
  الأخيرِ وعودةُ آخرِ صفٍّ بعدَ الإغلاق.

- **قوائمُ تشغيلٍ من صنعِ المستخدم**: ترتيبٌ لا نسخة — مساراتٌ تُحفَظُ لا ملفّات.

- **نصُّ حقولِ الإدخالِ في وسطِها** في الصنفِ المشترَك، فيشملُ الروابطَ والمساراتِ
  وحدودَ الاقتصاصِ وحقولَ الإعدادات.

- **وفحصُ التنقُّلِ في `test:app`** كانَ يسقطُ بلا عطبٍ في البرنامج: الملفُّ
  الشخصيُّ للاختبارِ يبدأُ بلا لغةٍ محفوظةٍ فتظهرُ الواجهةُ بالإنجليزيّةِ والفحصُ
  يطلبُ نصّاً عربيّاً. صارَ يقبلُ اللغتَين: 31 نجحت، 0 سقطت.
مزامنةٌ مع نسخةِ الهاتفِ بعدَ فحصِ الفروقِ بينهما، وإصلاحُ عطبٍ لم يظهر بعد.

- **الاقتصاصُ عندَ التنزيلِ كانَ يموتُ على يوتيوب ولا بديل**: المسلكُ الأوّلُ
  يطلبُ من الخادمِ الجزءَ وحدَه، وبعضُ المواقعِ ترفضُ الطلبَ الجزئيّ (403). صارَ
  الفشلُ يُتبَعُ تلقائيّاً بتنزيلٍ كاملٍ إلى ملفٍّ مؤقَّتٍ ثمّ اقتصاصٍ بـffmpeg
  نسخاً للتيّارات، ثمّ يُمحى المؤقَّت — وهو مسلكُ الهاتفِ نفسُه. والمحاولةُ الأولى
  صامتةٌ: لا صندوقَ فشلٍ ولا سطرَ سجلٍّ لمحاولةٍ نجحَ بديلُها. وحذفُ المؤقَّتِ
  مقصورٌ على ما بادئتُه `gmd-clip-` ولاحقتُه وسائطُ معروفة.

- **والمشغّلُ كانَ يمحو صفَّه المحفوظَ عندَ الإقلاع**: أوّلُ تمريرةٍ حالتُها فارغةٌ
  فتُكتَبُ فوقَ المحفوظِ قبلَ أن يُقرَأ. صارَ الحفظُ لا يبدأُ قبلَ الاستعادة.

- **مهلةُ تراجعٍ قبلَ حذفِ المقاطع**: تختفي من المعرضِ فورَ التأكيدِ ولا تُنقَلُ
  إلى المهملاتِ إلّا بعدَ ثماني ثوانٍ، وشريطٌ فيه «تراجع» يُعيدُها.

- **حقولُ الوقتِ ثلاثُ خاناتٍ رقميّة**، والنقرُ يُحدِّدُ ما فيها، وتُفرَغُ فتبقى
  فارغة. ومدّةُ المقطعِ تُملأُ في «إلى» من معلوماتِ الرابطِ المجلوبةِ تلقائيّاً.

- **وفحصُ `test:app` كانَ يتذبذب**: خادمُ Vite يردُّ على `/` قبلَ أن ينتهيَ من
  تحسينِ الاعتماديّات ثمّ يُعيدُ تحميلَ الصفحةِ فتُمحى حالةُ React في منتصفِ
  الفحص. صارَ ينتظرُ تركيبَ الواجهةِ قبلَ أوّلِ فحص: 31 نجحت، 0 سقطت.
مزامنةٌ أخيرةٌ مع نسخةِ الهاتف.

- **السجلُّ يُفتَحُ لحظةَ بدءِ المهمّةِ لا عندَ انتهائِها**: ما قُتِلَ في منتصفِه
  كانَ لا يترُكُ أثراً إطلاقاً. صارَ المدخلُ موسوماً «يجري» ثمّ يُغلَقُ نجاحاً أو
  فشلاً أو إلغاءً، وما بقيَ «يجري» من جلسةٍ سابقةٍ صارَ «انقطع» عندَ الإقلاع.
  وفي الشاشةِ أربعُ حالاتٍ بأيقوناتِها، والمداخلُ القديمةُ تُقرَأُ كما كانت.

- **تنبيهُ «سبقَ تنزيلُه»**: مقارنةٌ على رابطٍ مُطبَّعٍ (مضيفٌ ومسارٌ وv وlist)
  فلا تُخدَعُ بصيغةٍ مختصرةٍ ولا بوسمِ تتبُّع، وسؤالٌ في نافذةٍ مترجَمةٍ تتّبعُ
  اتّجاهَ الواجهةِ لا `confirm()` الأصليّ.

- **ومهلةُ الفحصِ بينَ الشاشاتِ رُفِعَت**: الانتقالُ ينتظرُ خروجَ الشاشةِ السابقةِ
  في `AnimatePresence`، وذلك يطولُ على جهازٍ مشغول، فكانت ثلاثةُ فحوصٍ تسقطُ بلا
  عطبٍ في البرنامج.
- **مستوى الصوتِ في المشغّل**: أيقونةٌ وحدَها في الشريطِ السفليّ تفتحُ فقاعةً فيها
  مِزلاقٌ عموديٌّ فوقَها، فلا تأخذُ من عرضِ الشريطِ شيئاً — وهو ضيّقٌ أصلاً فيه
  الاسمُ والزمنُ وخمسةُ أزرار. ومعها النسبةُ وزرُّ كتم، والأيقونةُ تتبدّلُ
  بالمستوى. وفي شاشةِ المشغّلِ الأيقونةُ نفسُها إلى جانبِ الأزرار. والمستوى محفوظٌ
  بينَ الجلساتِ كموضعِ الاستماع.

- **اسمُ المقطعِ العربيُّ كانَ يُرسَمُ مبعثَراً في واجهةٍ إنجليزيّة**: الاسمُ من
  المادّةِ لا من الواجهة، فاتّجاهُه اتّجاهُ نصِّه لا اتّجاهُ الحاوية. صارَ
  `dir="auto"` في المشغّلِ والمعرضِ والسجلِّ وبطاقةِ الرابط، ومسارُ الحفظِ
  `dir="ltr"` صراحةً فهو لاتينيٌّ دائماً.
- **المعرضُ لا يكتشفُ مجلَّدَ القائمةِ في الحزمةِ المثبَّتة** وهو يكتشفُه في شجرةِ
  التطوير: القراءةُ كانت تنزلُ مستوىً واحداً وتنسبُ القائمةَ إلى أوّلِ مجلَّدٍ تحتَ
  الجذر، فمن ضبطَ حفظَه على `~/Music` بينما ينزّلُ في `~/Music/GMD/‹القائمة›` لم
  يُرَ له إلّا مجلَّدُ `GMD` قائمةً واحدةً وضاعت قوائمُه تحتَه. صارت القراءةُ تنزلُ
  ثلاثةَ مستويات، والقائمةُ هي المجلَّدُ الحاضنُ للملفّ.

- **وصفُّ التشغيلِ المستعادُ كانَ يظهرُ ولا يُشغَّلُ منه شيء** حتّى يُفتَحَ المعرضُ:
  `media://` لا يخدمُ إلّا ما سُجّلَ في هذه الجلسة. صارت مجلَّداتُ الحفظِ تُقرَأُ مع
  استعادةِ الصفِّ عندَ الإقلاع.
تسعُ جولاتِ تجربةٍ سبقتْه، ومعه نسخةُ الهاتفِ بالرقمِ نفسِه. وكلُّ ما فيه مُجرَّبٌ
في حزمةٍ مثبَّتةٍ لا في شجرةِ التطويرِ وحدَها — وآخرُ ما كشفَه ذلك أنّ المعرضَ كانَ
لا يرى قوائمَ التشغيلِ إن كانَ مجلَّدُ الحفظِ فوقَها بمستوى.

وصفةُ الإصدارِ صارت تُقرَأُ من الوسمِ نفسِه لا من يدٍ تُبدّلُها: `prerelease` إن
كانت في الوسمِ لاحقةُ ما قبلَ الإصدار، ومستقرٌّ إن خلا منها.

الفحوص: `test:app` 31/31 · `test:updater` 14/14 · الترجمة 344 مفتاحاً · لا أرقام
مشرقيّة.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant