mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-10-25 03:43:46 +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:
@@ -1,5 +1,7 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@@ -27,23 +29,24 @@ class Mullvad extends AppSource {
|
|||||||
String standardUrl,
|
String standardUrl,
|
||||||
Map<String, dynamic> additionalSettings,
|
Map<String, dynamic> additionalSettings,
|
||||||
) async {
|
) async {
|
||||||
Response res = await get(Uri.parse('$standardUrl/en/download/android'));
|
var details = await HTML().getLatestAPKDetails(
|
||||||
if (res.statusCode == 200) {
|
'$standardUrl/en/download/android', additionalSettings);
|
||||||
var version = parse(res.body)
|
var fileName = details.apkUrls[0].split('/').last;
|
||||||
.querySelector('p.subtitle.is-6')
|
var versionMatch = RegExp('[0-9]+(\\.[0-9]+)+').firstMatch(fileName);
|
||||||
?.querySelector('a')
|
if (versionMatch == null) {
|
||||||
?.attributes['href']
|
throw NoVersionError();
|
||||||
?.split('/')
|
|
||||||
.last;
|
|
||||||
if (version == null) {
|
|
||||||
throw NoVersionError();
|
|
||||||
}
|
|
||||||
return APKDetails(
|
|
||||||
version,
|
|
||||||
['https://mullvad.net/download/app/apk/latest'],
|
|
||||||
AppNames(name, 'Mullvad-VPN'));
|
|
||||||
} else {
|
|
||||||
throw getObtainiumHttpError(res);
|
|
||||||
}
|
}
|
||||||
|
details.version = fileName.substring(versionMatch.start, versionMatch.end);
|
||||||
|
details.names = AppNames(name, 'Mullvad-VPN');
|
||||||
|
try {
|
||||||
|
details.changeLog = (await GitHub().getLatestAPKDetails(
|
||||||
|
'https://github.com/mullvad/mullvadvpn-app',
|
||||||
|
{'fallbackToOlderReleases': true}))
|
||||||
|
.changeLog;
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class GeneratedFormTextField extends GeneratedFormItem {
|
|||||||
|
|
||||||
class GeneratedFormDropdown extends GeneratedFormItem {
|
class GeneratedFormDropdown extends GeneratedFormItem {
|
||||||
late List<MapEntry<String, String>>? opts;
|
late List<MapEntry<String, String>>? opts;
|
||||||
|
List<String>? disabledOptKeys;
|
||||||
|
|
||||||
GeneratedFormDropdown(
|
GeneratedFormDropdown(
|
||||||
String key,
|
String key,
|
||||||
@@ -55,6 +56,7 @@ class GeneratedFormDropdown extends GeneratedFormItem {
|
|||||||
String label = 'Input',
|
String label = 'Input',
|
||||||
List<Widget> belowWidgets = const [],
|
List<Widget> belowWidgets = const [],
|
||||||
String defaultValue = '',
|
String defaultValue = '',
|
||||||
|
this.disabledOptKeys,
|
||||||
List<String? Function(String? value)> additionalValidators = const [],
|
List<String? Function(String? value)> additionalValidators = const [],
|
||||||
}) : super(key,
|
}) : super(key,
|
||||||
label: label,
|
label: label,
|
||||||
@@ -225,10 +227,15 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
return DropdownButtonFormField(
|
return DropdownButtonFormField(
|
||||||
decoration: InputDecoration(labelText: formItem.label),
|
decoration: InputDecoration(labelText: formItem.label),
|
||||||
value: values[formItem.key],
|
value: values[formItem.key],
|
||||||
items: formItem.opts!
|
items: formItem.opts!.map((e2) {
|
||||||
.map((e2) =>
|
var enabled =
|
||||||
DropdownMenuItem(value: e2.key, child: Text(e2.value)))
|
formItem.disabledOptKeys?.contains(e2.key) != true;
|
||||||
.toList(),
|
return DropdownMenuItem(
|
||||||
|
value: e2.key,
|
||||||
|
enabled: enabled,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: enabled ? 1 : 0.5, child: Text(e2.value)));
|
||||||
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
values[formItem.key] = value ?? formItem.opts!.first.key;
|
values[formItem.key] = value ?? formItem.opts!.first.key;
|
||||||
|
|||||||
@@ -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.11.15';
|
const String currentVersion = '0.11.16';
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
bool additionalSettingsValid = true;
|
bool additionalSettingsValid = true;
|
||||||
List<String> pickedCategories = [];
|
List<String> pickedCategories = [];
|
||||||
int searchnum = 0;
|
int searchnum = 0;
|
||||||
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
|
|
||||||
bool doingSomething = gettingAppInfo || searching;
|
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 {
|
addApp({bool resetUserInputAfter = false}) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
gettingAppInfo = true;
|
gettingAppInfo = true;
|
||||||
});
|
});
|
||||||
var settingsProvider = context.read<SettingsProvider>();
|
try {
|
||||||
() async {
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
var userPickedTrackOnly = additionalSettings['trackOnly'] == true;
|
||||||
var cont = true;
|
App? app;
|
||||||
if ((userPickedTrackOnly || pickedSource!.enforceTrackOnly) &&
|
if ((await getTrackOnlyConfirmationIfNeeded(userPickedTrackOnly)) &&
|
||||||
// ignore: use_build_context_synchronously
|
(await getReleaseDateAsVersionConfirmationIfNeeded(
|
||||||
await showDialog(
|
userPickedTrackOnly))) {
|
||||||
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();
|
|
||||||
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
var trackOnly = pickedSource!.enforceTrackOnly || userPickedTrackOnly;
|
||||||
App app = await sourceProvider.getApp(
|
app = await sourceProvider.getApp(
|
||||||
pickedSource!, userInput, additionalSettings,
|
pickedSource!, userInput, additionalSettings,
|
||||||
trackOnlyOverride: trackOnly);
|
trackOnlyOverride: trackOnly);
|
||||||
if (!trackOnly) {
|
if (!trackOnly) {
|
||||||
@@ -150,27 +141,232 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
}
|
}
|
||||||
app.categories = pickedCategories;
|
app.categories = pickedCategories;
|
||||||
await appsProvider.saveApps([app], onlyIfExists: false);
|
await appsProvider.saveApps([app], onlyIfExists: false);
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
.then((app) {
|
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
Navigator.push(globalNavigatorKey.currentContext ?? context,
|
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);
|
showError(e, context);
|
||||||
}).whenComplete(() {
|
} finally {
|
||||||
setState(() {
|
setState(() {
|
||||||
gettingAppInfo = false;
|
gettingAppInfo = false;
|
||||||
if (resetUserInputAfter) {
|
if (resetUserInputAfter) {
|
||||||
changeUserInput('', false, true);
|
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(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@@ -181,230 +377,16 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
getUrlInputRow(),
|
||||||
children: [
|
if (shouldShowSearchBar())
|
||||||
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)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
if (sourceProvider.sources
|
if (shouldShowSearchBar()) getSearchBarRow(),
|
||||||
.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 (pickedSource != null)
|
if (pickedSource != null)
|
||||||
Column(
|
getAdditionalOptsCol()
|
||||||
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;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
Expanded(
|
getSourcesListWidget(),
|
||||||
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()
|
|
||||||
])),
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
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/main.dart';
|
import 'package:obtainium/main.dart';
|
||||||
@@ -34,406 +35,414 @@ class _AppPageState extends State<AppPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool areDownloadsRunning = appsProvider.areDownloadsRunning();
|
||||||
|
|
||||||
var sourceProvider = SourceProvider();
|
var sourceProvider = SourceProvider();
|
||||||
AppInMemory? app = appsProvider.apps[widget.appId];
|
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||||
var source = app != null ? sourceProvider.getSource(app.app.url) : null;
|
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;
|
prevApp = app;
|
||||||
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(
|
bool isVersionDetectionStandard =
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
app?.app.additionalSettings['versionDetection'] ==
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
'standardVersionDetection';
|
||||||
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(
|
getInfoColumn() => Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 125),
|
GestureDetector(
|
||||||
app?.installedInfo != null
|
onTap: () {
|
||||||
? Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
if (app?.app.url != null) {
|
||||||
Image.memory(
|
launchUrlString(app?.app.url ?? '',
|
||||||
app!.installedInfo!.icon!,
|
mode: LaunchMode.externalApplication);
|
||||||
height: 150,
|
}
|
||||||
gaplessPlayback: true,
|
},
|
||||||
)
|
child: Text(
|
||||||
])
|
app?.app.url ?? '',
|
||||||
: Container(),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(
|
style: const TextStyle(
|
||||||
height: 25,
|
decoration: TextDecoration.underline,
|
||||||
),
|
fontStyle: FontStyle.italic,
|
||||||
Text(
|
fontSize: 12),
|
||||||
app?.installedInfo?.name ?? app?.app.name ?? tr('app'),
|
)),
|
||||||
textAlign: TextAlign.center,
|
const SizedBox(
|
||||||
style: Theme.of(context).textTheme.displayLarge,
|
height: 32,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
tr('byX', args: [app?.app.author ?? tr('unknown')]),
|
tr('latestVersionX',
|
||||||
textAlign: TextAlign.center,
|
args: [app?.app.latestVersion ?? tr('unknown')]),
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
textAlign: TextAlign.center,
|
||||||
),
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
const SizedBox(
|
),
|
||||||
height: 8,
|
Text(
|
||||||
),
|
'${tr('installedVersionX', args: [
|
||||||
Text(
|
app?.installedInfo?.versionName ??
|
||||||
app?.app.id ?? '',
|
app?.app.installedVersion ??
|
||||||
textAlign: TextAlign.center,
|
tr('none')
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
])}${trackOnly ? ' ${tr('estimateInBrackets')}\n\n${tr('xIsTrackOnly', args: [
|
||||||
),
|
tr('app')
|
||||||
app?.app.releaseDate == null
|
])}' : ''}',
|
||||||
? const SizedBox.shrink()
|
textAlign: TextAlign.center,
|
||||||
: Text(
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
app!.app.releaseDate.toString(),
|
),
|
||||||
textAlign: TextAlign.center,
|
if (app?.app.installedVersion != null &&
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
!isVersionDetectionStandard)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tr('noVersionDetection'),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
infoColumn,
|
Text(
|
||||||
const SizedBox(height: 150)
|
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(
|
return Scaffold(
|
||||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
child: settingsProvider.showAppWebpage
|
child: settingsProvider.showAppWebpage
|
||||||
? app != null
|
? getAppWebView()
|
||||||
? WebViewWidget(
|
: CustomScrollView(
|
||||||
controller: WebViewController()
|
slivers: [
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
SliverToBoxAdapter(
|
||||||
..setBackgroundColor(
|
child: Column(children: [getFullInfoColumn()])),
|
||||||
Theme.of(context).colorScheme.background)
|
],
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
),
|
||||||
..setNavigationDelegate(
|
onRefresh: () async {
|
||||||
NavigationDelegate(
|
if (app != null) {
|
||||||
onWebResourceError: (WebResourceError error) {
|
getUpdate(app.app.id);
|
||||||
if (error.isForMainFrame == true) {
|
}
|
||||||
showError(
|
}),
|
||||||
ObtainiumError(error.description,
|
bottomSheet: getBottomSheetMenu());
|
||||||
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))
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
var appsProvider = context.read<AppsProvider>();
|
var appsProvider = context.read<AppsProvider>();
|
||||||
var settingsProvider = context.read<SettingsProvider>();
|
var settingsProvider = context.read<SettingsProvider>();
|
||||||
|
|
||||||
var outlineButtonStyle = ButtonStyle(
|
var outlineButtonStyle = ButtonStyle(
|
||||||
shape: MaterialStateProperty.all(
|
shape: MaterialStateProperty.all(
|
||||||
StadiumBorder(
|
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(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
@@ -120,18 +308,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
onPressed: appsProvider.apps.isEmpty ||
|
onPressed: appsProvider.apps.isEmpty ||
|
||||||
importInProgress
|
importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: runObtainiumExport,
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
appsProvider
|
|
||||||
.exportApps()
|
|
||||||
.then((String path) {
|
|
||||||
showError(
|
|
||||||
tr('exportedTo', args: [path]),
|
|
||||||
context);
|
|
||||||
}).catchError((e) {
|
|
||||||
showError(e, context);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(tr('obtainiumExport')))),
|
child: Text(tr('obtainiumExport')))),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
@@ -141,59 +318,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
style: outlineButtonStyle,
|
style: outlineButtonStyle,
|
||||||
onPressed: importInProgress
|
onPressed: importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: 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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(tr('obtainiumImport'))))
|
child: Text(tr('obtainiumImport'))))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -216,49 +341,15 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: importInProgress
|
onPressed:
|
||||||
? null
|
importInProgress ? null : urlListImport,
|
||||||
: () {
|
|
||||||
urlListImport();
|
|
||||||
},
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('importFromURLList'),
|
tr('importFromURLList'),
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: importInProgress
|
onPressed:
|
||||||
? null
|
importInProgress ? null : 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'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('importFromURLsInFile'),
|
tr('importFromURLsInFile'),
|
||||||
)),
|
)),
|
||||||
@@ -275,106 +366,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
onPressed: importInProgress
|
onPressed: importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
() async {
|
runSourceSearch(source);
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('searchX', args: [source.name])))
|
tr('searchX', args: [source.name])))
|
||||||
@@ -390,93 +382,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
onPressed: importInProgress
|
onPressed: importInProgress
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
() async {
|
runMassSourceImport(source);
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('importX', args: [source.name])))
|
tr('importX', args: [source.name])))
|
||||||
|
|||||||
@@ -73,6 +73,18 @@ List<String> generateStandardVersionRegExStrings() {
|
|||||||
List<String> standardVersionRegExStrings =
|
List<String> standardVersionRegExStrings =
|
||||||
generateStandardVersionRegExStrings();
|
generateStandardVersionRegExStrings();
|
||||||
|
|
||||||
|
Set<String> findStandardFormatsForVersion(String version, bool strict) {
|
||||||
|
// If !strict, even a substring match is valid
|
||||||
|
Set<String> results = {};
|
||||||
|
for (var pattern in standardVersionRegExStrings) {
|
||||||
|
if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}')
|
||||||
|
.hasMatch(version)) {
|
||||||
|
results.add(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
class AppsProvider with ChangeNotifier {
|
class AppsProvider with ChangeNotifier {
|
||||||
// In memory App state (should always be kept in sync with local storage versions)
|
// In memory App state (should always be kept in sync with local storage versions)
|
||||||
Map<String, AppInMemory> apps = {};
|
Map<String, AppInMemory> apps = {};
|
||||||
@@ -472,94 +484,113 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the App says it is installed but installedInfo is null, set it to not installed
|
bool isVersionDetectionPossible(AppInMemory? app) {
|
||||||
// If there is any other mismatch between installedInfo and installedVersion, try reconciling them intelligently
|
return app?.app.additionalSettings['trackOnly'] != true &&
|
||||||
// If that fails, just set it to the actual version string (all we can do at that point)
|
app?.installedInfo?.versionName != null &&
|
||||||
// Don't save changes, just return the object if changes were made (else null)
|
app?.app.installedVersion != null &&
|
||||||
|
reconcileVersionDifferences(
|
||||||
|
app!.installedInfo!.versionName!, app.app.installedVersion!) !=
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given an App and it's on-device info...
|
||||||
|
// Reconcile unexpected differences between its reported installed version, real installed version, and reported latest version
|
||||||
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
|
App? getCorrectedInstallStatusAppIfPossible(App app, AppInfo? installedInfo) {
|
||||||
var modded = false;
|
var modded = false;
|
||||||
var trackOnly = app.additionalSettings['trackOnly'] == true;
|
var trackOnly = app.additionalSettings['trackOnly'] == true;
|
||||||
var noVersionDetection = app.additionalSettings['versionDetection'] !=
|
var noVersionDetection = app.additionalSettings['versionDetection'] !=
|
||||||
'standardVersionDetection';
|
'standardVersionDetection';
|
||||||
|
// FIRST, COMPARE THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE ONE IS NULL
|
||||||
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
|
if (installedInfo == null && app.installedVersion != null && !trackOnly) {
|
||||||
|
// App says it's installed but isn't really (and isn't track only) - set to not installed
|
||||||
app.installedVersion = null;
|
app.installedVersion = null;
|
||||||
modded = true;
|
modded = true;
|
||||||
} else if (installedInfo?.versionName != null &&
|
} else if (installedInfo?.versionName != null &&
|
||||||
app.installedVersion == null) {
|
app.installedVersion == null) {
|
||||||
|
// App says it's not installed but really is - set to installed and use real package versionName
|
||||||
app.installedVersion = installedInfo!.versionName;
|
app.installedVersion = installedInfo!.versionName;
|
||||||
modded = true;
|
modded = true;
|
||||||
} else if (installedInfo?.versionName != null &&
|
}
|
||||||
|
// SECOND, RECONCILE DIFFERENCES BETWEEN THE APP'S REPORTED AND REAL INSTALLED VERSIONS, WHERE NEITHER IS NULL
|
||||||
|
if (installedInfo?.versionName != null &&
|
||||||
installedInfo!.versionName != app.installedVersion &&
|
installedInfo!.versionName != app.installedVersion &&
|
||||||
!noVersionDetection) {
|
!noVersionDetection) {
|
||||||
String? correctedInstalledVersion = reconcileRealAndInternalVersions(
|
// App's reported version and real version don't match (and it uses standard version detection)
|
||||||
|
// If they share a standard format (and are still different under it), update the reported version accordingly
|
||||||
|
var correctedInstalledVersion = reconcileVersionDifferences(
|
||||||
installedInfo.versionName!, app.installedVersion!);
|
installedInfo.versionName!, app.installedVersion!);
|
||||||
if (correctedInstalledVersion != null) {
|
if (correctedInstalledVersion?.key == false) {
|
||||||
app.installedVersion = correctedInstalledVersion;
|
app.installedVersion = correctedInstalledVersion!.value;
|
||||||
modded = true;
|
modded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// THIRD, RECONCILE THE APP'S REPORTED INSTALLED AND LATEST VERSIONS
|
||||||
if (app.installedVersion != null &&
|
if (app.installedVersion != null &&
|
||||||
app.installedVersion != app.latestVersion &&
|
app.installedVersion != app.latestVersion &&
|
||||||
!noVersionDetection) {
|
!noVersionDetection) {
|
||||||
app.installedVersion = reconcileRealAndInternalVersions(
|
// App's reported installed and latest versions don't match (and it uses standard version detection)
|
||||||
app.installedVersion!, app.latestVersion,
|
// If they share a standard format, make sure the App's reported installed version uses that format
|
||||||
matchMode: true) ??
|
var correctedInstalledVersion =
|
||||||
app.installedVersion;
|
reconcileVersionDifferences(app.installedVersion!, app.latestVersion);
|
||||||
|
if (correctedInstalledVersion?.key == true) {
|
||||||
|
app.installedVersion = correctedInstalledVersion!.value;
|
||||||
|
modded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FOURTH, DISABLE VERSION DETECTION IF ENABLED AND THE REPORTED/REAL INSTALLED VERSIONS ARE NOT STANDARDIZED
|
||||||
|
if (installedInfo != null &&
|
||||||
|
!isVersionDetectionPossible(AppInMemory(app, null, installedInfo))) {
|
||||||
|
app.additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||||
|
logs.add('Could not reconcile version formats for: ${app.id}');
|
||||||
modded = true;
|
modded = true;
|
||||||
}
|
}
|
||||||
|
// if (app.installedVersion != null &&
|
||||||
|
// app.additionalSettings['versionDetection'] ==
|
||||||
|
// 'standardVersionDetection') {
|
||||||
|
// var correctedInstalledVersion =
|
||||||
|
// reconcileVersionDifferences(app.installedVersion!, app.latestVersion);
|
||||||
|
// if (correctedInstalledVersion == null) {
|
||||||
|
// app.additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||||
|
// logs.add('Could not reconcile version formats for: ${app.id}');
|
||||||
|
// modded = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
return modded ? app : null;
|
return modded ? app : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? reconcileRealAndInternalVersions(
|
MapEntry<bool, String>? reconcileVersionDifferences(
|
||||||
String realVersion, String internalVersion,
|
String templateVersion, String comparisonVersion) {
|
||||||
{bool matchMode = false}) {
|
// Returns null if the versions don't share a common standard format
|
||||||
// 1. If one or both of these can't be converted to a "standard" format, return null (leave as is)
|
// Returns <true, comparisonVersion> if they share a common format and are equal
|
||||||
// 2. If both have a "standard" format under which they are equal, return null (leave as is)
|
// Returns <false, templateVersion> if they share a common format but are not equal
|
||||||
// 3. If both have a "standard" format in common but are unequal, return realVersion (this means it was changed externally)
|
// templateVersion must fully match a standard format, while comparisonVersion can have a substring match
|
||||||
// If in matchMode, the outcomes of rules 2 and 3 are reversed, and the "real" version is not matched strictly
|
var templateVersionFormats =
|
||||||
// Matchmode to be used when comparing internal install version and internal latest version
|
findStandardFormatsForVersion(templateVersion, true);
|
||||||
|
var comparisonVersionFormats =
|
||||||
bool doStringsMatchUnderRegEx(
|
findStandardFormatsForVersion(comparisonVersion, false);
|
||||||
String pattern, String value1, String value2) {
|
|
||||||
var r = RegExp(pattern);
|
|
||||||
var m1 = r.firstMatch(value1);
|
|
||||||
var m2 = r.firstMatch(value2);
|
|
||||||
return m1 != null && m2 != null
|
|
||||||
? value1.substring(m1.start, m1.end) ==
|
|
||||||
value2.substring(m2.start, m2.end)
|
|
||||||
: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> findStandardFormatsForVersion(String version, bool strict) {
|
|
||||||
Set<String> results = {};
|
|
||||||
for (var pattern in standardVersionRegExStrings) {
|
|
||||||
if (RegExp('${strict ? '^' : ''}$pattern${strict ? '\$' : ''}')
|
|
||||||
.hasMatch(version)) {
|
|
||||||
results.add(pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
var realStandardVersionFormats =
|
|
||||||
findStandardFormatsForVersion(realVersion, true);
|
|
||||||
var internalStandardVersionFormats =
|
|
||||||
findStandardFormatsForVersion(internalVersion, false);
|
|
||||||
var commonStandardFormats =
|
var commonStandardFormats =
|
||||||
realStandardVersionFormats.intersection(internalStandardVersionFormats);
|
templateVersionFormats.intersection(comparisonVersionFormats);
|
||||||
if (commonStandardFormats.isEmpty) {
|
if (commonStandardFormats.isEmpty) {
|
||||||
return null; // Incompatible; no "enhanced detection"
|
return null;
|
||||||
}
|
}
|
||||||
for (String pattern in commonStandardFormats) {
|
for (String pattern in commonStandardFormats) {
|
||||||
if (doStringsMatchUnderRegEx(pattern, internalVersion, realVersion)) {
|
if (doStringsMatchUnderRegEx(
|
||||||
return matchMode
|
pattern, comparisonVersion, templateVersion)) {
|
||||||
? internalVersion
|
return MapEntry(true, comparisonVersion);
|
||||||
: null; // Enhanced detection says no change
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matchMode
|
return MapEntry(false, templateVersion);
|
||||||
? null
|
}
|
||||||
: realVersion; // Enhanced detection says something changed
|
|
||||||
|
bool doStringsMatchUnderRegEx(String pattern, String value1, String value2) {
|
||||||
|
var r = RegExp(pattern);
|
||||||
|
var m1 = r.firstMatch(value1);
|
||||||
|
var m2 = r.firstMatch(value2);
|
||||||
|
return m1 != null && m2 != null
|
||||||
|
? value1.substring(m1.start, m1.end) ==
|
||||||
|
value2.substring(m2.start, m2.end)
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadApps() async {
|
Future<void> loadApps() async {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import 'package:obtainium/app_sources/sourceforge.dart';
|
|||||||
import 'package:obtainium/app_sources/steammobile.dart';
|
import 'package:obtainium/app_sources/steammobile.dart';
|
||||||
import 'package:obtainium/app_sources/telegramapp.dart';
|
import 'package:obtainium/app_sources/telegramapp.dart';
|
||||||
import 'package:obtainium/app_sources/vlc.dart';
|
import 'package:obtainium/app_sources/vlc.dart';
|
||||||
import 'package:obtainium/app_sources/whatsapp.dart';
|
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||||
@@ -111,16 +110,16 @@ class App {
|
|||||||
// Convert bool style version detection options to dropdown style
|
// Convert bool style version detection options to dropdown style
|
||||||
if (additionalSettings['noVersionDetection'] == true) {
|
if (additionalSettings['noVersionDetection'] == true) {
|
||||||
additionalSettings['versionDetection'] = 'noVersionDetection';
|
additionalSettings['versionDetection'] = 'noVersionDetection';
|
||||||
}
|
if (additionalSettings['releaseDateAsVersion'] == true) {
|
||||||
if (additionalSettings['releaseDateAsVersion'] == true) {
|
additionalSettings['versionDetection'] = 'releaseDateAsVersion';
|
||||||
additionalSettings['versionDetection'] = 'releaseDateAsVersion';
|
additionalSettings.remove('releaseDateAsVersion');
|
||||||
additionalSettings.remove('releaseDateAsVersion');
|
}
|
||||||
}
|
if (additionalSettings['noVersionDetection'] != null) {
|
||||||
if (additionalSettings['noVersionDetection'] != null) {
|
additionalSettings.remove('noVersionDetection');
|
||||||
additionalSettings.remove('noVersionDetection');
|
}
|
||||||
}
|
if (additionalSettings['releaseDateAsVersion'] != null) {
|
||||||
if (additionalSettings['releaseDateAsVersion'] != null) {
|
additionalSettings.remove('releaseDateAsVersion');
|
||||||
additionalSettings.remove('releaseDateAsVersion');
|
}
|
||||||
}
|
}
|
||||||
// Ensure additionalSettings are correctly typed
|
// Ensure additionalSettings are correctly typed
|
||||||
for (var item in formItems) {
|
for (var item in formItems) {
|
||||||
|
|||||||
28
pubspec.lock
28
pubspec.lock
@@ -409,10 +409,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9"
|
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.13"
|
version: "2.0.14"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -425,10 +425,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "12eee51abdf4d34c590f043f45073adbb45514a108bd9db4491547a2fd891059"
|
sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -473,10 +473,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_apple
|
name: permission_handler_apple
|
||||||
sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163"
|
sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.7"
|
version: "9.0.8"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -553,10 +553,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41
|
sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.18"
|
version: "2.0.20"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -585,10 +585,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
|
sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.2.0"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -710,10 +710,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1"
|
sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.25"
|
version: "6.0.26"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -806,10 +806,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: ab12479f7a0cf112b9420c36aaf206a1ca47cd60cd42de74a4be2e97a697587b
|
sha256: d601aba11ad8d4481e17a34a76fa1d30dee92dcbbe2c58b0df3120e9453099c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.3"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -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.11.15+136 # When changing this, update the tag in main() accordingly
|
version: 0.11.16+138 # 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