diff --git a/README.md b/README.md index 3d21466..e218721 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ Currently supported App sources: - [Signal](https://signal.org/) - [SourceForge](https://sourceforge.net/) - [APKMirror](https://apkmirror.com/) (Track-Only) -- Third Party F-Droid Repos (URLs ending with `/fdroid/repo`) +- Third Party F-Droid Repos + - Any URLs ending with `/fdroid/`, where `` can be anything - most often `repo` - [Steam](https://store.steampowered.com/mobile) ## Limitations diff --git a/assets/translations/de.json b/assets/translations/de.json index aea452e..441421c 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -188,25 +188,27 @@ "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", - "install": "Install", - "markInstalled": "Mark Installed", - "update": "Update", - "markUpdated": "Mark Updated", - "additionalOptions": "Additional Options", - "disableVersionDetection": "Disable Version Detection", - "noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.", - "downloadingX": "Downloading {}", - "downloadNotifDescription": "Notifies the user of the progress in downloading an App", - "noAPKFound": "No APK found", - "noVersionDetection": "No version detection", - "categorize": "Categorize", - "categories": "Categories", - "category": "Category", - "noCategory": "No Category", - "deleteCategoryQuestion": "Delete Category?", - "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", - "addCategory": "Add Category", - "label": "Label", + "install": "Installieren", + "markInstalled": "Als Installiert markieren", + "update": "Aktualisieren", + "markUpdated": "Als Aktuell markieren", + "additionalOptions": "Zusätzliche Optionen", + "disableVersionDetection": "Versionsermittlung deaktivieren", + "noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.", + "downloadingX": "Lade {} herunter", + "downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App", + "noAPKFound": "Keine APK gefunden", + "noVersionDetection": "Keine Versionserkennung", + "categorize": "Kategorisieren", + "categories": "Kategorien", + "category": "Kategorie", + "noCategory": "Keine Kategorie", + "noCategories": "Keine Kategorien", + "deleteCategoriesQuestion": "Kategorien löschen?", + "categoryDeleteWarning": "Alle Apps in gelöschten Kategorien werden auf nicht kategorisiert gesetzt.", + "addCategory": "Kategorie hinzufügen", + "label": "Bezeichnung", + "language": "Sprache", "tooManyRequestsTryAgainInMinutes": { "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut", "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut" diff --git a/assets/translations/en.json b/assets/translations/en.json index 99b57e7..8231108 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -203,10 +203,12 @@ "categories": "Categories", "category": "Category", "noCategory": "No Category", - "deleteCategoryQuestion": "Delete Category?", - "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", + "noCategories": "No Categories", + "deleteCategoriesQuestion": "Delete Categories?", + "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", "addCategory": "Add Category", "label": "Label", + "language": "Language", "tooManyRequestsTryAgainInMinutes": { "one": "Too many requests (rate limited) - try again in {} minute", "other": "Too many requests (rate limited) - try again in {} minutes" diff --git a/assets/translations/hu.json b/assets/translations/hu.json index 996c836..0bd8dae 100644 --- a/assets/translations/hu.json +++ b/assets/translations/hu.json @@ -28,13 +28,13 @@ "noDescription": "Nincs leírás", "cancel": "Mégse", "continue": "Tovább", - "requiredInBrackets": "(Kötlező)", + "requiredInBrackets": "(Kötelező)", "dropdownNoOptsError": "HIBA: A LEDOBÁST LEGALÁBB EGY OPCIÓHOZ KELL RENDELNI", "colour": "Szín", "githubStarredRepos": "GitHub Csillagos Repo-k", "uname": "Felh.név", "wrongArgNum": "Rossz számú argumentumot adott meg", - "xIsTrackOnly": "{} csak nyomkövethető", + "xIsTrackOnly": "A(z) {} csak nyomkövethető", "source": "Forrás", "app": "App", "appsFromSourceAreTrackOnly": "Az ebből a forrásból származó alkalmazások 'Csak nyomon követhetőek'.", @@ -65,11 +65,11 @@ "estimateInBrackets": "(Becslés)", "selectAll": "Mindet kiválaszt", "deselectN": "Törölje {} kijelölését", - "xWillBeRemovedButRemainInstalled": "{} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.", + "xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.", "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?", "removeSelectedApps": "Távolítsa el a kiválasztott appokat", "updateX": "Frissítés: {}", - "installX": "Telepítés {}", + "installX": "Telepítés: {}", "markXTrackOnlyAsUpdated": "Jelölje meg: {}\n(Csak nyomon követhető)\nas Frissítve", "changeX": "Változás {}", "installUpdateApps": "Appok telepítése/frissítése", @@ -118,7 +118,7 @@ "selectURLs": "Kiválasztott URL-ek", "pick": "Válasszon", "theme": "Téma", - "dark": "Söét", + "dark": "Sötét", "light": "Világos", "followSystem": "Rendszer szerint", "obtainium": "Obtainium", @@ -184,7 +184,7 @@ "appIdOrName": "App ID vagy név", "appWithIdOrNameNotFound": "Nem található app ezzel az azonosítóval vagy névvel", "reposHaveMultipleApps": "A repók több alkalmazást is tartalmazhatnak", - "fdroidThirdPartyRepo": "F-Droid Harmadik fél Repo", + "fdroidThirdPartyRepo": "F-Droid Harmadik-fél Repo", "steam": "Steam", "steamMobile": "Steam Mobile", "steamChat": "Steam Chat", diff --git a/assets/translations/it.json b/assets/translations/it.json index d4a660b..2499dfb 100644 --- a/assets/translations/it.json +++ b/assets/translations/it.json @@ -203,10 +203,12 @@ "categories": "Categories", "category": "Category", "noCategory": "No Category", - "deleteCategoryQuestion": "Delete Category?", - "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", + "noCategories": "No Categories", + "deleteCategoriesQuestion": "Delete Categories?", + "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", "addCategory": "Add Category", "label": "Label", + "language": "Language", "tooManyRequestsTryAgainInMinutes": { "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" diff --git a/assets/translations/ja.json b/assets/translations/ja.json index b500a4e..86c9b3f 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -27,7 +27,7 @@ "invalidRegEx": "無効な正規表現", "noDescription": "説明はありません", "cancel": "キャンセル", - "continue": "続ける", + "continue": "続行", "requiredInBrackets": "(必須)", "dropdownNoOptsError": "エラー: ドロップダウンには、少なくとも1つのオプションが必要です", "colour": "カラー", @@ -64,7 +64,7 @@ "notInstalled": "未インストール", "estimateInBrackets": "(推定)", "selectAll": "すべて選択", - "deselectN": "{}件を選択解除", + "deselectN": "{}件の選択を解除", "xWillBeRemovedButRemainInstalled": "{}はObtainiumから削除されますが、デバイスにはインストールされたままです。", "removeSelectedAppsQuestion": "選択したアプリを削除しますか?", "removeSelectedApps": "選択したアプリを削除する", @@ -135,7 +135,7 @@ "appearance": "外観", "showWebInAppView": "アプリビューにソースウェブページを表示する", "pinUpdates": "アップデートがあるアプリをトップに固定する", - "updates": "更新", + "updates": "アップデート", "sourceSpecific": "Github アクセストークン", "appSource": "アプリのソース", "noLogs": "ログはありません", @@ -144,7 +144,7 @@ "share": "共有", "appNotFound": "アプリが見つかりません", "obtainiumExportHyphenatedLowercase": "obtainium-エクスポート", - "pickAnAPK": "APKを選ぶ", + "pickAnAPK": "APKを選択", "appHasMoreThanOnePackage": "{}は複数のパッケージが存在します: ", "deviceSupportsXArch": "お使いのデバイスは{} CPUアーキテクチャに対応しています。", "deviceSupportsFollowingArchs": "お使いのデバイスは、以下のCPUアーキテクチャをサポートしています:", @@ -203,10 +203,12 @@ "categories": "カテゴリ", "category": "カテゴリ", "noCategory": "カテゴリなし", - "deleteCategoryQuestion": "カテゴリを削除しますか?", - "categoryDeleteWarning": "「{}」内のすべてのアプリは未分類に設定されます。", + "noCategories": "カテゴリなし", + "deleteCategoriesQuestion": "カテゴリを削除しますか?", + "categoryDeleteWarning": "削除されたカテゴリ内のアプリは未分類に設定されます。", "addCategory": "カテゴリを追加", "label": "ラベル", + "language": "言語", "tooManyRequestsTryAgainInMinutes": { "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 2d968ca..36cf8c2 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -203,10 +203,12 @@ "categories": "Categories", "category": "Category", "noCategory": "No Category", - "deleteCategoryQuestion": "Delete Category?", - "categoryDeleteWarning": "All Apps in {} will be set to uncategorized.", + "noCategories": "No Categories", + "deleteCategoriesQuestion": "Delete Categories?", + "categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", "addCategory": "Add Category", "label": "Label", + "language": "Language", "tooManyRequestsTryAgainInMinutes": { "one": "请求过多 (API 限制) - 在 {} 分钟后重试", "other": "请求过多 (API 限制) - 在 {} 分钟后重试" diff --git a/lib/app_sources/fdroidrepo.dart b/lib/app_sources/fdroidrepo.dart index 0f63de5..0e9a7f7 100644 --- a/lib/app_sources/fdroidrepo.dart +++ b/lib/app_sources/fdroidrepo.dart @@ -22,7 +22,7 @@ class FDroidRepo extends AppSource { @override String standardizeURL(String url) { RegExp standardUrlRegExp = - RegExp('^https?://.+/fdroid/(repo(/|\\?)|repo\$)'); + RegExp('^https?://.+/fdroid/([^/]+(/|\\?)|[^/]+\$)'); RegExpMatch? match = standardUrlRegExp.firstMatch(url.toLowerCase()); if (match == null) { throw InvalidURLError(name); diff --git a/lib/components/generated_form.dart b/lib/components/generated_form.dart index f1a845a..bc0e237 100644 --- a/lib/components/generated_form.dart +++ b/lib/components/generated_form.dart @@ -1,5 +1,9 @@ +import 'dart:math'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:obtainium/components/generated_form_modal.dart'; +import 'package:obtainium/providers/settings_provider.dart'; abstract class GeneratedFormItem { late String key; @@ -82,6 +86,33 @@ class GeneratedFormSwitch extends GeneratedFormItem { } } +class GeneratedFormTagInput extends GeneratedFormItem { + late MapEntry? deleteConfirmationMessage; + late bool singleSelect; + late WrapAlignment alignment; + late String emptyMessage; + GeneratedFormTagInput(String key, + {String label = 'Input', + List belowWidgets = const [], + Map> defaultValue = const {}, + List> value)> + additionalValidators = const [], + this.deleteConfirmationMessage, + this.singleSelect = false, + this.alignment = WrapAlignment.start, + this.emptyMessage = 'Input'}) + : super(key, + label: label, + belowWidgets: belowWidgets, + defaultValue: defaultValue, + additionalValidators: additionalValidators); + + @override + Map> ensureType(val) { + return val is Map> ? val : {}; + } +} + typedef OnValueChanges = void Function( Map values, bool valid, bool isBuilding); @@ -120,6 +151,21 @@ class _GeneratedFormState extends State { widget.onValueChanges(returnValues, valid, isBuilding); } + // Generates a random light color +// Courtesy of ChatGPT 😭 (with a bugfix 🥳) + Color generateRandomLightColor() { + // Create a random number generator + final Random random = Random(); + + // Generate random hue, saturation, and value values + final double hue = random.nextDouble() * 360; + final double saturation = 0.5 + random.nextDouble() * 0.5; + final double value = 0.9 + random.nextDouble() * 0.1; + + // Create a HSV color with the random values + return HSVColor.fromAHSV(1.0, hue, saturation, value).toColor(); + } + @override void initState() { super.initState(); @@ -212,6 +258,158 @@ class _GeneratedFormState extends State { }) ], ); + } else if (widget.items[r][e] is GeneratedFormTagInput) { + formInputs[r][e] = Wrap( + alignment: (widget.items[r][e] as GeneratedFormTagInput).alignment, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + (values[widget.items[r][e].key] + as Map>?) + ?.isEmpty == + true + ? Text( + (widget.items[r][e] as GeneratedFormTagInput) + .emptyMessage, + style: const TextStyle(fontWeight: FontWeight.bold), + ) + : const SizedBox.shrink(), + ...(values[widget.items[r][e].key] + as Map>?) + ?.entries + .map((e2) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: ChoiceChip( + label: Text(e2.key), + backgroundColor: Color(e2.value.key).withAlpha(50), + selectedColor: Color(e2.value.key), + visualDensity: VisualDensity.compact, + selected: e2.value.value, + onSelected: (value) { + setState(() { + (values[widget.items[r][e].key] as Map>)[e2.key] = + MapEntry( + (values[widget.items[r][e].key] as Map< + String, + MapEntry>)[e2.key]! + .key, + value); + if ((widget.items[r][e] as GeneratedFormTagInput) + .singleSelect && + value == true) { + for (var key in (values[widget.items[r][e].key] + as Map>) + .keys) { + if (key != e2.key) { + (values[widget.items[r][e].key] as Map< + String, + MapEntry>)[key] = MapEntry( + (values[widget.items[r][e].key] as Map< + String, + MapEntry>)[key]! + .key, + false); + } + } + } + someValueChanged(); + }); + }, + )); + }) ?? + [const SizedBox.shrink()], + (values[widget.items[r][e].key] + as Map>?) + ?.values + .where((e) => e.value) + .isNotEmpty == + true + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: IconButton( + onPressed: () { + fn() { + setState(() { + var temp = values[widget.items[r][e].key] + as Map>; + temp.removeWhere((key, value) => value.value); + values[widget.items[r][e].key] = temp; + someValueChanged(); + }); + } + + if ((widget.items[r][e] as GeneratedFormTagInput) + .deleteConfirmationMessage != + null) { + var message = + (widget.items[r][e] as GeneratedFormTagInput) + .deleteConfirmationMessage!; + showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: message.key, + message: message.value, + items: const []); + }).then((value) { + if (value != null) { + fn(); + } + }); + } else { + fn(); + } + }, + icon: const Icon(Icons.remove), + visualDensity: VisualDensity.compact, + tooltip: tr('remove'), + )) + : const SizedBox.shrink(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: IconButton( + onPressed: () { + showDialog?>( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: widget.items[r][e].label, + items: [ + [ + GeneratedFormTextField('label', + label: tr('label')) + ] + ]); + }).then((value) { + String? label = value?['label']; + if (label != null) { + setState(() { + var temp = values[widget.items[r][e].key] + as Map>?; + temp ??= {}; + var singleSelect = + (widget.items[r][e] as GeneratedFormTagInput) + .singleSelect; + var someSelected = temp.entries + .where((element) => element.value.value) + .isNotEmpty; + temp[label] = MapEntry( + generateRandomLightColor().value, + !(someSelected && singleSelect)); + values[widget.items[r][e].key] = temp; + someValueChanged(); + }); + } + }); + }, + icon: const Icon(Icons.add), + visualDensity: VisualDensity.compact, + tooltip: tr('add'), + )), + ], + ); } } } diff --git a/lib/main.dart b/lib/main.dart index 6614be9..6f90007 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart'; // ignore: implementation_imports import 'package:easy_localization/src/localization.dart'; -const String currentVersion = '0.9.2'; +const String currentVersion = '0.9.4'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES @@ -43,12 +43,16 @@ final globalNavigatorKey = GlobalKey(); Future loadTranslations() async { // See easy_localization/issues/210 await EasyLocalizationController.initEasyLocation(); + var s = SettingsProvider(); + await s.initializeSettings(); + var forceLocale = s.forcedLocale; final controller = EasyLocalizationController( saveLocale: true, + forceLocale: forceLocale != null ? Locale(forceLocale) : null, fallbackLocale: fallbackLocale, supportedLocales: supportedLocales, assetLoader: const RootBundleAssetLoader(), - useOnlyLangCode: false, + useOnlyLangCode: true, useFallbackTranslations: true, path: localeDir, onLoadError: (FlutterError e) { @@ -160,6 +164,7 @@ void main() async { supportedLocales: supportedLocales, path: localeDir, fallbackLocale: fallbackLocale, + useOnlyLangCode: true, child: const Obtainium()), )); } diff --git a/lib/pages/app.dart b/lib/pages/app.dart index ae6c070..3315569 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -5,6 +5,7 @@ import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/main.dart'; +import 'package:obtainium/pages/settings.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; @@ -152,49 +153,22 @@ class _AppPageState extends State { fontStyle: FontStyle.italic, fontSize: 12), ), const SizedBox( - height: 32, + height: 48, ), - app?.app.category != null - ? Chip( - label: Text(app!.app.category!), - backgroundColor: - Color(categories[app.app.category!] ?? 0x0), - onDeleted: () { - app.app.category = null; - appsProvider.saveApps([app.app]); - }, - visualDensity: VisualDensity.compact, - ) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () { - showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: 'Pick a Category', - items: [ - [ - settingsProvider - .getCategoryFormItem() - ] - ]); - }).then((value) { - if (value != null && app != null) { - String? cat = (value['category'] - ?.isNotEmpty ?? - false) - ? value['category'] - : null; - app.app.category = cat; - appsProvider.saveApps([app.app]); - } - }); - }, - child: Text(tr('categorize'))) - ]) + CategoryEditorSelector( + alignment: WrapAlignment.center, + singleSelect: true, + preselected: app?.app.category != null + ? {app!.app.category!} + : {}, + onSelected: (categories) { + if (app != null) { + app.app.category = categories.isNotEmpty + ? categories[0] + : null; + appsProvider.saveApps([app.app]); + } + }) ], )), ], diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index bc0e70e..ecf1d5b 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -6,6 +6,7 @@ import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/main.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; @@ -41,7 +42,6 @@ class _SettingsPageState extends State { Widget build(BuildContext context) { SettingsProvider settingsProvider = context.watch(); SourceProvider sourceProvider = SourceProvider(); - AppsProvider appsProvider = context.read(); if (settingsProvider.prefs == null) { settingsProvider.initializeSettings(); } @@ -130,6 +130,28 @@ class _SettingsPageState extends State { } }); + var localeDropdown = DropdownButtonFormField( + decoration: InputDecoration(labelText: tr('language')), + value: settingsProvider.forcedLocale, + items: [ + DropdownMenuItem( + value: null, + child: Text(tr('followSystem')), + ), + ...supportedLocales.map((e) => DropdownMenuItem( + value: e.toLanguageTag(), + child: Text(e.toLanguageTag().toUpperCase()), + )) + ], + onChanged: (value) { + settingsProvider.forcedLocale = value; + if (value != null) { + context.setLocale(Locale(value)); + } else { + context.resetLocale(); + } + }); + var intervalDropdown = DropdownButtonFormField( decoration: InputDecoration(labelText: tr('bgUpdateCheckInterval')), value: settingsProvider.updateInterval, @@ -178,8 +200,6 @@ class _SettingsPageState extends State { height: 16, ); - var categories = settingsProvider.categories; - return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView(slivers: [ @@ -213,6 +233,8 @@ class _SettingsPageState extends State { ], ), height16, + localeDropdown, + height16, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -264,85 +286,7 @@ class _SettingsPageState extends State { color: Theme.of(context).colorScheme.primary), ), height16, - Wrap( - children: [ - ...categories.entries.toList().map((e) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4), - child: Chip( - label: Text(e.key), - backgroundColor: Color(e.value), - visualDensity: VisualDensity.compact, - onDeleted: () { - showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr( - 'deleteCategoryQuestion'), - message: tr( - 'categoryDeleteWarning', - args: [e.key]), - items: []); - }).then((value) { - if (value != null) { - setState(() { - categories.remove(e.key); - settingsProvider.categories = - categories; - }); - appsProvider.saveApps(appsProvider - .apps.values - .where((element) => - element.app.category == - e.key) - .map((e) { - var a = e.app; - a.category = null; - return a; - }).toList()); - } - }); - }, - )); - }), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4), - child: IconButton( - onPressed: () { - showDialog?>( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: tr('addCategory'), - items: [ - [ - GeneratedFormTextField( - 'label', - label: tr('label')) - ] - ]); - }).then((value) { - String? label = value?['label']; - if (label != null) { - setState(() { - categories[label] = - generateRandomLightColor() - .value; - settingsProvider.categories = - categories; - }); - } - }); - }, - icon: const Icon(Icons.add), - visualDensity: VisualDensity.compact, - tooltip: tr('add'), - )) - ], - ) + const CategoryEditorSelector() ], ))), SliverToBoxAdapter( @@ -457,3 +401,59 @@ class _LogsDialogState extends State { ); } } + +class CategoryEditorSelector extends StatefulWidget { + final void Function(List categories)? onSelected; + final bool singleSelect; + final Set preselected; + final WrapAlignment alignment; + const CategoryEditorSelector( + {super.key, + this.onSelected, + this.singleSelect = false, + this.preselected = const {}, + this.alignment = WrapAlignment.start}); + + @override + State createState() => _CategoryEditorSelectorState(); +} + +class _CategoryEditorSelectorState extends State { + Map> storedValues = {}; + + @override + Widget build(BuildContext context) { + var settingsProvider = context.watch(); + storedValues = settingsProvider.categories.map((key, value) => MapEntry( + key, + MapEntry(value, + storedValues[key]?.value ?? widget.preselected.contains(key)))); + return GeneratedForm( + items: [ + [ + GeneratedFormTagInput('categories', + label: tr('category'), + emptyMessage: tr('noCategories'), + defaultValue: storedValues, + alignment: widget.alignment, + deleteConfirmationMessage: MapEntry( + tr('deleteCategoriesQuestion'), + tr('categoryDeleteWarning')), + singleSelect: widget.singleSelect) + ] + ], + onValueChanges: ((values, valid, isBuilding) { + if (!isBuilding) { + storedValues = + values['categories'] as Map>; + settingsProvider.categories = + storedValues.map((key, value) => MapEntry(key, value.key)); + if (widget.onSelected != null) { + widget.onSelected!(storedValues.keys + .where((k) => storedValues[k]!.value) + .toList()); + } + } + })); + } +} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 9e887b0..07fcbee 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/components/generated_form.dart'; +import 'package:obtainium/main.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -153,6 +154,7 @@ class SettingsProvider with ChangeNotifier { set categories(Map cats) { prefs?.setString('categories', jsonEncode(cats)); + notifyListeners(); } getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown( @@ -163,4 +165,24 @@ class SettingsProvider with ChangeNotifier { ...categories.entries.map((e) => MapEntry(e.key, e.key)).toList() ], defaultValue: initCategory); + + String? get forcedLocale { + var fl = prefs?.getString('forcedLocale'); + return supportedLocales + .where((element) => element.toLanguageTag() == fl) + .isNotEmpty + ? fl + : null; + } + + set forcedLocale(String? fl) { + if (fl == null) { + prefs?.remove('forcedLocale'); + } else if (supportedLocales + .where((element) => element.toLanguageTag() == fl) + .isNotEmpty) { + prefs?.setString('forcedLocale', fl); + } + notifyListeners(); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 4e85d02..043e466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.9.2+90 # When changing this, update the tag in main() accordingly +version: 0.9.4+92 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0'