mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-25 18:40:45 +02:00
Compare commits
16 Commits
v0.9.10-be
...
v0.9.13-be
Author | SHA1 | Date | |
---|---|---|---|
|
b68cf5a1be | ||
|
4eb7499591 | ||
|
98fafe2aa4 | ||
|
9bac74aadd | ||
|
0a93117bf0 | ||
|
451cc41c45 | ||
|
3b449d0982 | ||
|
1863f55372 | ||
|
0c4b8ac79d | ||
|
e287087753 | ||
|
82bcc46d42 | ||
|
1f26188ec6 | ||
|
794c3e1a81 | ||
|
16369b4adf | ||
|
8f16f745be | ||
|
8ddeb3d776 |
@@ -1,4 +1,4 @@
|
|||||||
#  Obtainium
|
#  Obtainium
|
||||||
|
|
||||||
Get Android App Updates Directly From the Source.
|
Get Android App Updates Directly From the Source.
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.1 KiB |
BIN
assets/graphics/icon_small.png
Normal file
BIN
assets/graphics/icon_small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
@@ -210,6 +210,7 @@
|
|||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"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"
|
||||||
|
@@ -210,6 +210,7 @@
|
|||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"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"
|
||||||
|
@@ -209,6 +209,7 @@
|
|||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"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"
|
||||||
|
@@ -210,6 +210,7 @@
|
|||||||
"label": "Etichetta",
|
"label": "Etichetta",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"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"
|
||||||
|
@@ -210,6 +210,7 @@
|
|||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"storagePermissionDenied": "Storage permission denied",
|
"storagePermissionDenied": "Storage permission denied",
|
||||||
|
"selectedCategorizeWarning": "This will replace any existing category settings for the selected Apps.",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
"one": "リクエストが多すぎます(レート制限)- {}分後に再試行してください",
|
||||||
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
"other": "リクエストが多すぎます(レート制限)- {}分後に再試行してください"
|
||||||
|
@@ -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,17 +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": "Storage permission denied",
|
"storagePermissionDenied": "存储权限已被拒绝",
|
||||||
|
"selectedCategorizeWarning": "这将取代所选应用程序的任何现有类别",
|
||||||
"tooManyRequestsTryAgainInMinutes": {
|
"tooManyRequestsTryAgainInMinutes": {
|
||||||
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
"one": "请求过多 (API 限制) - 在 {} 分钟后重试",
|
||||||
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
"other": "请求过多 (API 限制) - 在 {} 分钟后重试"
|
||||||
|
@@ -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,8 +66,11 @@ 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')
|
||||||
|
: widget.singleNullReturnButton!)),
|
||||||
|
widget.singleNullReturnButton == null
|
||||||
|
? TextButton(
|
||||||
onPressed: !valid
|
onPressed: !valid
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
@@ -75,6 +80,7 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(tr('continue')))
|
child: Text(tr('continue')))
|
||||||
|
: const SizedBox.shrink()
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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.10';
|
const String currentVersion = '0.9.13';
|
||||||
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
|
||||||
|
|
||||||
|
@@ -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;
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -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(
|
||||||
|
@@ -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)
|
||||||
@@ -339,6 +349,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
children: [
|
children: [
|
||||||
selectedApps.isEmpty
|
selectedApps.isEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectThese(sortedApps.map((e) => e.app).toList());
|
selectThese(sortedApps.map((e) => e.app).toList());
|
||||||
},
|
},
|
||||||
@@ -348,6 +359,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
),
|
),
|
||||||
tooltip: tr('selectAll'))
|
tooltip: tr('selectAll'))
|
||||||
: 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())
|
||||||
@@ -492,6 +505,75 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.file_download_outlined,
|
Icons.file_download_outlined,
|
||||||
)),
|
)),
|
||||||
|
selectedApps.isEmpty
|
||||||
|
? const SizedBox()
|
||||||
|
: IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () 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),
|
||||||
|
),
|
||||||
selectedApps.isEmpty
|
selectedApps.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: IconButton(
|
: IconButton(
|
||||||
@@ -688,12 +770,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 +870,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;
|
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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.10+98 # When changing this, update the tag in main() accordingly
|
version: 0.9.13+103 # 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'
|
||||||
|
Reference in New Issue
Block a user