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) در پروژه ما را مطالعه نمایید.
diff --git a/README_ru.md b/README_ru.md
index f8c6811a..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/). Для получения дополнительной информации вы можете прочитать наши Рекомендации по участию в разработке.
+Приложение разработано с использованием [Flutter](https://flutter.dev/) и [Go](https://go.dev/). Для получения дополнительной информации о разработке вы можете прочитать наши [Рекомендации по участию](CONTRIBUTING.md) .

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
diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json
index a774f9a0..24a7dfce 100644
--- a/assets/translations/strings_fa.i18n.json
+++ b/assets/translations/strings_fa.i18n.json
@@ -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
+}
diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json
index 4d75e461..e8223be9 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": {
@@ -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": "Георесурсы отсутствуют. Изменените выбранный ресурс или загрузите собственный в настройках.",
"invalidConfigOptions": "Неправильные параметры конфигурации",
"invalidConfig": "Неправильная конфигурация",
"create": "Ошибка создания сервиса",
- "start": "Ошибка запуска сервиса",
- "missingGeoAssets": "Отсутствующие географические ресурсы",
- "missingGeoAssetsMsg": "Георесурсы отсутствуют. рассмотрите возможность изменения активного актива или загрузите выбранный в настройках."
+ "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
diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json
index 15309c26..f381d98e 100644
--- a/assets/translations/strings_tr.i18n.json
+++ b/assets/translations/strings_tr.i18n.json
@@ -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
+}
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/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/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/lib/bootstrap.dart b/lib/bootstrap.dart
index 4cfa3d18..465d22d6 100644
--- a/lib/bootstrap.dart
+++ b/lib/bootstrap.dart
@@ -10,8 +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';
@@ -86,6 +88,8 @@ 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/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/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 186f7ddd..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/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';
-
-@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: GeoAssetsPage(),
- );
- }
-}
-
-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 79c28024..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/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';
-
-@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: GeoAssetsPage(),
- );
- }
-}
-
-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 76410f5f..00000000
--- a/lib/core/router/routes/shared_routes.dart
+++ /dev/null
@@ -1,127 +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_detail/view/view.dart';
-import 'package:hiddify/features/profiles/view/view.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) => ProfilesModal(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: ProfileDetailPage("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: ProfileDetailPage(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 f1c47452..d70bef5b 100644
--- a/lib/data/data_providers.dart
+++ b/lib/data/data_providers.dart
@@ -4,18 +4,16 @@ 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/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/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';
@@ -49,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));
@@ -70,38 +54,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 +83,8 @@ Future configOptions(ConfigOptionsRef ref) async {
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/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
deleted file mode 100644
index 571afd89..00000000
--- a/lib/data/local/data_mappers.dart
+++ /dev/null
@@ -1,100 +0,0 @@
-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() {
- 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,
- ),
- };
- }
-}
-
-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..e18897d6 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/features/profile/model/profile_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..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/domain/rules/geo_asset.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 d254726c..be953ec1 100644
--- a/lib/data/repository/core_facade_impl.dart
+++ b/lib/data/repository/core_facade_impl.dart
@@ -10,6 +10,8 @@ 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/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';
@@ -19,6 +21,8 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
CoreFacadeImpl(
this.singbox,
this.filesEditor,
+ this.geoAssetPathResolver,
+ this.profilePathResolver,
this.platformServices,
this.clash,
this.debug,
@@ -27,6 +31,8 @@ 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;
@@ -38,8 +44,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());
}
@@ -112,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);
@@ -130,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}",
@@ -152,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),
);
},
@@ -174,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/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