Compare commits

...

24 Commits

Author SHA1 Message Date
Imran Remtulla
5c4bb8f84c Merge pull request #217 from ImranR98/dev
Bugfixes and UI Tweaks (#213, #215, #216)
2023-01-06 21:11:58 -05:00
Imran Remtulla
1c8e759494 Increment version + updated packages 2023-01-06 21:11:13 -05:00
Imran Remtulla
081c2a07d2 Categories re-added on import (#213) 2023-01-06 21:10:04 -05:00
Imran Remtulla
02751fe8fa Made GitHub PATs hidden (password field) (#215) 2023-01-06 20:57:26 -05:00
Imran Remtulla
95f3362a84 Apps bottom bar tweaks (#216) 2023-01-06 20:47:22 -05:00
Imran Remtulla
b68cf5a1be Increment version 2023-01-02 02:05:35 -05:00
Imran Remtulla
4eb7499591 Merge pull request #211 from RanTranslations/main
assets: Update Simplified Chinese
2023-01-02 02:04:34 -05:00
JohnsonRan
98fafe2aa4 assets: Update Simplified Chinese 2023-01-02 11:18:27 +08:00
Imran Remtulla
9bac74aadd Icon fixed in readme 2022-12-28 06:42:42 -05:00
Imran Remtulla
0a93117bf0 Merge pull request #208 from ImranR98/dev
Tiny bugfix + increment version
2022-12-28 06:31:42 -05:00
Imran Remtulla
451cc41c45 Tiny bugfix + increment version 2022-12-28 06:30:58 -05:00
Imran Remtulla
3b449d0982 Merge pull request #207 from ImranR98/dev
Categorization Improvements
2022-12-27 22:39:17 -05:00
Imran Remtulla
1863f55372 Increment build num 2022-12-27 22:38:07 -05:00
Imran Remtulla
0c4b8ac79d Made notif icon white for consistency on some OS skins 2022-12-27 22:37:49 -05:00
Imran Remtulla
e287087753 Increment build number 2022-12-27 21:15:06 -05:00
Imran Remtulla
82bcc46d42 Fixed search error on Add App page (#202) 2022-12-27 21:14:11 -05:00
Imran Remtulla
1f26188ec6 Potential fix for rangeError for no URL Apps (#201) 2022-12-27 21:00:46 -05:00
Imran Remtulla
794c3e1a81 Increment version 2022-12-27 20:42:21 -05:00
Imran Remtulla
16369b4adf App page with Webview now on par with no webview
+ ratelimit error bugfix
2022-12-27 20:41:44 -05:00
Imran Remtulla
8f16f745be Added categorize in multi select menu 2022-12-27 20:15:56 -05:00
Imran Remtulla
8ddeb3d776 Apps now support multiple categories 2022-12-27 19:37:13 -05:00
Imran Remtulla
21cf9c98d9 Merge pull request #200 from ImranR98/dev
Fixed export error on Android SDK <= 28
2022-12-25 22:30:47 -05:00
Imran Remtulla
358f910d19 Increment version 2022-12-25 22:30:01 -05:00
Imran Remtulla
7a3d74bd05 Fixed export error on Android SDK <= 28 2022-12-25 22:29:39 -05:00
25 changed files with 692 additions and 492 deletions

View File

@@ -1,4 +1,4 @@
# ![Obtainium Icon](./android/app/src/main/res/drawable/ic_notification.png) Obtainium # ![Obtainium Icon](./assets/graphics/icon_small.png) Obtainium
Get Android App Updates Directly From the Source. Get Android App Updates Directly From the Source.

View File

@@ -51,4 +51,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
</manifest> </manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -209,6 +209,8 @@
"addCategory": "Kategorie hinzufügen", "addCategory": "Kategorie hinzufügen",
"label": "Bezeichnung", "label": "Bezeichnung",
"language": "Sprache", "language": "Sprache",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut", "one": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minute erneut",
"other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut" "other": "Zu viele Anfragen (Rate begrenzt) - versuchen Sie es in {} Minuten erneut"

View File

@@ -209,6 +209,8 @@
"addCategory": "Add Category", "addCategory": "Add Category",
"label": "Label", "label": "Label",
"language": "Language", "language": "Language",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Too many requests (rate limited) - try again in {} minute", "one": "Too many requests (rate limited) - try again in {} minute",
"other": "Too many requests (rate limited) - try again in {} minutes" "other": "Too many requests (rate limited) - try again in {} minutes"

View File

@@ -207,6 +207,9 @@
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.", "categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
"addCategory": "Új kategória", "addCategory": "Új kategória",
"label": "Címke", "label": "Címke",
"language": "Language",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva", "one": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva",
"other": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva" "other": "Túl sok kérés (korlátozott arány) próbálja újra {} perc múlva"

View File

@@ -209,6 +209,8 @@
"addCategory": "Aggiungi categoria", "addCategory": "Aggiungi categoria",
"label": "Etichetta", "label": "Etichetta",
"language": "Lingua", "language": "Lingua",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Troppe richieste (traffico limitato) - riprova tra {} minuto", "one": "Troppe richieste (traffico limitato) - riprova tra {} minuto",
"other": "Troppe richieste (traffico limitato) - riprova tra {} minuti" "other": "Troppe richieste (traffico limitato) - riprova tra {} minuti"

View File

@@ -209,6 +209,8 @@
"addCategory": "カテゴリを追加", "addCategory": "カテゴリを追加",
"label": "ラベル", "label": "ラベル",
"language": "言語", "language": "言語",
"storagePermissionDenied": "Storage permission denied",
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください", "one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください" "other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"

View File

@@ -12,7 +12,7 @@
"ok": "好的", "ok": "好的",
"and": "和", "and": "和",
"startedBgUpdateTask": "开始后台检查更新任务", "startedBgUpdateTask": "开始后台检查更新任务",
"bgUpdateIgnoreAfterIs": "Bg update ignoreAfter is {}", "bgUpdateIgnoreAfterIs": "下次后台更新检查 {}",
"startedActualBGUpdateCheck": "后台检查更新已开始", "startedActualBGUpdateCheck": "后台检查更新已开始",
"bgUpdateTaskFinished": "后台检查更新已完成", "bgUpdateTaskFinished": "后台检查更新已完成",
"firstRun": "这是你第一次运行 Obtainium", "firstRun": "这是你第一次运行 Obtainium",
@@ -199,16 +199,18 @@
"downloadNotifDescription": "通知用户下载进度", "downloadNotifDescription": "通知用户下载进度",
"noAPKFound": "未找到安装包", "noAPKFound": "未找到安装包",
"noVersionDetection": "无版本检测", "noVersionDetection": "无版本检测",
"categorize": "Categorize", "categorize": "归档",
"categories": "Categories", "categories": "归档",
"category": "Category", "category": "类别",
"noCategory": "No Category", "noCategory": "无类别",
"noCategories": "No Categories", "noCategories": "无类别",
"deleteCategoriesQuestion": "Delete Categories?", "deleteCategoriesQuestion": "删除所有类别?",
"categoryDeleteWarning": "All Apps in deleted categories will be set to uncategorized.", "categoryDeleteWarning": "所有被删除类别的应用程序将被设置为无类别",
"addCategory": "Add Category", "addCategory": "添加类别",
"label": "Label", "label": "标签",
"language": "Language", "language": "语言",
"storagePermissionDenied": "存储权限已被拒绝",
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "请求过多 (API 限制) - 在 {} 分钟后重试", "one": "请求过多 (API 限制) - 在 {} 分钟后重试",
"other": "请求过多 (API 限制) - 在 {} 分钟后重试" "other": "请求过多 (API 限制) - 在 {} 分钟后重试"

View File

@@ -15,6 +15,7 @@ class GitHub extends AppSource {
additionalSourceSpecificSettingFormItems = [ additionalSourceSpecificSettingFormItems = [
GeneratedFormTextField('github-creds', GeneratedFormTextField('github-creds',
label: tr('githubPATLabel'), label: tr('githubPATLabel'),
password: true,
required: false, required: false,
additionalValidators: [ additionalValidators: [
(value) { (value) {

View File

@@ -3,7 +3,6 @@ import 'dart:math';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/providers/settings_provider.dart';
abstract class GeneratedFormItem { abstract class GeneratedFormItem {
late String key; late String key;
@@ -24,6 +23,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
late bool required; late bool required;
late int max; late int max;
late String? hint; late String? hint;
late bool password;
GeneratedFormTextField(String key, GeneratedFormTextField(String key,
{String label = 'Input', {String label = 'Input',
@@ -32,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
List<String? Function(String? value)> additionalValidators = const [], List<String? Function(String? value)> additionalValidators = const [],
this.required = true, this.required = true,
this.max = 1, this.max = 1,
this.hint}) this.hint,
this.password = false})
: super(key, : super(key,
label: label, label: label,
belowWidgets: belowWidgets, belowWidgets: belowWidgets,
@@ -129,6 +130,21 @@ class GeneratedForm extends StatefulWidget {
State<GeneratedForm> createState() => _GeneratedFormState(); State<GeneratedForm> createState() => _GeneratedFormState();
} }
// 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();
}
class _GeneratedFormState extends State<GeneratedForm> { class _GeneratedFormState extends State<GeneratedForm> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
Map<String, dynamic> values = {}; Map<String, dynamic> values = {};
@@ -153,21 +169,6 @@ class _GeneratedFormState extends State<GeneratedForm> {
widget.onValueChanges(returnValues, valid, isBuilding); 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -188,6 +189,9 @@ class _GeneratedFormState extends State<GeneratedForm> {
if (formItem is GeneratedFormTextField) { if (formItem is GeneratedFormTextField) {
final formFieldKey = GlobalKey<FormFieldState>(); final formFieldKey = GlobalKey<FormFieldState>();
return TextFormField( return TextFormField(
obscureText: formItem.password,
autocorrect: !formItem.password,
enableSuggestions: !formItem.password,
key: formFieldKey, key: formFieldKey,
initialValue: values[formItem.key], initialValue: values[formItem.key],
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,

View File

@@ -10,13 +10,15 @@ class GeneratedFormModal extends StatefulWidget {
required this.items, required this.items,
this.initValid = false, this.initValid = false,
this.message = '', this.message = '',
this.additionalWidgets = const []}); this.additionalWidgets = const [],
this.singleNullReturnButton});
final String title; final String title;
final String message; final String message;
final List<List<GeneratedFormItem>> items; final List<List<GeneratedFormItem>> items;
final bool initValid; final bool initValid;
final List<Widget> additionalWidgets; final List<Widget> additionalWidgets;
final String? singleNullReturnButton;
@override @override
State<GeneratedFormModal> createState() => _GeneratedFormModalState(); State<GeneratedFormModal> createState() => _GeneratedFormModalState();
@@ -64,17 +66,21 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
onPressed: () { onPressed: () {
Navigator.of(context).pop(null); Navigator.of(context).pop(null);
}, },
child: Text(tr('cancel'))), child: Text(widget.singleNullReturnButton == null
TextButton( ? tr('cancel')
onPressed: !valid : widget.singleNullReturnButton!)),
? null widget.singleNullReturnButton == null
: () { ? TextButton(
if (valid) { onPressed: !valid
HapticFeedback.selectionClick(); ? null
Navigator.of(context).pop(values); : () {
} if (valid) {
}, HapticFeedback.selectionClick();
child: Text(tr('continue'))) Navigator.of(context).pop(values);
}
},
child: Text(tr('continue')))
: const SizedBox.shrink()
], ],
); );
} }

View File

@@ -13,13 +13,10 @@ class ObtainiumError {
} }
} }
class RateLimitError { class RateLimitError extends ObtainiumError {
late int remainingMinutes; late int remainingMinutes;
RateLimitError(this.remainingMinutes); RateLimitError(this.remainingMinutes)
: super(plural('tooManyRequestsTryAgainInMinutes', remainingMinutes));
@override
String toString() =>
plural('tooManyRequestsTryAgainInMinutes', remainingMinutes);
} }
class InvalidURLError extends ObtainiumError { class InvalidURLError extends ObtainiumError {

View File

@@ -21,7 +21,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
const String currentVersion = '0.9.9'; const String currentVersion = '0.9.14';
const String currentReleaseTag = const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@@ -30,7 +30,7 @@ class _AddAppPageState extends State<AddAppPage> {
AppSource? pickedSource; AppSource? pickedSource;
Map<String, dynamic> additionalSettings = {}; Map<String, dynamic> additionalSettings = {};
bool additionalSettingsValid = true; bool additionalSettingsValid = true;
String? category; List<String> pickedCategories = [];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -127,9 +127,7 @@ class _AddAppPageState extends State<AddAppPage> {
if (app.additionalSettings['trackOnly'] == true) { if (app.additionalSettings['trackOnly'] == true) {
app.installedVersion = app.latestVersion; app.installedVersion = app.latestVersion;
} }
if (category != null) { app.categories = pickedCategories;
app.category = category;
}
await appsProvider.saveApps([app]); await appsProvider.saveApps([app]);
return app; return app;
@@ -290,7 +288,7 @@ class _AddAppPageState extends State<AddAppPage> {
if (selectedUrls != null && if (selectedUrls != null &&
selectedUrls.isNotEmpty) { selectedUrls.isNotEmpty) {
changeUserInput( changeUserInput(
selectedUrls[0], true, true); selectedUrls[0], true, false);
addApp(resetUserInputAfter: true); addApp(resetUserInputAfter: true);
} }
}).catchError((e) { }).catchError((e) {
@@ -334,11 +332,8 @@ class _AddAppPageState extends State<AddAppPage> {
), ),
CategoryEditorSelector( CategoryEditorSelector(
alignment: WrapAlignment.start, alignment: WrapAlignment.start,
singleSelect: true,
onSelected: (categories) { onSelected: (categories) {
category = categories.isEmpty pickedCategories = categories;
? null
: categories.first;
}), }),
], ],
), ),

View File

@@ -42,6 +42,106 @@ class _AppPageState extends State<AppPage> {
getUpdate(app.app.id); getUpdate(app.app.id);
} }
var trackOnly = app?.app.additionalSettings['trackOnly'] == true; var trackOnly = app?.app.additionalSettings['trackOnly'] == true;
var infoColumn = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GestureDetector(
onTap: () {
if (app?.app.url != null) {
launchUrlString(app?.app.url ?? '',
mode: LaunchMode.externalApplication);
}
},
child: Text(
app?.app.url ?? '',
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic,
fontSize: 12),
)),
const SizedBox(
height: 32,
),
Text(
tr('latestVersionX', args: [app?.app.latestVersion ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'${tr('installedVersionX', args: [
app?.app.installedVersion ?? tr('none')
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
tr('app')
])}' : ''}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(
height: 32,
),
Text(
tr('lastUpdateCheckX', args: [
app?.app.lastUpdateCheck == null
? tr('never')
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
]),
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
),
const SizedBox(
height: 48,
),
CategoryEditorSelector(
alignment: WrapAlignment.center,
preselected:
app?.app.categories != null ? app!.app.categories.toSet() : {},
onSelected: (categories) {
if (app != null) {
app.app.categories = categories;
appsProvider.saveApps([app.app]);
}
}),
],
);
var fullInfoColumn = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 150),
app?.installedInfo != null
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Image.memory(
app!.installedInfo!.icon!,
height: 150,
gaplessPlayback: true,
)
])
: Container(),
const SizedBox(
height: 25,
),
Text(
app?.installedInfo?.name ?? app?.app.name ?? tr('app'),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayLarge,
),
Text(
tr('byX', args: [app?.app.author ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(
height: 32,
),
infoColumn,
const SizedBox(height: 150)
],
);
return Scaffold( return Scaffold(
appBar: settingsProvider.showAppWebpage ? AppBar() : null, appBar: settingsProvider.showAppWebpage ? AppBar() : null,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
@@ -71,106 +171,7 @@ class _AppPageState extends State<AppPage> {
: CustomScrollView( : CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: Column( child: Column(children: [fullInfoColumn])),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 150),
app?.installedInfo != null
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.memory(
app!.installedInfo!.icon!,
height: 150,
gaplessPlayback: true,
)
])
: Container(),
const SizedBox(
height: 25,
),
Text(
app?.installedInfo?.name ??
app?.app.name ??
tr('app'),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayLarge,
),
Text(
tr('byX', args: [app?.app.author ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(
height: 32,
),
GestureDetector(
onTap: () {
if (app?.app.url != null) {
launchUrlString(app?.app.url ?? '',
mode: LaunchMode.externalApplication);
}
},
child: Text(
app?.app.url ?? '',
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic,
fontSize: 12),
)),
const SizedBox(
height: 32,
),
Text(
tr('latestVersionX',
args: [app?.app.latestVersion ?? tr('unknown')]),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'${tr('installedVersionX', args: [
app?.app.installedVersion ?? tr('none')
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
tr('app')
])}' : ''}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(
height: 32,
),
Text(
tr('lastUpdateCheckX', args: [
app?.app.lastUpdateCheck == null
? tr('never')
: '\n${app?.app.lastUpdateCheck?.toLocal()}'
]),
textAlign: TextAlign.center,
style: const TextStyle(
fontStyle: FontStyle.italic, fontSize: 12),
),
const SizedBox(
height: 48,
),
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]);
}
}),
const SizedBox(height: 150)
],
)),
], ],
), ),
onRefresh: () async { onRefresh: () async {
@@ -289,6 +290,31 @@ class _AppPageState extends State<AppPage> {
}, },
tooltip: tr('additionalOptions'), tooltip: tr('additionalOptions'),
icon: const Icon(Icons.settings)), icon: const Icon(Icons.settings)),
if (app != null && settingsProvider.showAppWebpage)
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
scrollable: true,
content: infoColumn,
title: Text(
'${app.app.name} ${tr('byX', args: [
app.app.author
])}'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(tr('continue')))
],
);
});
},
icon: const Icon(Icons.more_horiz),
tooltip: tr('more')),
const SizedBox(width: 16.0), const SizedBox(width: 16.0),
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(

View File

@@ -55,7 +55,8 @@ class AppsPageState extends State<AppsPage> {
var appsProvider = context.watch<AppsProvider>(); var appsProvider = context.watch<AppsProvider>();
var settingsProvider = context.watch<SettingsProvider>(); var settingsProvider = context.watch<SettingsProvider>();
var sortedApps = appsProvider.apps.values.toList(); var sortedApps = appsProvider.apps.values.toList();
var currentFilterIsUpdatesOnly = filter.isIdenticalTo(updatesOnlyFilter); var currentFilterIsUpdatesOnly =
filter.isIdenticalTo(updatesOnlyFilter, settingsProvider);
selectedApps = selectedApps selectedApps = selectedApps
.where((element) => sortedApps.map((e) => e.app).contains(element)) .where((element) => sortedApps.map((e) => e.app).contains(element))
@@ -102,7 +103,9 @@ class AppsPageState extends State<AppsPage> {
} }
} }
if (filter.categoryFilter.isNotEmpty && if (filter.categoryFilter.isNotEmpty &&
!filter.categoryFilter.contains(app.app.category)) { filter.categoryFilter
.intersection(app.app.categories.toSet())
.isEmpty) {
return false; return false;
} }
return true; return true;
@@ -224,14 +227,21 @@ class AppsPageState extends State<AppsPage> {
String? changesUrl = SourceProvider() String? changesUrl = SourceProvider()
.getSource(sortedApps[index].app.url) .getSource(sortedApps[index].app.url)
.changeLogPageFromStandardUrl(sortedApps[index].app.url); .changeLogPageFromStandardUrl(sortedApps[index].app.url);
var transparent = const Color.fromARGB(0, 0, 0, 0).value;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.symmetric( border: Border.symmetric(
vertical: BorderSide( vertical: BorderSide(
width: 4, width: 4,
color: Color(settingsProvider.categories[ color: Color(
sortedApps[index].app.category] ?? sortedApps[index].app.categories.isNotEmpty
const Color.fromARGB(0, 0, 0, 0).value)))), ? settingsProvider.categories[
sortedApps[index]
.app
.categories
.first] ??
transparent
: transparent)))),
child: ListTile( child: ListTile(
tileColor: sortedApps[index].app.pinned tileColor: sortedApps[index].app.pinned
? Colors.grey.withOpacity(0.1) ? Colors.grey.withOpacity(0.1)
@@ -338,7 +348,9 @@ class AppsPageState extends State<AppsPage> {
Row( Row(
children: [ children: [
selectedApps.isEmpty selectedApps.isEmpty
? IconButton( ? TextButton.icon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectThese(sortedApps.map((e) => e.app).toList()); selectThese(sortedApps.map((e) => e.app).toList());
}, },
@@ -346,8 +358,10 @@ class AppsPageState extends State<AppsPage> {
Icons.select_all_outlined, Icons.select_all_outlined,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
tooltip: tr('selectAll')) label: Text(sortedApps.length.toString()))
: TextButton.icon( : TextButton.icon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
onPressed: () { onPressed: () {
selectedApps.isEmpty selectedApps.isEmpty
? selectThese(sortedApps.map((e) => e.app).toList()) ? selectThese(sortedApps.map((e) => e.app).toList())
@@ -362,307 +376,415 @@ class AppsPageState extends State<AppsPage> {
label: Text(selectedApps.length.toString())), label: Text(selectedApps.length.toString())),
const VerticalDivider(), const VerticalDivider(),
Expanded( Expanded(
child: Row( child: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, scrollDirection: Axis.horizontal,
children: [ child: Row(
selectedApps.isEmpty mainAxisAlignment: MainAxisAlignment.spaceEvenly,
? const SizedBox() children: [
: IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
onPressed: () { onPressed: selectedApps.isEmpty
showDialog<Map<String, dynamic>?>( ? null
context: context, : () {
builder: (BuildContext ctx) { showDialog<Map<String, dynamic>?>(
return GeneratedFormModal( context: context,
title: tr('removeSelectedAppsQuestion'), builder: (BuildContext ctx) {
items: const [], return GeneratedFormModal(
initValid: true, title:
message: tr( tr('removeSelectedAppsQuestion'),
'xWillBeRemovedButRemainInstalled', items: const [],
args: [ initValid: true,
plural('apps', selectedApps.length) message: tr(
]), 'xWillBeRemovedButRemainInstalled',
); args: [
}).then((values) { plural(
if (values != null) { 'apps', selectedApps.length)
appsProvider.removeApps( ]),
selectedApps.map((e) => e.id).toList()); );
} }).then((values) {
}); if (values != null) {
}, appsProvider.removeApps(selectedApps
tooltip: tr('removeSelectedApps'), .map((e) => e.id)
icon: const Icon(Icons.delete_outline_outlined), .toList());
), }
IconButton(
visualDensity: VisualDensity.compact,
onPressed: appsProvider.areDownloadsRunning() ||
(existingUpdateIdsAllOrSelected.isEmpty &&
newInstallIdsAllOrSelected.isEmpty &&
trackOnlyUpdateIdsAllOrSelected.isEmpty)
? null
: () {
HapticFeedback.heavyImpact();
List<GeneratedFormItem> formItems = [];
if (existingUpdateIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('updates',
label: tr('updateX', args: [
plural('apps',
existingUpdateIdsAllOrSelected.length)
]),
defaultValue: true));
}
if (newInstallIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('installs',
label: tr('installX', args: [
plural('apps',
newInstallIdsAllOrSelected.length)
]),
defaultValue: existingUpdateIdsAllOrSelected
.isNotEmpty));
}
if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
formItems.add(GeneratedFormSwitch('trackonlies',
label: tr('markXTrackOnlyAsUpdated', args: [
plural('apps',
trackOnlyUpdateIdsAllOrSelected.length)
]),
defaultValue: existingUpdateIdsAllOrSelected
.isNotEmpty ||
newInstallIdsAllOrSelected.isNotEmpty));
}
showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
var totalApps = existingUpdateIdsAllOrSelected
.length +
newInstallIdsAllOrSelected.length +
trackOnlyUpdateIdsAllOrSelected.length;
return GeneratedFormModal(
title: tr('changeX',
args: [plural('apps', totalApps)]),
items: formItems.map((e) => [e]).toList(),
initValid: true,
);
}).then((values) {
if (values != null) {
if (values.isEmpty) {
values = getDefaultValuesFromFormItems(
[formItems]);
}
bool shouldInstallUpdates =
values['updates'] == true;
bool shouldInstallNew =
values['installs'] == true;
bool shouldMarkTrackOnlies =
values['trackonlies'] == true;
(() async {
if (shouldInstallNew ||
shouldInstallUpdates) {
await settingsProvider
.getInstallPermission();
}
})()
.then((_) {
List<String> toInstall = [];
if (shouldInstallUpdates) {
toInstall
.addAll(existingUpdateIdsAllOrSelected);
}
if (shouldInstallNew) {
toInstall
.addAll(newInstallIdsAllOrSelected);
}
if (shouldMarkTrackOnlies) {
toInstall.addAll(
trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(toInstall,
globalNavigatorKey.currentContext)
.catchError((e) {
showError(e, context);
}); });
}); },
} tooltip: tr('removeSelectedApps'),
}); icon: const Icon(Icons.delete_outline_outlined),
}, ),
tooltip: selectedApps.isEmpty IconButton(
? tr('installUpdateApps') visualDensity: VisualDensity.compact,
: tr('installUpdateSelectedApps'), onPressed: appsProvider.areDownloadsRunning() ||
icon: const Icon( (existingUpdateIdsAllOrSelected.isEmpty &&
Icons.file_download_outlined, newInstallIdsAllOrSelected.isEmpty &&
)), trackOnlyUpdateIdsAllOrSelected.isEmpty)
selectedApps.isEmpty ? null
? const SizedBox() : () {
: IconButton( HapticFeedback.heavyImpact();
visualDensity: VisualDensity.compact, List<GeneratedFormItem> formItems = [];
onPressed: () { if (existingUpdateIdsAllOrSelected
showDialog( .isNotEmpty) {
context: context, formItems.add(GeneratedFormSwitch(
builder: (BuildContext ctx) { 'updates',
return AlertDialog( label: tr('updateX', args: [
scrollable: true, plural(
content: Padding( 'apps',
padding: const EdgeInsets.only(top: 6), existingUpdateIdsAllOrSelected
child: Row( .length)
mainAxisAlignment: ]),
MainAxisAlignment.spaceAround, defaultValue: true));
children: [ }
IconButton( if (newInstallIdsAllOrSelected.isNotEmpty) {
onPressed: formItems.add(GeneratedFormSwitch(
appsProvider 'installs',
.areDownloadsRunning() label: tr('installX', args: [
? null plural(
: () { 'apps',
showDialog( newInstallIdsAllOrSelected
context: context, .length)
builder: ]),
(BuildContext defaultValue:
ctx) { existingUpdateIdsAllOrSelected
return AlertDialog( .isNotEmpty));
title: Text(tr( }
'markXSelectedAppsAsUpdated', if (trackOnlyUpdateIdsAllOrSelected
args: [ .isNotEmpty) {
selectedApps formItems.add(GeneratedFormSwitch(
.length 'trackonlies',
.toString() label: tr('markXTrackOnlyAsUpdated',
])), args: [
content: Text( plural(
tr('onlyWorksWithNonEVDApps'), 'apps',
style: const TextStyle( trackOnlyUpdateIdsAllOrSelected
fontWeight: .length)
FontWeight ]),
.bold, defaultValue:
fontStyle: existingUpdateIdsAllOrSelected
FontStyle.italic), .isNotEmpty ||
), newInstallIdsAllOrSelected
actions: [ .isNotEmpty));
TextButton( }
onPressed: showDialog<Map<String, dynamic>?>(
() { context: context,
Navigator.of(context) builder: (BuildContext ctx) {
.pop(); var totalApps =
}, existingUpdateIdsAllOrSelected.length +
child: Text( newInstallIdsAllOrSelected
tr('no'))), .length +
TextButton( trackOnlyUpdateIdsAllOrSelected
onPressed: .length;
() { return GeneratedFormModal(
HapticFeedback title: tr('changeX', args: [
.selectionClick(); plural('apps', totalApps)
appsProvider ]),
.saveApps(selectedApps.map((a) { items: formItems
if (a.installedVersion != .map((e) => [e])
null) { .toList(),
a.installedVersion = a.latestVersion; initValid: true,
} );
return a; }).then((values) {
}).toList()); if (values != null) {
if (values.isEmpty) {
values =
getDefaultValuesFromFormItems(
[formItems]);
}
bool shouldInstallUpdates =
values['updates'] == true;
bool shouldInstallNew =
values['installs'] == true;
bool shouldMarkTrackOnlies =
values['trackonlies'] == true;
(() async {
if (shouldInstallNew ||
shouldInstallUpdates) {
await settingsProvider
.getInstallPermission();
}
})()
.then((_) {
List<String> toInstall = [];
if (shouldInstallUpdates) {
toInstall.addAll(
existingUpdateIdsAllOrSelected);
}
if (shouldInstallNew) {
toInstall.addAll(
newInstallIdsAllOrSelected);
}
if (shouldMarkTrackOnlies) {
toInstall.addAll(
trackOnlyUpdateIdsAllOrSelected);
}
appsProvider
.downloadAndInstallLatestApps(
toInstall,
globalNavigatorKey
.currentContext)
.catchError((e) {
showError(e, context);
});
});
}
});
},
tooltip: selectedApps.isEmpty
? tr('installUpdateApps')
: tr('installUpdateSelectedApps'),
icon: const Icon(
Icons.file_download_outlined,
)),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: selectedApps.isEmpty
? null
: () async {
try {
Set<String>? preselected;
var showPrompt = false;
for (var element in selectedApps) {
var currentCats =
element.categories.toSet();
if (preselected == null) {
preselected = currentCats;
} else {
if (!settingsProvider.setEqual(
currentCats, preselected)) {
showPrompt = true;
break;
}
}
}
var cont = true;
if (showPrompt) {
cont = await showDialog<
Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('categorize'),
items: const [],
initValid: true,
message: tr(
'selectedCategorizeWarning'),
);
}) !=
null;
}
if (cont) {
await showDialog<Map<String, dynamic>?>(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr('categorize'),
items: const [],
initValid: true,
singleNullReturnButton:
tr('continue'),
additionalWidgets: [
CategoryEditorSelector(
preselected: !showPrompt
? preselected ?? {}
: {},
showLabelWhenNotEmpty: false,
onSelected: (categories) {
appsProvider.saveApps(
selectedApps.map((e) {
e.categories = categories;
return e;
}).toList());
},
)
],
);
});
}
} catch (err) {
showError(err, context);
}
},
tooltip: tr('categorize'),
icon: const Icon(Icons.category_outlined),
),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: selectedApps.isEmpty
? null
: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
scrollable: true,
content: Padding(
padding:
const EdgeInsets.only(top: 6),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.spaceAround,
children: [
IconButton(
onPressed: appsProvider
.areDownloadsRunning()
? null
: () {
showDialog(
context:
context,
builder:
(BuildContext
ctx) {
return AlertDialog(
title: Text(tr(
'markXSelectedAppsAsUpdated',
args: [
selectedApps.length.toString()
])),
content:
Text(
tr('onlyWorksWithNonEVDApps'),
style: const TextStyle(
fontWeight:
FontWeight.bold,
fontStyle: FontStyle.italic),
),
actions: [
TextButton(
onPressed:
() {
Navigator.of(context).pop();
},
child:
Text(tr('no'))),
TextButton(
onPressed:
() {
HapticFeedback.selectionClick();
appsProvider.saveApps(selectedApps.map((a) {
if (a.installedVersion != null) {
a.installedVersion = a.latestVersion;
}
return a;
}).toList());
Navigator.of(context) Navigator.of(context).pop();
.pop(); },
}, child:
child: Text( Text(tr('yes')))
tr('yes'))) ],
], );
); }).whenComplete(() {
}).whenComplete(() { Navigator.of(
Navigator.of( context)
context) .pop();
.pop(); });
}); },
}, tooltip: tr(
tooltip: 'markSelectedAppsUpdated'),
tr('markSelectedAppsUpdated'), icon: const Icon(
icon: const Icon(Icons.done)), Icons.done)),
IconButton( IconButton(
onPressed: () { onPressed: () {
var pinStatus = selectedApps var pinStatus =
.where((element) => selectedApps
element.pinned) .where((element) =>
.isEmpty; element
appsProvider.saveApps( .pinned)
selectedApps.map((e) { .isEmpty;
e.pinned = pinStatus; appsProvider.saveApps(
return e; selectedApps.map((e) {
}).toList()); e.pinned = pinStatus;
Navigator.of(context).pop(); return e;
}, }).toList());
tooltip: selectedApps Navigator.of(context)
.where((element) => .pop();
element.pinned) },
.isEmpty tooltip: selectedApps
? tr('pinToTop') .where((element) =>
: tr('unpinFromTop'), element.pinned)
icon: Icon(selectedApps .isEmpty
.where((element) => ? tr('pinToTop')
element.pinned) : tr('unpinFromTop'),
.isEmpty icon: Icon(selectedApps
? Icons.bookmark_outline_rounded .where((element) =>
: Icons element.pinned)
.bookmark_remove_outlined), .isEmpty
? Icons
.bookmark_outline_rounded
: Icons
.bookmark_remove_outlined),
),
IconButton(
onPressed: () {
String urls = '';
for (var a
in selectedApps) {
urls += '${a.url}\n';
}
urls = urls.substring(
0, urls.length - 1);
Share.share(urls,
subject: tr(
'selectedAppURLsFromObtainium'));
Navigator.of(context)
.pop();
},
tooltip: tr(
'shareSelectedAppURLs'),
icon:
const Icon(Icons.share),
),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext
ctx) {
return GeneratedFormModal(
title: tr(
'resetInstallStatusForSelectedAppsQuestion'),
items: const [],
initValid: true,
message: tr(
'installStatusOfXWillBeResetExplanation',
args: [
plural(
'app',
selectedApps
.length)
]),
);
}).then((values) {
if (values != null) {
appsProvider.saveApps(
selectedApps
.map((e) {
e.installedVersion =
null;
return e;
}).toList());
}
}).whenComplete(() {
Navigator.of(context)
.pop();
});
},
tooltip: tr(
'resetInstallStatus'),
icon: const Icon(Icons
.restore_page_outlined),
),
]),
), ),
IconButton( );
onPressed: () { });
String urls = ''; },
for (var a in selectedApps) { tooltip: tr('more'),
urls += '${a.url}\n'; icon: const Icon(Icons.more_horiz),
} ),
urls = urls.substring( ],
0, urls.length - 1); ))),
Share.share(urls,
subject: tr(
'selectedAppURLsFromObtainium'));
Navigator.of(context).pop();
},
tooltip: tr('shareSelectedAppURLs'),
icon: const Icon(Icons.share),
),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext ctx) {
return GeneratedFormModal(
title: tr(
'resetInstallStatusForSelectedAppsQuestion'),
items: const [],
initValid: true,
message: tr(
'installStatusOfXWillBeResetExplanation',
args: [
plural(
'app',
selectedApps
.length)
]),
);
}).then((values) {
if (values != null) {
appsProvider.saveApps(
selectedApps.map((e) {
e.installedVersion = null;
return e;
}).toList());
}
}).whenComplete(() {
Navigator.of(context).pop();
});
},
tooltip: tr('resetInstallStatus'),
icon: const Icon(
Icons.restore_page_outlined),
),
]),
),
);
});
},
tooltip: tr('more'),
icon: const Icon(Icons.more_horiz),
),
],
)),
const VerticalDivider(), const VerticalDivider(),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -688,12 +810,15 @@ class AppsPageState extends State<AppsPage> {
appsProvider.apps.isEmpty appsProvider.apps.isEmpty
? const SizedBox() ? const SizedBox()
: TextButton.icon( : TextButton.icon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
label: Text( label: Text(
filter.isIdenticalTo(neutralFilter) filter.isIdenticalTo(neutralFilter, settingsProvider)
? tr('filter') ? tr('filter')
: tr('filterActive'), : tr('filterActive'),
style: TextStyle( style: TextStyle(
fontWeight: filter.isIdenticalTo(neutralFilter) fontWeight: filter.isIdenticalTo(
neutralFilter, settingsProvider)
? FontWeight.normal ? FontWeight.normal
: FontWeight.bold), : FontWeight.bold),
), ),
@@ -785,12 +910,10 @@ class AppsFilter {
includeNonInstalled = values['nonInstalledApps']; includeNonInstalled = values['nonInstalledApps'];
} }
bool isIdenticalTo(AppsFilter other) => bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
authorFilter.trim() == other.authorFilter.trim() && authorFilter.trim() == other.authorFilter.trim() &&
nameFilter.trim() == other.nameFilter.trim() && nameFilter.trim() == other.nameFilter.trim() &&
includeUptodate == other.includeUptodate && includeUptodate == other.includeUptodate &&
includeNonInstalled == other.includeNonInstalled && includeNonInstalled == other.includeNonInstalled &&
categoryFilter.length == other.categoryFilter.length && settingsProvider.setEqual(categoryFilter, other.categoryFilter);
categoryFilter.union(other.categoryFilter).length ==
categoryFilter.length;
} }

View File

@@ -9,6 +9,7 @@ import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -28,6 +29,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
var appsProvider = context.read<AppsProvider>(); var appsProvider = context.read<AppsProvider>();
var settingsProvider = context.read<SettingsProvider>();
var outlineButtonStyle = ButtonStyle( var outlineButtonStyle = ButtonStyle(
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
StadiumBorder( StadiumBorder(
@@ -66,6 +68,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
showError( showError(
tr('exportedTo', args: [path]), tr('exportedTo', args: [path]),
context); context);
}).catchError((e) {
showError(e, context);
}); });
}, },
child: Text(tr('obtainiumExport')))), child: Text(tr('obtainiumExport')))),
@@ -98,6 +102,21 @@ class _ImportExportPageState extends State<ImportExportPage> {
appsProvider appsProvider
.importApps(data) .importApps(data)
.then((value) { .then((value) {
var cats =
settingsProvider.categories;
appsProvider.apps
.forEach((key, value) {
for (var c
in value.app.categories) {
if (!cats.containsKey(c)) {
cats[c] =
generateRandomLightColor()
.value;
}
}
});
settingsProvider.categories =
cats;
showError( showError(
tr('importedX', args: [ tr('importedX', args: [
plural('apps', value) plural('apps', value)

View File

@@ -436,7 +436,7 @@ class _CategoryEditorSelectorState extends State<CategoryEditorSelector> {
items: [ items: [
[ [
GeneratedFormTagInput('categories', GeneratedFormTagInput('categories',
label: tr('category'), label: tr('categories'),
emptyMessage: tr('noCategories'), emptyMessage: tr('noCategories'),
defaultValue: storedValues, defaultValue: storedValues,
alignment: widget.alignment, alignment: widget.alignment,

View File

@@ -17,6 +17,7 @@ import 'package:obtainium/providers/logs_provider.dart';
import 'package:obtainium/providers/notifications_provider.dart'; import 'package:obtainium/providers/notifications_provider.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
import 'package:package_archive_info/package_archive_info.dart'; import 'package:package_archive_info/package_archive_info.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:flutter_fgbg/flutter_fgbg.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart';
@@ -706,6 +707,14 @@ class AppsProvider with ChangeNotifier {
exportDir = await getExternalStorageDirectory(); exportDir = await getExternalStorageDirectory();
path = exportDir!.path; path = exportDir!.path;
} }
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt <= 28) {
if (await Permission.storage.isDenied) {
await Permission.storage.request();
}
if (await Permission.storage.isDenied) {
throw ObtainiumError(tr('storagePermissionDenied'));
}
}
File export = File( File export = File(
'${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json'); '${exportDir.path}/${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().millisecondsSinceEpoch}.json');
export.writeAsStringSync( export.writeAsStringSync(

View File

@@ -157,15 +157,6 @@ class SettingsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
getCategoryFormItem({String initCategory = ''}) => GeneratedFormDropdown(
'category',
label: tr('category'),
[
MapEntry('', tr('noCategory')),
...categories.entries.map((e) => MapEntry(e.key, e.key)).toList()
],
defaultValue: initCategory);
String? get forcedLocale { String? get forcedLocale {
var fl = prefs?.getString('forcedLocale'); var fl = prefs?.getString('forcedLocale');
return supportedLocales return supportedLocales
@@ -185,4 +176,7 @@ class SettingsProvider with ChangeNotifier {
} }
notifyListeners(); notifyListeners();
} }
bool setEqual(Set<String> a, Set<String> b) =>
a.length == b.length && a.union(b).length == a.length;
} }

View File

@@ -48,7 +48,7 @@ class App {
late Map<String, dynamic> additionalSettings; late Map<String, dynamic> additionalSettings;
late DateTime? lastUpdateCheck; late DateTime? lastUpdateCheck;
bool pinned = false; bool pinned = false;
String? category; List<String> categories;
App( App(
this.id, this.id,
this.url, this.url,
@@ -61,7 +61,7 @@ class App {
this.additionalSettings, this.additionalSettings,
this.lastUpdateCheck, this.lastUpdateCheck,
this.pinned, this.pinned,
{this.category}); {this.categories = const []});
@override @override
String toString() { String toString() {
@@ -103,6 +103,12 @@ class App {
item.ensureType(additionalSettings[item.key]); item.ensureType(additionalSettings[item.key]);
} }
} }
int preferredApkIndex = json['preferredApkIndex'] == null
? 0
: json['preferredApkIndex'] as int;
if (preferredApkIndex < 0) {
preferredApkIndex = 0;
}
return App( return App(
json['id'] as String, json['id'] as String,
json['url'] as String, json['url'] as String,
@@ -115,15 +121,19 @@ class App {
json['apkUrls'] == null json['apkUrls'] == null
? [] ? []
: List<String>.from(jsonDecode(json['apkUrls'])), : List<String>.from(jsonDecode(json['apkUrls'])),
json['preferredApkIndex'] == null preferredApkIndex,
? 0
: json['preferredApkIndex'] as int,
additionalSettings, additionalSettings,
json['lastUpdateCheck'] == null json['lastUpdateCheck'] == null
? null ? null
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']), : DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
json['pinned'] ?? false, json['pinned'] ?? false,
category: json['category']); categories: json['categories'] != null
? (json['categories'] as List<dynamic>)
.map((e) => e.toString())
.toList()
: json['category'] != null
? [json['category'] as String]
: []);
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@@ -138,7 +148,7 @@ class App {
'additionalSettings': jsonEncode(additionalSettings), 'additionalSettings': jsonEncode(additionalSettings),
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch, 'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
'pinned': pinned, 'pinned': pinned,
'category': category 'categories': categories
}; };
} }
@@ -360,11 +370,11 @@ class SourceProvider {
currentApp?.installedVersion, currentApp?.installedVersion,
apkVersion, apkVersion,
apk.apkUrls, apk.apkUrls,
apk.apkUrls.length - 1, apk.apkUrls.length - 1 >= 0 ? apk.apkUrls.length - 1 : 0,
additionalSettings, additionalSettings,
DateTime.now(), DateTime.now(),
currentApp?.pinned ?? false, currentApp?.pinned ?? false,
category: currentApp?.category); categories: currentApp?.categories ?? const []);
} }
// Returns errors in [results, errors] instead of throwing them // Returns errors in [results, errors] instead of throwing them

View File

@@ -56,7 +56,7 @@ packages:
name: checked_yaml name: checked_yaml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@@ -286,7 +286,7 @@ packages:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.2" version: "3.3.0"
install_plugin_v2: install_plugin_v2:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -559,7 +559,7 @@ packages:
name: shared_preferences_macos name: shared_preferences_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.5"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -655,7 +655,7 @@ packages:
name: timezone name: timezone
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.0" version: "0.9.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 0.9.9+97 # When changing this, update the tag in main() accordingly version: 0.9.14+104 # When changing this, update the tag in main() accordingly
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.18.2 <3.0.0'