From 3d4412a74d6c4e8eb70fce3e37f41e2b915420d7 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:14:28 +0300 Subject: [PATCH 01/22] inlang: update translations --- assets/translations/strings_fa.i18n.json | 6 +++--- assets/translations/strings_ru.i18n.json | 10 +++++----- assets/translations/strings_tr.i18n.json | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index a774f9a0..672fce99 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "سرویس در حال اجرا نیست", "missingPrivilege": "نیازمند دسترسی", "missingPrivilegeMsg": "حالت VPN به دسترسی administrator نیاز دارد. یا برنامه را به عنوان سرپرست راه اندازی مجدد کنید یا حالت سرویس را تغییر دهید.", + "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", + "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید.", "invalidConfigOptions": "تنظیمات کانفیگ نامعتبر", "invalidConfig": "کانفیگ غیر معتبر", "create": "در ایجاد سرویس خطایی رخ داده", - "start": "در راه‌اندازی سرویس خطایی رخ داده", - "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", - "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید." + "start": "در راه‌اندازی سرویس خطایی رخ داده" }, "connectivity": { "unexpected": "خطای غیرمنتظره", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 4d75e461..4e160aab 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -10,11 +10,11 @@ "disable": "Отключить" }, "sort": "Сортировка", - "sortBy": "Сортировка…", + "sortBy": "Сортировка", "addToClipboard": "Копировать в буфер обмена" }, "intro": { - "termsAndPolicyCaution(rich)": "Продолжая, вы соглашаетесь с ${tap(@:about.termsAndConditions)}", + "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", "start": "Начать" }, "home": { @@ -270,12 +270,12 @@ "serviceNotRunning": "Сервис не запущен", "missingPrivilege": "Отсутствие прав", "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.", + "missingGeoAssets": "Отсутствующие географические ресурсы", + "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках.", "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", "create": "Ошибка создания сервиса", - "start": "Ошибка запуска сервиса", - "missingGeoAssets": "Отсутствующие географические ресурсы", - "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках." + "start": "Ошибка запуска сервиса" }, "connectivity": { "unexpected": "Неожиданная ошибка", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index 15309c26..dab9f2ae 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "Servis çalışmıyor", "missingPrivilege": "Eksik Ayrıcalık", "missingPrivilegeMsg": "VPN modu yönetici ayrıcalıkları gerektirir. Uygulamayı yönetici olarak yeniden başlatın veya hizmet modunu değiştirin.", + "missingGeoAssets": "Eksik Coğrafi Varlıklar", + "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün.", "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri", "invalidConfig": "Geçersiz Yapılandırma", "create": "Servis oluşturma hatası", - "start": "Servis başlatma hatası", - "missingGeoAssets": "Eksik Coğrafi Varlıklar", - "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün." + "start": "Servis başlatma hatası" }, "connectivity": { "unexpected": "Beklenmedik Hata", From f37fd80ef8d35278fb6580b59e43d85308b31697 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:32:14 +0300 Subject: [PATCH 02/22] inlang: update translations --- assets/translations/strings_ru.i18n.json | 48 ++++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 4e160aab..e8223be9 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -49,8 +49,8 @@ "noTraffic": "Нет доступного трафика" }, "sortBy": { - "lastUpdate": "Последнее обновление", - "name": "Алфавит" + "lastUpdate": "по последнему обновлению", + "name": "по названию" }, "add": { "buttonText": "Новый профиль", @@ -58,19 +58,19 @@ "fromClipboard": "Добавить из буфера обмена", "scanQr": "Сканировать QR-код", "qrScanner": { - "permissionDeniedError": "Доступ запрещён", + "permissionDeniedError": "Нет прав", "unexpectedError": "Неизвестная ошибка", "torchSemanticLabel": "Вспышка", "facingSemanticLabel": "Фронтальная камера" }, - "manually": "Ручной ввод", + "manually": "Ввести вручную", "addingProfileMsg": "Добавление профиля", - "failureMsg": "Невозможно добавить профиль" + "failureMsg": "Не удалось добавить профиль" }, "update": { "buttonTxt": "Обновить", "tooltip": "Обновить профиль", - "failureMsg": "Ошибка обновления", + "failureMsg": "Не удалось обновить профиль", "successMsg": "Профиль успешно обновлён" }, "share": { @@ -93,7 +93,7 @@ "save": { "buttonText": "Сохранить", "successMsg": "Профиль успешно сохранён", - "failureMsg": "Невозможно сохранить профиль" + "failureMsg": "Не удалось сохранить профиль" }, "detailsForm": { "nameLabel": "Имя", @@ -130,7 +130,7 @@ }, "settings": { "pageTitle": "Настройки", - "requiresRestartMsg": "Для применения перезапустите приложение.", + "requiresRestartMsg": "Чтобы применить изменения, перезапустите приложение.", "general": { "sectionTitle": "Основные", "locale": "Язык", @@ -150,7 +150,7 @@ "black": "Чёрная тема" }, "enableAnalytics": "Сбор аналитики", - "enableAnalyticsMsg": "Сбор аналитических данных и отправка отчётов о сбоях для улучшения приложения.", + "enableAnalyticsMsg": "Сбор данных аналитики и отправка отчётов о сбоях для улучшения приложения", "autoStart": "Запуск при загрузке", "silentStart": "Тихий запуск", "openWorkingDir": "Открыть рабочую папку", @@ -160,7 +160,7 @@ "advanced": { "sectionTitle": "Расширенные", "debugMode": "Режим отладки", - "debugModeMsg": "Для применения перезапустите приложение.", + "debugModeMsg": "Чтобы применить изменения, перезапустите приложение.", "memoryLimit": "Ограничение памяти" }, "network": { @@ -200,9 +200,9 @@ "prefer": "Предпочтительно", "only": "Исключительно" }, - "remoteDnsAddress": "Удалённая DNS", + "remoteDnsAddress": "Удалённый DNS", "remoteDnsDomainStrategy": "Стратегия удалённого домена DNS", - "directDnsAddress": "Прямая DNS", + "directDnsAddress": "Прямой DNS", "directDnsDomainStrategy": "Стратегия прямого домена DNS", "mixedPort": "Смешанный порт", "localDnsPort": "Локальный порт DNS", @@ -222,10 +222,10 @@ "pageTitle": "Активы маршрутизации", "version": "Версия ${version}", "fileMissing": "Файл отсутствует", - "update": "Обновлять", + "update": "Обновить", "download": "Скачать", - "failureMsg": "Не удалось обновить объект.", - "successMsg": "Объект успешно обновлен.", + "failureMsg": "Не удалось обновить объект", + "successMsg": "Объект успешно обновлен", "addRecommended": "Добавить рекомендуемые активы" } }, @@ -260,31 +260,31 @@ } }, "failure": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "clash": { - "unexpected": "Неожиданная ошибка (Clash)", + "unexpected": "Непредвиденная ошибка (Clash)", "core": "Ошибка ${reason}" }, "singbox": { - "unexpected": "Неожиданная ошибка (SingBox)", + "unexpected": "Непредвиденная ошибка (SingBox)", "serviceNotRunning": "Сервис не запущен", "missingPrivilege": "Отсутствие прав", "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение от имени администратора или измените режим работы приложения.", - "missingGeoAssets": "Отсутствующие географические ресурсы", - "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках.", + "missingGeoAssets": "Отсутствуют географические ресурсы", + "missingGeoAssetsMsg": "Георесурсы отсутствуют. Изменените выбранный ресурс или загрузите собственный в настройках.", "invalidConfigOptions": "Неправильные параметры конфигурации", "invalidConfig": "Неправильная конфигурация", "create": "Ошибка создания сервиса", "start": "Ошибка запуска сервиса" }, "connectivity": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "missingVpnPermission": "Отсутствует разрешение VPN", - "missingNotificationPermission": "Отсутствует разрешение на отображение уведомлений", + "missingNotificationPermission": "Отсутствует разрешение на показ уведомлений", "core": "Ошибка ядра" }, "profiles": { - "unexpected": "Неожиданная ошибка", + "unexpected": "Непредвиденная ошибка", "notFound": "Профиль не найден", "invalidConfig": "Неправильная конфигурация", "invalidUrl": "Неправильный URL" @@ -305,6 +305,6 @@ "play": { "title": "Hiddify Next (Preview)", "short_description": "Автовыбор, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", - "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на выбранный вами удалённый сервер, используя разрешение VPN–сервиса.\n\nПримечание: мы не предоставляем серверы, пользователи должны обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы.\n \nПоддерживаемые серверы:\n— Обычная ссылка на подписку V2ray/Xray\n— Ссылка на подписку Clash\n— Ссылка на подписку на Sing–Box\n\nВ чём уникальные особенности?\n — Удобство\n — Оптимизация и скорость\n — Автоматический выбор минимальной задержки\n — Отображение информации об использовании\n — Простой импорт ссылок одним щелчком мыши\n — Бесплатно и без рекламы\n — Простое переключение ссылок\n — …и много больше\n\nПоддержка:\n• Все протоколы, поддерживаемые Sing-Box\n• VLESS + xtls reality, vision\n• VMESS\n• Trojan\n• ShoadowSocks\n• Reality\n• V2ray\n• Hystria2\n• TUIC\n• SSH\n• ShadowTLS\n\n\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.\nЯдро приложения основано на открытом исходном коде Sing–Box.\n\nОписание разрешений:\n— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.\n— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет включать или исключать определённые приложения для туннелирования.\n— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.\n— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, поскольку используется приоритетная служба для обеспечения непрерывной работы службы VPN.\n— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." + "full_description": "Основная цель HiddifyNext — предоставить безопасный, удобный и эффективный клиент туннелирования. Он позволяет направлять весь трафик или трафик выбранного приложения на указанный Вами удалённый сервер.\nПримечание: мы не предоставляем серверы, пользователи должны сами обеспечивать конфиденциальность своих действий в Интернете, используя собственный сервер или доверенные серверы. Поддерживаются сервера с:— Обычной ссылка на подписку V2ray/Xray— Ссылкой на подписку Clash— Ссылко на подписку на Sing–Box\nВ чём уникальные особенности? — Удобство — Оптимизация и скорость — Автоматический выбор минимальной задержки — Отображение информации об использовании — Простой импорт ссылок одним щелчком мыши — Бесплатно и без рекламы — Простое переключение ссылок — …и много больше\nПоддерживаются:• Все протоколы, поддерживаемые Sing-Box• VLESS + xtls reality, vision• VMESS• Trojan• ShoadowSocks• Reality• V2ray• Hystria2• TUIC• SSH• ShadowTLS\nИсходный код доступен по адресу https://github.com/hiddify/Hiddify-Next.Ядро приложения основано на открытом исходном коде Sing–Box.\nОписание разрешений:— СЛУЖБА VPN: поскольку целью данного приложения является предоставление безопасного, удобного и эффективного клиента туннелирования, это разрешение необходимо, чтобы иметь возможность направлять трафик через туннель на удалённый сервер.— ЗАПРОС ВСЕХ ПАКЕТОВ: это разрешение позволяет добавлять или удалять определённые приложения из списка для туннелирования.— ИНФОРМИРОВАНИЕ О ЗАВЕРШЕНИИ ЗАГРУЗКИ: это разрешение можно включить или отключить в настройках приложения, чтобы (де)активировать запуск приложения при загрузке устройства.— ПОСТОЯННОЕ УВЕДОМЛЕНИЕ: это разрешение необходимо, так как используется приоритетная служба для обеспечения непрерывной работы VPN.— Приложение не содержит рекламы. Сбор аналитики и данных о сбоях происходят только с явного согласия пользователя при первом использовании приложения." } } \ No newline at end of file From e50319035fca744c168643ff6b43496eeb93ab95 Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:34:26 +0300 Subject: [PATCH 03/22] Nothing changed strings_fa.i18n.json --- assets/translations/strings_fa.i18n.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 672fce99..24a7dfce 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "سرویس در حال اجرا نیست", "missingPrivilege": "نیازمند دسترسی", "missingPrivilegeMsg": "حالت VPN به دسترسی administrator نیاز دارد. یا برنامه را به عنوان سرپرست راه اندازی مجدد کنید یا حالت سرویس را تغییر دهید.", - "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", - "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید.", "invalidConfigOptions": "تنظیمات کانفیگ نامعتبر", "invalidConfig": "کانفیگ غیر معتبر", "create": "در ایجاد سرویس خطایی رخ داده", - "start": "در راه‌اندازی سرویس خطایی رخ داده" + "start": "در راه‌اندازی سرویس خطایی رخ داده", + "missingGeoAssets": "دارایی های جغرافیایی از دست رفته", + "missingGeoAssetsMsg": "دارایی های جغرافیایی گم شده اند. تغییر دارایی فعال را در نظر بگیرید یا یکی را در تنظیمات دانلود کنید." }, "connectivity": { "unexpected": "خطای غیرمنتظره", @@ -307,4 +307,4 @@ "short_description": "Auto, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "هدف اصلی HiddifyNext ارائه یک کلاینت تونل زنی ایمن، کاربرپسند و کارآمد است. این به شما امکان می دهد تا با استفاده از مجوز VPN-Service، تمام ترافیک یا ترافیک برنامه انتخابی را به یک سرور راه دور مورد نظر خود هدایت کنید.\n\nتوجه: ما هیچ سروری ارائه نمی دهیم. کاربران موظفند با استفاده از سرورهای خود میزبان یا سرورهای مورد اعتماد، فعالیت‌های آنلاین خود را خصوصی نگه دارند.\n \nما از سرورهایی با موارد زیر پشتیبانی می کنیم:\n- لینک اشتراک V2ray/Xray معمولی\n- لینک اشتراک کلش\n- لینک اشتراک Sing-Box\n\nویژگی های منحصر به فرد ما چیست؟\n - کاربر پسند\n - بهینه و سریع\n - به طور خودکار LowestPing را انتخاب کنید\n - نمایش اطلاعات استفاده کاربر\n - به راحتی لینک فرعی را با یک کلیک با استفاده از دیپ لینک وارد کنید\n - رایگان و بدون تبلیغات\n - به راحتی پیوندهای فرعی کاربر را تغییر دهید\n - بیشتر و بیشتر\n\nحمایت کردن:\n- تمام پروتکل های پشتیبانی شده توسط Sing-Box\n- VLESS + xtls \n- VMESS\n- تروجان\n- ShoadowSocks\n- ریالیتی\n- V2ray\n- هیستریا 2\n- TUIC\n- SSH\n- ShadowTLS\n\n\nکد منبع در https://github.com/hiddify/Hiddify-Next وجود دارد\nهسته برنامه بر اساس sing-box منبع باز است.\n\nتوضیحات مجوز:\n- سرویس VPN: از آنجا که هدف این برنامه ارائه یک کلاینت تونل زنی ایمن، کاربر پسند و کارآمد است، ما به این مجوز نیاز داریم تا بتوانیم ترافیک را از طریق تونل به سرور راه دور هدایت کنیم.\n- QUERY ALL PACKAGES: این مجوز برای اجازه دادن به کاربران برای گنجاندن یا حذف برنامه های کاربردی خاص برای تونل زدن استفاده می شود.\n- دریافت بوت تکمیل شد: این مجوز را می توان از تنظیمات برنامه فعال یا غیرفعال کرد تا این برنامه پس از بوت شدن دستگاه فعال شود.\n- اعلان های ارسالی: این مجوز ضروری است زیرا ما از یک سرویس پیش زمینه برای اطمینان از عملکرد مداوم سرویس VPN استفاده می کنیم.\n- این برنامه بدون تبلیغات است. تجزیه و تحلیل و داده های اشکال فقط با رضایت صریح کاربر در اولین استفاده از برنامه اتفاق می افتد." } -} \ No newline at end of file +} From 0b48d0a87c39bd9f1dfc9be71d0580522edd0cee Mon Sep 17 00:00:00 2001 From: jomertix <150632538+jomertix@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:35:31 +0300 Subject: [PATCH 04/22] Nothing changed strings_tr.i18n.json --- assets/translations/strings_tr.i18n.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index dab9f2ae..f381d98e 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -270,12 +270,12 @@ "serviceNotRunning": "Servis çalışmıyor", "missingPrivilege": "Eksik Ayrıcalık", "missingPrivilegeMsg": "VPN modu yönetici ayrıcalıkları gerektirir. Uygulamayı yönetici olarak yeniden başlatın veya hizmet modunu değiştirin.", - "missingGeoAssets": "Eksik Coğrafi Varlıklar", - "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün.", "invalidConfigOptions": "Geçersiz yapılandırma seçenekleri", "invalidConfig": "Geçersiz Yapılandırma", "create": "Servis oluşturma hatası", - "start": "Servis başlatma hatası" + "start": "Servis başlatma hatası", + "missingGeoAssets": "Eksik Coğrafi Varlıklar", + "missingGeoAssetsMsg": "Coğrafi öğeler eksik. Aktif varlığı değiştirmeyi veya ayarlarda seçileni indirmeyi düşünün." }, "connectivity": { "unexpected": "Beklenmedik Hata", @@ -307,4 +307,4 @@ "short_description": "Otomatik, SSH, VLESS, Vmess, Trojan, Reality, Sing-Box, Clash, Xray, Shadowsocks", "full_description": "HiddifyNext'in temel hedefi güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamaktır. VPN Hizmeti iznini kullanarak tüm trafiği veya seçilen uygulama trafiğini seçtiğiniz uzak bir sunucuya yönlendirmenizi sağlar. Not: Herhangi bir sunucu sağlamıyoruz; kullanıcıların kendi barındırılan sunucularını veya güvenilir sunucularını kullanarak çevrimiçi etkinliklerinin gizli kalmasını sağlamaları gerekir. Sunucuları aşağıdakilerle destekliyoruz: - Normal V2ray/Xray Abonelik Bağlantısı - Clash Abonelik Bağlantısı - Sing-Box Abonelik Bağlantısı Benzersiz özelliklerimiz nelerdir? - Kullanıcı Dostu - Optimize Edilmiş ve Hızlı - En Düşük Ping'i otomatik olarak seçin - Kullanıcı kullanım bilgilerini gösterin - Derin bağlantı kullanarak tek tıklamayla alt bağlantıyı kolayca içe aktarın - Ücretsiz ve ADS Yok - Kullanıcı alt bağlantılarını kolayca değiştirin - giderek daha fazla Destek: - Sing-Box tarafından desteklenen tüm Protokoller - VLESS + xtls gerçeklik, vizyon - VMESS - Trojan - ShoadowSocks - Reality - V2ray - Hystria2 - TUIC - SSH - ShadowTLS Kaynak kodu https://github.com/hiddify/Hiddify-Next adresinde mevcuttur. Uygulama çekirdeği açık tabanlıdır. kaynak şarkı kutusu. İzin Açıklaması: - VPN Hizmeti: Bu uygulamanın amacı güvenli, kullanıcı dostu ve verimli bir tünel istemcisi sağlamak olduğundan, trafiği tünel aracılığıyla uzak sunucuya yönlendirebilmek için bu izne ihtiyacımız var. - TÜM PAKETLERİ SORGULAYIN: Bu izin, kullanıcıların tünelleme için belirli uygulamaları dahil etmesine veya hariç tutmasına izin vermek için kullanılır. - ALMA ÖNYÜKLEME TAMAMLANDI: Bu izin, cihaz önyüklemesi sırasında bu uygulamayı etkinleştirmek için uygulama ayarlarından etkinleştirilebilir veya devre dışı bırakılabilir. - BİLDİRİMLER SONRASI: VPN hizmetinin sürekli çalışmasını sağlamak için bir ön plan hizmeti kullandığımız için bu izin önemlidir. - Bu uygulama reklam içermez. Analitik ve kilitlenme verileri yalnızca uygulamanın ilk kullanımında kullanıcının açık rızası ile gerçekleşir." } -} \ No newline at end of file +} From 82bbcf34074584405780f244a83f0541d5cea903 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 20 Nov 2023 14:05:44 +0330 Subject: [PATCH 05/22] Update appcast --- appcast.xml | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/appcast.xml b/appcast.xml index 37c7f296..ad44f335 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,31 +3,32 @@ Release - Version 0.10.0 - Sat, 28 Oct 2023 12:00:00 +0000 - + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + - Version 0.10.9 - Wed, 15 Nov 2023 19:00:00 +0000 + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-windows-x64-setup.zip" + sparkle:version="0.11.1" sparkle:os="windows" /> - Version 0.10.9 - Wed, 15 Nov 2023 19:00:00 +0000 + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-macos-universal.zip" + sparkle:version="0.11.1" sparkle:os="macos" /> - Version 0.10.9 - Wed, 15 Nov 2023 19:00:00 +0000 + Version 0.11.1 + Sun, 20 Nov 2023 22:00:00 +0000 + url="https://github.com/hiddify/hiddify-next/releases/download/v0.11.1/hiddify-linux-x64.zip" + sparkle:version="0.11.1" sparkle:os="linux" /> \ No newline at end of file From 095921cfa9b7eb954309634152656294edbba7f1 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:03:26 +0330 Subject: [PATCH 06/22] Update release_message.md --- .github/release_message.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release_message.md b/.github/release_message.md index 4a03dfac..001fed44 100644 --- a/.github/release_message.md +++ b/.github/release_message.md @@ -51,7 +51,7 @@ Linux - + From 5706024921ac9fb04e40c5f45f5180124037b3b5 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:30:23 +0330 Subject: [PATCH 07/22] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 36963f8e..782ae47e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,12 @@

+
+ + [![GP-Intalls](https://img.shields.io/endpoint?color=green&logo=google-play&logoColor=green&url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dapp.hiddify.com%26l%3DGoogle%2520Play%26m%3D%24shortinstalls&style=flat-square)](https://play.google.com/store/apps/details?id=app.hiddify.com) [![Downloads](https://img.shields.io/github/downloads/hiddify/hiddify-next/total?style=flat-square&logo=github)](https://github.com/hiddify/hiddify-next/releases/)[![Last Version](https://img.shields.io/github/release/hiddify/hiddify-next/all.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![Last Release Date](https://img.shields.io/github/release-date/hiddify/hiddify-next.svg?style=flat-square)](https://github.com/hiddify/hiddify-next/releases/)[![commits](https://img.shields.io/github/commit-activity/m/hiddify/hiddify-next?style=flat-square)](https://github.com/hiddify/hiddify-next/) [![Youtube](https://img.shields.io/youtube/channel/views/UCxrmeMvVryNfB4XL35lXQNg?label=Youtube&style=flat-square&logo=youtube)](https://www.youtube.com/@hiddify)[![Telegram Channel](https://img.shields.io/endpoint?label=Channel&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify&color=blue)](https://telegram.dog/hiddify)[![Telegram Group](https://img.shields.io/endpoint?color=neon&label=Support%20Group&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fhiddify_board)](https://telegram.dog/hiddify_board/5) From 650766831f26ee7370c40f9b152e6041d9bb6db0 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:02:46 +0330 Subject: [PATCH 08/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 782ae47e..30cd47d5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

A multi-platform auto-client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.

-The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) for development. +The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information about development, you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) .
English Demo From 0a3d9f945ae5ebf30e40d786f713768c9a186059 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:06:36 +0330 Subject: [PATCH 09/22] Update README_ru.md --- README_ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_ru.md b/README_ru.md index f8c6811a..d77f319f 100644 --- a/README_ru.md +++ b/README_ru.md @@ -17,7 +17,7 @@ ## Что такое Hiddify-Next? Многоплатформенный авто-клиент на основе [Sing-box](https://github.com/SagerNet/sing-box), который служит универсальным набором инструментов прокси. Это приложение предлагает широкий спектр возможностей, которые перечислены ниже. Он также поддерживает большое количество протоколов. Приложение бесплатное, без рекламы и с открытым исходным кодом. Он предоставляет безопасный и конфиденциальный инструмент для получения доступа к бесплатному Интернету. -Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации вы можете прочитать наши Рекомендации по участию в разработке. +Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md).
English Demo From 5b770ce8f5bd81585de10b2deb07e8e382735433 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:07:01 +0330 Subject: [PATCH 10/22] Update README_ru.md --- README_ru.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_ru.md b/README_ru.md index d77f319f..7a1bf5d3 100644 --- a/README_ru.md +++ b/README_ru.md @@ -17,7 +17,7 @@ ## Что такое Hiddify-Next? Многоплатформенный авто-клиент на основе [Sing-box](https://github.com/SagerNet/sing-box), который служит универсальным набором инструментов прокси. Это приложение предлагает широкий спектр возможностей, которые перечислены ниже. Он также поддерживает большое количество протоколов. Приложение бесплатное, без рекламы и с открытым исходным кодом. Он предоставляет безопасный и конфиденциальный инструмент для получения доступа к бесплатному Интернету. -Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md). +Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md) .
English Demo From f4b22716a9ed3207d0fc7c5cdf3b5b066a87e739 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:10:07 +0330 Subject: [PATCH 11/22] Update README_cn.md --- README_cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_cn.md b/README_cn.md index 847a9157..f92eff6c 100644 --- a/README_cn.md +++ b/README_cn.md @@ -20,7 +20,7 @@

一个基于 Sing-box 的跨平台自动客户端,用作通用代理工具链。该应用提供了广泛的功能,如下所列。它还支持大量协议。该应用免费使用、无广告且开源。它为访问自由互联网提供了一个安全且私密的工具。

-该应用是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 欲了解更多信息,您可以参阅我们的开发贡献指南。 +该应用是使用 [Flutter](https://flutter.dev/) 和 [Go](https://go.dev/) 开发的。 有关开发的更多信息,您可以阅读我们的[贡献指南](CONTRIBUTING.md)。
English Demo From e493b4c052ad1873d89dd05ffd924e090da3016f Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:10:34 +0330 Subject: [PATCH 12/22] Update README_fa.md --- README_fa.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_fa.md b/README_fa.md index a76f6de5..d8d23408 100644 --- a/README_fa.md +++ b/README_fa.md @@ -17,7 +17,7 @@ یک کلاینت خودکار مالتی‌پلتفرم مبتنی بر [سینگ‌باکس](https://github.com/SagerNet/sing-box) که به عنوان یک ابزار عمومی برای پروکسی عمل می‌کند. این برنامه طیف گسترده‌ای از قابلیت‌ها را ارائه می‌دهد که در زیر لیست شده است. همچنین از تعداد زیادی پروتکل پشتیبانی می‌کند. این برنامه رایگان، بدون آگهی و منبع باز است. این یک ابزار امن و مطمئن برای دسترسی به اینترنت رایگان فراهم می‌کند. -این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه می‌توانید [دستورالعمل‌های مشارکت](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) در پروژه ما را مطالعه نمایید. +این برنامه با استفاده از [Flutter](https://flutter.dev/) و [Go](https://go.dev/) توسعه یافته است. برای اطلاعات بیشتر در خصوص توسعه می‌توانید [دستورالعمل‌های مشارکت](CONTRIBUTING.md) در پروژه ما را مطالعه نمایید. From 9537703513816de80529f18a3b03eca372dcc3e2 Mon Sep 17 00:00:00 2001 From: lymanjre <125398461+lymanjre@users.noreply.github.com> Date: Fri, 24 Nov 2023 23:11:01 +0330 Subject: [PATCH 13/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30cd47d5..476871d9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@

A multi-platform auto-client based on Sing-box that serves as a universal proxy tool-chain. This app offers a wide range of capabilities, which are listed below. It also supports a large number of protocols. The app is free to use, ad-free, and open-source. It provides a secure and private tool for getting access to the free internet.

-The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information about development, you can read through our [Contribution Guidelines](https://github.com/hiddify/hiddify-next/blob/main/CONTRIBUTING.md) . +The app is developed using [Flutter](https://flutter.dev) and [Go](https://go.dev). For more information about development, you can read through our [Contribution Guidelines](CONTRIBUTING.md) .
English Demo From 2441d3a5b268b616ba43ab0bd16ad9876ef54fe9 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 14:32:50 +0330 Subject: [PATCH 14/22] Bump sdk version --- pubspec.lock | 26 +++++++++++++------------- pubspec.yaml | 2 +- windows/flutter/CMakeLists.txt | 7 ++++++- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6030cb3b..ede38555 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" color: dependency: transitive description: @@ -817,10 +817,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -1374,10 +1374,10 @@ packages: dependency: "direct main" description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1390,10 +1390,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1430,10 +1430,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" time: dependency: transitive description: @@ -1646,10 +1646,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: "direct main" description: @@ -1715,5 +1715,5 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index fc84b711..c731a51b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 0.11.1+1101 environment: - sdk: ">=3.1.0 <4.0.0" + sdk: ">=3.2.0 <4.0.0" dependencies: flutter: diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d2071..903f4899 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From f177de1d9825fcc0bc99365ca280fcf1a7cbc151 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 14:36:32 +0330 Subject: [PATCH 15/22] Bump ci sdk version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b60afe9..d19e9e09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,7 +78,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.13.x' + flutter-version: '3.16.x' channel: 'stable' cache: true From 6040eae6ce63cf28deff455e60525961cd2afe03 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 15:22:42 +0330 Subject: [PATCH 16/22] Update dependencies --- ios/Podfile.lock | 18 ++++----- macos/Podfile.lock | 18 ++++----- pubspec.lock | 94 +++++++++++++++++++++++----------------------- pubspec.yaml | 35 +++++++++-------- 4 files changed, 82 insertions(+), 83 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3bd07b0d..deee190a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,7 +54,7 @@ PODS: - GTMSessionFetcher/Core (< 3.0, >= 1.1) - MLImage (= 1.0.0-beta4) - MLKitCommon (~> 9.0) - - mobile_scanner (3.5.2): + - mobile_scanner (3.5.4): - Flutter - GoogleMLKit/BarcodeScanning (~> 4.0.0) - nanopb (2.30909.1): @@ -70,13 +70,13 @@ PODS: - PromisesObjC (2.3.1) - protocol_handler (0.0.1): - Flutter - - Sentry/HybridSDK (8.14.2): - - SentryPrivate (= 8.14.2) + - Sentry/HybridSDK (8.15.2): + - SentryPrivate (= 8.15.2) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.14.2) - - SentryPrivate (8.14.2) + - Sentry/HybridSDK (= 8.15.2) + - SentryPrivate (8.15.2) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -176,15 +176,15 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49 - mobile_scanner: 5090a13b7a35fc1c25b0d97e18e84f271a6eb605 + mobile_scanner: e866af997a851f0d43e293621443713cb6222fe3 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 protocol_handler: ae9efcf3b307f3fdffcd9d5252775b9f7d9f0d09 - Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 - sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed - SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f + Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 + sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b + SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 06bee27f..81f49c08 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -4,7 +4,7 @@ PODS: - device_info_plus (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (3.5.2): + - mobile_scanner (3.5.4): - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS @@ -15,13 +15,13 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - Sentry/HybridSDK (8.14.2): - - SentryPrivate (= 8.14.2) + - Sentry/HybridSDK (8.15.2): + - SentryPrivate (= 8.15.2) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.14.2) - - SentryPrivate (8.14.2) + - Sentry/HybridSDK (= 8.15.2) + - SentryPrivate (8.15.2) - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -108,14 +108,14 @@ SPEC CHECKSUMS: cupertino_http: afa11b9e2786b62da2671e4ddd32caf792503748 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - mobile_scanner: 621cf2c34e1c74ae7ce5c6793638ab600723bdea + mobile_scanner: a33715761775cdbe498fd9de24d13ef142225962 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 protocol_handler: 587e1caf6c0b92ce351ab14081968dae49cb8cc6 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - Sentry: e0ea366f95ebb68f26d6030d8c22d6b2e6d23dd0 - sentry_flutter: 9a04c51c373d76ee22167bf1e65bc468c0a91fed - SentryPrivate: 949a21fa59872427edc73b524c3ec8456761d97f + Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 + sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b + SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqlite3: 6e2d4a4879854d0ec86b476bf3c3e30870bac273 diff --git a/pubspec.lock b/pubspec.lock index ede38555..7d9c6045 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" archive: dependency: transitive description: @@ -285,26 +285,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778 + sha256: "198ec6b8e084d22f508a76556c9afcfb71706ad3f42b083fe0ee923351a96d90" url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883 + sha256: dfcfa987d2bd9d0ba751ef4bdef0f6c1aa0062f2a67fe716fd5f3f8b709d6418 url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650 + sha256: f84c3fe2f27ef3b8831953e477e59d4a29c2952623f9eac450d7b40d9cdd94cc url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" dart_style: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7" + sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" url: "https://pub.dev" source: hosted - version: "5.3.3" + version: "5.3.4" drift: dependency: "direct main" description: @@ -365,10 +365,10 @@ packages: dependency: "direct dev" description: name: drift_dev - sha256: f79281f13411abe4229d6b57956202f047cc49b2c4e0d26ffae7273d6e5e97b1 + sha256: "369d2769d84e0c2d2cb4cd420e4fdb4f975852c83ebb934733c3b382c62961cd" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.2" equatable: dependency: transitive description: @@ -450,10 +450,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" + sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7" url: "https://pub.dev" source: hosted - version: "4.2.0+1" + version: "4.3.0" flutter_gen_core: dependency: transitive description: @@ -503,18 +503,18 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: d93394f22f73e810bda59e11ebe83329c5511d6460b6b7509c4e1f3c92d6d625 + sha256: c4d899312b36e7454bedfd0a4740275837b99e532d81c8477579d8183db1de6c url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" flutter_riverpod: dependency: transitive description: name: flutter_riverpod - sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe" + sha256: d261b0f2461e0595b96f92ed807841eb72cea84a6b12b8fd0c76e5ed803e7921 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.8" flutter_svg: dependency: "direct main" description: @@ -593,10 +593,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "5098760d7478aabfe682a462bf121d61bc5dbe5df5aac8dad733564a0aee33bc" + sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895 url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "12.1.1" go_router_builder: dependency: "direct dev" description: @@ -617,10 +617,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "2827136ecc0c2abffc3a58e575db6d5b84d159977fa1edc223c97bf566aa8c73" + sha256: b271e06606e718cf8185db9a792d1af00e2049e7bf5da09583654e020c00fbaa url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.8" hotreloader: dependency: transitive description: @@ -833,18 +833,18 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: cf978740676ba5b0c17399baf117984b31190bb7a6eaa43e51229ed46abc42ee + sha256: c9ed2bb1bbf4b98394bc4a8477984c8ba2b55f706d634bf27cd9dd1c2e9b3a23 url: "https://pub.dev" source: hosted - version: "3.5.2" + version: "3.5.4" native_dio_adapter: dependency: "direct main" description: name: native_dio_adapter - sha256: "1967cabe3e9ea68ea5ad6da7a0ed25fa75cf335ec6b92cdf6f32185efa93364b" + sha256: a9af4430278dd5a86ded4c5857e58d80e5bc81097e6cc8a176725ac8a0ab39a6 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" neat_periodic_task: dependency: "direct main" description: @@ -1009,10 +1009,10 @@ packages: dependency: "direct main" description: name: posix - sha256: "3ad26924254fd2354b0e2b95fc8b45ac392ad87434f8e64807b3a1ac077f2256" + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.1" process: dependency: transitive description: @@ -1097,42 +1097,42 @@ packages: dependency: transitive description: name: riverpod - sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55" + sha256: "08451ddbaad6eae73e2422d8109775885623340d721c6637b8719c9f4b478848" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.8" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "22a089135785f27e601075023f93c6622c43ef28c3ba1bef303cfbc314028e64" + sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57 url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "0.5.0" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "9330309e4400f40e39a2a1d1c340e775d0fd23451cf2dd2286e03c7896fd2bd5" + sha256: "02c9bced96ed3ed8d9970820d1ce7b16600955bc01aa8b2276f09dd3d9d29ed9" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "0a1c8eeb3dba2ce704eb1a4c3b8043716d52bedaaaa5b2725e0bde67ca38a46e" + sha256: "94b6c49bba879729611d690d434796e3b4e7c72a27e88b482b92c505e90f90d9" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.8" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "97342543496f07c5172e0d1ce98c29499d8245776c94bfc837ceea5525c01ade" + sha256: "6fc64ae102ba39b0889b7aa7f4ef6c5a8f71a2ad215b90c787f319a9407a128b" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.6" rxdart: dependency: "direct main" description: @@ -1153,10 +1153,10 @@ packages: dependency: transitive description: name: sentry - sha256: "9cfd325611ab54b57d5e26957466823f05bea9d6cfcc8d48f11817b8bcedf0d1" + sha256: e7ded42974bac5f69e4ca4ddc57d30499dd79381838f24b7e8fd9aa4139e7b79 url: "https://pub.dev" source: hosted - version: "7.12.0" + version: "7.13.2" sentry_dart_plugin: dependency: "direct main" description: @@ -1169,10 +1169,10 @@ packages: dependency: "direct main" description: name: sentry_flutter - sha256: "0cd7d622cb63c94fd1b2f87ab508e158b950bd281e2a80f327ebf73bb217eaf3" + sha256: d6f55ec7a1f681784165021f749007712a72ff57eadf91e963331b6ae326f089 url: "https://pub.dev" source: hosted - version: "7.12.0" + version: "7.13.2" share_plus: dependency: "direct main" description: @@ -1371,7 +1371,7 @@ packages: source: hosted version: "0.32.0" stack_trace: - dependency: "direct main" + dependency: transitive description: name: stack_trace sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" @@ -1502,10 +1502,10 @@ packages: dependency: "direct main" description: name: upgrader - sha256: "889c1ece7af143df32e8ee2126f2ef17b2ab6bb7ed8fc3b1b022d7faa4fdab20" + sha256: "204c5d5d5ac1c09fa956422dee94d7f44f1b612750d29028b7ec7a43b474c135" url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.3.0" url_launcher: dependency: "direct main" description: @@ -1662,10 +1662,10 @@ packages: dependency: "direct main" description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c731a51b..3906824d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,20 +20,20 @@ dependencies: fpdart: ^1.1.0 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 - hooks_riverpod: ^2.4.5 + hooks_riverpod: ^2.4.8 flutter_hooks: ^0.20.3 - riverpod_annotation: ^2.3.0 + riverpod_annotation: ^2.3.2 rxdart: ^0.27.7 drift: ^2.13.1 sqlite3_flutter_libs: ^0.5.18 shared_preferences: ^2.2.2 - dio: ^5.3.3 + dio: ^5.3.4 web_socket_channel: ^2.4.0 ffi: ^2.1.0 path_provider: ^2.1.1 - mobile_scanner: ^3.5.2 + mobile_scanner: ^3.5.4 protocol_handler: ^0.1.5 - flutter_native_splash: ^2.3.5 + flutter_native_splash: ^2.3.6 share_plus: ^7.2.1 window_manager: ^0.3.7 tray_manager: ^0.2.0 @@ -41,14 +41,13 @@ dependencies: url_launcher: ^6.2.1 vclibs: ^0.1.0 launch_at_startup: ^0.2.2 - sentry_flutter: ^7.12.0 + sentry_flutter: ^7.13.2 sentry_dart_plugin: ^1.6.3 combine: ^0.5.6 path: ^1.8.3 loggy: ^2.0.3 flutter_loggy: ^2.0.2 - meta: ^1.9.1 - stack_trace: ^1.11.0 + meta: ^1.10.0 dartx: ^1.2.0 uuid: ^4.2.1 tint: ^2.0.1 @@ -56,22 +55,22 @@ dependencies: neat_periodic_task: ^2.0.1 retry: ^3.1.2 watcher: ^1.1.0 - go_router: ^12.1.0 + go_router: ^12.1.1 flex_color_scheme: ^7.3.1 - flutter_animate: ^4.2.0+1 + flutter_animate: ^4.3.0 flutter_svg: ^2.0.9 gap: ^3.0.1 percent_indicator: ^4.2.3 sliver_tools: ^0.2.12 flutter_adaptive_scaffold: ^0.1.7+1 humanizer: ^2.2.0 - upgrader: ^8.2.0 + upgrader: ^8.3.0 toastification: ^1.1.0 version: ^3.0.2 - posix: ^5.0.0 - win32: ^5.0.9 + posix: ^6.0.1 + win32: ^5.1.0 qr_flutter: ^4.1.0 - native_dio_adapter: ^1.1.0 + native_dio_adapter: ^1.1.1 dev_dependencies: flutter_test: @@ -80,14 +79,14 @@ dev_dependencies: build_runner: ^2.4.6 json_serializable: ^6.7.1 freezed: ^2.4.5 - riverpod_generator: ^2.3.5 - drift_dev: ^2.13.1 + riverpod_generator: ^2.3.8 + drift_dev: ^2.13.2 ffigen: ^8.0.2 slang_build_runner: ^3.25.0 flutter_gen_runner: ^5.3.2 go_router_builder: ^2.3.4 - custom_lint: ^0.5.6 - riverpod_lint: ^2.3.3 + custom_lint: ^0.5.7 + riverpod_lint: ^2.3.6 icons_launcher: ^2.1.5 flutter: From e2d9d5e53ec7b318dc02b1ad3ee6aeefbe6a4b5c Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:00:40 +0330 Subject: [PATCH 17/22] Refactor geo assets --- build.yaml | 2 - lib/bootstrap.dart | 2 + lib/core/router/routes/desktop_routes.dart | 4 +- lib/core/router/routes/mobile_routes.dart | 4 +- lib/data/data_providers.dart | 29 +-- lib/data/local/dao/dao.dart | 2 - lib/data/local/dao/geo_assets_dao.dart | 46 ---- lib/data/local/data_mappers.dart | 27 -- lib/data/local/database.dart | 13 +- lib/data/local/tables.dart | 2 +- lib/data/repository/core_facade_impl.dart | 7 +- .../repository/geo_assets_repository.dart | 168 ------------- .../repository/profiles_repository_impl.dart | 2 +- lib/domain/rules/geo_asset.dart | 69 ------ lib/domain/rules/geo_assets_repository.dart | 16 -- .../geo_asset/data/geo_asset_data_mapper.dart | 31 +++ .../data/geo_asset_data_providers.dart | 31 +++ .../geo_asset/data/geo_asset_data_source.dart | 59 +++++ .../data/geo_asset_path_resolver.dart | 31 +++ .../geo_asset/data/geo_asset_repository.dart | 232 ++++++++++++++++++ .../geo_asset/model/default_geo_assets.dart | 39 +++ .../geo_asset/model/geo_asset_entity.dart | 27 ++ .../geo_asset/model}/geo_asset_failure.dart | 2 +- .../notifier/geo_asset_notifier.dart | 33 +++ .../geo_assets_overview_notifier.dart | 43 ++++ .../overview/geo_assets_overview_page.dart} | 19 +- .../widget}/geo_asset_tile.dart | 65 +++-- .../geo_assets/geo_assets_notifier.dart | 49 ---- lib/services/files_editor_service.dart | 47 ---- 29 files changed, 594 insertions(+), 507 deletions(-) delete mode 100644 lib/data/local/dao/dao.dart delete mode 100644 lib/data/local/dao/geo_assets_dao.dart delete mode 100644 lib/data/repository/geo_assets_repository.dart delete mode 100644 lib/domain/rules/geo_asset.dart delete mode 100644 lib/domain/rules/geo_assets_repository.dart create mode 100644 lib/features/geo_asset/data/geo_asset_data_mapper.dart create mode 100644 lib/features/geo_asset/data/geo_asset_data_providers.dart create mode 100644 lib/features/geo_asset/data/geo_asset_data_source.dart create mode 100644 lib/features/geo_asset/data/geo_asset_path_resolver.dart create mode 100644 lib/features/geo_asset/data/geo_asset_repository.dart create mode 100644 lib/features/geo_asset/model/default_geo_assets.dart create mode 100644 lib/features/geo_asset/model/geo_asset_entity.dart rename lib/{domain/rules => features/geo_asset/model}/geo_asset_failure.dart (97%) create mode 100644 lib/features/geo_asset/notifier/geo_asset_notifier.dart create mode 100644 lib/features/geo_asset/overview/geo_assets_overview_notifier.dart rename lib/features/{settings/geo_assets/geo_assets_page.dart => geo_asset/overview/geo_assets_overview_page.dart} (65%) rename lib/features/{settings/geo_assets => geo_asset/widget}/geo_asset_tile.dart (63%) delete mode 100644 lib/features/settings/geo_assets/geo_assets_notifier.dart diff --git a/build.yaml b/build.yaml index b33aa2b2..c34cb667 100644 --- a/build.yaml +++ b/build.yaml @@ -2,8 +2,6 @@ targets: $default: builders: drift_dev: - generate_for: - - "lib/data/local/**" options: store_date_time_values_as_text: true slang_build_runner: diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 4cfa3d18..7f4c42fe 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -12,6 +12,7 @@ import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/domain/environment.dart'; import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/services/deep_link_service.dart'; @@ -86,6 +87,7 @@ Future _lazyBootstrap( final filesEditor = container.read(filesEditorServiceProvider); await filesEditor.init(); + await container.read(geoAssetRepositoryProvider.future); initLoggers(container.read, debug); _logger.info(container.read(appInfoProvider).format()); diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart index 186f7ddd..3830b7c3 100644 --- a/lib/core/router/routes/desktop_routes.dart +++ b/lib/core/router/routes/desktop_routes.dart @@ -3,8 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'desktop_routes.g.dart'; @@ -117,7 +117,7 @@ class GeoAssetsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: GeoAssetsPage(), + child: GeoAssetsOverviewPage(), ); } } diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index 79c28024..5dcf0802 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -4,8 +4,8 @@ import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'mobile_routes.g.dart'; @@ -155,7 +155,7 @@ class GeoAssetsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: GeoAssetsPage(), + child: GeoAssetsOverviewPage(), ); } } diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index f1c47452..8ffd3327 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -4,18 +4,17 @@ import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/data/api/clash_api.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/dao/profiles_dao.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; -import 'package:hiddify/data/repository/geo_assets_repository.dart'; import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_assets_repository.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -70,38 +69,25 @@ AppRepository appRepository(AppRepositoryRef ref) => @Riverpod(keepAlive: true) ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort); -@Riverpod(keepAlive: true) -GeoAssetsDao geoAssetsDao(GeoAssetsDaoRef ref) => GeoAssetsDao( - ref.watch(appDatabaseProvider), - ); - -@Riverpod(keepAlive: true) -GeoAssetsRepository geoAssetsRepository(GeoAssetsRepositoryRef ref) { - return GeoAssetsRepositoryImpl( - geoAssetsDao: ref.watch(geoAssetsDaoProvider), - dio: ref.watch(dioProvider), - filesEditor: ref.watch(filesEditorServiceProvider), - ); -} - @riverpod Future configOptions(ConfigOptionsRef ref) async { final geoAssets = await ref - .watch(geoAssetsRepositoryProvider) + .watch(geoAssetRepositoryProvider) + .requireValue .getActivePair() .getOrElse((l) => throw l) .run(); - final filesEditor = ref.watch(filesEditorServiceProvider); + final geoAssetsPathResolver = ref.watch(geoAssetPathResolverProvider); final serviceMode = ref.watch(serviceModeStoreProvider); return ref.watch(configPreferencesProvider).copyWith( enableTun: serviceMode == ServiceMode.tun, setSystemProxy: serviceMode == ServiceMode.systemProxy, - geoipPath: filesEditor.geoAssetRelativePath( + geoipPath: geoAssetsPathResolver.relativePath( geoAssets.geoip.providerName, geoAssets.geoip.fileName, ), - geositePath: filesEditor.geoAssetRelativePath( + geositePath: geoAssetsPathResolver.relativePath( geoAssets.geosite.providerName, geoAssets.geosite.fileName, ), @@ -112,6 +98,7 @@ Future configOptions(ConfigOptionsRef ref) async { CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), + ref.watch(geoAssetPathResolverProvider), ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), diff --git a/lib/data/local/dao/dao.dart b/lib/data/local/dao/dao.dart deleted file mode 100644 index e267403f..00000000 --- a/lib/data/local/dao/dao.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'geo_assets_dao.dart'; -export 'profiles_dao.dart'; diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart deleted file mode 100644 index 25ef879a..00000000 --- a/lib/data/local/dao/geo_assets_dao.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/data_mappers.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/local/tables.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; - -part 'geo_assets_dao.g.dart'; - -@DriftAccessor(tables: [GeoAssetEntries]) -class GeoAssetsDao extends DatabaseAccessor - with _$GeoAssetsDaoMixin, InfraLogger { - GeoAssetsDao(super.db); - - Future add(GeoAsset geoAsset) async { - await into(geoAssetEntries).insert(geoAsset.toCompanion()); - } - - Future getActive(GeoAssetType type) async { - return (geoAssetEntries.select() - ..where((tbl) => tbl.active.equals(true)) - ..where((tbl) => tbl.type.equalsValue(type)) - ..limit(1)) - .map(GeoAssetMapper.fromEntry) - .getSingleOrNull(); - } - - Stream> watchAll() { - return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch(); - } - - Future edit(GeoAsset patch) async { - await transaction( - () async { - if (patch.active) { - await (update(geoAssetEntries) - ..where((tbl) => tbl.active.equals(true)) - ..where((tbl) => tbl.type.equalsValue(patch.type))) - .write(const GeoAssetEntriesCompanion(active: Value(false))); - } - await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id))) - .write(patch.toCompanion()); - }, - ); - } -} diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart index 571afd89..0646a749 100644 --- a/lib/data/local/data_mappers.dart +++ b/lib/data/local/data_mappers.dart @@ -1,7 +1,6 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; extension ProfileMapper on Profile { ProfileEntriesCompanion toCompanion() { @@ -72,29 +71,3 @@ extension ProfileMapper on Profile { }; } } - -extension GeoAssetMapper on GeoAsset { - GeoAssetEntriesCompanion toCompanion() { - return GeoAssetEntriesCompanion.insert( - id: id, - type: type, - active: active, - name: name, - providerName: providerName, - version: Value(version), - lastCheck: Value(lastCheck), - ); - } - - static GeoAsset fromEntry(GeoAssetEntry e) { - return GeoAsset( - id: e.id, - name: e.name, - type: e.type, - active: e.active, - providerName: e.providerName, - version: e.version, - lastCheck: e.lastCheck, - ); - } -} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index 2800f77e..f1ecaea9 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -2,22 +2,19 @@ import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; -import 'package:hiddify/data/local/data_mappers.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; +import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:path/path.dart' as p; part 'database.g.dart'; -@DriftDatabase( - tables: [ProfileEntries, GeoAssetEntries], - daos: [ProfilesDao, GeoAssetsDao], -) +@DriftDatabase(tables: [ProfileEntries, GeoAssetEntries]) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); @@ -57,7 +54,7 @@ class AppDatabase extends _$AppDatabase { Future _prePopulateGeoAssets() async { await transaction(() async { - final geoAssets = defaultGeoAssets.map((e) => e.toCompanion()); + final geoAssets = defaultGeoAssets.map((e) => e.toEntry()); for (final geoAsset in geoAssets) { await into(geoAssetEntries).insert(geoAsset); } diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index 18fe4cdc..f8a09291 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; @DataClassName('ProfileEntry') class ProfileEntries extends Table { diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index d254726c..15bdde09 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -10,6 +10,7 @@ import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -19,6 +20,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { CoreFacadeImpl( this.singbox, this.filesEditor, + this.geoAssetPathResolver, this.platformServices, this.clash, this.debug, @@ -27,6 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final SingboxService singbox; final FilesEditorService filesEditor; + final GeoAssetPathResolver geoAssetPathResolver; final PlatformServices platformServices; final ClashApi clash; final bool debug; @@ -38,8 +41,8 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { return exceptionHandler( () async { final options = await configOptions(); - final geoip = filesEditor.resolveGeoAssetPath(options.geoipPath); - final geosite = filesEditor.resolveGeoAssetPath(options.geositePath); + final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); + final geosite = geoAssetPathResolver.resolvePath(options.geositePath); if (!await File(geoip).exists() || !await File(geosite).exists()) { return left(const CoreMissingGeoAssets()); } diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart deleted file mode 100644 index 57c7c638..00000000 --- a/lib/data/repository/geo_assets_repository.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:io'; - -import 'package:dartx/dartx_io.dart'; -import 'package:dio/dio.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; -import 'package:hiddify/domain/rules/geo_assets_repository.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:watcher/watcher.dart'; - -class GeoAssetsRepositoryImpl - with ExceptionHandler, InfraLogger - implements GeoAssetsRepository { - GeoAssetsRepositoryImpl({ - required this.geoAssetsDao, - required this.dio, - required this.filesEditor, - }); - - final GeoAssetsDao geoAssetsDao; - final Dio dio; - final FilesEditorService filesEditor; - - @override - TaskEither - getActivePair() { - return exceptionHandler( - () async { - final geoip = await geoAssetsDao.getActive(GeoAssetType.geoip); - final geosite = await geoAssetsDao.getActive(GeoAssetType.geosite); - if (geoip == null || geosite == null) { - return left(const GeoAssetFailure.activeAssetNotFound()); - } - return right((geoip: geoip, geosite: geosite)); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - Stream>> watchAll() { - final persistedStream = geoAssetsDao.watchAll(); - final filesStream = _watchGeoFiles(); - - return Rx.combineLatest2( - persistedStream, - filesStream, - (assets, files) => assets.map( - (e) { - final path = filesEditor.geoAssetPath(e.providerName, e.fileName); - final file = files.firstOrNullWhere((e) => e.path == path); - final stat = file?.statSync(); - return (e, stat?.size); - }, - ).toList(), - ).handleExceptions(GeoAssetUnexpectedFailure.new); - } - - Iterable _geoFiles = []; - Stream> _watchGeoFiles() async* { - yield await _readGeoFiles(); - yield* Watcher( - filesEditor.geoAssetsDir.path, - pollingDelay: const Duration(seconds: 1), - ).events.asyncMap((event) async { - await _readGeoFiles(); - return _geoFiles; - }); - } - - Future> _readGeoFiles() async { - return _geoFiles = Directory(filesEditor.geoAssetsDir.path) - .listSync() - .whereType() - .where((e) => e.extension == '.db'); - } - - @override - TaskEither update(GeoAsset geoAsset) { - return exceptionHandler( - () async { - loggy.debug( - "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", - ); - final response = await dio.get(geoAsset.repositoryUrl); - if (response.statusCode != 200 || response.data == null) { - return left( - GeoAssetFailure.unexpected("invalid response", StackTrace.current), - ); - } - - final path = - filesEditor.geoAssetPath(geoAsset.providerName, geoAsset.name); - final tagName = response.data!['tag_name'] as String; - loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); - if (tagName == geoAsset.version && await File(path).exists()) { - await geoAssetsDao.edit(geoAsset.copyWith(lastCheck: DateTime.now())); - return left(const GeoAssetFailure.noUpdateAvailable()); - } - - final assets = (response.data!['assets'] as List) - .whereType>(); - final asset = - assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); - if (asset == null) { - return left( - GeoAssetFailure.unexpected( - "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", - StackTrace.current, - ), - ); - } - - final downloadUrl = asset["browser_download_url"] as String; - loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); - final tempPath = "$path.tmp"; - await File(path).parent.create(recursive: true); - await dio.download(downloadUrl, tempPath); - await File(tempPath).rename(path); - - await geoAssetsDao.edit( - geoAsset.copyWith( - version: tagName, - lastCheck: DateTime.now(), - ), - ); - - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - TaskEither markAsActive(GeoAsset geoAsset) { - return exceptionHandler( - () async { - await geoAssetsDao.edit(geoAsset.copyWith(active: true)); - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - TaskEither addRecommended() { - return exceptionHandler( - () async { - final persistedIds = await geoAssetsDao - .watchAll() - .first - .then((value) => value.map((e) => e.id)); - final missing = - recommendedGeoAssets.where((e) => !persistedIds.contains(e.id)); - for (final geoAsset in missing) { - await geoAssetsDao.add(geoAsset); - } - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } -} diff --git a/lib/data/repository/profiles_repository_impl.dart b/lib/data/repository/profiles_repository_impl.dart index cc05cd30..d73eb585 100644 --- a/lib/data/repository/profiles_repository_impl.dart +++ b/lib/data/repository/profiles_repository_impl.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/dao/dao.dart'; +import 'package:hiddify/data/local/dao/profiles_dao.dart'; import 'package:hiddify/data/repository/exception_handlers.dart'; import 'package:hiddify/domain/enums.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; diff --git a/lib/domain/rules/geo_asset.dart b/lib/domain/rules/geo_asset.dart deleted file mode 100644 index 409b7b82..00000000 --- a/lib/domain/rules/geo_asset.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'geo_asset.freezed.dart'; -part 'geo_asset.g.dart'; - -enum GeoAssetType { geoip, geosite } - -typedef GeoAssetWithFileSize = (GeoAsset geoAsset, int? size); - -@freezed -class GeoAsset with _$GeoAsset { - const GeoAsset._(); - - const factory GeoAsset({ - required String id, - required String name, - required GeoAssetType type, - required bool active, - required String providerName, - String? version, - DateTime? lastCheck, - }) = _GeoAsset; - - factory GeoAsset.fromJson(Map json) => - _$GeoAssetFromJson(json); - - String get fileName => name; - - String get repositoryUrl => - "https://api.github.com/repos/$providerName/releases/latest"; -} - -/// default geoip asset bundled with the app -const defaultGeoip = GeoAsset( - id: "sing-box-geoip", - name: "geoip.db", - type: GeoAssetType.geoip, - active: true, - providerName: "SagerNet/sing-geoip", -); - -/// default geosite asset bundled with the app -const defaultGeosite = GeoAsset( - id: "sing-box-geosite", - name: "geosite.db", - type: GeoAssetType.geosite, - active: true, - providerName: "SagerNet/sing-geosite", -); - -const defaultGeoAssets = [defaultGeoip, defaultGeosite]; - -const recommendedGeoAssets = [ - ...defaultGeoAssets, - GeoAsset( - id: "chocolate4U-geoip", - name: "geoip.db", - type: GeoAssetType.geoip, - active: false, - providerName: "Chocolate4U/Iran-sing-box-rules", - ), - GeoAsset( - id: "chocolate4U-geosite", - name: "geosite.db", - type: GeoAssetType.geosite, - active: false, - providerName: "Chocolate4U/Iran-sing-box-rules", - ), -]; diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart deleted file mode 100644 index 2e55632d..00000000 --- a/lib/domain/rules/geo_assets_repository.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; - -abstract interface class GeoAssetsRepository { - TaskEither - getActivePair(); - - Stream>> watchAll(); - - TaskEither update(GeoAsset geoAsset); - - TaskEither markAsActive(GeoAsset geoAsset); - - TaskEither addRecommended(); -} diff --git a/lib/features/geo_asset/data/geo_asset_data_mapper.dart b/lib/features/geo_asset/data/geo_asset_data_mapper.dart new file mode 100644 index 00000000..7906ac0e --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_mapper.dart @@ -0,0 +1,31 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; + +extension GeoAssetEntityMapper on GeoAssetEntity { + GeoAssetEntriesCompanion toEntry() { + return GeoAssetEntriesCompanion.insert( + id: id, + type: type, + active: active, + name: name, + providerName: providerName, + version: Value(version), + lastCheck: Value(lastCheck), + ); + } +} + +extension GeoAssetEntryMapper on GeoAssetEntry { + GeoAssetEntity toEntity() { + return GeoAssetEntity( + id: id, + name: name, + type: type, + active: active, + providerName: providerName, + version: version, + lastCheck: lastCheck, + ); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_data_providers.dart b/lib/features/geo_asset/data/geo_asset_data_providers.dart new file mode 100644 index 00000000..1e9be492 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_providers.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_asset_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +Future geoAssetRepository(GeoAssetRepositoryRef ref) async { + final repo = GeoAssetRepositoryImpl( + geoAssetDataSource: ref.watch(geoAssetDataSourceProvider), + geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), + dio: ref.watch(dioProvider), + ); + await repo.init().getOrElse((l) => throw l).run(); + return repo; +} + +@Riverpod(keepAlive: true) +GeoAssetDataSource geoAssetDataSource(GeoAssetDataSourceRef ref) { + return GeoAssetsDao(ref.watch(appDatabaseProvider)); +} + +@Riverpod(keepAlive: true) +GeoAssetPathResolver geoAssetPathResolver(GeoAssetPathResolverRef ref) { + return GeoAssetPathResolver( + ref.watch(filesEditorServiceProvider).dirs.workingDir, + ); +} diff --git a/lib/features/geo_asset/data/geo_asset_data_source.dart b/lib/features/geo_asset/data/geo_asset_data_source.dart new file mode 100644 index 00000000..b72b751d --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_source.dart @@ -0,0 +1,59 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +part 'geo_asset_data_source.g.dart'; + +abstract interface class GeoAssetDataSource { + Future insert(GeoAssetEntriesCompanion entry); + Future getActiveAssetByType(GeoAssetType type); + Stream> watchAll(); + Future patch(String id, GeoAssetEntriesCompanion entry); +} + +@DriftAccessor(tables: [GeoAssetEntries]) +class GeoAssetsDao extends DatabaseAccessor + with _$GeoAssetsDaoMixin, InfraLogger + implements GeoAssetDataSource { + GeoAssetsDao(super.db); + + @override + Future insert(GeoAssetEntriesCompanion entry) async { + await into(geoAssetEntries).insert(entry); + } + + @override + Future getActiveAssetByType(GeoAssetType type) async { + return (geoAssetEntries.select() + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(type)) + ..limit(1)) + .getSingleOrNull(); + } + + @override + Stream> watchAll() { + return geoAssetEntries.select().watch(); + } + + @override + Future patch(String id, GeoAssetEntriesCompanion entry) async { + await transaction( + () async { + if (entry.active.present && entry.active.value) { + final baseEntry = await (select(geoAssetEntries) + ..where((tbl) => tbl.id.equals(id))) + .getSingle(); + await (update(geoAssetEntries) + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(baseEntry.type))) + .write(const GeoAssetEntriesCompanion(active: Value(false))); + } + await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(id))) + .write(entry); + }, + ); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_path_resolver.dart b/lib/features/geo_asset/data/geo_asset_path_resolver.dart new file mode 100644 index 00000000..5dc1d117 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_path_resolver.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class GeoAssetPathResolver { + const GeoAssetPathResolver(this._workingDir); + + final Directory _workingDir; + + Directory get directory => Directory(p.join(_workingDir.path, "geo-assets")); + + File file(String providerName, String fileName) { + final prefix = providerName.replaceAll("/", "-").toLowerCase().trim(); + return File( + p.join( + directory.path, + "$prefix${prefix.isEmpty ? "" : "-"}$fileName", + ), + ); + } + + /// geoasset's path relative to working directory + String relativePath(String providerName, String fileName) { + final fullPath = file(providerName, fileName).path; + return p.relative(fullPath, from: _workingDir.path); + } + + String resolvePath(String path) { + return p.absolute(_workingDir.path, path); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_repository.dart b/lib/features/geo_asset/data/geo_asset_repository.dart new file mode 100644 index 00000000..74abb850 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_repository.dart @@ -0,0 +1,232 @@ +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart'; +import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:watcher/watcher.dart'; + +abstract interface class GeoAssetRepository { + /// populate bundled geo assets directory with bundled files if needed + TaskEither init(); + TaskEither + getActivePair(); + Stream>> watchAll(); + TaskEither update(GeoAssetEntity geoAsset); + TaskEither markAsActive(GeoAssetEntity geoAsset); + TaskEither addRecommended(); +} + +class GeoAssetRepositoryImpl + with ExceptionHandler, InfraLogger + implements GeoAssetRepository { + GeoAssetRepositoryImpl({ + required this.geoAssetDataSource, + required this.geoAssetPathResolver, + required this.dio, + }); + + final GeoAssetDataSource geoAssetDataSource; + final GeoAssetPathResolver geoAssetPathResolver; + final Dio dio; + + @override + TaskEither init() { + return exceptionHandler( + () async { + loggy.debug("initializing"); + final geoipFile = geoAssetPathResolver.file( + defaultGeoip.providerName, + defaultGeoip.fileName, + ); + final geositeFile = geoAssetPathResolver.file( + defaultGeosite.providerName, + defaultGeosite.fileName, + ); + + final dirExists = await geoAssetPathResolver.directory.exists(); + if (!dirExists) { + await geoAssetPathResolver.directory.create(recursive: true); + } + + if (!dirExists || !await geoipFile.exists()) { + final bundledGeoip = await rootBundle.load(Assets.core.geoip); + await geoipFile.writeAsBytes(bundledGeoip.buffer.asInt8List()); + } + if (!dirExists || !await geositeFile.exists()) { + final bundledGeosite = await rootBundle.load(Assets.core.geosite); + await geositeFile.writeAsBytes(bundledGeosite.buffer.asInt8List()); + } + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither + getActivePair() { + return exceptionHandler( + () async { + final geoip = + await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geoip); + final geosite = + await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geosite); + if (geoip == null || geosite == null) { + return left(const GeoAssetFailure.activeAssetNotFound()); + } + return right((geoip: geoip.toEntity(), geosite: geosite.toEntity())); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + Stream>> watchAll() { + final persistedStream = geoAssetDataSource + .watchAll() + .map((event) => event.map((e) => e.toEntity())); + final filesStream = _watchGeoFiles(); + + return Rx.combineLatest2( + persistedStream, + filesStream, + (assets, files) => assets.map( + (e) { + final path = + geoAssetPathResolver.file(e.providerName, e.fileName).path; + final file = files.firstOrNullWhere((e) => e.path == path); + final stat = file?.statSync(); + return (e, stat?.size); + }, + ).toList(), + ).handleExceptions(GeoAssetUnexpectedFailure.new); + } + + Iterable _geoFiles = []; + Stream> _watchGeoFiles() async* { + yield await _readGeoFiles(); + yield* Watcher( + geoAssetPathResolver.directory.path, + pollingDelay: const Duration(seconds: 1), + ).events.asyncMap((event) async { + await _readGeoFiles(); + return _geoFiles; + }); + } + + Future> _readGeoFiles() async { + return _geoFiles = Directory(geoAssetPathResolver.directory.path) + .listSync() + .whereType() + .where((e) => e.extension == '.db'); + } + + @override + TaskEither update(GeoAssetEntity geoAsset) { + return exceptionHandler( + () async { + loggy.debug( + "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + ); + final response = await dio.get(geoAsset.repositoryUrl); + if (response.statusCode != 200 || response.data == null) { + return left( + GeoAssetUnexpectedFailure.new( + "invalid response", + StackTrace.current, + ), + ); + } + + final file = + geoAssetPathResolver.file(geoAsset.providerName, geoAsset.name); + final tagName = response.data!['tag_name'] as String; + loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); + if (tagName == geoAsset.version && await file.exists()) { + await geoAssetDataSource.patch( + geoAsset.id, + GeoAssetEntriesCompanion(lastCheck: Value(DateTime.now())), + ); + return left(const GeoAssetFailure.noUpdateAvailable()); + } + + final assets = (response.data!['assets'] as List) + .whereType>(); + final asset = + assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); + if (asset == null) { + return left( + GeoAssetUnexpectedFailure.new( + "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + StackTrace.current, + ), + ); + } + + final downloadUrl = asset["browser_download_url"] as String; + loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); + final tempPath = "${file.path}.tmp"; + await file.parent.create(recursive: true); + await dio.download(downloadUrl, tempPath); + await File(tempPath).rename(file.path); + + await geoAssetDataSource.patch( + geoAsset.id, + GeoAssetEntriesCompanion( + version: Value(tagName), + lastCheck: Value(DateTime.now()), + ), + ); + + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither markAsActive(GeoAssetEntity geoAsset) { + return exceptionHandler( + () async { + await geoAssetDataSource.patch( + geoAsset.id, + const GeoAssetEntriesCompanion(active: Value(true)), + ); + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither addRecommended() { + return exceptionHandler( + () async { + final persistedIds = await geoAssetDataSource + .watchAll() + .first + .then((value) => value.map((e) => e.id)); + final missing = + recommendedGeoAssets.where((e) => !persistedIds.contains(e.id)); + for (final geoAsset in missing) { + await geoAssetDataSource.insert(geoAsset.toEntry()); + } + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/geo_asset/model/default_geo_assets.dart b/lib/features/geo_asset/model/default_geo_assets.dart new file mode 100644 index 00000000..df00d7a2 --- /dev/null +++ b/lib/features/geo_asset/model/default_geo_assets.dart @@ -0,0 +1,39 @@ +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; + +/// default geoip asset bundled with the app +const defaultGeoip = GeoAssetEntity( + id: "sing-box-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: true, + providerName: "SagerNet/sing-geoip", +); + +/// default geosite asset bundled with the app +const defaultGeosite = GeoAssetEntity( + id: "sing-box-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: true, + providerName: "SagerNet/sing-geosite", +); + +const defaultGeoAssets = [defaultGeoip, defaultGeosite]; + +const recommendedGeoAssets = [ + ...defaultGeoAssets, + GeoAssetEntity( + id: "chocolate4U-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: false, + providerName: "Chocolate4U/Iran-sing-box-rules", + ), + GeoAssetEntity( + id: "chocolate4U-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: false, + providerName: "Chocolate4U/Iran-sing-box-rules", + ), +]; diff --git a/lib/features/geo_asset/model/geo_asset_entity.dart b/lib/features/geo_asset/model/geo_asset_entity.dart new file mode 100644 index 00000000..44a3f9fd --- /dev/null +++ b/lib/features/geo_asset/model/geo_asset_entity.dart @@ -0,0 +1,27 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'geo_asset_entity.freezed.dart'; + +enum GeoAssetType { geoip, geosite } + +typedef GeoAssetWithFileSize = (GeoAssetEntity geoAsset, int? size); + +@freezed +class GeoAssetEntity with _$GeoAssetEntity { + const GeoAssetEntity._(); + + const factory GeoAssetEntity({ + required String id, + required String name, + required GeoAssetType type, + required bool active, + required String providerName, + String? version, + DateTime? lastCheck, + }) = _GeoAssetEntity; + + String get fileName => name; + + String get repositoryUrl => + "https://api.github.com/repos/$providerName/releases/latest"; +} diff --git a/lib/domain/rules/geo_asset_failure.dart b/lib/features/geo_asset/model/geo_asset_failure.dart similarity index 97% rename from lib/domain/rules/geo_asset_failure.dart rename to lib/features/geo_asset/model/geo_asset_failure.dart index 7beb8ef2..161b193e 100644 --- a/lib/domain/rules/geo_asset_failure.dart +++ b/lib/features/geo_asset/model/geo_asset_failure.dart @@ -6,7 +6,7 @@ part 'geo_asset_failure.freezed.dart'; @freezed sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { - const GeoAssetFailure._(); + const GeoAssetFailure._(); const factory GeoAssetFailure.unexpected([ Object? error, diff --git a/lib/features/geo_asset/notifier/geo_asset_notifier.dart b/lib/features/geo_asset/notifier/geo_asset_notifier.dart new file mode 100644 index 00000000..94f1ddac --- /dev/null +++ b/lib/features/geo_asset/notifier/geo_asset_notifier.dart @@ -0,0 +1,33 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_asset_notifier.g.dart'; + +@riverpod +class FetchGeoAsset extends _$FetchGeoAsset with AppLogger { + @override + Future build(String id) async { + ref.disposeDelay(const Duration(seconds: 10)); + return null; + } + + Future fetch(GeoAssetEntity geoAsset) async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref + .read(geoAssetRepositoryProvider) + .requireValue + .update(geoAsset) + .getOrElse( + (failure) { + loggy.warning("error updating geo asset $failure", failure); + throw failure; + }, + ).run(), + ); + } +} diff --git a/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart b/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart new file mode 100644 index 00000000..d3ead5b9 --- /dev/null +++ b/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart @@ -0,0 +1,43 @@ +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'geo_assets_overview_notifier.g.dart'; + +@riverpod +class GeoAssetsOverviewNotifier extends _$GeoAssetsOverviewNotifier + with AppLogger { + @override + Stream> build() { + ref.disposeDelay(const Duration(seconds: 5)); + return ref + .watch(geoAssetRepositoryProvider) + .requireValue + .watchAll() + .map((event) => event.getOrElse((l) => throw l)); + } + + GeoAssetRepository get _geoAssetRepo => + ref.read(geoAssetRepositoryProvider).requireValue; + + Future markAsActive(GeoAssetEntity geoAsset) async { + await _geoAssetRepo.markAsActive(geoAsset).getOrElse( + (f) { + loggy.warning("error marking geo asset as active", f); + throw f; + }, + ).run(); + } + + Future addRecommended() async { + await _geoAssetRepo.addRecommended().getOrElse( + (f) { + loggy.warning("error adding recommended geo assets", f); + throw f; + }, + ).run(); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_page.dart b/lib/features/geo_asset/overview/geo_assets_overview_page.dart similarity index 65% rename from lib/features/settings/geo_assets/geo_assets_page.dart rename to lib/features/geo_asset/overview/geo_assets_overview_page.dart index b53aedf1..a611fabd 100644 --- a/lib/features/settings/geo_assets/geo_assets_page.dart +++ b/lib/features/geo_asset/overview/geo_assets_overview_page.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart'; +import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class GeoAssetsPage extends HookConsumerWidget { - const GeoAssetsPage({super.key}); +class GeoAssetsOverviewPage extends HookConsumerWidget { + const GeoAssetsOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final state = ref.watch(geoAssetsNotifierProvider); + final state = ref.watch(geoAssetsOverviewNotifierProvider); return Scaffold( body: CustomScrollView( @@ -25,7 +25,7 @@ class GeoAssetsPage extends HookConsumerWidget { child: Text(t.settings.geoAssets.addRecommended), onTap: () { ref - .read(geoAssetsNotifierProvider.notifier) + .read(geoAssetsOverviewNotifierProvider.notifier) .addRecommended(); }, ), @@ -38,7 +38,12 @@ class GeoAssetsPage extends HookConsumerWidget { AsyncData(value: final geoAssets) => SliverList.builder( itemBuilder: (context, index) { final geoAsset = geoAssets[index]; - return GeoAssetTile(geoAsset); + return GeoAssetTile( + geoAsset, + onMarkAsActive: () => ref + .read(geoAssetsOverviewNotifierProvider.notifier) + .markAsActive(geoAsset.$1), + ); }, itemCount: geoAssets.length, ), diff --git a/lib/features/settings/geo_assets/geo_asset_tile.dart b/lib/features/geo_asset/widget/geo_asset_tile.dart similarity index 63% rename from lib/features/settings/geo_assets/geo_asset_tile.dart rename to lib/features/geo_asset/widget/geo_asset_tile.dart index db1f6de0..620f1a21 100644 --- a/lib/features/settings/geo_assets/geo_asset_tile.dart +++ b/lib/features/geo_asset/widget/geo_asset_tile.dart @@ -2,40 +2,44 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; -import 'package:hiddify/utils/alerts.dart'; -import 'package:hiddify/utils/async_mutation.dart'; -import 'package:hiddify/utils/date_time_formatter.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart'; +import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; class GeoAssetTile extends HookConsumerWidget { - GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key}) - : geoAsset = geoAssetWithFileSize.$1, + GeoAssetTile( + GeoAssetWithFileSize geoAssetWithFileSize, { + super.key, + required this.onMarkAsActive, + }) : geoAsset = geoAssetWithFileSize.$1, size = geoAssetWithFileSize.$2; - final GeoAsset geoAsset; + final GeoAssetEntity geoAsset; final int? size; + final VoidCallback onMarkAsActive; @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final fetchState = ref.watch(fetchGeoAssetProvider(geoAsset.id)); final fileMissing = size == null; - final updateMutation = useMutation( - initialOnFailure: (err) { - if (err case GeoAssetNoUpdateAvailable()) { - CustomToast(t.failure.geoAssets.notUpdate).show(context); - } else { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.settings.geoAssets.failureMsg), - ).show(context); + ref.listen( + fetchGeoAssetProvider(geoAsset.id), + (_, next) { + switch (next) { + case AsyncError(:final error): + if (error case GeoAssetNoUpdateAvailable()) { + return CustomToast(t.failure.geoAssets.notUpdate).show(context); + } + CustomAlertDialog.fromErr(t.presentError(error)).show(context); + case AsyncData(value: final _?): + CustomToast.success(t.settings.geoAssets.successMsg).show(context); } }, - initialOnSuccess: () => - CustomToast.success(t.settings.geoAssets.successMsg).show(context), ); return ListTile( @@ -49,7 +53,7 @@ class GeoAssetTile extends HookConsumerWidget { ), ), isThreeLine: true, - subtitle: updateMutation.state.isInProgress + subtitle: fetchState.isLoading ? const LinearProgressIndicator() : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -89,26 +93,15 @@ class GeoAssetTile extends HookConsumerWidget { ], ), selected: geoAsset.active, - onTap: () async { - await ref - .read(geoAssetsNotifierProvider.notifier) - .markAsActive(geoAsset); - }, + onTap: onMarkAsActive, trailing: PopupMenuButton( itemBuilder: (context) { return [ PopupMenuItem( - enabled: !updateMutation.state.isInProgress, - onTap: () { - if (updateMutation.state.isInProgress) { - return; - } - updateMutation.setFuture( - ref - .read(geoAssetsNotifierProvider.notifier) - .updateGeoAsset(geoAsset), - ); - }, + enabled: !fetchState.isLoading, + onTap: () => ref + .read(FetchGeoAssetProvider(geoAsset.id).notifier) + .fetch(geoAsset), child: fileMissing ? Text(t.settings.geoAssets.download) : Text(t.settings.geoAssets.update), diff --git a/lib/features/settings/geo_assets/geo_assets_notifier.dart b/lib/features/settings/geo_assets/geo_assets_notifier.dart deleted file mode 100644 index 0e1cb3db..00000000 --- a/lib/features/settings/geo_assets/geo_assets_notifier.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:hiddify/utils/riverpod_utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'geo_assets_notifier.g.dart'; - -@riverpod -class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger { - @override - Stream> build() { - ref.disposeDelay(const Duration(seconds: 5)); - return ref - .watch(geoAssetsRepositoryProvider) - .watchAll() - .map((event) => event.getOrElse((l) => throw l)); - } - - Future updateGeoAsset(GeoAsset geoAsset) async { - await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse( - (f) { - loggy.warning("error updating geo asset", f); - throw f; - }, - ).run(); - } - - Future markAsActive(GeoAsset geoAsset) async { - await ref - .read(geoAssetsRepositoryProvider) - .markAsActive(geoAsset) - .getOrElse( - (f) { - loggy.warning("error marking geo asset as active", f); - throw f; - }, - ).run(); - } - - Future addRecommended() async { - await ref.read(geoAssetsRepositoryProvider).addRecommended().getOrElse( - (f) { - loggy.warning("error adding recommended geo assets", f); - throw f; - }, - ).run(); - } -} diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index db048e81..8b6995d3 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,10 +1,6 @@ import 'dart:io'; -import 'package:dartx/dartx.dart'; -import 'package:flutter/services.dart'; import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:path/path.dart' as p; @@ -28,9 +24,6 @@ class FilesEditorService with InfraLogger { Directory(p.join(workingDir.path, Constants.configsFolderName)); Directory get logsDir => dirs.workingDir; - Directory get geoAssetsDir => - Directory(p.join(workingDir.path, "geo-assets")); - File get appLogsFile => File(p.join(logsDir.path, "app.log")); File get coreLogsFile => File(p.join(logsDir.path, "box.log")); @@ -53,9 +46,6 @@ class FilesEditorService with InfraLogger { if (!await configsDir.exists()) { await configsDir.create(recursive: true); } - if (!await geoAssetsDir.exists()) { - await geoAssetsDir.create(recursive: true); - } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -68,8 +58,6 @@ class FilesEditorService with InfraLogger { } else { await coreLogsFile.create(recursive: true); } - - await _populateGeoAssets(); } static Future getDatabaseDirectory() async { @@ -85,44 +73,9 @@ class FilesEditorService with InfraLogger { return p.join(configsDir.path, "$fileName.json"); } - String geoAssetPath(String providerName, String fileName) { - final prefix = providerName.replaceAll("/", "-").toLowerCase(); - return p.join( - geoAssetsDir.path, - "$prefix${prefix.isBlank ? "" : "-"}$fileName", - ); - } - - /// geoasset's path relative to working directory - String geoAssetRelativePath(String providerName, String fileName) { - final fullPath = geoAssetPath(providerName, fileName); - return p.relative(fullPath, from: workingDir.path); - } - - String resolveGeoAssetPath(String path) { - return p.absolute(workingDir.path, path); - } - String tempConfigPath(String fileName) => configPath("temp_$fileName"); Future deleteConfig(String fileName) { return File(configPath(fileName)).delete(); } - - Future _populateGeoAssets() async { - loggy.debug('populating geo assets'); - final geoipPath = - geoAssetPath(defaultGeoip.providerName, defaultGeoip.fileName); - if (!await File(geoipPath).exists()) { - final bundledGeoip = await rootBundle.load(Assets.core.geoip); - await File(geoipPath).writeAsBytes(bundledGeoip.buffer.asInt8List()); - } - - final geositePath = - geoAssetPath(defaultGeosite.providerName, defaultGeosite.fileName); - if (!await File(geositePath).exists()) { - final bundledGeosite = await rootBundle.load(Assets.core.geosite); - await File(geositePath).writeAsBytes(bundledGeosite.buffer.asInt8List()); - } - } } From e6c6ec59ad9acaded516d98790359b37080c3a01 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:07:29 +0330 Subject: [PATCH 18/22] Add soffchen geo assets --- .../geo_asset/model/default_geo_assets.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/features/geo_asset/model/default_geo_assets.dart b/lib/features/geo_asset/model/default_geo_assets.dart index df00d7a2..40b3aaa6 100644 --- a/lib/features/geo_asset/model/default_geo_assets.dart +++ b/lib/features/geo_asset/model/default_geo_assets.dart @@ -36,4 +36,18 @@ const recommendedGeoAssets = [ active: false, providerName: "Chocolate4U/Iran-sing-box-rules", ), + GeoAssetEntity( + id: "soffchen-geoip", + name: "geoip.db", + type: GeoAssetType.geoip, + active: false, + providerName: "soffchen/sing-geoip", + ), + GeoAssetEntity( + id: "soffchen-geosite", + name: "geosite.db", + type: GeoAssetType.geosite, + active: false, + providerName: "soffchen/sing-geosite", + ), ]; From 2a977f4142fa30353ead10c0827d01373ca55cec Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:48:22 +0330 Subject: [PATCH 19/22] Fix chinese typography bug --- ...trings_zh.i18n.json => strings_zh-CN.i18n.json} | 0 lib/core/prefs/locale_prefs.dart | 14 +++++++++++++- lib/features/common/general_pref_tiles.dart | 12 ++---------- 3 files changed, 15 insertions(+), 11 deletions(-) rename assets/translations/{strings_zh.i18n.json => strings_zh-CN.i18n.json} (100%) diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh-CN.i18n.json similarity index 100% rename from assets/translations/strings_zh.i18n.json rename to assets/translations/strings_zh-CN.i18n.json diff --git a/lib/core/prefs/locale_prefs.dart b/lib/core/prefs/locale_prefs.dart index 0f294743..95352687 100644 --- a/lib/core/prefs/locale_prefs.dart +++ b/lib/core/prefs/locale_prefs.dart @@ -1,3 +1,4 @@ +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/gen/fonts.gen.dart'; import 'package:hiddify/gen/translations.g.dart'; @@ -14,7 +15,13 @@ class LocaleNotifier extends _$LocaleNotifier { ref.watch(sharedPreferencesProvider), "locale", AppLocaleUtils.findDeviceLocale(), - mapFrom: AppLocale.values.byName, + mapFrom: (String value) { + // keep backward compatibility with chinese after changing zh to zh_CN + if (value == "zh") { + return AppLocale.zhCn; + } + return AppLocale.values.byName(value); + }, mapTo: (value) => value.name, ); @@ -30,4 +37,9 @@ class LocaleNotifier extends _$LocaleNotifier { extension AppLocaleX on AppLocale { String get preferredFontFamily => this == AppLocale.fa ? FontFamily.shabnam : ""; + + String get localeName => + LocaleNamesLocalizationsDelegate + .nativeLocaleNames[flutterLocale.toString()] ?? + name; } diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index 7114bafd..69a354cb 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; @@ -17,10 +16,7 @@ class LocalePrefTile extends HookConsumerWidget { return ListTile( title: Text(t.settings.general.locale), - subtitle: Text( - LocaleNamesLocalizationsDelegate.nativeLocaleNames[locale.name] ?? - locale.name, - ), + subtitle: Text(locale.localeName), leading: const Icon(Icons.language), onTap: () async { final selectedLocale = await showDialog( @@ -31,11 +27,7 @@ class LocalePrefTile extends HookConsumerWidget { children: AppLocale.values .map( (e) => RadioListTile( - title: Text( - LocaleNamesLocalizationsDelegate - .nativeLocaleNames[e.name] ?? - e.name, - ), + title: Text(e.localeName), value: e, groupValue: locale, onChanged: (e) => context.pop(e), From e2f5f511768aedff97056f0e02f8a35ddee639ac Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 25 Nov 2023 22:54:25 +0330 Subject: [PATCH 20/22] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ecc3bee..dd1f9043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Unreleased + +### New Features and Improvements + +- Added soffchen to recommended geo assets + +### Bug Fixes + +- Fixed geo assets bug where assets were deactivated +- Fixed Chinese typography bug (thanks to [betaxab](https://github.com/betaxab)) +- Fixed localization mistakes in Russian. [PR#189](https://github.com/hiddify/hiddify-next/pull/189) by [jomertix](https://github.com/jomertix) + ## [0.11.1] - 2023-11-19 ### Bug Fixes From 829d58a1a2637a431ec3491bf11d42114e4cd9e6 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 26 Nov 2023 21:20:58 +0330 Subject: [PATCH 21/22] Refactor profiles --- lib/bootstrap.dart | 4 +- .../in_app_notification_controller.dart | 97 +++++++++ lib/core/router/routes/shared_routes.dart | 12 +- lib/core/widget/custom_alert_dialog.dart | 50 +++++ lib/data/data_providers.dart | 18 +- lib/data/local/data_mappers.dart | 73 ------- lib/data/local/database.dart | 2 +- lib/data/local/tables.dart | 2 +- lib/data/repository/core_facade_impl.dart | 17 +- lib/data/repository/repository.dart | 1 - lib/domain/profiles/profile.dart | 174 --------------- lib/domain/profiles/profiles.dart | 4 - lib/domain/profiles/profiles_repository.dart | 37 ---- .../active_profile_notifier.dart | 18 -- .../has_any_profile_notifier.dart | 14 -- lib/features/common/app_update_notifier.dart | 22 -- lib/features/common/common_controllers.dart | 10 +- .../connectivity/connectivity_controller.dart | 4 +- lib/features/home/view/home_page.dart | 5 +- .../add}/add_profile_modal.dart | 68 ++---- .../profile/data/profile_data_mapper.dart | 85 ++++++++ .../profile/data/profile_data_providers.dart | 32 +++ .../profile/data/profile_data_source.dart} | 89 ++++---- lib/features/profile/data/profile_parser.dart | 105 +++++++++ .../profile/data/profile_path_resolver.dart | 17 ++ .../profile/data/profile_repository.dart} | 203 ++++++++++++------ .../details/profile_details_notifier.dart} | 91 ++++---- .../details/profile_details_page.dart} | 101 +++++---- .../details/profile_details_state.dart | 22 ++ .../profile/model/profile_entity.dart | 57 +++++ .../profile/model/profile_failure.dart} | 2 +- .../profile/model/profile_sort_enum.dart} | 2 + .../notifier/active_profile_notifier.dart | 31 +++ .../profile/notifier/profile_notifier.dart | 140 ++++++++++++ .../notifier/profiles_update_notifier.dart | 92 ++++++++ .../overview/profiles_overview_notifier.dart | 83 +++++++ .../overview/profiles_overview_page.dart} | 31 ++- .../widget}/profile_tile.dart | 68 ++---- .../profile_detail/notifier/notifier.dart | 2 - .../notifier/profile_detail_state.dart | 22 -- lib/features/profile_detail/view/view.dart | 1 - lib/features/profiles/notifier/notifier.dart | 2 - .../profiles/notifier/profiles_notifier.dart | 140 ------------ .../notifier/profiles_update_notifier.dart | 55 ----- lib/features/profiles/view/view.dart | 2 - lib/services/cron_service.dart | 73 ------- lib/services/files_editor_service.dart | 16 -- lib/services/service_providers.dart | 9 - .../profile/data/profile_parser_test.dart} | 25 ++- 49 files changed, 1206 insertions(+), 1024 deletions(-) create mode 100644 lib/core/notification/in_app_notification_controller.dart create mode 100644 lib/core/widget/custom_alert_dialog.dart delete mode 100644 lib/data/local/data_mappers.dart delete mode 100644 lib/domain/profiles/profile.dart delete mode 100644 lib/domain/profiles/profiles.dart delete mode 100644 lib/domain/profiles/profiles_repository.dart delete mode 100644 lib/features/common/active_profile/active_profile_notifier.dart delete mode 100644 lib/features/common/active_profile/has_any_profile_notifier.dart rename lib/features/{profiles/view => profile/add}/add_profile_modal.dart (80%) create mode 100644 lib/features/profile/data/profile_data_mapper.dart create mode 100644 lib/features/profile/data/profile_data_providers.dart rename lib/{data/local/dao/profiles_dao.dart => features/profile/data/profile_data_source.dart} (57%) create mode 100644 lib/features/profile/data/profile_parser.dart create mode 100644 lib/features/profile/data/profile_path_resolver.dart rename lib/{data/repository/profiles_repository_impl.dart => features/profile/data/profile_repository.dart} (53%) rename lib/features/{profile_detail/notifier/profile_detail_notifier.dart => profile/details/profile_details_notifier.dart} (60%) rename lib/features/{profile_detail/view/profile_detail_page.dart => profile/details/profile_details_page.dart} (79%) create mode 100644 lib/features/profile/details/profile_details_state.dart create mode 100644 lib/features/profile/model/profile_entity.dart rename lib/{domain/profiles/profiles_failure.dart => features/profile/model/profile_failure.dart} (97%) rename lib/{domain/profiles/profile_enums.dart => features/profile/model/profile_sort_enum.dart} (91%) create mode 100644 lib/features/profile/notifier/active_profile_notifier.dart create mode 100644 lib/features/profile/notifier/profile_notifier.dart create mode 100644 lib/features/profile/notifier/profiles_update_notifier.dart create mode 100644 lib/features/profile/overview/profiles_overview_notifier.dart rename lib/features/{profiles/view/profiles_modal.dart => profile/overview/profiles_overview_page.dart} (81%) rename lib/features/{common => profile/widget}/profile_tile.dart (87%) delete mode 100644 lib/features/profile_detail/notifier/notifier.dart delete mode 100644 lib/features/profile_detail/notifier/profile_detail_state.dart delete mode 100644 lib/features/profile_detail/view/view.dart delete mode 100644 lib/features/profiles/notifier/notifier.dart delete mode 100644 lib/features/profiles/notifier/profiles_notifier.dart delete mode 100644 lib/features/profiles/notifier/profiles_update_notifier.dart delete mode 100644 lib/features/profiles/view/view.dart delete mode 100644 lib/services/cron_service.dart rename test/{domain/profiles/profile_test.dart => features/profile/data/profile_parser_test.dart} (69%) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 7f4c42fe..465d22d6 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -10,9 +10,10 @@ import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/domain/environment.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/services/deep_link_service.dart'; @@ -88,6 +89,7 @@ Future _lazyBootstrap( final filesEditor = container.read(filesEditorServiceProvider); await filesEditor.init(); await container.read(geoAssetRepositoryProvider.future); + await container.read(profileRepositoryProvider.future); initLoggers(container.read, debug); _logger.info(container.read(appInfoProvider).format()); diff --git a/lib/core/notification/in_app_notification_controller.dart b/lib/core/notification/in_app_notification_controller.dart new file mode 100644 index 00000000..1894e13e --- /dev/null +++ b/lib/core/notification/in_app_notification_controller.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:toastification/toastification.dart'; + +part 'in_app_notification_controller.g.dart'; + +@Riverpod(keepAlive: true) +InAppNotificationController inAppNotificationController( + InAppNotificationControllerRef ref, +) { + return InAppNotificationController(); +} + +enum NotificationType { + info, + error, + success, +} + +class InAppNotificationController with AppLogger { + void showToast( + BuildContext context, + String message, { + NotificationType type = NotificationType.info, + Duration duration = const Duration(seconds: 3), + }) { + toastification.show( + context: context, + title: message, + type: type._toastificationType, + alignment: Alignment.bottomLeft, + autoCloseDuration: duration, + style: ToastificationStyle.fillColored, + pauseOnHover: true, + showProgressBar: false, + dragToClose: true, + closeOnClick: true, + closeButtonShowType: CloseButtonShowType.onHover, + ); + } + + void showErrorToast(String message) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + showToast( + context, + message, + type: NotificationType.error, + duration: const Duration(seconds: 5), + ); + } + + void showSuccessToast(String message) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + showToast( + context, + message, + type: NotificationType.success, + ); + } + + void showInfoToast(String message) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + showToast(context, message); + } + + Future showErrorDialog(PresentableError error) async { + final context = RootScaffold.stateKey.currentContext; + if (context == null) { + loggy.warning("context is null"); + return; + } + CustomAlertDialog.fromErr(error).show(context); + } +} + +extension NotificationTypeX on NotificationType { + ToastificationType get _toastificationType => switch (this) { + NotificationType.success => ToastificationType.success, + NotificationType.error => ToastificationType.error, + NotificationType.info => ToastificationType.info, + }; +} diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart index 76410f5f..1960b7cc 100644 --- a/lib/core/router/routes/shared_routes.dart +++ b/lib/core/router/routes/shared_routes.dart @@ -3,8 +3,9 @@ import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/features/home/view/view.dart'; import 'package:hiddify/features/intro/intro_page.dart'; -import 'package:hiddify/features/profile_detail/view/view.dart'; -import 'package:hiddify/features/profiles/view/view.dart'; +import 'package:hiddify/features/profile/add/add_profile_modal.dart'; +import 'package:hiddify/features/profile/details/profile_details_page.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; import 'package:hiddify/features/proxies/view/view.dart'; import 'package:hiddify/utils/utils.dart'; @@ -86,7 +87,8 @@ class ProfilesRoute extends GoRouteData { Page buildPage(BuildContext context, GoRouterState state) { return BottomSheetPage( name: name, - builder: (controller) => ProfilesModal(scrollController: controller), + builder: (controller) => + ProfilesOverviewModal(scrollController: controller), ); } } @@ -103,7 +105,7 @@ class NewProfileRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: ProfileDetailPage("new"), + child: ProfileDetailsPage("new"), ); } } @@ -121,7 +123,7 @@ class ProfileDetailsRoute extends GoRouteData { return MaterialPage( fullscreenDialog: true, name: name, - child: ProfileDetailPage(id), + child: ProfileDetailsPage(id), ); } } diff --git a/lib/core/widget/custom_alert_dialog.dart b/lib/core/widget/custom_alert_dialog.dart new file mode 100644 index 00000000..ad96ed1f --- /dev/null +++ b/lib/core/widget/custom_alert_dialog.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/domain/failures.dart'; + +class CustomAlertDialog extends StatelessWidget { + const CustomAlertDialog({ + super.key, + this.title, + required this.message, + }); + + final String? title; + final String message; + + factory CustomAlertDialog.fromError(PresentableError error) => + CustomAlertDialog( + title: error.message == null ? null : error.type, + message: error.message ?? error.type, + ); + + Future show(BuildContext context) async { + await showDialog( + context: context, + useRootNavigator: true, + builder: (context) => this, + ); + } + + @override + Widget build(BuildContext context) { + final localizations = MaterialLocalizations.of(context); + + return AlertDialog( + title: title != null ? Text(title!) : null, + content: SingleChildScrollView( + child: SizedBox( + width: 468, + child: Text(message), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.okButtonLabel), + ), + ], + ); + } +} diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index 8ffd3327..d70bef5b 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -4,7 +4,6 @@ import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/data/api/clash_api.dart'; -import 'package:hiddify/data/local/dao/profiles_dao.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/config_options_store.dart'; @@ -12,9 +11,9 @@ import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -48,20 +47,6 @@ Dio dio(DioRef ref) { return dio; } -@Riverpod(keepAlive: true) -ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao( - ref.watch(appDatabaseProvider), - ); - -@Riverpod(keepAlive: true) -ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) => - ProfilesRepositoryImpl( - profilesDao: ref.watch(profilesDaoProvider), - filesEditor: ref.watch(filesEditorServiceProvider), - singbox: ref.watch(coreFacadeProvider), - dio: ref.watch(dioProvider), - ); - @Riverpod(keepAlive: true) AppRepository appRepository(AppRepositoryRef ref) => AppRepositoryImpl(ref.watch(dioProvider)); @@ -99,6 +84,7 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), ref.watch(geoAssetPathResolverProvider), + ref.watch(profilePathResolverProvider), ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart deleted file mode 100644 index 0646a749..00000000 --- a/lib/data/local/data_mappers.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; - -extension ProfileMapper on Profile { - ProfileEntriesCompanion toCompanion() { - return switch (this) { - RemoteProfile(:final url, :final options, :final subInfo) => - ProfileEntriesCompanion.insert( - id: id, - type: ProfileType.remote, - active: active, - name: name, - url: Value(url), - lastUpdate: lastUpdate, - updateInterval: Value(options?.updateInterval), - upload: Value(subInfo?.upload), - download: Value(subInfo?.download), - total: Value(subInfo?.total), - expire: Value(subInfo?.expire), - webPageUrl: Value(subInfo?.webPageUrl), - supportUrl: Value(subInfo?.supportUrl), - ), - LocalProfile() => ProfileEntriesCompanion.insert( - id: id, - type: ProfileType.local, - active: active, - name: name, - lastUpdate: lastUpdate, - ), - }; - } - - static Profile fromEntry(ProfileEntry e) { - ProfileOptions? options; - if (e.updateInterval != null) { - options = ProfileOptions(updateInterval: e.updateInterval!); - } - - SubscriptionInfo? subInfo; - if (e.upload != null && - e.download != null && - e.total != null && - e.expire != null) { - subInfo = SubscriptionInfo( - upload: e.upload!, - download: e.download!, - total: e.total!, - expire: e.expire!, - webPageUrl: e.webPageUrl, - supportUrl: e.supportUrl, - ); - } - - return switch (e.type) { - ProfileType.remote => RemoteProfile( - id: e.id, - active: e.active, - name: e.name, - url: e.url!, - lastUpdate: e.lastUpdate, - options: options, - subInfo: subInfo, - ), - ProfileType.local => LocalProfile( - id: e.id, - active: e.active, - name: e.name, - lastUpdate: e.lastUpdate, - ), - }; - } -} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index f1ecaea9..e18897d6 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -5,10 +5,10 @@ import 'package:drift/native.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart'; import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:path/path.dart' as p; diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index f8a09291..ab1d9a59 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; @DataClassName('ProfileEntry') class ProfileEntries extends Table { diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index 15bdde09..be953ec1 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -11,6 +11,7 @@ import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -21,6 +22,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { this.singbox, this.filesEditor, this.geoAssetPathResolver, + this.profilePathResolver, this.platformServices, this.clash, this.debug, @@ -30,6 +32,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final SingboxService singbox; final FilesEditorService filesEditor; final GeoAssetPathResolver geoAssetPathResolver; + final ProfilePathResolver profilePathResolver; final PlatformServices platformServices; final ClashApi clash; final bool debug; @@ -115,12 +118,14 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ) { return TaskEither.Do( ($) async { - final configPath = filesEditor.configPath(fileName); + final configFile = profilePathResolver.file(fileName); final options = await $(_getConfigOptions()); await $(setup()); await $(changeConfigOptions(options)); return await $( - singbox.generateConfig(configPath).mapLeft(CoreServiceFailure.other), + singbox + .generateConfig(configFile.path) + .mapLeft(CoreServiceFailure.other), ); }, ).handleExceptions(CoreServiceFailure.unexpected); @@ -133,7 +138,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ) { return TaskEither.Do( ($) async { - final configPath = filesEditor.configPath(fileName); + final configFile = profilePathResolver.file(fileName); final options = await $(_getConfigOptions()); loggy.info( "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", @@ -155,7 +160,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { await $(changeConfigOptions(options)); return await $( singbox - .start(configPath, disableMemoryLimit) + .start(configFile.path, disableMemoryLimit) .mapLeft(CoreServiceFailure.start), ); }, @@ -177,12 +182,12 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ) { return exceptionHandler( () async { - final configPath = filesEditor.configPath(fileName); + final configFile = profilePathResolver.file(fileName); return _getConfigOptions() .flatMap((options) => changeConfigOptions(options)) .andThen( () => singbox - .restart(configPath, disableMemoryLimit) + .restart(configFile.path, disableMemoryLimit) .mapLeft(CoreServiceFailure.start), ) .run(); diff --git a/lib/data/repository/repository.dart b/lib/data/repository/repository.dart index 4f454cc4..a5687644 100644 --- a/lib/data/repository/repository.dart +++ b/lib/data/repository/repository.dart @@ -1,2 +1 @@ export 'core_facade_impl.dart'; -export 'profiles_repository_impl.dart'; diff --git a/lib/domain/profiles/profile.dart b/lib/domain/profiles/profile.dart deleted file mode 100644 index c1f826e3..00000000 --- a/lib/domain/profiles/profile.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:convert'; - -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:loggy/loggy.dart'; -import 'package:uuid/uuid.dart'; - -part 'profile.freezed.dart'; -part 'profile.g.dart'; - -final _loggy = Loggy('Profile'); - -enum ProfileType { remote, local } - -@freezed -sealed class Profile with _$Profile { - const Profile._(); - - const factory Profile.remote({ - required String id, - required bool active, - required String name, - required String url, - required DateTime lastUpdate, - ProfileOptions? options, - SubscriptionInfo? subInfo, - }) = RemoteProfile; - - const factory Profile.local({ - required String id, - required bool active, - required String name, - required DateTime lastUpdate, - }) = LocalProfile; - - // ignore: prefer_constructors_over_static_methods - static RemoteProfile fromResponse( - String url, - Map> headers, - ) { - _loggy.debug("Profile Headers: $headers"); - - final titleHeader = headers['profile-title']?.single; - var title = ''; - if (titleHeader != null) { - if (titleHeader.startsWith("base64:")) { - // TODO handle errors - title = - utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); - } else { - title = titleHeader; - } - } - - if (title.isEmpty) { - final contentDisposition = headers['content-disposition']?.single; - if (contentDisposition != null) { - final RegExp regExp = RegExp('filename="([^"]*)"'); - final match = regExp.firstMatch(contentDisposition); - if (match != null && match.groupCount >= 1) { - title = match.group(1) ?? ''; - } - } - } - if (title.isEmpty) { - final part = url.split("#").lastOrNull; - if (part != null) { - title = part; - } - } - if (title.isEmpty) { - final part = url.split("/").lastOrNull; - if (part != null) { - final pattern = RegExp(r"\.(json|yaml|yml|txt)[\s\S]*"); - title = part.replaceFirst(pattern, ""); - } - } - - final updateIntervalHeader = headers['profile-update-interval']?.single; - ProfileOptions? options; - if (updateIntervalHeader != null) { - final updateInterval = Duration(hours: int.parse(updateIntervalHeader)); - options = ProfileOptions(updateInterval: updateInterval); - } - - final subscriptionInfoHeader = headers['subscription-userinfo']?.single; - SubscriptionInfo? subInfo; - if (subscriptionInfoHeader != null) { - subInfo = SubscriptionInfo.fromResponseHeader(subscriptionInfoHeader); - } - - final webPageUrlHeader = headers['profile-web-page-url']?.single; - final supportUrlHeader = headers['support-url']?.single; - if (subInfo != null) { - subInfo = subInfo.copyWith( - webPageUrl: isUrl(webPageUrlHeader ?? "") ? webPageUrlHeader : null, - supportUrl: isUrl(supportUrlHeader ?? "") ? supportUrlHeader : null, - ); - } - - return RemoteProfile( - id: const Uuid().v4(), - active: false, - name: title.isBlank ? "Remote Profile" : title, - url: url, - lastUpdate: DateTime.now(), - options: options, - subInfo: subInfo, - ); - } - - factory Profile.fromJson(Map json) => - _$ProfileFromJson(json); -} - -@freezed -class ProfileOptions with _$ProfileOptions { - const factory ProfileOptions({ - required Duration updateInterval, - }) = _ProfileOptions; - - factory ProfileOptions.fromJson(Map json) => - _$ProfileOptionsFromJson(json); -} - -@freezed -class SubscriptionInfo with _$SubscriptionInfo { - const SubscriptionInfo._(); - - const factory SubscriptionInfo({ - required int upload, - required int download, - @JsonKey(fromJson: _fromJsonTotal, defaultValue: 9223372036854775807) - required int total, - @JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) required DateTime expire, - String? webPageUrl, - String? supportUrl, - }) = _SubscriptionInfo; - - bool get isExpired => expire <= DateTime.now(); - - int get consumption => upload + download; - - double get ratio => (consumption / total).clamp(0, 1); - - Duration get remaining => expire.difference(DateTime.now()); - - factory SubscriptionInfo.fromResponseHeader(String header) { - final values = header.split(';'); - final map = { - for (final v in values) - v.split('=').first.trim(): - num.tryParse(v.split('=').second.trim())?.toInt(), - }; - _loggy.debug("Subscription Info: $map"); - return SubscriptionInfo.fromJson(map); - } - - factory SubscriptionInfo.fromJson(Map json) => - _$SubscriptionInfoFromJson(json); -} - -int _fromJsonTotal(dynamic total) { - final totalInt = total as int? ?? -1; - return totalInt > 0 ? totalInt : 9223372036854775807; -} - -DateTime _dateTimeFromSecondsSinceEpoch(dynamic expire) { - final expireInt = expire as int? ?? -1; - return DateTime.fromMillisecondsSinceEpoch( - (expireInt > 0 ? expireInt : 92233720368) * 1000, - ); -} diff --git a/lib/domain/profiles/profiles.dart b/lib/domain/profiles/profiles.dart deleted file mode 100644 index fb63afe8..00000000 --- a/lib/domain/profiles/profiles.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'profile.dart'; -export 'profile_enums.dart'; -export 'profiles_failure.dart'; -export 'profiles_repository.dart'; diff --git a/lib/domain/profiles/profiles_repository.dart b/lib/domain/profiles/profiles_repository.dart deleted file mode 100644 index 7476d2a2..00000000 --- a/lib/domain/profiles/profiles_repository.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; - -abstract class ProfilesRepository { - TaskEither get(String id); - - Stream> watchActiveProfile(); - - Stream> watchHasAnyProfile(); - - Stream>> watchAll({ - ProfilesSort sort = ProfilesSort.lastUpdate, - SortMode mode = SortMode.ascending, - }); - - TaskEither addByUrl( - String url, { - bool markAsActive = false, - }); - - TaskEither addByContent( - String content, { - required String name, - bool markAsActive = false, - }); - - TaskEither add(RemoteProfile baseProfile); - - TaskEither update(RemoteProfile baseProfile); - - TaskEither edit(Profile profile); - - TaskEither setAsActive(String id); - - TaskEither delete(String id); -} diff --git a/lib/features/common/active_profile/active_profile_notifier.dart b/lib/features/common/active_profile/active_profile_notifier.dart deleted file mode 100644 index af11965d..00000000 --- a/lib/features/common/active_profile/active_profile_notifier.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'active_profile_notifier.g.dart'; - -@Riverpod(keepAlive: true) -class ActiveProfile extends _$ActiveProfile with AppLogger { - @override - Stream build() { - loggy.debug("watching active profile"); - return ref - .watch(profilesRepositoryProvider) - .watchActiveProfile() - .map((event) => event.getOrElse((l) => throw l)); - } -} diff --git a/lib/features/common/active_profile/has_any_profile_notifier.dart b/lib/features/common/active_profile/has_any_profile_notifier.dart deleted file mode 100644 index 8ac28b21..00000000 --- a/lib/features/common/active_profile/has_any_profile_notifier.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'has_any_profile_notifier.g.dart'; - -@Riverpod(keepAlive: true) -Stream hasAnyProfile( - HasAnyProfileRef ref, -) { - return ref - .watch(profilesRepositoryProvider) - .watchHasAnyProfile() - .map((event) => event.getOrElse((l) => throw l)); -} diff --git a/lib/features/common/app_update_notifier.dart b/lib/features/common/app_update_notifier.dart index 669c718a..a3a39092 100644 --- a/lib/features/common/app_update_notifier.dart +++ b/lib/features/common/app_update_notifier.dart @@ -100,26 +100,4 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { await _ignoreReleasePref.update(versionInfo.version); state = AppUpdateStateIgnored(versionInfo); } - - // Future _schedule() async { - // loggy.debug("scheduling app update checker"); - // return ref.read(cronServiceProvider).schedule( - // key: 'app_update', - // duration: const Duration(hours: 8), - // callback: () async { - // await Future.delayed(const Duration(seconds: 5)); - // final updateState = await check(); - // final context = rootNavigatorKey.currentContext; - // if (context != null && context.mounted) { - // if (updateState - // case AppUpdateStateAvailable(:final versionInfo)) { - // await NewVersionDialog( - // ref.read(appInfoProvider).presentVersion, - // versionInfo, - // ).show(context); - // } - // } - // }, - // ); - // } } diff --git a/lib/features/common/common_controllers.dart b/lib/features/common/common_controllers.dart index 3854b879..a8172a49 100644 --- a/lib/features/common/common_controllers.dart +++ b/lib/features/common/common_controllers.dart @@ -1,9 +1,8 @@ import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; -import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/utils/platform_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -17,7 +16,7 @@ void commonControllers(CommonControllersRef ref) { introCompletedProvider, (_, completed) async { if (completed) { - await ref.read(cronServiceProvider).startScheduler(); + await ref.read(foregroundProfilesUpdateNotifierProvider.future); } }, fireImmediately: true, @@ -27,11 +26,6 @@ void commonControllers(CommonControllersRef ref) { (previous, next) {}, fireImmediately: true, ); - ref.listen( - profilesUpdateNotifierProvider, - (previous, next) {}, - fireImmediately: true, - ); if (PlatformUtils.isDesktop) { ref.listen( windowControllerProvider, diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/common/connectivity/connectivity_controller.dart index cea73393..8f835b6b 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/common/connectivity/connectivity_controller.dart @@ -3,7 +3,7 @@ import 'package:hiddify/core/prefs/service_prefs.dart'; import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; @@ -25,7 +25,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { }, ); return _core.watchConnectionStatus().doOnData((event) { - if (event case Disconnected(:final connectionFailure?) + if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) { ref.read(startedByUserProvider.notifier).update(false); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index e3ed99ba..86512ad8 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -3,11 +3,10 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; -import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/common/profile_tile.dart'; import 'package:hiddify/features/home/widgets/widgets.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; +import 'package:hiddify/features/profile/widget/profile_tile.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/profiles/view/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart similarity index 80% rename from lib/features/profiles/view/add_profile_modal.dart rename to lib/features/profile/add/add_profile_modal.dart index a0ce2ecb..11217720 100644 --- a/lib/features/profiles/view/add_profile_modal.dart +++ b/lib/features/profile/add/add_profile_modal.dart @@ -5,10 +5,8 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/qr_code_scanner_screen.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -25,40 +23,26 @@ class AddProfileModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final addProfileState = ref.watch(addProfileProvider); - final mutationTriggered = useState(false); - final addProfileMutation = useMutation( - initialOnFailure: (err) { - mutationTriggered.value = false; - if (err case ProfileInvalidUrlFailure()) { - CustomToast.error( - t.failure.profiles.invalidUrl, - ).show(context); - } else { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.profile.add.failureMsg), - ).show(context); + ref.listen( + addProfileProvider, + (previous, next) { + if (next case AsyncData(value: final _?)) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); } }, - initialOnSuccess: () { - CustomToast.success(t.profile.save.successMsg).show(context); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (context.mounted) context.pop(); - }, - ); - }, ); - final showProgressIndicator = - addProfileMutation.state.isInProgress || mutationTriggered.value; - useMemoized(() async { await Future.delayed(const Duration(milliseconds: 200)); if (url != null && context.mounted) { - addProfileMutation.setFuture( - ref.read(profilesNotifierProvider.notifier).addProfile(url!), - ); + if (addProfileState.isLoading) return; + ref.read(addProfileProvider.notifier).add(url!); } }); @@ -112,13 +96,10 @@ class AddProfileModal extends HookConsumerWidget { final captureResult = await Clipboard.getData(Clipboard.kTextPlain) .then((value) => value?.text ?? ''); - if (addProfileMutation.state.isInProgress) return; - mutationTriggered.value = true; - addProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .addProfile(captureResult), - ); + if (addProfileState.isLoading) return; + ref + .read(addProfileProvider.notifier) + .add(captureResult); }, ), const Gap(buttonsGap), @@ -133,15 +114,10 @@ class AddProfileModal extends HookConsumerWidget { await const QRCodeScannerScreen() .open(context); if (captureResult == null) return; - if (addProfileMutation.state.isInProgress) { - return; - } - mutationTriggered.value = true; - addProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .addProfile(captureResult), - ); + if (addProfileState.isLoading) return; + ref + .read(addProfileProvider.notifier) + .add(captureResult); }, ) else @@ -205,7 +181,7 @@ class AddProfileModal extends HookConsumerWidget { const Gap(24), ], ), - crossFadeState: showProgressIndicator + crossFadeState: addProfileState.isLoading ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 250), diff --git a/lib/features/profile/data/profile_data_mapper.dart b/lib/features/profile/data/profile_data_mapper.dart new file mode 100644 index 00000000..38d15061 --- /dev/null +++ b/lib/features/profile/data/profile_data_mapper.dart @@ -0,0 +1,85 @@ +import 'package:drift/drift.dart'; +import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; + +extension ProfileEntityMapper on ProfileEntity { + ProfileEntriesCompanion toEntry() { + return switch (this) { + RemoteProfileEntity(:final url, :final options, :final subInfo) => + ProfileEntriesCompanion.insert( + id: id, + type: ProfileType.remote, + active: active, + name: name, + url: Value(url), + lastUpdate: lastUpdate, + updateInterval: Value(options?.updateInterval), + upload: Value(subInfo?.upload), + download: Value(subInfo?.download), + total: Value(subInfo?.total), + expire: Value(subInfo?.expire), + webPageUrl: Value(subInfo?.webPageUrl), + supportUrl: Value(subInfo?.supportUrl), + ), + LocalProfileEntity() => ProfileEntriesCompanion.insert( + id: id, + type: ProfileType.local, + active: active, + name: name, + lastUpdate: lastUpdate, + ), + }; + } +} + +extension RemoteProfileEntityMapper on RemoteProfileEntity { + ProfileEntriesCompanion subInfoPatch() { + return ProfileEntriesCompanion( + upload: Value(subInfo?.upload), + download: Value(subInfo?.download), + total: Value(subInfo?.total), + expire: Value(subInfo?.expire), + webPageUrl: Value(subInfo?.webPageUrl), + supportUrl: Value(subInfo?.supportUrl), + ); + } +} + +extension ProfileEntryMapper on ProfileEntry { + ProfileEntity toEntity() { + ProfileOptions? options; + if (updateInterval != null) { + options = ProfileOptions(updateInterval: updateInterval!); + } + + SubscriptionInfo? subInfo; + if (upload != null && download != null && total != null && expire != null) { + subInfo = SubscriptionInfo( + upload: upload!, + download: download!, + total: total!, + expire: expire!, + webPageUrl: webPageUrl, + supportUrl: supportUrl, + ); + } + + return switch (type) { + ProfileType.remote => RemoteProfileEntity( + id: id, + active: active, + name: name, + url: url!, + lastUpdate: lastUpdate, + options: options, + subInfo: subInfo, + ), + ProfileType.local => LocalProfileEntity( + id: id, + active: active, + name: name, + lastUpdate: lastUpdate, + ), + }; + } +} diff --git a/lib/features/profile/data/profile_data_providers.dart b/lib/features/profile/data/profile_data_providers.dart new file mode 100644 index 00000000..5ff0f515 --- /dev/null +++ b/lib/features/profile/data/profile_data_providers.dart @@ -0,0 +1,32 @@ +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_source.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profile_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +Future profileRepository(ProfileRepositoryRef ref) async { + final repo = ProfileRepositoryImpl( + profileDataSource: ref.watch(profileDataSourceProvider), + profilePathResolver: ref.watch(profilePathResolverProvider), + configValidator: ref.watch(coreFacadeProvider).parseConfig, + dio: ref.watch(dioProvider), + ); + await repo.init().getOrElse((l) => throw l).run(); + return repo; +} + +@Riverpod(keepAlive: true) +ProfileDataSource profileDataSource(ProfileDataSourceRef ref) { + return ProfileDao(ref.watch(appDatabaseProvider)); +} + +@Riverpod(keepAlive: true) +ProfilePathResolver profilePathResolver(ProfilePathResolverRef ref) { + return ProfilePathResolver( + ref.watch(filesEditorServiceProvider).dirs.workingDir, + ); +} diff --git a/lib/data/local/dao/profiles_dao.dart b/lib/features/profile/data/profile_data_source.dart similarity index 57% rename from lib/data/local/dao/profiles_dao.dart rename to lib/features/profile/data/profile_data_source.dart index 3d41c934..a5f6b241 100644 --- a/lib/data/local/dao/profiles_dao.dart +++ b/lib/features/profile/data/profile_data_source.dart @@ -1,12 +1,24 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/data_mappers.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/local/tables.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; import 'package:hiddify/utils/utils.dart'; -part 'profiles_dao.g.dart'; +part 'profile_data_source.g.dart'; + +abstract interface class ProfileDataSource { + Future getById(String id); + Future getByUrl(String url); + Stream watchActiveProfile(); + Stream watchProfilesCount(); + Stream> watchAll({ + required ProfilesSort sort, + required SortMode sortMode, + }); + Future insert(ProfileEntriesCompanion entry); + Future edit(String id, ProfileEntriesCompanion entry); + Future deleteById(String id); +} Map orderMap = { SortMode.ascending: OrderingMode.asc, @@ -14,41 +26,45 @@ Map orderMap = { }; @DriftAccessor(tables: [ProfileEntries]) -class ProfilesDao extends DatabaseAccessor - with _$ProfilesDaoMixin, InfraLogger { - ProfilesDao(super.db); +class ProfileDao extends DatabaseAccessor + with _$ProfileDaoMixin, InfraLogger + implements ProfileDataSource { + ProfileDao(super.db); - Future getById(String id) async { + @override + Future getById(String id) async { return (profileEntries.select()..where((tbl) => tbl.id.equals(id))) - .map(ProfileMapper.fromEntry) .getSingleOrNull(); } - Future getProfileByUrl(String url) async { - return (select(profileEntries)..where((tbl) => tbl.url.like('%$url%'))) - .map(ProfileMapper.fromEntry) - .get() - .then((value) => value.firstOrNull); + @override + Future getByUrl(String url) async { + return (select(profileEntries) + ..where((tbl) => tbl.url.like('%$url%')) + ..limit(1)) + .getSingleOrNull(); } - Stream watchActiveProfile() { + @override + Stream watchActiveProfile() { return (profileEntries.select() ..where((tbl) => tbl.active.equals(true)) ..limit(1)) - .map(ProfileMapper.fromEntry) .watchSingleOrNull(); } - Stream watchProfileCount() { + @override + Stream watchProfilesCount() { final count = profileEntries.id.count(); return (profileEntries.selectOnly()..addColumns([count])) .map((exp) => exp.read(count)!) .watchSingle(); } - Stream> watchAll({ - ProfilesSort sort = ProfilesSort.lastUpdate, - SortMode mode = SortMode.ascending, + @override + Stream> watchAll({ + required ProfilesSort sort, + required SortMode sortMode, }) { return (profileEntries.select() ..orderBy( @@ -67,56 +83,47 @@ class ProfilesDao extends DatabaseAccessor switch (sort) { ProfilesSort.name => (tbl) => OrderingTerm( expression: tbl.name, - mode: orderMap[mode]!, + mode: orderMap[sortMode]!, ), ProfilesSort.lastUpdate => (tbl) => OrderingTerm( expression: tbl.lastUpdate, - mode: orderMap[mode]!, + mode: orderMap[sortMode]!, ), }, ], )) - .map(ProfileMapper.fromEntry) .watch(); } - Future create(Profile profile) async { + @override + Future insert(ProfileEntriesCompanion entry) async { await transaction( () async { - if (profile.active) { + if (entry.active.present && entry.active.value) { await update(profileEntries) .write(const ProfileEntriesCompanion(active: Value(false))); } - await into(profileEntries).insert(profile.toCompanion()); + await into(profileEntries).insert(entry); }, ); } - Future edit(Profile patch) async { + @override + Future edit(String id, ProfileEntriesCompanion entry) async { await transaction( () async { - if (patch.active) { + if (entry.active.present && entry.active.value) { await update(profileEntries) .write(const ProfileEntriesCompanion(active: Value(false))); } - await (update(profileEntries)..where((tbl) => tbl.id.equals(patch.id))) - .write(patch.toCompanion()); - }, - ); - } - - Future setAsActive(String id) async { - await transaction( - () async { - await update(profileEntries) - .write(const ProfileEntriesCompanion(active: Value(false))); await (update(profileEntries)..where((tbl) => tbl.id.equals(id))) - .write(const ProfileEntriesCompanion(active: Value(true))); + .write(entry); }, ); } - Future removeById(String id) async { + @override + Future deleteById(String id) async { await transaction( () async { await (delete(profileEntries)..where((tbl) => tbl.id.equals(id))).go(); diff --git a/lib/features/profile/data/profile_parser.dart b/lib/features/profile/data/profile_parser.dart new file mode 100644 index 00000000..5496658b --- /dev/null +++ b/lib/features/profile/data/profile_parser.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +import 'package:dartx/dartx.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:uuid/uuid.dart'; + +/// parse profile subscription url and headers for data +/// +/// ***name parser hierarchy:*** +/// - `profile-title` header +/// - `content-disposition` header +/// - url fragment (example: `https://example.com/config#user`) -> name=`user` +/// - url filename extension (example: `https://example.com/config.json`) -> name=`config` +/// - if none of these methods return a non-blank string, fallback to `Remote Profile` +abstract class ProfileParser { + static RemoteProfileEntity parse( + String url, + Map> headers, + ) { + var name = ''; + if (headers['profile-title'] case [final titleHeader]) { + if (titleHeader.startsWith("base64:")) { + name = + utf8.decode(base64.decode(titleHeader.replaceFirst("base64:", ""))); + } else { + name = titleHeader.trim(); + } + } + if (headers['content-disposition'] case [final contentDispositionHeader] + when name.isEmpty) { + final regExp = RegExp('filename="([^"]*)"'); + final match = regExp.firstMatch(contentDispositionHeader); + if (match != null && match.groupCount >= 1) { + name = match.group(1) ?? ''; + } + } + if (Uri.parse(url).fragment case final fragment when name.isEmpty) { + name = fragment; + } + if (url.split("/").lastOrNull case final part? when name.isEmpty) { + final pattern = RegExp(r"\.(json|yaml|yml|txt)[\s\S]*"); + name = part.replaceFirst(pattern, ""); + } + if (name.isBlank) name = "Remote Profile"; + + ProfileOptions? options; + if (headers['profile-update-interval'] case [final updateIntervalStr]) { + final updateInterval = Duration(hours: int.parse(updateIntervalStr)); + options = ProfileOptions(updateInterval: updateInterval); + } + + SubscriptionInfo? subInfo; + if (headers['subscription-userinfo'] case [final subInfoStr]) { + subInfo = parseSubscriptionInfo(subInfoStr); + } + + if (subInfo != null) { + if (headers['profile-web-page-url'] case [final profileWebPageUrl] + when isUrl(profileWebPageUrl)) { + subInfo = subInfo.copyWith(webPageUrl: profileWebPageUrl); + } + if (headers['support-url'] case [final profileSupportUrl] + when isUrl(profileSupportUrl)) { + subInfo = subInfo.copyWith(supportUrl: profileSupportUrl); + } + } + + return RemoteProfileEntity( + id: const Uuid().v4(), + active: false, + name: name, + url: url, + lastUpdate: DateTime.now(), + options: options, + subInfo: subInfo, + ); + } + + static SubscriptionInfo? parseSubscriptionInfo(String subInfoStr) { + final values = subInfoStr.split(';'); + final map = { + for (final v in values) + v.split('=').first.trim(): + num.tryParse(v.split('=').second.trim())?.toInt(), + }; + if (map + case { + "upload": final upload?, + "download": final download?, + "total": final total, + "expire": final expire + }) { + return SubscriptionInfo( + upload: upload, + download: download, + total: total ?? 9223372036854775807, + expire: DateTime.fromMillisecondsSinceEpoch( + (expire ?? 92233720368) * 1000, + ), + ); + } + return null; + } +} diff --git a/lib/features/profile/data/profile_path_resolver.dart b/lib/features/profile/data/profile_path_resolver.dart new file mode 100644 index 00000000..ea340344 --- /dev/null +++ b/lib/features/profile/data/profile_path_resolver.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +class ProfilePathResolver { + const ProfilePathResolver(this._workingDir); + + final Directory _workingDir; + + Directory get directory => Directory(p.join(_workingDir.path, "configs")); + + File file(String fileName) { + return File(p.join(directory.path, "$fileName.json")); + } + + File tempFile(String fileName) => file("$fileName.tmp"); +} diff --git a/lib/data/repository/profiles_repository_impl.dart b/lib/features/profile/data/profile_repository.dart similarity index 53% rename from lib/data/repository/profiles_repository_impl.dart rename to lib/features/profile/data/profile_repository.dart index d73eb585..4a099e1a 100644 --- a/lib/data/repository/profiles_repository_impl.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -1,44 +1,103 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/dao/profiles_dao.dart'; +import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/domain/core_service_failure.dart'; +import 'package:hiddify/features/profile/data/profile_data_mapper.dart'; +import 'package:hiddify/features/profile/data/profile_data_source.dart'; +import 'package:hiddify/features/profile/data/profile_parser.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_failure.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/link_parsers.dart'; import 'package:meta/meta.dart'; import 'package:retry/retry.dart'; import 'package:uuid/uuid.dart'; -class ProfilesRepositoryImpl +abstract interface class ProfileRepository { + TaskEither init(); + TaskEither getById(String id); + Stream> watchActiveProfile(); + Stream> watchHasAnyProfile(); + + Stream>> watchAll({ + ProfilesSort sort = ProfilesSort.lastUpdate, + SortMode sortMode = SortMode.ascending, + }); + + TaskEither addByUrl( + String url, { + bool markAsActive = false, + }); + + TaskEither addByContent( + String content, { + required String name, + bool markAsActive = false, + }); + + TaskEither add(RemoteProfileEntity baseProfile); + + TaskEither updateSubscription( + RemoteProfileEntity baseProfile, + ); + + TaskEither patch(ProfileEntity profile); + TaskEither setAsActive(String id); + TaskEither deleteById(String id); +} + +class ProfileRepositoryImpl with ExceptionHandler, InfraLogger - implements ProfilesRepository { - ProfilesRepositoryImpl({ - required this.profilesDao, - required this.filesEditor, - required this.singbox, + implements ProfileRepository { + ProfileRepositoryImpl({ + required this.profileDataSource, + required this.profilePathResolver, + required this.configValidator, required this.dio, }); - final ProfilesDao profilesDao; - final FilesEditorService filesEditor; - final SingboxFacade singbox; + final ProfileDataSource profileDataSource; + final ProfilePathResolver profilePathResolver; + final TaskEither Function( + String path, + String tempPath, + bool debug, + ) configValidator; final Dio dio; @override - TaskEither get(String id) { - return TaskEither.tryCatch( - () => profilesDao.getById(id), + TaskEither init() { + return exceptionHandler( + () async { + if (!await profilePathResolver.directory.exists()) { + await profilePathResolver.directory.create(recursive: true); + } + return right(unit); + }, ProfileUnexpectedFailure.new, ); } @override - Stream> watchActiveProfile() { - return profilesDao.watchActiveProfile().handleExceptions( + TaskEither getById(String id) { + return TaskEither.tryCatch( + () => profileDataSource.getById(id).then((value) => value?.toEntity()), + ProfileUnexpectedFailure.new, + ); + } + + @override + Stream> watchActiveProfile() { + return profileDataSource + .watchActiveProfile() + .map((event) => event?.toEntity()) + .handleExceptions( (error, stackTrace) { loggy.error("error watching active profile", error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace); @@ -48,19 +107,20 @@ class ProfilesRepositoryImpl @override Stream> watchHasAnyProfile() { - return profilesDao - .watchProfileCount() + return profileDataSource + .watchProfilesCount() .map((event) => event != 0) .handleExceptions(ProfileUnexpectedFailure.new); } @override - Stream>> watchAll({ + Stream>> watchAll({ ProfilesSort sort = ProfilesSort.lastUpdate, - SortMode mode = SortMode.ascending, + SortMode sortMode = SortMode.ascending, }) { - return profilesDao - .watchAll(sort: sort, mode: mode) + return profileDataSource + .watchAll(sort: sort, sortMode: sortMode) + .map((event) => event.map((e) => e.toEntity()).toList()) .handleExceptions(ProfileUnexpectedFailure.new); } @@ -71,13 +131,15 @@ class ProfilesRepositoryImpl }) { return exceptionHandler( () async { - final existingProfile = await profilesDao.getProfileByUrl(url); - if (existingProfile case RemoteProfile()) { + final existingProfile = await profileDataSource + .getByUrl(url) + .then((value) => value?.toEntity()); + if (existingProfile case RemoteProfileEntity()) { loggy.info("profile with same url already exists, updating"); final baseProfile = markAsActive ? existingProfile.copyWith(active: true) : existingProfile; - return update(baseProfile).run(); + return updateSubscription(baseProfile).run(); } final profileId = const Uuid().v4(); @@ -85,11 +147,10 @@ class ProfilesRepositoryImpl .flatMap( (profile) => TaskEither( () async { - await profilesDao.create( - profile.copyWith( - id: profileId, - active: markAsActive, - ), + await profileDataSource.insert( + profile + .copyWith(id: profileId, active: markAsActive) + .toEntry(), ); return right(unit); }, @@ -113,30 +174,31 @@ class ProfilesRepositoryImpl return exceptionHandler( () async { final profileId = const Uuid().v4(); - final tempPath = filesEditor.tempConfigPath(profileId); - final path = filesEditor.configPath(profileId); + final file = profilePathResolver.file(profileId); + final tempFile = profilePathResolver.tempFile(profileId); + try { - await File(tempPath).writeAsString(content); + await tempFile.writeAsString(content); final parseResult = - await singbox.parseConfig(path, tempPath, false).run(); + await configValidator(file.path, tempFile.path, false).run(); return parseResult.fold( (err) async { loggy.warning("error parsing config", err); return left(ProfileFailure.invalidConfig(err.msg)); }, (_) async { - final profile = LocalProfile( + final profile = LocalProfileEntity( id: profileId, active: markAsActive, name: name, lastUpdate: DateTime.now(), ); - await profilesDao.create(profile); + await profileDataSource.insert(profile.toEntry()); return right(unit); }, ); } finally { - if (File(tempPath).existsSync()) File(tempPath).deleteSync(); + if (tempFile.existsSync()) tempFile.deleteSync(); } }, (error, stackTrace) { @@ -147,17 +209,19 @@ class ProfilesRepositoryImpl } @override - TaskEither add(RemoteProfile baseProfile) { + TaskEither add(RemoteProfileEntity baseProfile) { return exceptionHandler( () async { return fetch(baseProfile.url, baseProfile.id) .flatMap( (remoteProfile) => TaskEither(() async { - await profilesDao.create( - baseProfile.copyWith( - subInfo: remoteProfile.subInfo, - lastUpdate: DateTime.now(), - ), + await profileDataSource.insert( + baseProfile + .copyWith( + subInfo: remoteProfile.subInfo, + lastUpdate: DateTime.now(), + ) + .toEntry(), ); return right(unit); }), @@ -172,7 +236,9 @@ class ProfilesRepositoryImpl } @override - TaskEither update(RemoteProfile baseProfile) { + TaskEither updateSubscription( + RemoteProfileEntity baseProfile, + ) { return exceptionHandler( () async { loggy.debug( @@ -181,11 +247,11 @@ class ProfilesRepositoryImpl return fetch(baseProfile.url, baseProfile.id) .flatMap( (remoteProfile) => TaskEither(() async { - await profilesDao.edit( - baseProfile.copyWith( - subInfo: remoteProfile.subInfo, - lastUpdate: DateTime.now(), - ), + await profileDataSource.edit( + baseProfile.id, + remoteProfile + .subInfoPatch() + .copyWith(lastUpdate: Value(DateTime.now())), ); return right(unit); }), @@ -200,13 +266,13 @@ class ProfilesRepositoryImpl } @override - TaskEither edit(Profile profile) { + TaskEither patch(ProfileEntity profile) { return exceptionHandler( () async { loggy.debug( "editing profile [${profile.name} (${profile.id})]", ); - await profilesDao.edit(profile); + await profileDataSource.edit(profile.id, profile.toEntry()); return right(unit); }, (error, stackTrace) { @@ -220,7 +286,10 @@ class ProfilesRepositoryImpl TaskEither setAsActive(String id) { return TaskEither.tryCatch( () async { - await profilesDao.setAsActive(id); + await profileDataSource.edit( + id, + const ProfileEntriesCompanion(active: Value(true)), + ); return unit; }, ProfileUnexpectedFailure.new, @@ -228,11 +297,11 @@ class ProfilesRepositoryImpl } @override - TaskEither delete(String id) { + TaskEither deleteById(String id) { return TaskEither.tryCatch( () async { - await profilesDao.removeById(id); - await filesEditor.deleteConfig(id); + await profileDataSource.deleteById(id); + await profilePathResolver.file(id).delete(); return unit; }, ProfileUnexpectedFailure.new, @@ -249,35 +318,35 @@ class ProfilesRepositoryImpl ]; @visibleForTesting - TaskEither fetch( + TaskEither fetch( String url, String fileName, ) { return TaskEither( () async { - final tempPath = filesEditor.tempConfigPath(fileName); - final path = filesEditor.configPath(fileName); + final file = profilePathResolver.file(fileName); + final tempFile = profilePathResolver.tempFile(fileName); try { final response = await retry( - () async => dio.download(url.trim(), tempPath), + () async => dio.download(url.trim(), tempFile.path), maxAttempts: 3, ); final headers = - await _populateHeaders(response.headers.map, tempPath); + await _populateHeaders(response.headers.map, tempFile.path); final parseResult = - await singbox.parseConfig(path, tempPath, false).run(); + await configValidator(file.path, tempFile.path, false).run(); return parseResult.fold( (err) async { loggy.warning("error parsing config", err); return left(ProfileFailure.invalidConfig(err.msg)); }, (_) async { - final profile = Profile.fromResponse(url, headers); + final profile = ProfileParser.parse(url, headers); return right(profile); }, ); } finally { - if (File(tempPath).existsSync()) File(tempPath).deleteSync(); + if (tempFile.existsSync()) tempFile.deleteSync(); } }, ); diff --git a/lib/features/profile_detail/notifier/profile_detail_notifier.dart b/lib/features/profile/details/profile_details_notifier.dart similarity index 60% rename from lib/features/profile_detail/notifier/profile_detail_notifier.dart rename to lib/features/profile/details/profile_details_notifier.dart index 6397164e..6cd5df65 100644 --- a/lib/features/profile_detail/notifier/profile_detail_notifier.dart +++ b/lib/features/profile/details/profile_details_notifier.dart @@ -1,25 +1,27 @@ import 'package:dartx/dartx.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/profile_detail/notifier/profile_detail_state.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/features/profile/details/profile_details_state.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_failure.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:uuid/uuid.dart'; -part 'profile_detail_notifier.g.dart'; +part 'profile_details_notifier.g.dart'; @riverpod -class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { +class ProfileDetailsNotifier extends _$ProfileDetailsNotifier with AppLogger { @override - Future build( + Future build( String id, { String? url, String? profileName, }) async { if (id == 'new') { - return ProfileDetailState( - profile: RemoteProfile( + return ProfileDetailsState( + profile: RemoteProfileEntity( id: const Uuid().v4(), active: true, name: profileName ?? "", @@ -28,7 +30,7 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { ), ); } - final failureOrProfile = await _profilesRepo.get(id).run(); + final failureOrProfile = await _profilesRepo.getById(id).run(); return failureOrProfile.match( (err) { loggy.warning('failed to load profile', err); @@ -40,13 +42,14 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { throw const ProfileNotFoundFailure(); } _originalProfile = profile; - return ProfileDetailState(profile: profile, isEditing: true); + return ProfileDetailsState(profile: profile, isEditing: true); }, ); } - ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); - Profile? _originalProfile; + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + ProfileEntity? _originalProfile; void setField({String? name, String? url, Option? updateInterval}) { if (state case AsyncData(:final value)) { @@ -74,41 +77,47 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { Future save() async { if (state case AsyncData(:final value)) { - if (value.save.isInProgress) return; + if (value.save case AsyncLoading()) return; + final profile = value.profile; Either? failureOrSuccess; - state = AsyncData(value.copyWith(save: const MutationInProgress())); + state = AsyncData(value.copyWith(save: const AsyncLoading())); + switch (profile) { - case RemoteProfile(): + case RemoteProfileEntity(): loggy.debug( 'saving profile, url: [${profile.url}], name: [${profile.name}]', ); if (profile.name.isBlank || profile.url.isBlank) { - loggy.debug('profile save: invalid arguments'); + loggy.debug('save: invalid arguments'); } else if (value.isEditing) { - if (_originalProfile case RemoteProfile(:final url) + if (_originalProfile case RemoteProfileEntity(:final url) when url == profile.url) { loggy.debug('editing profile'); - failureOrSuccess = await _profilesRepo.edit(profile).run(); + failureOrSuccess = await _profilesRepo.patch(profile).run(); } else { loggy.debug('updating profile'); - failureOrSuccess = await _profilesRepo.update(profile).run(); + failureOrSuccess = + await _profilesRepo.updateSubscription(profile).run(); } } else { loggy.debug('adding profile, url: [${profile.url}]'); failureOrSuccess = await _profilesRepo.add(profile).run(); } - case LocalProfile() when value.isEditing: + + case LocalProfileEntity() when value.isEditing: loggy.debug('editing profile'); - failureOrSuccess = await _profilesRepo.edit(profile).run(); + failureOrSuccess = await _profilesRepo.patch(profile).run(); + default: loggy.warning("local profile can't be added manually"); } + state = AsyncData( value.copyWith( save: failureOrSuccess?.fold( - (l) => MutationFailure(l), - (_) => const MutationSuccess(), + (l) => AsyncError(l, StackTrace.current), + (_) => const AsyncData(null), ) ?? value.save, showErrorMessages: true, @@ -119,24 +128,25 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { Future updateProfile() async { if (state case AsyncData(:final value)) { - loggy.debug('updating profile'); - if (value.profile case LocalProfile()) { + if (value.update?.isLoading ?? false || !value.isEditing) return; + if (value.profile case LocalProfileEntity()) { loggy.warning("local profile can't be updated"); return; } - if (value.update.isInProgress || !value.isEditing) return; + final profile = value.profile; - loggy.debug('updating profile'); - state = AsyncData(value.copyWith(update: const MutationInProgress())); + state = AsyncData(value.copyWith(update: const AsyncLoading())); + final failureOrUpdatedProfile = await _profilesRepo - .update(profile as RemoteProfile) - .flatMap((_) => _profilesRepo.get(id)) + .updateSubscription(profile as RemoteProfileEntity) + .flatMap((_) => _profilesRepo.getById(id)) .run(); + state = AsyncData( value.copyWith( update: failureOrUpdatedProfile.match( - (l) => MutationFailure(l), - (_) => const MutationSuccess(), + (l) => AsyncError(l, StackTrace.current), + (_) => const AsyncData(null), ), profile: failureOrUpdatedProfile.match( (_) => profile, @@ -149,17 +159,18 @@ class ProfileDetailNotifier extends _$ProfileDetailNotifier with AppLogger { Future delete() async { if (state case AsyncData(:final value)) { - if (value.delete.isInProgress) return; + if (value.delete case AsyncLoading()) return; final profile = value.profile; - loggy.debug('deleting profile'); - state = AsyncData(value.copyWith(delete: const MutationInProgress())); - final result = await _profilesRepo.delete(profile.id).run(); + state = AsyncData(value.copyWith(delete: const AsyncLoading())); + state = AsyncData( value.copyWith( - delete: result.match( - (l) => MutationFailure(l), - (_) => const MutationSuccess(), - ), + delete: await AsyncValue.guard(() async { + await _profilesRepo + .deleteById(profile.id) + .getOrElse((l) => throw l) + .run(); + }), ), ); } diff --git a/lib/features/profile_detail/view/profile_detail_page.dart b/lib/features/profile/details/profile_details_page.dart similarity index 79% rename from lib/features/profile_detail/view/profile_detail_page.dart rename to lib/features/profile/details/profile_details_page.dart index 2c8eb0ff..ce1e5b23 100644 --- a/lib/features/profile_detail/view/profile_detail_page.dart +++ b/lib/features/profile/details/profile_details_page.dart @@ -3,16 +3,16 @@ import 'package:fpdart/fpdart.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; -import 'package:hiddify/features/profile_detail/notifier/notifier.dart'; +import 'package:hiddify/features/profile/details/profile_details_notifier.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/settings/widgets/widgets.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; -class ProfileDetailPage extends HookConsumerWidget with PresLogger { - const ProfileDetailPage(this.id, {super.key}); +class ProfileDetailsPage extends HookConsumerWidget with PresLogger { + const ProfileDetailsPage(this.id, {super.key}); final String id; @@ -20,65 +20,59 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final provider = profileDetailNotifierProvider(id); + final provider = profileDetailsNotifierProvider(id); final notifier = ref.watch(provider.notifier); ref.listen( - provider.select((data) => data.whenData((value) => value.save)), - (_, asyncSave) { - if (asyncSave case AsyncData(value: final save)) { - switch (save) { - case MutationFailure(:final failure): - final String action; - if (ref.read(provider) case AsyncData(value: final data) - when data.isEditing) { - action = t.profile.save.failureMsg; - } else { - action = t.profile.add.failureMsg; - } - CustomAlertDialog.fromErr(t.presentError(failure, action: action)) - .show(context); - case MutationSuccess(): - CustomToast.success(t.profile.save.successMsg).show(context); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (context.mounted) context.pop(); - }, - ); - } + provider.selectAsync((data) => data.save), + (_, next) async { + switch (await next) { + case AsyncData(): + CustomToast.success(t.profile.save.successMsg).show(context); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); + case AsyncError(:final error): + final String action; + if (ref.read(provider) case AsyncData(value: final data) + when data.isEditing) { + action = t.profile.save.failureMsg; + } else { + action = t.profile.add.failureMsg; + } + CustomAlertDialog.fromErr(t.presentError(error, action: action)) + .show(context); } }, ); ref.listen( - provider.select((data) => data.whenData((value) => value.update)), - (_, asyncUpdate) { - if (asyncUpdate case AsyncData(value: final update)) { - switch (update) { - case MutationFailure(:final failure): - CustomAlertDialog.fromErr(t.presentError(failure)).show(context); - case MutationSuccess(): - CustomToast.success(t.profile.update.successMsg).show(context); - } + provider.selectAsync((data) => data.update), + (_, next) async { + switch (await next) { + case AsyncData(): + CustomToast.success(t.profile.update.successMsg).show(context); + case AsyncError(:final error): + CustomAlertDialog.fromErr(t.presentError(error)).show(context); } }, ); ref.listen( - provider.select((data) => data.whenData((value) => value.delete)), - (_, asyncDelete) { - if (asyncDelete case AsyncData(value: final delete)) { - switch (delete) { - case MutationFailure(:final failure): - CustomToast.error(t.presentShortError(failure)).show(context); - case MutationSuccess(): - CustomToast.success(t.profile.delete.successMsg).show(context); - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (context.mounted) context.pop(); - }, - ); - } + provider.selectAsync((data) => data.delete), + (_, next) async { + switch (await next) { + case AsyncData(): + CustomToast.success(t.profile.delete.successMsg).show(context); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (context.mounted) context.pop(); + }, + ); + case AsyncError(:final error): + CustomToast.error(t.presentShortError(error)).show(context); } }, ); @@ -102,7 +96,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { PopupMenuButton( itemBuilder: (context) { return [ - if (state.profile case RemoteProfile()) + if (state.profile case RemoteProfileEntity()) PopupMenuItem( child: Text(t.profile.update.buttonTxt), onTap: () async { @@ -151,7 +145,10 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger { ), ), if (state.profile - case RemoteProfile(:final url, :final options)) ...[ + case RemoteProfileEntity( + :final url, + :final options + )) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 16, diff --git a/lib/features/profile/details/profile_details_state.dart b/lib/features/profile/details/profile_details_state.dart new file mode 100644 index 00000000..894abf24 --- /dev/null +++ b/lib/features/profile/details/profile_details_state.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +part 'profile_details_state.freezed.dart'; + +@freezed +class ProfileDetailsState with _$ProfileDetailsState { + const ProfileDetailsState._(); + + const factory ProfileDetailsState({ + required ProfileEntity profile, + @Default(false) bool isEditing, + @Default(false) bool showErrorMessages, + AsyncValue? save, + AsyncValue? update, + AsyncValue? delete, + }) = _ProfileDetailsState; + + bool get isBusy => + save is AsyncLoading || delete is AsyncLoading || update is AsyncLoading; +} diff --git a/lib/features/profile/model/profile_entity.dart b/lib/features/profile/model/profile_entity.dart new file mode 100644 index 00000000..144546be --- /dev/null +++ b/lib/features/profile/model/profile_entity.dart @@ -0,0 +1,57 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'profile_entity.freezed.dart'; + +enum ProfileType { remote, local } + +@freezed +sealed class ProfileEntity with _$ProfileEntity { + const ProfileEntity._(); + + const factory ProfileEntity.remote({ + required String id, + required bool active, + required String name, + required String url, + required DateTime lastUpdate, + ProfileOptions? options, + SubscriptionInfo? subInfo, + }) = RemoteProfileEntity; + + const factory ProfileEntity.local({ + required String id, + required bool active, + required String name, + required DateTime lastUpdate, + }) = LocalProfileEntity; +} + +@freezed +class ProfileOptions with _$ProfileOptions { + const factory ProfileOptions({ + required Duration updateInterval, + }) = _ProfileOptions; +} + +@freezed +class SubscriptionInfo with _$SubscriptionInfo { + const SubscriptionInfo._(); + + const factory SubscriptionInfo({ + required int upload, + required int download, + required int total, + required DateTime expire, + String? webPageUrl, + String? supportUrl, + }) = _SubscriptionInfo; + + bool get isExpired => expire <= DateTime.now(); + + int get consumption => upload + download; + + double get ratio => (consumption / total).clamp(0, 1); + + Duration get remaining => expire.difference(DateTime.now()); +} diff --git a/lib/domain/profiles/profiles_failure.dart b/lib/features/profile/model/profile_failure.dart similarity index 97% rename from lib/domain/profiles/profiles_failure.dart rename to lib/features/profile/model/profile_failure.dart index 7a9edb8a..529b5269 100644 --- a/lib/domain/profiles/profiles_failure.dart +++ b/lib/features/profile/model/profile_failure.dart @@ -2,7 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/domain/failures.dart'; -part 'profiles_failure.freezed.dart'; +part 'profile_failure.freezed.dart'; @freezed sealed class ProfileFailure with _$ProfileFailure, Failure { diff --git a/lib/domain/profiles/profile_enums.dart b/lib/features/profile/model/profile_sort_enum.dart similarity index 91% rename from lib/domain/profiles/profile_enums.dart rename to lib/features/profile/model/profile_sort_enum.dart index 1ad081bd..04b8f6d4 100644 --- a/lib/domain/profiles/profile_enums.dart +++ b/lib/features/profile/model/profile_sort_enum.dart @@ -17,3 +17,5 @@ enum ProfilesSort { name => Icons.sort_by_alpha, }; } + +enum SortMode { ascending, descending } diff --git a/lib/features/profile/notifier/active_profile_notifier.dart b/lib/features/profile/notifier/active_profile_notifier.dart new file mode 100644 index 00000000..74e4214a --- /dev/null +++ b/lib/features/profile/notifier/active_profile_notifier.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'active_profile_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ActiveProfile extends _$ActiveProfile with AppLogger { + @override + Stream build() { + loggy.debug("watching active profile"); + return ref + .watch(profileRepositoryProvider) + .requireValue + .watchActiveProfile() + .map((event) => event.getOrElse((l) => throw l)); + } +} + +// TODO: move to specific feature +@Riverpod(keepAlive: true) +Stream hasAnyProfile( + HasAnyProfileRef ref, +) { + return ref + .watch(profileRepositoryProvider) + .requireValue + .watchHasAnyProfile() + .map((event) => event.getOrElse((l) => throw l)); +} diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart new file mode 100644 index 00000000..263fdf85 --- /dev/null +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -0,0 +1,140 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/notification/in_app_notification_controller.dart'; +import 'package:hiddify/core/prefs/general_prefs.dart'; +import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_failure.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profile_notifier.g.dart'; + +@riverpod +class AddProfile extends _$AddProfile with AppLogger { + @override + AsyncValue build() { + ref.disposeDelay(const Duration(minutes: 1)); + ref.listenSelf( + (previous, next) { + final t = ref.read(translationsProvider); + final notification = ref.read(inAppNotificationControllerProvider); + switch (next) { + case AsyncData(value: final _?): + notification.showSuccessToast(t.profile.save.successMsg); + case AsyncError(:final error): + if (error case ProfileInvalidUrlFailure()) { + notification.showErrorToast(t.failure.profiles.invalidUrl); + } else { + notification.showErrorDialog( + t.presentError(error, action: t.profile.add.failureMsg), + ); + } + } + }, + ); + return const AsyncData(null); + } + + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + + Future add(String rawInput) async { + if (state.isLoading) return; + state = const AsyncLoading(); + state = await AsyncValue.guard( + () async { + final activeProfile = await ref.read(activeProfileProvider.future); + final markAsActive = + activeProfile == null || ref.read(markNewProfileActiveProvider); + final TaskEither task; + if (LinkParser.parse(rawInput) case (final link)?) { + loggy.debug("adding profile, url: [${link.url}]"); + task = _profilesRepo.addByUrl(link.url, markAsActive: markAsActive); + } else if (LinkParser.protocol(rawInput) case (final parsed)?) { + loggy.debug("adding profile, content"); + task = _profilesRepo.addByContent( + parsed.content, + name: parsed.name, + markAsActive: markAsActive, + ); + } else { + loggy.debug("invalid content"); + throw const ProfileInvalidUrlFailure(); + } + return task.match( + (err) { + loggy.warning("failed to add profile", err); + throw err; + }, + (_) { + loggy.info( + "successfully added profile, mark as active? [$markAsActive]", + ); + return unit; + }, + ).run(); + }, + ); + } +} + +@riverpod +class UpdateProfile extends _$UpdateProfile with AppLogger { + @override + AsyncValue build(String id) { + ref.disposeDelay(const Duration(minutes: 1)); + ref.listenSelf( + (previous, next) { + final t = ref.read(translationsProvider); + final notification = ref.read(inAppNotificationControllerProvider); + switch (next) { + case AsyncData(value: final _?): + notification.showSuccessToast(t.profile.update.successMsg); + case AsyncError(:final error): + notification.showErrorDialog( + t.presentError(error, action: t.profile.update.failureMsg), + ); + } + }, + ); + return const AsyncData(null); + } + + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + + Future updateProfile(RemoteProfileEntity profile) async { + if (state.isLoading) return; + state = const AsyncLoading(); + state = await AsyncValue.guard( + () async { + return await _profilesRepo.updateSubscription(profile).match( + (err) { + loggy.warning("failed to update profile", err); + throw err; + }, + (_) async { + loggy.info( + 'successfully updated profile, was active? [${profile.active}]', + ); + + await ref.read(activeProfileProvider.future).then((active) async { + if (active != null && active.id == profile.id) { + await ref + .read(connectivityControllerProvider.notifier) + .reconnect(profile.id); + } + }); + return unit; + }, + ).run(); + }, + ); + } +} diff --git a/lib/features/profile/notifier/profiles_update_notifier.dart b/lib/features/profile/notifier/profiles_update_notifier.dart new file mode 100644 index 00000000..5f4325b3 --- /dev/null +++ b/lib/features/profile/notifier/profiles_update_notifier.dart @@ -0,0 +1,92 @@ +import 'package:dartx/dartx.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:meta/meta.dart'; +import 'package:neat_periodic_task/neat_periodic_task.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profiles_update_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ForegroundProfilesUpdateNotifier + extends _$ForegroundProfilesUpdateNotifier with AppLogger { + static const prefKey = "profiles_update_check"; + static const interval = Duration(minutes: 15); + + @override + Future build() async { + loggy.debug("initializing"); + var cycleCount = 0; + final scheduler = NeatPeriodicTaskScheduler( + name: 'profiles update worker', + interval: interval, + timeout: const Duration(minutes: 5), + task: () async { + loggy.debug("cycle [${cycleCount++}]"); + await updateProfiles(); + }, + ); + + ref.onDispose(() async { + await scheduler.stop(); + }); + + return scheduler.start(); + } + + @visibleForTesting + Future updateProfiles() async { + try { + final previousRun = DateTime.tryParse( + ref.read(sharedPreferencesProvider).getString(prefKey) ?? "", + ); + + if (previousRun != null && previousRun.add(interval) > DateTime.now()) { + loggy.debug("too soon! previous run: [$previousRun]"); + return; + } + loggy.debug("running, previous run: [$previousRun]"); + + final remoteProfiles = await ref + .read(profileRepositoryProvider) + .requireValue + .watchAll() + .map( + (event) => event.getOrElse((f) { + loggy.error("error getting profiles"); + throw f; + }).whereType(), + ) + .first; + + await for (final profile in Stream.fromIterable(remoteProfiles)) { + final updateInterval = profile.options?.updateInterval; + if (updateInterval != null && + updateInterval <= DateTime.now().difference(profile.lastUpdate)) { + await ref + .read(profileRepositoryProvider) + .requireValue + .updateSubscription(profile) + .mapLeft( + (l) => loggy.debug("error updating profile [${profile.id}]", l), + ) + .map( + (_) => + loggy.debug("profile [${profile.id}] updated successfully"), + ) + .run(); + } else { + loggy.debug( + "skipping profile [${profile.id}] update. last successful update: [${profile.lastUpdate}] - interval: [${profile.options?.updateInterval}]", + ); + } + } + } finally { + await ref + .read(sharedPreferencesProvider) + .setString(prefKey, DateTime.now().toIso8601String()); + } + } +} diff --git a/lib/features/profile/overview/profiles_overview_notifier.dart b/lib/features/profile/overview/profiles_overview_notifier.dart new file mode 100644 index 00000000..9434a2d9 --- /dev/null +++ b/lib/features/profile/overview/profiles_overview_notifier.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_repository.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'profiles_overview_notifier.g.dart'; + +@riverpod +class ProfilesOverviewSortNotifier extends _$ProfilesOverviewSortNotifier + with AppLogger { + @override + ({ProfilesSort by, SortMode mode}) build() { + return (by: ProfilesSort.lastUpdate, mode: SortMode.descending); + } + + void changeSort(ProfilesSort sortBy) => + state = (by: sortBy, mode: state.mode); + + void toggleMode() => state = ( + by: state.by, + mode: state.mode == SortMode.ascending + ? SortMode.descending + : SortMode.ascending + ); +} + +@riverpod +class ProfilesOverviewNotifier extends _$ProfilesOverviewNotifier + with AppLogger { + @override + Stream> build() { + final sort = ref.watch(profilesOverviewSortNotifierProvider); + return _profilesRepo + .watchAll(sort: sort.by, sortMode: sort.mode) + .map((event) => event.getOrElse((l) => throw l)); + } + + ProfileRepository get _profilesRepo => + ref.read(profileRepositoryProvider).requireValue; + + Future selectActiveProfile(String id) async { + loggy.debug('changing active profile to: [$id]'); + return _profilesRepo.setAsActive(id).getOrElse((err) { + loggy.warning('failed to set [$id] as active profile', err); + throw err; + }).run(); + } + + Future deleteProfile(ProfileEntity profile) async { + loggy.debug('deleting profile: ${profile.name}'); + await _profilesRepo.deleteById(profile.id).match( + (err) { + loggy.warning('failed to delete profile', err); + throw err; + }, + (_) { + loggy.info( + 'successfully deleted profile, was active? [${profile.active}]', + ); + return unit; + }, + ).run(); + } + + Future exportConfigToClipboard(ProfileEntity profile) async { + await ref.read(coreFacadeProvider).generateConfig(profile.id).match( + (err) { + loggy.warning('error generating config', err); + throw err; + }, + (configJson) async { + await Clipboard.setData(ClipboardData(text: configJson)); + }, + ).run(); + } +} diff --git a/lib/features/profiles/view/profiles_modal.dart b/lib/features/profile/overview/profiles_overview_page.dart similarity index 81% rename from lib/features/profiles/view/profiles_modal.dart rename to lib/features/profile/overview/profiles_overview_page.dart index f207d9e8..105177d4 100644 --- a/lib/features/profiles/view/profiles_modal.dart +++ b/lib/features/profile/overview/profiles_overview_page.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/enums.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/common/profile_tile.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; +import 'package:hiddify/features/profile/widget/profile_tile.dart'; +import 'package:hiddify/utils/placeholders.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class ProfilesModal extends HookConsumerWidget { - const ProfilesModal({ +class ProfilesOverviewModal extends HookConsumerWidget { + const ProfilesOverviewModal({ super.key, this.scrollController, }); @@ -21,7 +20,7 @@ class ProfilesModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final asyncProfiles = ref.watch(profilesNotifierProvider); + final asyncProfiles = ref.watch(profilesOverviewNotifierProvider); return Stack( children: [ @@ -85,12 +84,14 @@ class ProfilesSortModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final sortNotifier = + ref.watch(profilesOverviewSortNotifierProvider.notifier); return AlertDialog( title: Text(t.general.sortBy), content: Consumer( builder: (context, ref, child) { - final sort = ref.watch(profilesSortNotifierProvider); + final sort = ref.watch(profilesOverviewSortNotifierProvider); return SingleChildScrollView( child: Column( children: [ @@ -104,13 +105,9 @@ class ProfilesSortModal extends HookConsumerWidget { title: Text(e.present(t)), onTap: () { if (selected) { - ref - .read(profilesSortNotifierProvider.notifier) - .toggleMode(); + sortNotifier.toggleMode(); } else { - ref - .read(profilesSortNotifierProvider.notifier) - .changeSort(e); + sortNotifier.changeSort(e); } }, selected: selected, @@ -118,9 +115,7 @@ class ProfilesSortModal extends HookConsumerWidget { trailing: selected ? IconButton( onPressed: () { - ref - .read(profilesSortNotifierProvider.notifier) - .toggleMode(); + sortNotifier.toggleMode(); }, icon: AnimatedRotation( turns: arrowTurn, diff --git a/lib/features/common/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart similarity index 87% rename from lib/features/common/profile_tile.dart rename to lib/features/profile/widget/profile_tile.dart index c0c7781f..7d526491 100644 --- a/lib/features/common/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -7,10 +7,11 @@ import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/qr_code_dialog.dart'; -import 'package:hiddify/features/profiles/notifier/notifier.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; +import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:percent_indicator/percent_indicator.dart'; @@ -22,7 +23,7 @@ class ProfileTile extends HookConsumerWidget { this.isMain = false, }); - final Profile profile; + final ProfileEntity profile; /// home screen active profile card final bool isMain; @@ -42,7 +43,7 @@ class ProfileTile extends HookConsumerWidget { ); final subInfo = switch (profile) { - RemoteProfile(:final subInfo) => subInfo, + RemoteProfileEntity(:final subInfo) => subInfo, _ => null, }; @@ -65,7 +66,7 @@ class ProfileTile extends HookConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (profile is RemoteProfile || !isMain) ...[ + if (profile is RemoteProfileEntity || !isMain) ...[ SizedBox( width: 48, child: Semantics( @@ -95,7 +96,7 @@ class ProfileTile extends HookConsumerWidget { if (profile.active) return; selectActiveMutation.setFuture( ref - .read(profilesNotifierProvider.notifier) + .read(profilesOverviewNotifierProvider.notifier) .selectActiveProfile(profile.id), ); } @@ -173,39 +174,27 @@ class ProfileTile extends HookConsumerWidget { class ProfileActionButton extends HookConsumerWidget { const ProfileActionButton(this.profile, this.showAllActions, {super.key}); - final Profile profile; + final ProfileEntity profile; final bool showAllActions; @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final updateProfileMutation = useMutation( - initialOnFailure: (err) { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.profile.update.failureMsg), - ).show(context); - }, - initialOnSuccess: () => - CustomToast.success(t.profile.update.successMsg).show(context), - ); - - if (profile case RemoteProfile() when !showAllActions) { + if (profile case RemoteProfileEntity() when !showAllActions) { return Semantics( button: true, - enabled: !updateProfileMutation.state.isInProgress, + enabled: !ref.watch(updateProfileProvider(profile.id)).isLoading, child: Tooltip( message: t.profile.update.tooltip, child: InkWell( onTap: () { - if (updateProfileMutation.state.isInProgress) { + if (ref.read(updateProfileProvider(profile.id)).isLoading) { return; } - updateProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .updateProfile(profile as RemoteProfile), - ); + ref + .read(updateProfileProvider(profile.id).notifier) + .updateProfile(profile as RemoteProfileEntity); }, child: const Icon(Icons.update), ), @@ -239,7 +228,7 @@ class ProfileActionButton extends HookConsumerWidget { class ProfileActionsMenu extends HookConsumerWidget { const ProfileActionsMenu(this.profile, this.builder, {super.key, this.child}); - final Profile profile; + final ProfileEntity profile; final MenuAnchorChildBuilder builder; final Widget? child; @@ -247,15 +236,6 @@ class ProfileActionsMenu extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final updateProfileMutation = useMutation( - initialOnFailure: (err) { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.profile.update.failureMsg), - ).show(context); - }, - initialOnSuccess: () => - CustomToast.success(t.profile.update.successMsg).show(context), - ); final exportConfigMutation = useMutation( initialOnFailure: (err) { CustomToast.error(t.presentShortError(err)).show(context); @@ -273,24 +253,22 @@ class ProfileActionsMenu extends HookConsumerWidget { return MenuAnchor( builder: builder, menuChildren: [ - if (profile case RemoteProfile()) + if (profile case RemoteProfileEntity()) MenuItemButton( leadingIcon: const Icon(Icons.update), child: Text(t.profile.update.buttonTxt), onPressed: () { - if (updateProfileMutation.state.isInProgress) { + if (ref.read(updateProfileProvider(profile.id)).isLoading) { return; } - updateProfileMutation.setFuture( - ref - .read(profilesNotifierProvider.notifier) - .updateProfile(profile as RemoteProfile), - ); + ref + .read(updateProfileProvider(profile.id).notifier) + .updateProfile(profile as RemoteProfileEntity); }, ), SubmenuButton( menuChildren: [ - if (profile case RemoteProfile(:final url, :final name)) ...[ + if (profile case RemoteProfileEntity(:final url, :final name)) ...[ MenuItemButton( child: Text(t.profile.share.exportSubLinkToClipboard), onPressed: () async { @@ -325,7 +303,7 @@ class ProfileActionsMenu extends HookConsumerWidget { } exportConfigMutation.setFuture( ref - .read(profilesNotifierProvider.notifier) + .read(profilesOverviewNotifierProvider.notifier) .exportConfigToClipboard(profile), ); }, @@ -356,7 +334,7 @@ class ProfileActionsMenu extends HookConsumerWidget { if (deleteConfirmed) { deleteProfileMutation.setFuture( ref - .read(profilesNotifierProvider.notifier) + .read(profilesOverviewNotifierProvider.notifier) .deleteProfile(profile), ); } diff --git a/lib/features/profile_detail/notifier/notifier.dart b/lib/features/profile_detail/notifier/notifier.dart deleted file mode 100644 index a6381143..00000000 --- a/lib/features/profile_detail/notifier/notifier.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'profile_detail_notifier.dart'; -export 'profile_detail_state.dart'; diff --git a/lib/features/profile_detail/notifier/profile_detail_state.dart b/lib/features/profile_detail/notifier/profile_detail_state.dart deleted file mode 100644 index 344cd7c5..00000000 --- a/lib/features/profile_detail/notifier/profile_detail_state.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/utils/utils.dart'; - -part 'profile_detail_state.freezed.dart'; - -@freezed -class ProfileDetailState with _$ProfileDetailState { - const ProfileDetailState._(); - - const factory ProfileDetailState({ - required Profile profile, - @Default(false) bool isEditing, - @Default(false) bool showErrorMessages, - @Default(MutationState.initial()) MutationState save, - @Default(MutationState.initial()) MutationState update, - @Default(MutationState.initial()) MutationState delete, - }) = _ProfileDetailState; - - bool get isBusy => - save.isInProgress || delete.isInProgress || update.isInProgress; -} diff --git a/lib/features/profile_detail/view/view.dart b/lib/features/profile_detail/view/view.dart deleted file mode 100644 index bcb57dd1..00000000 --- a/lib/features/profile_detail/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'profile_detail_page.dart'; diff --git a/lib/features/profiles/notifier/notifier.dart b/lib/features/profiles/notifier/notifier.dart deleted file mode 100644 index 7fc6e689..00000000 --- a/lib/features/profiles/notifier/notifier.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'profiles_notifier.dart'; -export 'profiles_update_notifier.dart'; diff --git a/lib/features/profiles/notifier/profiles_notifier.dart b/lib/features/profiles/notifier/profiles_notifier.dart deleted file mode 100644 index 6fb165e5..00000000 --- a/lib/features/profiles/notifier/profiles_notifier.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/enums.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'profiles_notifier.g.dart'; - -@riverpod -class ProfilesSortNotifier extends _$ProfilesSortNotifier with AppLogger { - @override - ({ProfilesSort by, SortMode mode}) build() { - return (by: ProfilesSort.lastUpdate, mode: SortMode.descending); - } - - void changeSort(ProfilesSort sortBy) => - state = (by: sortBy, mode: state.mode); - - void toggleMode() => state = ( - by: state.by, - mode: state.mode == SortMode.ascending - ? SortMode.descending - : SortMode.ascending - ); -} - -@riverpod -class ProfilesNotifier extends _$ProfilesNotifier with AppLogger { - @override - Stream> build() { - final sort = ref.watch(profilesSortNotifierProvider); - return _profilesRepo - .watchAll(sort: sort.by, mode: sort.mode) - .map((event) => event.getOrElse((l) => throw l)); - } - - ProfilesRepository get _profilesRepo => ref.read(profilesRepositoryProvider); - - Future selectActiveProfile(String id) async { - loggy.debug('changing active profile to: [$id]'); - return _profilesRepo.setAsActive(id).getOrElse((err) { - loggy.warning('failed to set [$id] as active profile', err); - throw err; - }).run(); - } - - Future addProfile(String rawInput) async { - final activeProfile = await ref.read(activeProfileProvider.future); - final markAsActive = - activeProfile == null || ref.read(markNewProfileActiveProvider); - final TaskEither task; - if (LinkParser.parse(rawInput) case (final link)?) { - loggy.debug("adding profile, url: [${link.url}]"); - task = ref - .read(profilesRepositoryProvider) - .addByUrl(link.url, markAsActive: markAsActive); - } else if (LinkParser.protocol(rawInput) case (final parsed)?) { - loggy.debug("adding profile, content"); - task = ref.read(profilesRepositoryProvider).addByContent( - parsed.content, - name: parsed.name, - markAsActive: markAsActive, - ); - } else { - loggy.debug("invalid content"); - throw const ProfileInvalidUrlFailure(); - } - return task.match( - (err) { - loggy.warning("failed to add profile", err); - throw err; - }, - (_) { - loggy.info( - "successfully added profile, mark as active? [$markAsActive]", - ); - return unit; - }, - ).run(); - } - - Future updateProfile(RemoteProfile profile) async { - loggy.debug("updating profile"); - return await ref.read(profilesRepositoryProvider).update(profile).match( - (err) { - loggy.warning("failed to update profile", err); - throw err; - }, - (_) async { - loggy.info( - 'successfully updated profile, was active? [${profile.active}]', - ); - - await ref.read(activeProfileProvider.future).then((active) async { - if (active != null && active.id == profile.id) { - await ref - .read(connectivityControllerProvider.notifier) - .reconnect(profile.id); - } - }); - return unit; - }, - ).run(); - } - - Future deleteProfile(Profile profile) async { - loggy.debug('deleting profile: ${profile.name}'); - await _profilesRepo.delete(profile.id).match( - (err) { - loggy.warning('failed to delete profile', err); - throw err; - }, - (_) { - loggy.info( - 'successfully deleted profile, was active? [${profile.active}]', - ); - return unit; - }, - ).run(); - } - - Future exportConfigToClipboard(Profile profile) async { - await ref.read(coreFacadeProvider).generateConfig(profile.id).match( - (err) { - loggy.warning('error generating config', err); - throw err; - }, - (configJson) async { - await Clipboard.setData(ClipboardData(text: configJson)); - }, - ).run(); - } -} diff --git a/lib/features/profiles/notifier/profiles_update_notifier.dart b/lib/features/profiles/notifier/profiles_update_notifier.dart deleted file mode 100644 index cccc9588..00000000 --- a/lib/features/profiles/notifier/profiles_update_notifier.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/services/service_providers.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'profiles_update_notifier.g.dart'; - -typedef ProfileUpdateResult = ({ - String name, - Either failureOrSuccess -}); - -@Riverpod(keepAlive: true) -class ProfilesUpdateNotifier extends _$ProfilesUpdateNotifier with AppLogger { - @override - Stream build() { - _schedule(); - return const Stream.empty(); - } - - Future _schedule() async { - loggy.debug("scheduling profiles update worker"); - return ref.read(cronServiceProvider).schedule( - key: 'profiles_update', - duration: const Duration(minutes: 10), - callback: () async { - final failureOrProfiles = - await ref.read(profilesRepositoryProvider).watchAll().first; - if (failureOrProfiles case Right(value: final profiles)) { - for (final profile in profiles) { - if (profile case RemoteProfile()) { - loggy.debug("checking profile: [${profile.name}]"); - final updateInterval = profile.options?.updateInterval; - if (updateInterval != null && - updateInterval <= - DateTime.now().difference(profile.lastUpdate)) { - final failureOrSuccess = await ref - .read(profilesRepositoryProvider) - .update(profile) - .run(); - state = AsyncData( - (name: profile.name, failureOrSuccess: failureOrSuccess), - ); - } else { - loggy.debug("skipping profile: [${profile.name}]"); - } - } - } - } - }, - ); - } -} diff --git a/lib/features/profiles/view/view.dart b/lib/features/profiles/view/view.dart deleted file mode 100644 index cb18b1bf..00000000 --- a/lib/features/profiles/view/view.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'add_profile_modal.dart'; -export 'profiles_modal.dart'; diff --git a/lib/services/cron_service.dart b/lib/services/cron_service.dart deleted file mode 100644 index d3fe1cb9..00000000 --- a/lib/services/cron_service.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; - -import 'package:dartx/dartx.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:neat_periodic_task/neat_periodic_task.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -const _cronKeyPrefix = "cron_"; - -typedef Job = ( - String key, - Duration duration, - FutureOr Function() callback, -); - -class CronService with InfraLogger { - CronService(this.prefs); - - final SharedPreferences prefs; - - NeatPeriodicTaskScheduler? _scheduler; - Map jobs = {}; - - void schedule({ - required String key, - required Duration duration, - required FutureOr Function() callback, - }) { - loggy.debug("scheduling [$key]"); - jobs[key] = (key, duration, callback); - } - - Future run(Job job) async { - final key = job.$1; - final prefKey = "$_cronKeyPrefix$key"; - final previousRunTime = DateTime.tryParse(prefs.getString(prefKey) ?? ""); - loggy.debug( - "[$key] > ${previousRunTime == null ? "first run" : "previous run on [$previousRunTime]"}", - ); - - if (previousRunTime != null && - previousRunTime.add(job.$2) > DateTime.now()) { - loggy.debug("[$key] > didn't meet criteria"); - return; - } - - final result = await job.$3(); - await prefs.setString(prefKey, DateTime.now().toIso8601String()); - return result; - } - - Future startScheduler() async { - loggy.debug("starting job scheduler"); - await _scheduler?.stop(); - int runCount = 0; - _scheduler = NeatPeriodicTaskScheduler( - name: "cron job scheduler", - interval: const Duration(minutes: 10), - timeout: const Duration(minutes: 5), - minCycle: const Duration(minutes: 2), - task: () { - loggy.debug("in run ${runCount++}"); - return Future.wait(jobs.values.map(run)); - }, - ); - _scheduler!.start(); - } - - Future stopScheduler() async { - loggy.debug("stopping job scheduler"); - return _scheduler?.stop(); - } -} diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index 8b6995d3..9de8515e 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:path/path.dart' as p; @@ -20,8 +19,6 @@ class FilesEditorService with InfraLogger { late final Directories dirs; Directory get workingDir => dirs.workingDir; - Directory get configsDir => - Directory(p.join(workingDir.path, Constants.configsFolderName)); Directory get logsDir => dirs.workingDir; File get appLogsFile => File(p.join(logsDir.path, "app.log")); @@ -43,9 +40,6 @@ class FilesEditorService with InfraLogger { if (!await dirs.workingDir.exists()) { await dirs.workingDir.create(recursive: true); } - if (!await configsDir.exists()) { - await configsDir.create(recursive: true); - } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -68,14 +62,4 @@ class FilesEditorService with InfraLogger { } return getApplicationDocumentsDirectory(); } - - String configPath(String fileName) { - return p.join(configsDir.path, "$fileName.json"); - } - - String tempConfigPath(String fileName) => configPath("temp_$fileName"); - - Future deleteConfig(String fileName) { - return File(configPath(fileName)).delete(); - } } diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index ae7445e0..6b92132a 100644 --- a/lib/services/service_providers.dart +++ b/lib/services/service_providers.dart @@ -1,5 +1,3 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/services/cron_service.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -17,10 +15,3 @@ SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); @Riverpod(keepAlive: true) PlatformServices platformServices(PlatformServicesRef ref) => PlatformServices(); - -@Riverpod(keepAlive: true) -CronService cronService(CronServiceRef ref) { - final service = CronService(ref.watch(sharedPreferencesProvider)); - ref.onDispose(() => service.stopScheduler()); - return service; -} diff --git a/test/domain/profiles/profile_test.dart b/test/features/profile/data/profile_parser_test.dart similarity index 69% rename from test/domain/profiles/profile_test.dart rename to test/features/profile/data/profile_parser_test.dart index 15bfa2d0..96737245 100644 --- a/test/domain/profiles/profile_test.dart +++ b/test/features/profile/data/profile_parser_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:hiddify/domain/profiles/profiles.dart'; +import 'package:hiddify/features/profile/data/profile_parser.dart'; +import 'package:hiddify/features/profile/model/profile_entity.dart'; void main() { const validBaseUrl = "https://example.com/configurations/user1/filename.yaml"; @@ -8,14 +9,26 @@ void main() { const validSupportUrl = "https://example.com/support"; group( - "profile fromResponse", + "parse", () { test( - "with no additional metadata", + "url with file extension, no headers", () { - final profile = Profile.fromResponse(validExtendedUrl, {}); + final profile = ProfileParser.parse(validBaseUrl, {}); expect(profile.name, equals("filename")); + expect(profile.url, equals(validBaseUrl)); + expect(profile.options, isNull); + expect(profile.subInfo, isNull); + }, + ); + + test( + "url with url, no headers", + () { + final profile = ProfileParser.parse(validExtendedUrl, {}); + + expect(profile.name, equals("b")); expect(profile.url, equals(validExtendedUrl)); expect(profile.options, isNull); expect(profile.subInfo, isNull); @@ -23,7 +36,7 @@ void main() { ); test( - "with all metadata", + "with base64 profile-title header", () { final headers = >{ "profile-title": ["base64:ZXhhbXBsZVRpdGxl"], @@ -34,7 +47,7 @@ void main() { "profile-web-page-url": [validBaseUrl], "support-url": [validSupportUrl], }; - final profile = Profile.fromResponse(validExtendedUrl, headers); + final profile = ProfileParser.parse(validExtendedUrl, headers); expect(profile.name, equals("exampleTitle")); expect(profile.url, equals(validExtendedUrl)); From 0c1768e05e9b85d444cf63c713ef22b5133b571e Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 26 Nov 2023 21:59:57 +0330 Subject: [PATCH 22/22] Refactor router --- lib/core/router/app_router.dart | 4 +- lib/core/router/routes.dart | 390 +++++++++++++++++- lib/core/router/routes/desktop_routes.dart | 137 ------ lib/core/router/routes/mobile_routes.dart | 178 -------- lib/core/router/routes/shared_routes.dart | 129 ------ lib/features/common/nested_app_bar.dart | 2 +- .../widgets/empty_profiles_home_body.dart | 2 +- lib/features/profile/widget/profile_tile.dart | 2 +- 8 files changed, 383 insertions(+), 461 deletions(-) delete mode 100644 lib/core/router/routes/desktop_routes.dart delete mode 100644 lib/core/router/routes/mobile_routes.dart delete mode 100644 lib/core/router/routes/shared_routes.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 5de8f211..74a6587f 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -38,7 +38,9 @@ GoRouter router(RouterRef ref) { navigatorKey: rootNavigatorKey, initialLocation: initialLocation, debugLogDiagnostics: true, - routes: useMobileRouter ? mobileRoutes : desktopRoutes, + routes: [ + if (useMobileRouter) $mobileWrapperRoute else $desktopWrapperRoute, + ], refreshListenable: notifier, redirect: notifier.redirect, observers: [ diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index 5b9acd92..44a2089a 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -1,16 +1,380 @@ -import 'package:hiddify/core/router/routes/desktop_routes.dart' as desktop; -import 'package:hiddify/core/router/routes/mobile_routes.dart' as mobile; -import 'package:hiddify/core/router/routes/shared_routes.dart' as shared; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hiddify/core/router/app_router.dart'; +import 'package:hiddify/features/about/view/about_page.dart'; +import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; +import 'package:hiddify/features/home/view/view.dart'; +import 'package:hiddify/features/intro/intro_page.dart'; +import 'package:hiddify/features/logs/view/logs_page.dart'; +import 'package:hiddify/features/profile/add/add_profile_modal.dart'; +import 'package:hiddify/features/profile/details/profile_details_page.dart'; +import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; +import 'package:hiddify/features/proxies/view/view.dart'; +import 'package:hiddify/features/settings/view/config_options_page.dart'; +import 'package:hiddify/features/settings/view/per_app_proxy_page.dart'; +import 'package:hiddify/features/settings/view/settings_page.dart'; +import 'package:hiddify/utils/utils.dart'; -export 'routes/mobile_routes.dart'; -export 'routes/shared_routes.dart' hide $appRoutes; +part 'routes.g.dart'; -final mobileRoutes = [ - ...shared.$appRoutes, - ...mobile.$appRoutes, -]; +GlobalKey? _dynamicRootKey = + useMobileRouter ? rootNavigatorKey : null; -final desktopRoutes = [ - ...shared.$appRoutes, - ...desktop.$appRoutes, -]; +@TypedShellRoute( + routes: [ + TypedGoRoute(path: "/intro", name: IntroRoute.name), + TypedGoRoute( + path: "/", + name: HomeRoute.name, + routes: [ + TypedGoRoute( + path: "add", + name: AddProfileRoute.name, + ), + TypedGoRoute( + path: "profiles", + name: ProfilesOverviewRoute.name, + ), + TypedGoRoute( + path: "profiles/new", + name: NewProfileRoute.name, + ), + TypedGoRoute( + path: "profiles/:id", + name: ProfileDetailsRoute.name, + ), + TypedGoRoute( + path: "logs", + name: LogsRoute.name, + ), + TypedGoRoute( + path: "settings", + name: SettingsRoute.name, + routes: [ + TypedGoRoute( + path: "config-options", + name: ConfigOptionsRoute.name, + ), + TypedGoRoute( + path: "per-app-proxy", + name: PerAppProxyRoute.name, + ), + TypedGoRoute( + path: "routing-assets", + name: GeoAssetsRoute.name, + ), + ], + ), + TypedGoRoute( + path: "about", + name: AboutRoute.name, + ), + ], + ), + TypedGoRoute( + path: "/proxies", + name: ProxiesRoute.name, + ), + ], +) +class MobileWrapperRoute extends ShellRouteData { + const MobileWrapperRoute(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return AdaptiveRootScaffold(navigator); + } +} + +@TypedShellRoute( + routes: [ + TypedGoRoute(path: "/intro", name: IntroRoute.name), + TypedGoRoute( + path: "/", + name: HomeRoute.name, + routes: [ + TypedGoRoute( + path: "add", + name: AddProfileRoute.name, + ), + TypedGoRoute( + path: "profiles", + name: ProfilesOverviewRoute.name, + ), + TypedGoRoute( + path: "profiles/new", + name: NewProfileRoute.name, + ), + TypedGoRoute( + path: "profiles/:id", + name: ProfileDetailsRoute.name, + ), + ], + ), + TypedGoRoute( + path: "/proxies", + name: ProxiesRoute.name, + ), + TypedGoRoute( + path: "/logs", + name: LogsRoute.name, + ), + TypedGoRoute( + path: "/settings", + name: SettingsRoute.name, + routes: [ + TypedGoRoute( + path: "config-options", + name: ConfigOptionsRoute.name, + ), + TypedGoRoute( + path: "routing-assets", + name: GeoAssetsRoute.name, + ), + ], + ), + TypedGoRoute( + path: "/about", + name: AboutRoute.name, + ), + ], +) +class DesktopWrapperRoute extends ShellRouteData { + const DesktopWrapperRoute(); + + @override + Widget builder(BuildContext context, GoRouterState state, Widget navigator) { + return AdaptiveRootScaffold(navigator); + } +} + +class IntroRoute extends GoRouteData { + const IntroRoute(); + static const name = "Intro"; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: IntroPage(), + ); + } +} + +class HomeRoute extends GoRouteData { + const HomeRoute(); + static const name = "Home"; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage( + name: name, + child: HomePage(), + ); + } +} + +class ProxiesRoute extends GoRouteData { + const ProxiesRoute(); + static const name = "Proxies"; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const NoTransitionPage( + name: name, + child: ProxiesPage(), + ); + } +} + +class AddProfileRoute extends GoRouteData { + const AddProfileRoute({this.url}); + + final String? url; + + static const name = "Add Profile"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return BottomSheetPage( + fixed: true, + name: name, + builder: (controller) => AddProfileModal( + url: url, + scrollController: controller, + ), + ); + } +} + +class ProfilesOverviewRoute extends GoRouteData { + const ProfilesOverviewRoute(); + static const name = "Profiles"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return BottomSheetPage( + name: name, + builder: (controller) => + ProfilesOverviewModal(scrollController: controller), + ); + } +} + +class NewProfileRoute extends GoRouteData { + const NewProfileRoute(); + static const name = "New Profile"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: ProfileDetailsPage("new"), + ); + } +} + +class ProfileDetailsRoute extends GoRouteData { + const ProfileDetailsRoute(this.id); + final String id; + static const name = "Profile Details"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return MaterialPage( + fullscreenDialog: true, + name: name, + child: ProfileDetailsPage(id), + ); + } +} + +class LogsRoute extends GoRouteData { + const LogsRoute(); + static const name = "Logs"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: LogsPage(), + ); + } + return const NoTransitionPage(name: name, child: LogsPage()); + } +} + +class SettingsRoute extends GoRouteData { + const SettingsRoute(); + static const name = "Settings"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: SettingsPage(), + ); + } + return const NoTransitionPage(name: name, child: SettingsPage()); + } +} + +class ConfigOptionsRoute extends GoRouteData { + const ConfigOptionsRoute(); + static const name = "Config Options"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: ConfigOptionsPage(), + ); + } + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: ConfigOptionsPage(), + ); + } +} + +class PerAppProxyRoute extends GoRouteData { + const PerAppProxyRoute(); + static const name = "Per-app Proxy"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: PerAppProxyPage(), + ); + } +} + +class GeoAssetsRoute extends GoRouteData { + const GeoAssetsRoute(); + static const name = "Routing Assets"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsOverviewPage(), + ); + } + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: GeoAssetsOverviewPage(), + ); + } +} + +class AboutRoute extends GoRouteData { + const AboutRoute(); + static const name = "About"; + + static final GlobalKey? $parentNavigatorKey = _dynamicRootKey; + + @override + Page buildPage(BuildContext context, GoRouterState state) { + if (useMobileRouter) { + return const MaterialPage( + fullscreenDialog: true, + name: name, + child: AboutPage(), + ); + } + return const NoTransitionPage(name: name, child: AboutPage()); + } +} diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart deleted file mode 100644 index 3830b7c3..00000000 --- a/lib/core/router/routes/desktop_routes.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/router/routes/shared_routes.dart'; -import 'package:hiddify/features/about/view/view.dart'; -import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; -import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; -import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/view/view.dart'; - -part 'desktop_routes.g.dart'; - -@TypedShellRoute( - routes: [ - TypedGoRoute( - path: HomeRoute.path, - name: HomeRoute.name, - routes: [ - TypedGoRoute( - path: AddProfileRoute.path, - name: AddProfileRoute.name, - ), - TypedGoRoute( - path: ProfilesRoute.path, - name: ProfilesRoute.name, - ), - TypedGoRoute( - path: NewProfileRoute.path, - name: NewProfileRoute.name, - ), - TypedGoRoute( - path: ProfileDetailsRoute.path, - name: ProfileDetailsRoute.name, - ), - ], - ), - TypedGoRoute( - path: ProxiesRoute.path, - name: ProxiesRoute.name, - ), - TypedGoRoute( - path: LogsRoute.path, - name: LogsRoute.name, - ), - TypedGoRoute( - path: SettingsRoute.path, - name: SettingsRoute.name, - routes: [ - TypedGoRoute( - path: ConfigOptionsRoute.path, - name: ConfigOptionsRoute.name, - ), - TypedGoRoute( - path: GeoAssetsRoute.path, - name: GeoAssetsRoute.name, - ), - ], - ), - TypedGoRoute( - path: AboutRoute.path, - name: AboutRoute.name, - ), - ], -) -class DesktopWrapperRoute extends ShellRouteData { - const DesktopWrapperRoute(); - - @override - Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - return AdaptiveRootScaffold(navigator); - } -} - -class LogsRoute extends GoRouteData { - const LogsRoute(); - static const path = '/logs'; - static const name = 'Logs'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage(name: name, child: LogsPage()); - } -} - -class SettingsRoute extends GoRouteData { - const SettingsRoute(); - static const path = '/settings'; - static const name = 'Settings'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage(name: name, child: SettingsPage()); - } -} - -class ConfigOptionsRoute extends GoRouteData { - const ConfigOptionsRoute(); - static const path = 'config-options'; - static const name = 'Config Options'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ConfigOptionsPage(), - ); - } -} - -class GeoAssetsRoute extends GoRouteData { - const GeoAssetsRoute(); - static const path = 'routing-assets'; - static const name = 'Routing Assets'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: GeoAssetsOverviewPage(), - ); - } -} - -class AboutRoute extends GoRouteData { - const AboutRoute(); - static const path = '/about'; - static const name = 'About'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( - name: name, - child: AboutPage(), - ); - } -} diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart deleted file mode 100644 index 5dcf0802..00000000 --- a/lib/core/router/routes/mobile_routes.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/router/app_router.dart'; -import 'package:hiddify/core/router/routes/shared_routes.dart'; -import 'package:hiddify/features/about/view/view.dart'; -import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; -import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; -import 'package:hiddify/features/logs/view/view.dart'; -import 'package:hiddify/features/settings/view/view.dart'; - -part 'mobile_routes.g.dart'; - -@TypedShellRoute( - routes: [ - TypedGoRoute( - path: HomeRoute.path, - name: HomeRoute.name, - routes: [ - TypedGoRoute( - path: AddProfileRoute.path, - name: AddProfileRoute.name, - ), - TypedGoRoute( - path: ProfilesRoute.path, - name: ProfilesRoute.name, - ), - TypedGoRoute( - path: NewProfileRoute.path, - name: NewProfileRoute.name, - ), - TypedGoRoute( - path: ProfileDetailsRoute.path, - name: ProfileDetailsRoute.name, - ), - TypedGoRoute( - path: LogsRoute.path, - name: LogsRoute.name, - ), - TypedGoRoute( - path: SettingsRoute.path, - name: SettingsRoute.name, - routes: [ - TypedGoRoute( - path: ConfigOptionsRoute.path, - name: ConfigOptionsRoute.name, - ), - TypedGoRoute( - path: PerAppProxyRoute.path, - name: PerAppProxyRoute.name, - ), - TypedGoRoute( - path: GeoAssetsRoute.path, - name: GeoAssetsRoute.name, - ), - ], - ), - TypedGoRoute( - path: AboutRoute.path, - name: AboutRoute.name, - ), - ], - ), - TypedGoRoute( - path: ProxiesRoute.path, - name: ProxiesRoute.name, - ), - ], -) -class MobileWrapperRoute extends ShellRouteData { - const MobileWrapperRoute(); - - @override - Widget builder(BuildContext context, GoRouterState state, Widget navigator) { - return AdaptiveRootScaffold(navigator); - } -} - -class LogsRoute extends GoRouteData { - const LogsRoute(); - static const path = 'logs'; - static const name = 'Logs'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: LogsPage(), - ); - } -} - -class SettingsRoute extends GoRouteData { - const SettingsRoute(); - static const path = 'settings'; - static const name = 'Settings'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: SettingsPage(), - ); - } -} - -class ConfigOptionsRoute extends GoRouteData { - const ConfigOptionsRoute(); - static const path = 'config-options'; - static const name = 'Config Options'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ConfigOptionsPage(), - ); - } -} - -class PerAppProxyRoute extends GoRouteData { - const PerAppProxyRoute(); - static const path = 'per-app-proxy'; - static const name = 'Per-app Proxy'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: PerAppProxyPage(), - ); - } -} - -class GeoAssetsRoute extends GoRouteData { - const GeoAssetsRoute(); - static const path = 'routing-assets'; - static const name = 'Routing Assets'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: GeoAssetsOverviewPage(), - ); - } -} - -class AboutRoute extends GoRouteData { - const AboutRoute(); - static const path = 'about'; - static const name = 'About'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: AboutPage(), - ); - } -} diff --git a/lib/core/router/routes/shared_routes.dart b/lib/core/router/routes/shared_routes.dart deleted file mode 100644 index 1960b7cc..00000000 --- a/lib/core/router/routes/shared_routes.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/router/app_router.dart'; -import 'package:hiddify/features/home/view/view.dart'; -import 'package:hiddify/features/intro/intro_page.dart'; -import 'package:hiddify/features/profile/add/add_profile_modal.dart'; -import 'package:hiddify/features/profile/details/profile_details_page.dart'; -import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; -import 'package:hiddify/features/proxies/view/view.dart'; -import 'package:hiddify/utils/utils.dart'; - -part 'shared_routes.g.dart'; - -class HomeRoute extends GoRouteData { - const HomeRoute(); - static const path = '/'; - static const name = 'Home'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( - name: name, - child: HomePage(), - ); - } -} - -class ProxiesRoute extends GoRouteData { - const ProxiesRoute(); - static const path = '/proxies'; - static const name = 'Proxies'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const NoTransitionPage( - name: name, - child: ProxiesPage(), - ); - } -} - -class AddProfileRoute extends GoRouteData { - const AddProfileRoute({this.url}); - static const path = 'add'; - static const name = 'Add Profile'; - final String? url; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return BottomSheetPage( - fixed: true, - name: name, - builder: (controller) => AddProfileModal( - url: url, - scrollController: controller, - ), - ); - } -} - -@TypedGoRoute(path: IntroRoute.path, name: IntroRoute.name) -class IntroRoute extends GoRouteData { - const IntroRoute(); - static const path = '/intro'; - static const name = 'Intro'; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: IntroPage(), - ); - } -} - -class ProfilesRoute extends GoRouteData { - const ProfilesRoute(); - static const path = 'profiles'; - static const name = 'Profiles'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return BottomSheetPage( - name: name, - builder: (controller) => - ProfilesOverviewModal(scrollController: controller), - ); - } -} - -class NewProfileRoute extends GoRouteData { - const NewProfileRoute(); - static const path = 'profiles/new'; - static const name = 'New Profile'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ProfileDetailsPage("new"), - ); - } -} - -class ProfileDetailsRoute extends GoRouteData { - const ProfileDetailsRoute(this.id); - final String id; - static const path = 'profiles/:id'; - static const name = 'Profile Details'; - - static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - - @override - Page buildPage(BuildContext context, GoRouterState state) { - return MaterialPage( - fullscreenDialog: true, - name: name, - child: ProfileDetailsPage(id), - ); - } -} diff --git a/lib/features/common/nested_app_bar.dart b/lib/features/common/nested_app_bar.dart index 0d872532..e9281f03 100644 --- a/lib/features/common/nested_app_bar.dart +++ b/lib/features/common/nested_app_bar.dart @@ -7,7 +7,7 @@ bool showDrawerButton(BuildContext context) { if (!useMobileRouter) return true; final String location = GoRouterState.of(context).uri.path; if (location == const HomeRoute().location || - location == const ProfilesRoute().location) return true; + location == const ProfilesOverviewRoute().location) return true; if (location.startsWith(const ProxiesRoute().location)) return true; return false; } diff --git a/lib/features/home/widgets/empty_profiles_home_body.dart b/lib/features/home/widgets/empty_profiles_home_body.dart index a633dce9..8937ce18 100644 --- a/lib/features/home/widgets/empty_profiles_home_body.dart +++ b/lib/features/home/widgets/empty_profiles_home_body.dart @@ -44,7 +44,7 @@ class EmptyActiveProfileHomeBody extends HookConsumerWidget { Text(t.home.noActiveProfileMsg), const Gap(16), OutlinedButton( - onPressed: () => const ProfilesRoute().push(context), + onPressed: () => const ProfilesOverviewRoute().push(context), child: Text(t.profile.overviewPageTitle), ), ], diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart index 7d526491..c0000cb4 100644 --- a/lib/features/profile/widget/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -90,7 +90,7 @@ class ProfileTile extends HookConsumerWidget { child: InkWell( onTap: () { if (isMain) { - const ProfilesRoute().go(context); + const ProfilesOverviewRoute().go(context); } else { if (selectActiveMutation.state.isInProgress) return; if (profile.active) return;