diff --git a/.gitignore b/.gitignore index a78395b8..09936788 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ app.*.map.json Firebase related .firebase +/lib/utils/environment.dart \ No newline at end of file diff --git a/lib/app/app.dart b/lib/app/app.dart index 76c32b19..2f10b09a 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,3 +1,4 @@ +import 'package:revanced_manager/services/crowdin_api.dart'; import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/patcher_api.dart'; @@ -37,6 +38,7 @@ import 'package:stacked_services/stacked_services.dart'; LazySingleton(classType: PatcherAPI), LazySingleton(classType: RevancedAPI), LazySingleton(classType: GithubAPI), + LazySingleton(classType: CrowdinAPI), LazySingleton(classType: Toast), ], ) diff --git a/lib/main.dart b/lib/main.dart index eef3a22d..fd2b20b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,16 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:revanced_manager/app/app.locator.dart'; +import 'package:revanced_manager/services/crowdin_api.dart'; import 'package:revanced_manager/services/github_api.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/patcher_api.dart'; import 'package:revanced_manager/services/revanced_api.dart'; import 'package:revanced_manager/ui/theme/dynamic_theme_builder.dart'; import 'package:revanced_manager/ui/views/navigation/navigation_view.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:stacked_themes/stacked_themes.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:timezone/data/latest.dart' as tz; +late SharedPreferences prefs; Future main() async { await ThemeManager.initialise(); await setupLocator(); @@ -19,10 +22,12 @@ Future main() async { await locator().initialize(); String apiUrl = locator().getApiUrl(); await locator().initialize(apiUrl); + await locator().initialize(); // bool isSentryEnabled = locator().isSentryEnabled(); locator().initialize(); await locator().initialize(); tz.initializeTimeZones(); + prefs = await SharedPreferences.getInstance(); // Remove this section if you are building from source and don't have sentry configured // await SentryFlutter.init( @@ -55,15 +60,25 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + String rawLocale = prefs.getString('language') ?? 'en_US'; + String replaceLocale = rawLocale.replaceAll('_', '-'); + List localeList = replaceLocale.split('-'); + Locale locale = Locale(localeList[0], localeList[1]); + return DynamicThemeBuilder( title: 'ReVanced Manager', home: const NavigationView(), localizationsDelegates: [ FlutterI18nDelegate( translationLoader: FileTranslationLoader( - fallbackFile: 'en_US', + forcedLocale: locale, basePath: 'assets/i18n', + useCountryCode: true, ), + missingTranslationHandler: (key, locale) { + print( + '--> Missing translation: key: $key, languageCode: ${locale?.languageCode}'); + }, ), GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate diff --git a/lib/services/crowdin_api.dart b/lib/services/crowdin_api.dart new file mode 100644 index 00000000..29312ba5 --- /dev/null +++ b/lib/services/crowdin_api.dart @@ -0,0 +1,61 @@ +import 'package:dio/dio.dart'; +import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; +import 'package:injectable/injectable.dart' hide Environment; +import 'package:revanced_manager/utils/environment.dart'; +import 'package:sentry_dio/sentry_dio.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +@lazySingleton +class CrowdinAPI { + late Dio _dio = Dio(); + final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); + final apiKey = Environment.crowdinKEY; + + Future initialize() async { + try { + _dio = Dio(BaseOptions( + baseUrl: 'https://api.crowdin.com/api/v2', + )); + + _dio.interceptors.add(_dioCacheManager.interceptor); + _dio.addSentry( + captureFailedRequests: true, + ); + } on Exception catch (e, s) { + await Sentry.captureException(e, stackTrace: s); + } + } + + Future clearAllCache() async { + try { + await _dioCacheManager.clearAll(); + } on Exception catch (e, s) { + await Sentry.captureException(e, stackTrace: s); + } + } + + Future getLanguages() async { + try { + var response = await _dio.get( + '/projects', + options: buildCacheOptions( + const Duration(hours: 6), + maxStale: const Duration(days: 1), + options: Options( + headers: { + 'Authorization': 'Bearer $apiKey', + }, + contentType: 'application/json', + ), + ), + ); + List targetLanguages = + await response.data['data'][0]['data']['targetLanguages']; + + return targetLanguages; + } on Exception catch (e, s) { + await Sentry.captureException(e, stackTrace: s); + return []; + } + } +} diff --git a/lib/ui/views/settings/settings_view.dart b/lib/ui/views/settings/settings_view.dart index 96a74e82..702de165 100644 --- a/lib/ui/views/settings/settings_view.dart +++ b/lib/ui/views/settings/settings_view.dart @@ -92,7 +92,7 @@ class SettingsView extends StatelessWidget { SettingsTileDialog( padding: const EdgeInsets.symmetric(horizontal: 20.0), title: 'settingsView.languageLabel', - subtitle: 'English', + subtitle: model.selectedLanguage, onTap: () => model.showLanguagesDialog(context), ), _settingsDivider, diff --git a/lib/ui/views/settings/settings_viewmodel.dart b/lib/ui/views/settings/settings_viewmodel.dart index bcc602d5..33f35442 100644 --- a/lib/ui/views/settings/settings_viewmodel.dart +++ b/lib/ui/views/settings/settings_viewmodel.dart @@ -11,8 +11,11 @@ import 'package:logcat/logcat.dart'; import 'package:path_provider/path_provider.dart'; import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.router.dart'; +import 'package:revanced_manager/main.dart'; +import 'package:revanced_manager/services/crowdin_api.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/services/toast.dart'; +import 'package:revanced_manager/ui/views/navigation/navigation_viewmodel.dart'; import 'package:revanced_manager/ui/views/patcher/patcher_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:revanced_manager/ui/widgets/settingsView/custom_text_field.dart'; @@ -20,7 +23,8 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:share_extend/share_extend.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; -import 'package:timeago/timeago.dart'; +import 'package:timeago/timeago.dart' as timeago; +import 'package:shared_preferences/shared_preferences.dart'; // ignore: constant_identifier_names const int ANDROID_12_SDK_VERSION = 31; @@ -28,14 +32,28 @@ const int ANDROID_12_SDK_VERSION = 31; class SettingsViewModel extends BaseViewModel { final NavigationService _navigationService = locator(); final ManagerAPI _managerAPI = locator(); + final CrowdinAPI _crowdinAPI = locator(); final Toast _toast = locator(); final TextEditingController _orgPatSourceController = TextEditingController(); final TextEditingController _patSourceController = TextEditingController(); final TextEditingController _orgIntSourceController = TextEditingController(); final TextEditingController _intSourceController = TextEditingController(); final TextEditingController _apiUrlController = TextEditingController(); + late SharedPreferences _prefs; + String selectedLanguage = 'English'; + String selectedLanguageLocale = prefs.getString('language') ?? 'en_US'; + List languages = []; - void setLanguage(String language) { + Future initLang() async { + languages = await _crowdinAPI.getLanguages(); + languages.sort((a, b) => a['name'].compareTo(b['name'])); + notifyListeners(); + } + + Future initialize() async { + _prefs = await SharedPreferences.getInstance(); + selectedLanguageLocale = + _prefs.getString('language') ?? selectedLanguageLocale; notifyListeners(); } @@ -45,8 +63,13 @@ class SettingsViewModel extends BaseViewModel { Future updateLanguage(BuildContext context, String? value) async { if (value != null) { + selectedLanguageLocale = value; + _prefs = await SharedPreferences.getInstance(); + await _prefs.setString('language', value); await FlutterI18n.refresh(context, Locale(value)); - setLocaleMessages(value, EnMessages()); + timeago.setLocaleMessages(value, timeago.EnMessages()); + locator().notifyListeners(); + notifyListeners(); } } @@ -86,21 +109,33 @@ class SettingsViewModel extends BaseViewModel { notifyListeners(); } - Future showLanguagesDialog(BuildContext context) { + Future showLanguagesDialog(BuildContext parentContext) { + initLang(); return showDialog( - context: context, + context: parentContext, builder: (context) => SimpleDialog( title: I18nText('settingsView.languageLabel'), backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - children: [ - RadioListTile( - title: I18nText('settingsView.englishOption'), - value: 'en', - groupValue: 'en', - onChanged: (value) { - updateLanguage(context, value); - Navigator.of(context).pop(); - }, + children: [ + SizedBox( + height: 500, + child: ListView.builder( + itemCount: languages.length, + itemBuilder: (context, index) { + return RadioListTile( + title: Text(languages[index]['name']), + subtitle: Text(languages[index]['locale']), + value: languages[index]['locale'], + groupValue: selectedLanguageLocale, + onChanged: (value) { + selectedLanguage = languages[index]['name']; + _toast.show('settingsView.restartAppForChanges'); + updateLanguage(context, value); + Navigator.pop(context); + }, + ); + }, + ), ), ], ), @@ -355,16 +390,13 @@ class SettingsViewModel extends BaseViewModel { try { File outFile = File(_managerAPI.storedPatchesFile); if (outFile.existsSync()) { - String dateTime = DateTime.now() - .toString() - .replaceAll(' ', '_') - .split('.').first; - String tempFilePath = '${outFile.path.substring(0, outFile.path.lastIndexOf('/') + 1)}selected_patches_$dateTime.json'; + String dateTime = + DateTime.now().toString().replaceAll(' ', '_').split('.').first; + String tempFilePath = + '${outFile.path.substring(0, outFile.path.lastIndexOf('/') + 1)}selected_patches_$dateTime.json'; outFile.copySync(tempFilePath); await CRFileSaver.saveFileWithDialog(SaveFileDialogParams( - sourceFilePath: tempFilePath, - destinationFileName: '' - )); + sourceFilePath: tempFilePath, destinationFileName: '')); File(tempFilePath).delete(); locator().showBottom('settingsView.exportedPatches'); } else {