mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-01 21:30:16 +02:00
Version detection improvements, Mullvad web scraping fix and changelog addition, code readability improvements, general tweaks/bugfixes (#400)
1. Apps that don't have "standard" versioning formats now automatically stop using version detection. This will prevent users from having to learn about this feature and enable it manually. - For such Apps, the "standard" version detection option is greyed out. 2. The Mullvad Source recently broke due to a slight change in their website design. This is now fixed. - Mullvad also now provides an in-app changelog via their official GitHub repo. 3. Code has been refactored for readability (specifically the version detection code and UI code for most screens). 4. Minor UI tweaks and bugfixes.
This commit is contained in:
@@ -33,10 +33,10 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
bool additionalSettingsValid = true;
|
||||
List<String> pickedCategories = [];
|
||||
int searchnum = 0;
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||
|
||||
bool doingSomething = gettingAppInfo || searching;
|
||||
@@ -64,65 +64,56 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
}
|
||||
|
||||
getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly) async {
|
||||
return (!((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('xIsTrackOnly', args: [
|
||||
pickedSource!.enforceTrackOnly
|
||||
? tr('source')
|
||||
: tr('app')
|
||||
]),
|
||||
items: const [],
|
||||
message:
|
||||
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
||||
);
|
||||
}) ==
|
||||
null));
|
||||
}
|
||||
|
||||
getReleaseDateAsVersionConfirmationIfNeeded(
|
||||
bool userPickedTrackOnly) async {
|
||||
return (!(additionalSettings['versionDetection'] ==
|
||||
'releaseDateAsVersion' &&
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('releaseDateAsVersion'),
|
||||
items: const [],
|
||||
message: tr('releaseDateAsVersionExplanation'),
|
||||
);
|
||||
}) ==
|
||||
null));
|
||||
}
|
||||
|
||||
addApp({bool resetUserInputAfter = false}) async {
|
||||
setState(() {
|
||||
gettingAppInfo = true;
|
||||
});
|
||||
var settingsProvider = context.read<SettingsProvider>();
|
||||
() async {
|
||||
try {
|
||||
var settingsProvider = context.read<SettingsProvider>();
|
||||
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
||||
var cont = true;
|
||||
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('xIsTrackOnly', args: [
|
||||
pickedSource!.enforceTrackOnly
|
||||
? tr('source')
|
||||
: tr('app')
|
||||
]),
|
||||
items: const [],
|
||||
message:
|
||||
'${pickedSource!.enforceTrackOnly ? tr('appsFromSourceAreTrackOnly') : tr('youPickedTrackOnly')}\n\n${tr('trackOnlyAppDescription')}',
|
||||
);
|
||||
}) ==
|
||||
null) {
|
||||
cont = false;
|
||||
}
|
||||
if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('releaseDateAsVersion'),
|
||||
items: const [],
|
||||
message: tr('releaseDateAsVersionExplanation'),
|
||||
);
|
||||
}) ==
|
||||
null) {
|
||||
cont = false;
|
||||
}
|
||||
if (additionalSettings['versionDetection'] == 'noVersionDetection' &&
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('disableVersionDetection'),
|
||||
items: const [],
|
||||
message: tr('noVersionDetectionExplanation'),
|
||||
);
|
||||
}) ==
|
||||
null) {
|
||||
cont = false;
|
||||
}
|
||||
if (cont) {
|
||||
HapticFeedback.selectionClick();
|
||||
App? app;
|
||||
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
||||
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
||||
userPickedTrackOnly))) {
|
||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||
App app = await sourceProvider.getApp(
|
||||
app = await sourceProvider.getApp(
|
||||
pickedSource!, userInput, additionalSettings,
|
||||
trackOnlyOverride: trackOnly);
|
||||
if (!trackOnly) {
|
||||
@@ -150,27 +141,232 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
}
|
||||
app.categories = pickedCategories;
|
||||
await appsProvider.saveApps([app], onlyIfExists: false);
|
||||
|
||||
return app;
|
||||
}
|
||||
}()
|
||||
.then((app) {
|
||||
if (app != null) {
|
||||
Navigator.push(globalNavigatorKey.currentContext ?? context,
|
||||
MaterialPageRoute(builder: (context) => AppPage(appId: app.id)));
|
||||
MaterialPageRoute(builder: (context) => AppPage(appId: app!.id)));
|
||||
}
|
||||
}).catchError((e) {
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
} finally {
|
||||
setState(() {
|
||||
gettingAppInfo = false;
|
||||
if (resetUserInputAfter) {
|
||||
changeUserInput('', false, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget getUrlInputRow() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
key: Key(searchnum.toString()),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('appSourceURL',
|
||||
label: tr('appSourceURL'),
|
||||
defaultValue: userInput,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
try {
|
||||
sourceProvider
|
||||
.getSource(value ?? '')
|
||||
.standardizeURL(
|
||||
preStandardizeUrl(value ?? ''));
|
||||
} catch (e) {
|
||||
return e is String
|
||||
? e
|
||||
: e is ObtainiumError
|
||||
? e.toString()
|
||||
: tr('error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
changeUserInput(
|
||||
values['appSourceURL']!, valid, isBuilding);
|
||||
})),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
gettingAppInfo
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: doingSomething ||
|
||||
pickedSource == null ||
|
||||
(pickedSource!.combinedAppSpecificSettingFormItems
|
||||
.isNotEmpty &&
|
||||
!additionalSettingsValid)
|
||||
? null
|
||||
: () {
|
||||
HapticFeedback.selectionClick();
|
||||
addApp();
|
||||
},
|
||||
child: Text(tr('add')))
|
||||
],
|
||||
);
|
||||
|
||||
runSearch() async {
|
||||
setState(() {
|
||||
searching = true;
|
||||
});
|
||||
try {
|
||||
var results = await Future.wait(sourceProvider.sources
|
||||
.where((e) => e.canSearch)
|
||||
.map((e) => e.search(searchQuery)));
|
||||
|
||||
// .then((results) async {
|
||||
// Interleave results instead of simple reduce
|
||||
Map<String, String> res = {};
|
||||
var si = 0;
|
||||
var done = false;
|
||||
while (!done) {
|
||||
done = true;
|
||||
for (var r in results) {
|
||||
if (r.length > si) {
|
||||
done = false;
|
||||
res.addEntries([r.entries.elementAt(si)]);
|
||||
}
|
||||
}
|
||||
si++;
|
||||
}
|
||||
List<String>? selectedUrls = res.isEmpty
|
||||
? []
|
||||
// ignore: use_build_context_synchronously
|
||||
: await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: res,
|
||||
selectedByDefault: false,
|
||||
onlyOneSelectionAllowed: true,
|
||||
);
|
||||
});
|
||||
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
||||
changeUserInput(selectedUrls[0], true, false, isSearch: true);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
} finally {
|
||||
setState(() {
|
||||
searching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldShowSearchBar() =>
|
||||
sourceProvider.sources.where((e) => e.canSearch).isNotEmpty &&
|
||||
pickedSource == null &&
|
||||
userInput.isEmpty;
|
||||
|
||||
Widget getSearchBarRow() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('searchSomeSources',
|
||||
label: tr('searchSomeSourcesLabel'), required: false),
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (values.isNotEmpty && valid && !isBuilding) {
|
||||
setState(() {
|
||||
searchQuery = values['searchSomeSources']!.trim();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: searchQuery.isEmpty || doingSomething
|
||||
? null
|
||||
: () {
|
||||
runSearch();
|
||||
},
|
||||
child: Text(tr('search')))
|
||||
],
|
||||
);
|
||||
|
||||
Widget getAdditionalOptsCol() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(
|
||||
height: 64,
|
||||
),
|
||||
Text(
|
||||
tr('additionalOptsFor',
|
||||
args: [pickedSource?.name ?? tr('source')]),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
GeneratedForm(
|
||||
key: Key(pickedSource.runtimeType.toString()),
|
||||
items: pickedSource!.combinedAppSpecificSettingFormItems,
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
additionalSettings = values;
|
||||
additionalSettingsValid = valid;
|
||||
});
|
||||
}
|
||||
}),
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
CategoryEditorSelector(
|
||||
alignment: WrapAlignment.start,
|
||||
onSelected: (categories) {
|
||||
pickedCategories = categories;
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget getSourcesListWidget() => Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Text(
|
||||
tr('supportedSourcesBelow'),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
...sourceProvider.sources
|
||||
.map((e) => GestureDetector(
|
||||
onTap: e.host != null
|
||||
? () {
|
||||
launchUrlString('https://${e.host}',
|
||||
mode: LaunchMode.externalApplication);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||
style: TextStyle(
|
||||
decoration: e.host != null
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
fontStyle: FontStyle.italic),
|
||||
)))
|
||||
.toList()
|
||||
]));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
@@ -181,230 +377,16 @@ class _AddAppPageState extends State<AddAppPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
key: Key(searchnum.toString()),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('appSourceURL',
|
||||
label: tr('appSourceURL'),
|
||||
defaultValue: userInput,
|
||||
additionalValidators: [
|
||||
(value) {
|
||||
try {
|
||||
sourceProvider
|
||||
.getSource(value ?? '')
|
||||
.standardizeURL(
|
||||
preStandardizeUrl(
|
||||
value ?? ''));
|
||||
} catch (e) {
|
||||
return e is String
|
||||
? e
|
||||
: e is ObtainiumError
|
||||
? e.toString()
|
||||
: tr('error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
])
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
changeUserInput(values['appSourceURL']!,
|
||||
valid, isBuilding);
|
||||
})),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
gettingAppInfo
|
||||
? const CircularProgressIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: doingSomething ||
|
||||
pickedSource == null ||
|
||||
(pickedSource!
|
||||
.combinedAppSpecificSettingFormItems
|
||||
.isNotEmpty &&
|
||||
!additionalSettingsValid)
|
||||
? null
|
||||
: addApp,
|
||||
child: Text(tr('add')))
|
||||
],
|
||||
),
|
||||
if (sourceProvider.sources
|
||||
.where((e) => e.canSearch)
|
||||
.isNotEmpty &&
|
||||
pickedSource == null &&
|
||||
userInput.isEmpty)
|
||||
getUrlInputRow(),
|
||||
if (shouldShowSearchBar())
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (sourceProvider.sources
|
||||
.where((e) => e.canSearch)
|
||||
.isNotEmpty &&
|
||||
pickedSource == null &&
|
||||
userInput.isEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeneratedForm(
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField(
|
||||
'searchSomeSources',
|
||||
label: tr('searchSomeSourcesLabel'),
|
||||
required: false),
|
||||
]
|
||||
],
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (values.isNotEmpty &&
|
||||
valid &&
|
||||
!isBuilding) {
|
||||
setState(() {
|
||||
searchQuery =
|
||||
values['searchSomeSources']!.trim();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: searchQuery.isEmpty || doingSomething
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
searching = true;
|
||||
});
|
||||
Future.wait(sourceProvider.sources
|
||||
.where((e) => e.canSearch)
|
||||
.map((e) =>
|
||||
e.search(searchQuery)))
|
||||
.then((results) async {
|
||||
// Interleave results instead of simple reduce
|
||||
Map<String, String> res = {};
|
||||
var si = 0;
|
||||
var done = false;
|
||||
while (!done) {
|
||||
done = true;
|
||||
for (var r in results) {
|
||||
if (r.length > si) {
|
||||
done = false;
|
||||
res.addEntries(
|
||||
[r.entries.elementAt(si)]);
|
||||
}
|
||||
}
|
||||
si++;
|
||||
}
|
||||
List<String>? selectedUrls = res
|
||||
.isEmpty
|
||||
? []
|
||||
: await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: res,
|
||||
selectedByDefault: false,
|
||||
onlyOneSelectionAllowed:
|
||||
true,
|
||||
);
|
||||
});
|
||||
if (selectedUrls != null &&
|
||||
selectedUrls.isNotEmpty) {
|
||||
changeUserInput(
|
||||
selectedUrls[0], true, false,
|
||||
isSearch: true);
|
||||
}
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
searching = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
child: Text(tr('search')))
|
||||
],
|
||||
),
|
||||
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||
if (pickedSource != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(
|
||||
height: 64,
|
||||
),
|
||||
Text(
|
||||
tr('additionalOptsFor',
|
||||
args: [pickedSource?.name ?? tr('source')]),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary)),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
GeneratedForm(
|
||||
key: Key(pickedSource.runtimeType.toString()),
|
||||
items: pickedSource!
|
||||
.combinedAppSpecificSettingFormItems,
|
||||
onValueChanges: (values, valid, isBuilding) {
|
||||
if (!isBuilding) {
|
||||
setState(() {
|
||||
additionalSettings = values;
|
||||
additionalSettingsValid = valid;
|
||||
});
|
||||
}
|
||||
}),
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
CategoryEditorSelector(
|
||||
alignment: WrapAlignment.start,
|
||||
onSelected: (categories) {
|
||||
pickedCategories = categories;
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
getAdditionalOptsCol()
|
||||
else
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
Text(
|
||||
tr('supportedSourcesBelow'),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
...sourceProvider.sources
|
||||
.map((e) => GestureDetector(
|
||||
onTap: e.host != null
|
||||
? () {
|
||||
launchUrlString(
|
||||
'https://${e.host}',
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
|
||||
style: TextStyle(
|
||||
decoration: e.host != null
|
||||
? TextDecoration.underline
|
||||
: TextDecoration.none,
|
||||
fontStyle: FontStyle.italic),
|
||||
)))
|
||||
.toList()
|
||||
])),
|
||||
getSourcesListWidget(),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
@@ -34,406 +35,414 @@ class _AppPageState extends State<AppPage> {
|
||||
});
|
||||
}
|
||||
|
||||
bool areDownloadsRunning = appsProvider.areDownloadsRunning();
|
||||
|
||||
var sourceProvider = SourceProvider();
|
||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
||||
if (!appsProvider.areDownloadsRunning() && prevApp == null && app != null) {
|
||||
if (!areDownloadsRunning && prevApp == null && app != null) {
|
||||
prevApp = app;
|
||||
getUpdate(app.app.id);
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
bool isVersionDetectionStandard =
|
||||
app?.app.additionalSettings['versionDetection'] ==
|
||||
'standardVersionDetection';
|
||||
|
||||
var fullInfoColumn = Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 125),
|
||||
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: 8,
|
||||
),
|
||||
Text(
|
||||
app?.app.id ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
app?.app.releaseDate == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
app!.app.releaseDate.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
getInfoColumn() => 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?.installedInfo?.versionName ??
|
||||
app?.app.installedVersion ??
|
||||
tr('none')
|
||||
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
||||
tr('app')
|
||||
])}' : ''}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (app?.app.installedVersion != null &&
|
||||
!isVersionDetectionStandard)
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
tr('noVersionDetection'),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
infoColumn,
|
||||
const SizedBox(height: 150)
|
||||
],
|
||||
);
|
||||
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]);
|
||||
}
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
getFullInfoColumn() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 125),
|
||||
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: 8,
|
||||
),
|
||||
Text(
|
||||
app?.app.id ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
app?.app.releaseDate == null
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
app!.app.releaseDate.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
getInfoColumn(),
|
||||
const SizedBox(height: 150)
|
||||
],
|
||||
);
|
||||
|
||||
getAppWebView() => app != null
|
||||
? WebViewWidget(
|
||||
controller: WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Theme.of(context).colorScheme.background)
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
if (error.isForMainFrame == true) {
|
||||
showError(
|
||||
ObtainiumError(error.description, unexpected: true),
|
||||
context);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(app.app.url)))
|
||||
: Container();
|
||||
|
||||
showMarkUpdatedDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(tr('alreadyUpToDateQuestion')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('no'))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
var updatedApp = app?.app;
|
||||
if (updatedApp != null) {
|
||||
updatedApp.installedVersion = updatedApp.latestVersion;
|
||||
appsProvider.saveApps([updatedApp]);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('yesMarkUpdated')))
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
showAdditionalOptionsDialog() async {
|
||||
return await showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
var items =
|
||||
(source?.combinedAppSpecificSettingFormItems ?? []).map((row) {
|
||||
row = row.map((e) {
|
||||
if (app?.app.additionalSettings[e.key] != null) {
|
||||
e.defaultValue = app?.app.additionalSettings[e.key];
|
||||
}
|
||||
return e;
|
||||
}).toList();
|
||||
return row;
|
||||
}).toList();
|
||||
|
||||
items = items.map((row) {
|
||||
row = row.map((e) {
|
||||
if (e.key == 'versionDetection' && e is GeneratedFormDropdown) {
|
||||
e.disabledOptKeys ??= [];
|
||||
if (app?.app.installedVersion != null &&
|
||||
!appsProvider.isVersionDetectionPossible(app)) {
|
||||
e.disabledOptKeys!.add('standardVersionDetection');
|
||||
}
|
||||
if (app?.app.releaseDate == null) {
|
||||
e.disabledOptKeys!.add('releaseDateAsVersion');
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}).toList();
|
||||
return row;
|
||||
}).toList();
|
||||
|
||||
return GeneratedFormModal(
|
||||
title: tr('additionalOptions'),
|
||||
items: items,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleAdditionalOptionChanges(Map<String, dynamic>? values) {
|
||||
if (app != null && values != null) {
|
||||
Map<String, dynamic> originalSettings = app.app.additionalSettings;
|
||||
app.app.additionalSettings = values;
|
||||
if (source?.enforceTrackOnly == true) {
|
||||
app.app.additionalSettings['trackOnly'] = true;
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(tr('appsFromSourceAreTrackOnly'), context);
|
||||
}
|
||||
if (app.app.additionalSettings['versionDetection'] ==
|
||||
'releaseDateAsVersion') {
|
||||
if (originalSettings['versionDetection'] != 'releaseDateAsVersion') {
|
||||
if (app.app.releaseDate != null) {
|
||||
bool isUpdated =
|
||||
app.app.installedVersion == app.app.latestVersion;
|
||||
app.app.latestVersion =
|
||||
app.app.releaseDate!.microsecondsSinceEpoch.toString();
|
||||
if (isUpdated) {
|
||||
app.app.installedVersion = app.app.latestVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (originalSettings['versionDetection'] ==
|
||||
'releaseDateAsVersion') {
|
||||
app.app.installedVersion =
|
||||
app.installedInfo?.versionName ?? app.app.installedVersion;
|
||||
}
|
||||
appsProvider.saveApps([app.app]).then((value) {
|
||||
getUpdate(app.app.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getInstallOrUpdateButton() => TextButton(
|
||||
onPressed: (app?.app.installedVersion == null ||
|
||||
app?.app.installedVersion != app?.app.latestVersion) &&
|
||||
!areDownloadsRunning
|
||||
? () async {
|
||||
try {
|
||||
HapticFeedback.heavyImpact();
|
||||
if (app?.app.additionalSettings['trackOnly'] != true) {
|
||||
await settingsProvider.getInstallPermission();
|
||||
}
|
||||
var res = await appsProvider.downloadAndInstallLatestApps(
|
||||
[app!.app.id], globalNavigatorKey.currentContext);
|
||||
if (res.isNotEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(app?.app.installedVersion == null
|
||||
? !trackOnly
|
||||
? tr('install')
|
||||
: tr('markInstalled')
|
||||
: !trackOnly
|
||||
? tr('update')
|
||||
: tr('markUpdated')));
|
||||
|
||||
getBottomSheetMenu() => Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (app?.app.installedVersion != null &&
|
||||
app?.app.installedVersion != app?.app.latestVersion &&
|
||||
!isVersionDetectionStandard &&
|
||||
!trackOnly)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: showMarkUpdatedDialog,
|
||||
tooltip: tr('markUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
if (source != null &&
|
||||
source.combinedAppSpecificSettingFormItems.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () async {
|
||||
var values =
|
||||
await showAdditionalOptionsDialog();
|
||||
handleAdditionalOptionChanges(values);
|
||||
},
|
||||
tooltip: tr('additionalOptions'),
|
||||
icon: const Icon(Icons.edit)),
|
||||
if (app != null && app.installedInfo != null)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
appsProvider.openAppSettings(app.app.id);
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: tr('settings'),
|
||||
),
|
||||
if (app != null && settingsProvider.showAppWebpage)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
content: getInfoColumn(),
|
||||
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),
|
||||
Expanded(child: getInstallOrUpdateButton()),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
appsProvider.removeAppsWithModal(
|
||||
context, [app!.app]).then((value) {
|
||||
if (value == true) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
surfaceTintColor:
|
||||
Theme.of(context).colorScheme.error),
|
||||
child: Text(tr('remove')),
|
||||
)),
|
||||
])),
|
||||
if (app?.downloadProgress != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||
child: LinearProgressIndicator(
|
||||
value: app!.downloadProgress! / 100))
|
||||
],
|
||||
));
|
||||
|
||||
return Scaffold(
|
||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: RefreshIndicator(
|
||||
child: settingsProvider.showAppWebpage
|
||||
? app != null
|
||||
? WebViewWidget(
|
||||
controller: WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(
|
||||
Theme.of(context).colorScheme.background)
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
if (error.isForMainFrame == true) {
|
||||
showError(
|
||||
ObtainiumError(error.description,
|
||||
unexpected: true),
|
||||
context);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(app.app.url)))
|
||||
: Container()
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(children: [fullInfoColumn])),
|
||||
],
|
||||
),
|
||||
onRefresh: () async {
|
||||
if (app != null) {
|
||||
getUpdate(app.app.id);
|
||||
}
|
||||
}),
|
||||
bottomSheet: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (app?.app.additionalSettings['versionDetection'] !=
|
||||
'standardVersionDetection' &&
|
||||
!trackOnly &&
|
||||
app?.app.installedVersion != null &&
|
||||
app?.app.installedVersion != app?.app.latestVersion)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(tr(
|
||||
'alreadyUpToDateQuestion')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: Text(tr('no'))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
var updatedApp = app?.app;
|
||||
if (updatedApp != null) {
|
||||
updatedApp
|
||||
.installedVersion =
|
||||
updatedApp
|
||||
.latestVersion;
|
||||
appsProvider.saveApps(
|
||||
[updatedApp]);
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
},
|
||||
child: Text(
|
||||
tr('yesMarkUpdated')))
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
tooltip: tr('markUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
if (source != null &&
|
||||
source
|
||||
.combinedAppSpecificSettingFormItems.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
var items = source
|
||||
.combinedAppSpecificSettingFormItems
|
||||
.map((row) {
|
||||
row.map((e) {
|
||||
if (app?.app.additionalSettings[
|
||||
e.key] !=
|
||||
null) {
|
||||
e.defaultValue = app?.app
|
||||
.additionalSettings[
|
||||
e.key];
|
||||
}
|
||||
return e;
|
||||
}).toList();
|
||||
return row;
|
||||
}).toList();
|
||||
return GeneratedFormModal(
|
||||
title: tr('additionalOptions'),
|
||||
items: items,
|
||||
);
|
||||
}).then((values) {
|
||||
if (app != null && values != null) {
|
||||
Map<String, dynamic>
|
||||
originalSettings =
|
||||
app.app.additionalSettings;
|
||||
app.app.additionalSettings = values;
|
||||
if (source.enforceTrackOnly) {
|
||||
app.app.additionalSettings[
|
||||
'trackOnly'] = true;
|
||||
showError(
|
||||
tr('appsFromSourceAreTrackOnly'),
|
||||
context);
|
||||
}
|
||||
if (app.app.additionalSettings[
|
||||
'versionDetection'] ==
|
||||
'releaseDateAsVersion') {
|
||||
if (originalSettings[
|
||||
'versionDetection'] !=
|
||||
'releaseDateAsVersion') {
|
||||
if (app.app.releaseDate != null) {
|
||||
bool isUpdated =
|
||||
app.app.installedVersion ==
|
||||
app.app.latestVersion;
|
||||
app.app.latestVersion = app
|
||||
.app
|
||||
.releaseDate!
|
||||
.microsecondsSinceEpoch
|
||||
.toString();
|
||||
if (isUpdated) {
|
||||
app.app.installedVersion =
|
||||
app.app.latestVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (originalSettings[
|
||||
'versionDetection'] ==
|
||||
'releaseDateAsVersion') {
|
||||
app.app.installedVersion = app
|
||||
.installedInfo
|
||||
?.versionName ??
|
||||
app.app.installedVersion;
|
||||
}
|
||||
appsProvider.saveApps([app.app]).then(
|
||||
(value) {
|
||||
getUpdate(app.app.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
tooltip: tr('additionalOptions'),
|
||||
icon: const Icon(Icons.edit)),
|
||||
if (app != null && app.installedInfo != null)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
appsProvider.openAppSettings(app.app.id);
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: tr('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),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: (app?.app.installedVersion == null ||
|
||||
app?.app.installedVersion !=
|
||||
app?.app.latestVersion) &&
|
||||
!appsProvider.areDownloadsRunning()
|
||||
? () {
|
||||
HapticFeedback.heavyImpact();
|
||||
() async {
|
||||
if (app?.app.additionalSettings[
|
||||
'trackOnly'] !=
|
||||
true) {
|
||||
await settingsProvider
|
||||
.getInstallPermission();
|
||||
}
|
||||
}()
|
||||
.then((value) {
|
||||
appsProvider
|
||||
.downloadAndInstallLatestApps(
|
||||
[app!.app.id],
|
||||
globalNavigatorKey
|
||||
.currentContext).then(
|
||||
(res) {
|
||||
if (res.isNotEmpty && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
}
|
||||
: null,
|
||||
child: Text(app?.app.installedVersion == null
|
||||
? !trackOnly
|
||||
? tr('install')
|
||||
: tr('markInstalled')
|
||||
: !trackOnly
|
||||
? tr('update')
|
||||
: tr('markUpdated')))),
|
||||
const SizedBox(width: 16.0),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: app?.downloadProgress != null
|
||||
? null
|
||||
: () {
|
||||
appsProvider.removeAppsWithModal(
|
||||
context, [app!.app]).then((value) {
|
||||
if (value == true) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
surfaceTintColor:
|
||||
Theme.of(context).colorScheme.error),
|
||||
child: Text(tr('remove')),
|
||||
)),
|
||||
])),
|
||||
if (app?.downloadProgress != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
|
||||
child: LinearProgressIndicator(
|
||||
value: app!.downloadProgress! / 100))
|
||||
],
|
||||
)),
|
||||
);
|
||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: RefreshIndicator(
|
||||
child: settingsProvider.showAppWebpage
|
||||
? getAppWebView()
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(children: [getFullInfoColumn()])),
|
||||
],
|
||||
),
|
||||
onRefresh: () async {
|
||||
if (app != null) {
|
||||
getUpdate(app.app.id);
|
||||
}
|
||||
}),
|
||||
bottomSheet: getBottomSheetMenu());
|
||||
}
|
||||
}
|
||||
|
1528
lib/pages/apps.dart
1528
lib/pages/apps.dart
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
SourceProvider sourceProvider = SourceProvider();
|
||||
var appsProvider = context.read<AppsProvider>();
|
||||
var settingsProvider = context.read<SettingsProvider>();
|
||||
|
||||
var outlineButtonStyle = ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
StadiumBorder(
|
||||
@@ -101,6 +102,193 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
});
|
||||
}
|
||||
|
||||
runObtainiumExport() {
|
||||
HapticFeedback.selectionClick();
|
||||
appsProvider.exportApps().then((String path) {
|
||||
showError(tr('exportedTo', args: [path]), context);
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
}
|
||||
|
||||
runObtainiumImport() {
|
||||
HapticFeedback.selectionClick();
|
||||
FilePicker.platform.pickFiles().then((result) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
if (result != null) {
|
||||
String data = File(result.files.single.path!).readAsStringSync();
|
||||
try {
|
||||
jsonDecode(data);
|
||||
} catch (e) {
|
||||
throw ObtainiumError(tr('invalidInput'));
|
||||
}
|
||||
appsProvider.importApps(data).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(tr('importedX', args: [plural('apps', value)]), context);
|
||||
});
|
||||
} else {
|
||||
// User canceled the picker
|
||||
}
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
importInProgress = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
runUrlImport() {
|
||||
FilePicker.platform.pickFiles().then((result) {
|
||||
if (result != null) {
|
||||
urlListImport(
|
||||
overrideInitValid: true,
|
||||
initValue: RegExp('https?://[^"]+')
|
||||
.allMatches(
|
||||
File(result.files.single.path!).readAsStringSync())
|
||||
.map((e) => e.input.substring(e.start, e.end))
|
||||
.toSet()
|
||||
.toList()
|
||||
.where((url) {
|
||||
try {
|
||||
sourceProvider.getSource(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}).join('\n'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runSourceSearch(AppSource source) {
|
||||
() async {
|
||||
var values = await showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('searchX', args: [source.name]),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField('searchQuery',
|
||||
label: tr('searchQuery'))
|
||||
]
|
||||
],
|
||||
);
|
||||
});
|
||||
if (values != null &&
|
||||
(values['searchQuery'] as String?)?.isNotEmpty == true) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
var urlsWithDescriptions =
|
||||
await source.search(values['searchQuery'] as String);
|
||||
if (urlsWithDescriptions.isNotEmpty) {
|
||||
var selectedUrls =
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: urlsWithDescriptions,
|
||||
selectedByDefault: false,
|
||||
);
|
||||
});
|
||||
if (selectedUrls != null && selectedUrls.isNotEmpty) {
|
||||
var errors = await appsProvider.addAppsByURL(selectedUrls);
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
tr('importedX', args: [plural('app', selectedUrls.length)]),
|
||||
context);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return ImportErrorDialog(
|
||||
urlsLength: selectedUrls.length, errors: errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw ObtainiumError(tr('noResults'));
|
||||
}
|
||||
}
|
||||
}()
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
importInProgress = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
runMassSourceImport(MassAppUrlSource source) {
|
||||
() async {
|
||||
var values = await showDialog<Map<String, dynamic>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('importX', args: [source.name]),
|
||||
items: source.requiredArgs
|
||||
.map((e) => [GeneratedFormTextField(e, label: e)])
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
if (values != null) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
var urlsWithDescriptions = await source.getUrlsWithDescriptions(
|
||||
values.values.map((e) => e.toString()).toList());
|
||||
var selectedUrls =
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog<List<String>?>(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions: urlsWithDescriptions);
|
||||
});
|
||||
if (selectedUrls != null) {
|
||||
var errors = await appsProvider.addAppsByURL(selectedUrls);
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
tr('importedX', args: [plural('app', selectedUrls.length)]),
|
||||
context);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return ImportErrorDialog(
|
||||
urlsLength: selectedUrls.length, errors: errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
importInProgress = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: CustomScrollView(slivers: <Widget>[
|
||||
@@ -120,18 +308,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
onPressed: appsProvider.apps.isEmpty ||
|
||||
importInProgress
|
||||
? null
|
||||
: () {
|
||||
HapticFeedback.selectionClick();
|
||||
appsProvider
|
||||
.exportApps()
|
||||
.then((String path) {
|
||||
showError(
|
||||
tr('exportedTo', args: [path]),
|
||||
context);
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
});
|
||||
},
|
||||
: runObtainiumExport,
|
||||
child: Text(tr('obtainiumExport')))),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
@@ -141,59 +318,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
style: outlineButtonStyle,
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () {
|
||||
HapticFeedback.selectionClick();
|
||||
FilePicker.platform
|
||||
.pickFiles()
|
||||
.then((result) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
if (result != null) {
|
||||
String data = File(
|
||||
result.files.single.path!)
|
||||
.readAsStringSync();
|
||||
try {
|
||||
jsonDecode(data);
|
||||
} catch (e) {
|
||||
throw ObtainiumError(
|
||||
tr('invalidInput'));
|
||||
}
|
||||
appsProvider
|
||||
.importApps(data)
|
||||
.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(
|
||||
tr('importedX', args: [
|
||||
plural('apps', value)
|
||||
]),
|
||||
context);
|
||||
});
|
||||
} else {
|
||||
// User canceled the picker
|
||||
}
|
||||
}).catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
importInProgress = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
: runObtainiumImport,
|
||||
child: Text(tr('obtainiumImport'))))
|
||||
],
|
||||
),
|
||||
@@ -216,49 +341,15 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
height: 32,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () {
|
||||
urlListImport();
|
||||
},
|
||||
onPressed:
|
||||
importInProgress ? null : urlListImport,
|
||||
child: Text(
|
||||
tr('importFromURLList'),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () {
|
||||
FilePicker.platform
|
||||
.pickFiles()
|
||||
.then((result) {
|
||||
if (result != null) {
|
||||
urlListImport(
|
||||
overrideInitValid: true,
|
||||
initValue:
|
||||
RegExp('https?://[^"]+')
|
||||
.allMatches(File(result
|
||||
.files
|
||||
.single
|
||||
.path!)
|
||||
.readAsStringSync())
|
||||
.map((e) =>
|
||||
e.input.substring(
|
||||
e.start, e.end))
|
||||
.toSet()
|
||||
.toList()
|
||||
.where((url) {
|
||||
try {
|
||||
sourceProvider
|
||||
.getSource(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}).join('\n'));
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed:
|
||||
importInProgress ? null : runUrlImport,
|
||||
child: Text(
|
||||
tr('importFromURLsInFile'),
|
||||
)),
|
||||
@@ -275,106 +366,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () {
|
||||
() async {
|
||||
var values = await showDialog<
|
||||
Map<String,
|
||||
dynamic>?>(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('searchX',
|
||||
args: [
|
||||
source.name
|
||||
]),
|
||||
items: [
|
||||
[
|
||||
GeneratedFormTextField(
|
||||
'searchQuery',
|
||||
label: tr(
|
||||
'searchQuery'))
|
||||
]
|
||||
],
|
||||
);
|
||||
});
|
||||
if (values != null &&
|
||||
(values['searchQuery']
|
||||
as String?)
|
||||
?.isNotEmpty ==
|
||||
true) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
var urlsWithDescriptions =
|
||||
await source.search(
|
||||
values['searchQuery']
|
||||
as String);
|
||||
if (urlsWithDescriptions
|
||||
.isNotEmpty) {
|
||||
var selectedUrls =
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog<
|
||||
List<
|
||||
String>?>(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext
|
||||
ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions:
|
||||
urlsWithDescriptions,
|
||||
selectedByDefault:
|
||||
false,
|
||||
);
|
||||
});
|
||||
if (selectedUrls !=
|
||||
null &&
|
||||
selectedUrls
|
||||
.isNotEmpty) {
|
||||
var errors =
|
||||
await appsProvider
|
||||
.addAppsByURL(
|
||||
selectedUrls);
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
tr('importedX',
|
||||
args: [
|
||||
plural(
|
||||
'app',
|
||||
selectedUrls
|
||||
.length)
|
||||
]),
|
||||
context);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext
|
||||
ctx) {
|
||||
return ImportErrorDialog(
|
||||
urlsLength:
|
||||
selectedUrls
|
||||
.length,
|
||||
errors:
|
||||
errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw ObtainiumError(
|
||||
tr('noResults'));
|
||||
}
|
||||
}
|
||||
}()
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
importInProgress = false;
|
||||
});
|
||||
});
|
||||
runSourceSearch(source);
|
||||
},
|
||||
child: Text(
|
||||
tr('searchX', args: [source.name])))
|
||||
@@ -390,93 +382,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
||||
onPressed: importInProgress
|
||||
? null
|
||||
: () {
|
||||
() async {
|
||||
var values = await showDialog<
|
||||
Map<String,
|
||||
dynamic>?>(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext ctx) {
|
||||
return GeneratedFormModal(
|
||||
title: tr('importX',
|
||||
args: [
|
||||
source.name
|
||||
]),
|
||||
items:
|
||||
source
|
||||
.requiredArgs
|
||||
.map(
|
||||
(e) => [
|
||||
GeneratedFormTextField(e,
|
||||
label: e)
|
||||
])
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
if (values != null) {
|
||||
setState(() {
|
||||
importInProgress = true;
|
||||
});
|
||||
var urlsWithDescriptions =
|
||||
await source
|
||||
.getUrlsWithDescriptions(
|
||||
values.values
|
||||
.map((e) =>
|
||||
e.toString())
|
||||
.toList());
|
||||
var selectedUrls =
|
||||
// ignore: use_build_context_synchronously
|
||||
await showDialog<
|
||||
List<String>?>(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext
|
||||
ctx) {
|
||||
return UrlSelectionModal(
|
||||
urlsWithDescriptions:
|
||||
urlsWithDescriptions);
|
||||
});
|
||||
if (selectedUrls != null) {
|
||||
var errors =
|
||||
await appsProvider
|
||||
.addAppsByURL(
|
||||
selectedUrls);
|
||||
if (errors.isEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showError(
|
||||
tr('importedX',
|
||||
args: [
|
||||
plural(
|
||||
'app',
|
||||
selectedUrls
|
||||
.length)
|
||||
]),
|
||||
context);
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext
|
||||
ctx) {
|
||||
return ImportErrorDialog(
|
||||
urlsLength:
|
||||
selectedUrls
|
||||
.length,
|
||||
errors:
|
||||
errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
.catchError((e) {
|
||||
showError(e, context);
|
||||
}).whenComplete(() {
|
||||
setState(() {
|
||||
importInProgress = false;
|
||||
});
|
||||
});
|
||||
runMassSourceImport(source);
|
||||
},
|
||||
child: Text(
|
||||
tr('importX', args: [source.name])))
|
||||
|
Reference in New Issue
Block a user