mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-14 21:56:44 +02:00
Compare commits
12 Commits
v0.3.2-bet
...
v0.4.0-bet
Author | SHA1 | Date | |
---|---|---|---|
136628c9e6 | |||
a916167be3 | |||
420cf487d4 | |||
12855370b0 | |||
33fed1cb2f | |||
33238b56a9 | |||
428c208de4 | |||
9a4b0301be | |||
f58d26524c | |||
45e5544c5b | |||
0a9373e65a | |||
b65c6e1d41 |
@ -30,6 +30,16 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
5
android/app/src/main/res/xml/file_paths.xml
Normal file
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<external-path path="Android/data/dev.imranr.obtainium/" name="files_root" />
|
||||||
|
<external-path path="." name="external_storage_root" />
|
||||||
|
</paths>
|
@ -21,9 +21,9 @@ class GitHub implements AppSource {
|
|||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
var includePrereleases =
|
var includePrereleases =
|
||||||
additionalData.isNotEmpty && additionalData[0] == "true";
|
additionalData.isNotEmpty && additionalData[0] == 'true';
|
||||||
var fallbackToOlderReleases =
|
var fallbackToOlderReleases =
|
||||||
additionalData.length >= 2 && additionalData[1] == "true";
|
additionalData.length >= 2 && additionalData[1] == 'true';
|
||||||
var regexFilter = additionalData.length >= 3 && additionalData[2].isNotEmpty
|
var regexFilter = additionalData.length >= 3 && additionalData[2].isNotEmpty
|
||||||
? additionalData[2]
|
? additionalData[2]
|
||||||
: null;
|
: null;
|
||||||
@ -92,14 +92,14 @@ class GitHub implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<List<GeneratedFormItem>> additionalDataFormItems = [
|
List<List<GeneratedFormItem>> additionalDataFormItems = [
|
||||||
[GeneratedFormItem(label: "Include prereleases", type: FormItemType.bool)],
|
[GeneratedFormItem(label: 'Include prereleases', type: FormItemType.bool)],
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormItem(
|
||||||
label: "Fallback to older releases", type: FormItemType.bool)
|
label: 'Fallback to older releases', type: FormItemType.bool)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormItem(
|
||||||
label: "Filter Release Titles by Regular Expression",
|
label: 'Filter Release Titles by Regular Expression',
|
||||||
type: FormItemType.string,
|
type: FormItemType.string,
|
||||||
required: false,
|
required: false,
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
@ -110,7 +110,7 @@ class GitHub implements AppSource {
|
|||||||
try {
|
try {
|
||||||
RegExp(value);
|
RegExp(value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return "Invalid regular expression";
|
return 'Invalid regular expression';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -119,5 +119,5 @@ class GitHub implements AppSource {
|
|||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = ["true", "true", ""];
|
List<String> additionalDataDefaults = ['true', 'true', ''];
|
||||||
}
|
}
|
||||||
|
68
lib/app_sources/sourceforge.dart
Normal file
68
lib/app_sources/sourceforge.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
|
class SourceForge implements AppSource {
|
||||||
|
@override
|
||||||
|
late String host = 'sourceforge.net';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String standardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://$host/projects/[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw notValidURL(runtimeType.toString());
|
||||||
|
}
|
||||||
|
return url.substring(0, match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl, List<String> additionalData) async {
|
||||||
|
Response res = await get(Uri.parse('$standardUrl/rss?path=/'));
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var parsedHtml = parse(res.body);
|
||||||
|
var allDownloadLinks =
|
||||||
|
parsedHtml.querySelectorAll('guid').map((e) => e.innerHtml).toList();
|
||||||
|
getVersion(String url) {
|
||||||
|
try {
|
||||||
|
var tokens = url.split('/');
|
||||||
|
return tokens[tokens.length - 3];
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? version = getVersion(allDownloadLinks[0]);
|
||||||
|
if (version == null) {
|
||||||
|
throw couldNotFindLatestVersion;
|
||||||
|
}
|
||||||
|
var apkUrlListAllReleases = allDownloadLinks
|
||||||
|
.where((element) => element.toLowerCase().endsWith('.apk/download'))
|
||||||
|
.toList();
|
||||||
|
var apkUrlList =
|
||||||
|
apkUrlListAllReleases // This can be used skipped for fallback support later
|
||||||
|
.where((element) => getVersion(element) == version)
|
||||||
|
.toList();
|
||||||
|
if (apkUrlList.isEmpty) {
|
||||||
|
throw noAPKFound;
|
||||||
|
}
|
||||||
|
return APKDetails(version, apkUrlList);
|
||||||
|
} else {
|
||||||
|
throw couldNotFindReleases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AppNames getAppNames(String standardUrl) {
|
||||||
|
return AppNames(runtimeType.toString(),
|
||||||
|
standardUrl.substring(standardUrl.lastIndexOf('/') + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<List<GeneratedFormItem>> additionalDataFormItems = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> additionalDataDefaults = [];
|
||||||
|
}
|
@ -12,7 +12,7 @@ class GeneratedFormItem {
|
|||||||
late List<String? Function(String? value)> additionalValidators;
|
late List<String? Function(String? value)> additionalValidators;
|
||||||
|
|
||||||
GeneratedFormItem(
|
GeneratedFormItem(
|
||||||
{this.label = "Input",
|
{this.label = 'Input',
|
||||||
this.type = FormItemType.string,
|
this.type = FormItemType.string,
|
||||||
this.required = true,
|
this.required = true,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
@ -69,7 +69,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
.map((row) => row.map((e) {
|
.map((row) => row.map((e) {
|
||||||
return j < widget.defaultValues.length
|
return j < widget.defaultValues.length
|
||||||
? widget.defaultValues[j++]
|
? widget.defaultValues[j++]
|
||||||
: "";
|
: '';
|
||||||
}).toList())
|
}).toList())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
helperText: e.value.label + (e.value.required ? " *" : "")),
|
helperText: e.value.label + (e.value.required ? ' *' : '')),
|
||||||
minLines: e.value.max <= 1 ? null : e.value.max,
|
minLines: e.value.max <= 1 ? null : e.value.max,
|
||||||
maxLines: e.value.max <= 1 ? 1 : e.value.max,
|
maxLines: e.value.max <= 1 ? 1 : e.value.max,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@ -122,10 +122,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
children: [
|
children: [
|
||||||
Text(widget.items[r][e].label),
|
Text(widget.items[r][e].label),
|
||||||
Switch(
|
Switch(
|
||||||
value: values[r][e] == "true",
|
value: values[r][e] == 'true',
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
values[r][e] = value ? "true" : "";
|
values[r][e] = value ? 'true' : '';
|
||||||
someValueChanged();
|
someValueChanged();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -7,11 +7,15 @@ class GeneratedFormModal extends StatefulWidget {
|
|||||||
{super.key,
|
{super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.defaultValues});
|
required this.defaultValues,
|
||||||
|
this.initValid = false,
|
||||||
|
this.message = ''});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
final String message;
|
||||||
final List<List<GeneratedFormItem>> items;
|
final List<List<GeneratedFormItem>> items;
|
||||||
final List<String> defaultValues;
|
final List<String> defaultValues;
|
||||||
|
final bool initValid;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
State<GeneratedFormModal> createState() => _GeneratedFormModalState();
|
||||||
@ -21,12 +25,25 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
List<String> values = [];
|
List<String> values = [];
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
valid = widget.initValid;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
content: GeneratedForm(
|
content:
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||||
|
if (widget.message.isNotEmpty) Text(widget.message),
|
||||||
|
if (widget.message.isNotEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
GeneratedForm(
|
||||||
items: widget.items,
|
items: widget.items,
|
||||||
onValueChanges: (values, valid) {
|
onValueChanges: (values, valid) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -34,7 +51,8 @@ class _GeneratedFormModalState extends State<GeneratedFormModal> {
|
|||||||
this.valid = valid;
|
this.valid = valid;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
defaultValues: widget.defaultValues),
|
defaultValues: widget.defaultValues)
|
||||||
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -13,7 +13,7 @@ import 'package:dynamic_color/dynamic_color.dart';
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v0.3.2-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v0.4.0-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void bgTaskCallback() {
|
void bgTaskCallback() {
|
||||||
@ -26,9 +26,23 @@ void bgTaskCallback() {
|
|||||||
await notificationsProvider
|
await notificationsProvider
|
||||||
.cancel(ErrorCheckingUpdatesNotification('').id);
|
.cancel(ErrorCheckingUpdatesNotification('').id);
|
||||||
await appsProvider.loadApps();
|
await appsProvider.loadApps();
|
||||||
List<App> updates = await appsProvider.checkUpdates();
|
// List<String> existingUpdateIds = // TODO: Uncomment this and below when it works
|
||||||
if (updates.isNotEmpty) {
|
// appsProvider.getExistingUpdates(installedOnly: true);
|
||||||
notificationsProvider.notify(UpdateNotification(updates),
|
List<App> newUpdates = await appsProvider.checkUpdates();
|
||||||
|
// List<String> silentlyUpdated = await appsProvider
|
||||||
|
// .downloadAndInstallLatestApp(
|
||||||
|
// [...newUpdates.map((e) => e.id), ...existingUpdateIds], null);
|
||||||
|
// if (silentlyUpdated.isNotEmpty) {
|
||||||
|
// newUpdates
|
||||||
|
// .where((element) => !silentlyUpdated.contains(element.id))
|
||||||
|
// .toList();
|
||||||
|
// notificationsProvider.notify(
|
||||||
|
// SilentUpdateNotification(
|
||||||
|
// silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()),
|
||||||
|
// cancelExisting: true);
|
||||||
|
// }
|
||||||
|
if (newUpdates.isNotEmpty) {
|
||||||
|
notificationsProvider.notify(UpdateNotification(newUpdates),
|
||||||
cancelExisting: true);
|
cancelExisting: true);
|
||||||
}
|
}
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
@ -95,7 +109,8 @@ class MyApp extends StatelessWidget {
|
|||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||||
Permission.notification.request();
|
Permission.notification.request();
|
||||||
appsProvider.saveApp(App(
|
appsProvider.saveApps([
|
||||||
|
App(
|
||||||
'imranr98_obtainium_${GitHub().host}',
|
'imranr98_obtainium_${GitHub().host}',
|
||||||
'https://github.com/ImranR98/Obtainium',
|
'https://github.com/ImranR98/Obtainium',
|
||||||
'ImranR98',
|
'ImranR98',
|
||||||
@ -104,7 +119,8 @@ class MyApp extends StatelessWidget {
|
|||||||
currentReleaseTag,
|
currentReleaseTag,
|
||||||
[],
|
[],
|
||||||
0,
|
0,
|
||||||
["true"]));
|
['true'])
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,9 +19,10 @@ class AddAppPage extends StatefulWidget {
|
|||||||
class _AddAppPageState extends State<AddAppPage> {
|
class _AddAppPageState extends State<AddAppPage> {
|
||||||
bool gettingAppInfo = false;
|
bool gettingAppInfo = false;
|
||||||
|
|
||||||
String userInput = "";
|
String userInput = '';
|
||||||
AppSource? pickedSource;
|
AppSource? pickedSource;
|
||||||
List<String> additionalData = [];
|
List<String> additionalData = [];
|
||||||
|
String customName = '';
|
||||||
bool validAdditionalData = true;
|
bool validAdditionalData = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -44,19 +45,19 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
items: [
|
items: [
|
||||||
[
|
[
|
||||||
GeneratedFormItem(
|
GeneratedFormItem(
|
||||||
label: "App Source Url",
|
label: 'App Source Url',
|
||||||
additionalValidators: [
|
additionalValidators: [
|
||||||
(value) {
|
(value) {
|
||||||
try {
|
try {
|
||||||
sourceProvider
|
sourceProvider
|
||||||
.getSource(value ?? "")
|
.getSource(value ?? '')
|
||||||
.standardizeURL(
|
.standardizeURL(
|
||||||
makeUrlHttps(
|
makeUrlHttps(
|
||||||
value ?? ""));
|
value ?? ''));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e is String
|
return e is String
|
||||||
? e
|
? e
|
||||||
: "Error";
|
: 'Error';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -79,6 +80,9 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
.doesSourceHaveRequiredAdditionalData(
|
.doesSourceHaveRequiredAdditionalData(
|
||||||
source)
|
source)
|
||||||
: true;
|
: true;
|
||||||
|
if (source == null) {
|
||||||
|
customName = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -100,7 +104,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
});
|
});
|
||||||
sourceProvider
|
sourceProvider
|
||||||
.getApp(pickedSource!, userInput,
|
.getApp(pickedSource!, userInput,
|
||||||
additionalData)
|
additionalData,
|
||||||
|
customName: customName)
|
||||||
.then((app) {
|
.then((app) {
|
||||||
var appsProvider =
|
var appsProvider =
|
||||||
context.read<AppsProvider>();
|
context.read<AppsProvider>();
|
||||||
@ -113,7 +118,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
settingsProvider
|
settingsProvider
|
||||||
.getInstallPermission()
|
.getInstallPermission()
|
||||||
.then((_) {
|
.then((_) {
|
||||||
appsProvider.saveApp(app).then((_) {
|
appsProvider
|
||||||
|
.saveApps([app]).then((_) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@ -136,8 +142,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
child: const Text('Add'))
|
child: const Text('Add'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (pickedSource != null &&
|
if (pickedSource != null)
|
||||||
(pickedSource!.additionalDataFormItems.isNotEmpty))
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -152,6 +157,8 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
|
if (pickedSource!
|
||||||
|
.additionalDataFormItems.isNotEmpty)
|
||||||
GeneratedForm(
|
GeneratedForm(
|
||||||
items: pickedSource!.additionalDataFormItems,
|
items: pickedSource!.additionalDataFormItems,
|
||||||
onValueChanges: (values, valid) {
|
onValueChanges: (values, valid) {
|
||||||
@ -161,7 +168,27 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
defaultValues:
|
defaultValues:
|
||||||
pickedSource!.additionalDataDefaults)
|
pickedSource!.additionalDataDefaults),
|
||||||
|
if (pickedSource!
|
||||||
|
.additionalDataFormItems.isNotEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
if (pickedSource != null)
|
||||||
|
GeneratedForm(
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Custom App Name',
|
||||||
|
required: false)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onValueChanges: (values, valid) {
|
||||||
|
setState(() {
|
||||||
|
customName = values[0];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
defaultValues: [customName])
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
@ -25,8 +25,12 @@ class _AppPageState extends State<AppPage> {
|
|||||||
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 (app?.app.installedVersion != null) {
|
if (!appsProvider.areDownloadsRunning() && app != null) {
|
||||||
appsProvider.getUpdate(app!.app.id);
|
appsProvider.getUpdate(app.app.id).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||||
@ -96,7 +100,9 @@ class _AppPageState extends State<AppPage> {
|
|||||||
children: [
|
children: [
|
||||||
if (app?.app.installedVersion != app?.app.latestVersion)
|
if (app?.app.installedVersion != app?.app.latestVersion)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: app?.downloadProgress != null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -106,20 +112,25 @@ class _AppPageState extends State<AppPage> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context)
|
||||||
|
.pop();
|
||||||
},
|
},
|
||||||
child: const Text('No')),
|
child: const Text('No')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback
|
||||||
|
.selectionClick();
|
||||||
var updatedApp = app?.app;
|
var updatedApp = app?.app;
|
||||||
if (updatedApp != null) {
|
if (updatedApp != null) {
|
||||||
updatedApp.installedVersion =
|
updatedApp
|
||||||
updatedApp.latestVersion;
|
.installedVersion =
|
||||||
appsProvider
|
updatedApp
|
||||||
.saveApp(updatedApp);
|
.latestVersion;
|
||||||
|
appsProvider.saveApps(
|
||||||
|
[updatedApp]);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context)
|
||||||
|
.pop();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Yes, Mark as Installed'))
|
'Yes, Mark as Installed'))
|
||||||
@ -131,29 +142,36 @@ class _AppPageState extends State<AppPage> {
|
|||||||
icon: const Icon(Icons.done))
|
icon: const Icon(Icons.done))
|
||||||
else
|
else
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: app?.downloadProgress != null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('App Not Installed?'),
|
title: const Text(
|
||||||
|
'App Not Installed?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context)
|
||||||
|
.pop();
|
||||||
},
|
},
|
||||||
child: const Text('No')),
|
child: const Text('No')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback
|
||||||
|
.selectionClick();
|
||||||
var updatedApp = app?.app;
|
var updatedApp = app?.app;
|
||||||
if (updatedApp != null) {
|
if (updatedApp != null) {
|
||||||
updatedApp.installedVersion =
|
updatedApp
|
||||||
|
.installedVersion =
|
||||||
null;
|
null;
|
||||||
appsProvider
|
appsProvider.saveApps(
|
||||||
.saveApp(updatedApp);
|
[updatedApp]);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context)
|
||||||
|
.pop();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Yes, Mark as Not Installed'))
|
'Yes, Mark as Not Installed'))
|
||||||
@ -166,34 +184,29 @@ class _AppPageState extends State<AppPage> {
|
|||||||
if (source != null &&
|
if (source != null &&
|
||||||
source.additionalDataFormItems.isNotEmpty)
|
source.additionalDataFormItems.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: app?.downloadProgress != null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: 'Additional Options',
|
title: 'Additional Options',
|
||||||
items: source.additionalDataFormItems,
|
items: source
|
||||||
|
.additionalDataFormItems,
|
||||||
defaultValues: app != null
|
defaultValues: app != null
|
||||||
? app.app.additionalData
|
? app.app.additionalData
|
||||||
: source.additionalDataDefaults);
|
: source
|
||||||
|
.additionalDataDefaults);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (app != null && values != null) {
|
if (app != null && values != null) {
|
||||||
var changedApp = app.app;
|
var changedApp = app.app;
|
||||||
changedApp.additionalData = values;
|
changedApp.additionalData = values;
|
||||||
sourceProvider
|
appsProvider.saveApps([changedApp]);
|
||||||
.getApp(source, changedApp.url,
|
|
||||||
changedApp.additionalData)
|
|
||||||
.then((finalChangedApp) {
|
|
||||||
appsProvider.saveApp(finalChangedApp);
|
|
||||||
}).catchError((e) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(content: Text(e.toString())),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
tooltip: 'Additional Options',
|
||||||
icon: const Icon(Icons.settings)),
|
icon: const Icon(Icons.settings)),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -209,7 +222,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
.downloadAndInstallLatestApp(
|
.downloadAndInstallLatestApp(
|
||||||
[app!.app.id],
|
[app!.app.id],
|
||||||
context).then((res) {
|
context).then((res) {
|
||||||
if (res && mounted) {
|
if (res.isNotEmpty && mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -235,9 +248,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
HapticFeedback
|
HapticFeedback
|
||||||
.selectionClick();
|
.selectionClick();
|
||||||
appsProvider
|
appsProvider.removeApps(
|
||||||
.removeApp(app!.app.id)
|
[app!.app.id]).then((_) {
|
||||||
.then((_) {
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.popUntil((_) =>
|
.popUntil((_) =>
|
||||||
|
@ -7,28 +7,67 @@ import 'package:obtainium/pages/app.dart';
|
|||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
class AppsPage extends StatefulWidget {
|
class AppsPage extends StatefulWidget {
|
||||||
const AppsPage({super.key});
|
const AppsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppsPage> createState() => _AppsPageState();
|
State<AppsPage> createState() => AppsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppsPageState extends State<AppsPage> {
|
class AppsPageState extends State<AppsPage> {
|
||||||
AppsFilter? filter;
|
AppsFilter? filter;
|
||||||
|
Set<String> selectedIds = {};
|
||||||
|
|
||||||
|
clearSelected() {
|
||||||
|
if (selectedIds.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
selectedIds.clear();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectThese(List<String> appIds) {
|
||||||
|
if (selectedIds.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
for (var a in appIds) {
|
||||||
|
selectedIds.add(a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
var settingsProvider = context.watch<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
var existingUpdateAppIds = appsProvider.getExistingUpdates();
|
|
||||||
var sortedApps = appsProvider.apps.values.toList();
|
var sortedApps = appsProvider.apps.values.toList();
|
||||||
|
|
||||||
|
selectedIds = selectedIds
|
||||||
|
.where((element) => sortedApps.map((e) => e.app.id).contains(element))
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
toggleAppSelected(String appId) {
|
||||||
|
setState(() {
|
||||||
|
if (selectedIds.contains(appId)) {
|
||||||
|
selectedIds.remove(appId);
|
||||||
|
} else {
|
||||||
|
selectedIds.add(appId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (filter != null) {
|
if (filter != null) {
|
||||||
sortedApps = sortedApps.where((app) {
|
sortedApps = sortedApps.where((app) {
|
||||||
if (app.app.installedVersion == app.app.latestVersion &&
|
if (app.app.installedVersion == app.app.latestVersion &&
|
||||||
filter!.onlyNonLatest) {
|
!(filter!.includeUptodate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (app.app.installedVersion == null &&
|
||||||
|
!(filter!.includeNonInstalled)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (filter!.nameFilter.isEmpty && filter!.authorFilter.isEmpty) {
|
if (filter!.nameFilter.isEmpty && filter!.authorFilter.isEmpty) {
|
||||||
@ -75,76 +114,6 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
floatingActionButton:
|
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
|
||||||
existingUpdateAppIds.isEmpty || filter != null
|
|
||||||
? const SizedBox()
|
|
||||||
: ElevatedButton.icon(
|
|
||||||
onPressed: appsProvider.areDownloadsRunning()
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
settingsProvider.getInstallPermission().then((_) {
|
|
||||||
appsProvider.downloadAndInstallLatestApp(
|
|
||||||
existingUpdateAppIds, context);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.install_mobile_outlined),
|
|
||||||
label: const Text('Install All')),
|
|
||||||
const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
),
|
|
||||||
appsProvider.apps.isEmpty
|
|
||||||
? const SizedBox()
|
|
||||||
: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
showDialog<List<String>?>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return GeneratedFormModal(
|
|
||||||
title: 'Filter Apps',
|
|
||||||
items: [
|
|
||||||
[
|
|
||||||
GeneratedFormItem(
|
|
||||||
label: "App Name", required: false),
|
|
||||||
GeneratedFormItem(
|
|
||||||
label: "Author", required: false)
|
|
||||||
],
|
|
||||||
[
|
|
||||||
GeneratedFormItem(
|
|
||||||
label: "Ignore Up-to-Date Apps",
|
|
||||||
type: FormItemType.bool)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
defaultValues: filter == null
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
filter!.nameFilter,
|
|
||||||
filter!.authorFilter,
|
|
||||||
filter!.onlyNonLatest ? 'true' : ''
|
|
||||||
]);
|
|
||||||
}).then((values) {
|
|
||||||
if (values != null &&
|
|
||||||
values
|
|
||||||
.where((element) => element.isNotEmpty)
|
|
||||||
.isNotEmpty) {
|
|
||||||
setState(() {
|
|
||||||
filter = AppsFilter(
|
|
||||||
nameFilter: values[0],
|
|
||||||
authorFilter: values[1],
|
|
||||||
onlyNonLatest: values[2] == "true");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
filter = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
label: Text(filter == null ? 'Search' : 'Modify Search'),
|
|
||||||
icon: Icon(
|
|
||||||
filter == null ? Icons.search : Icons.manage_search)),
|
|
||||||
]),
|
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
@ -165,13 +134,18 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
appsProvider.apps.isEmpty
|
appsProvider.apps.isEmpty
|
||||||
? 'No Apps'
|
? 'No Apps'
|
||||||
: 'No Search Results',
|
: 'No Search Results',
|
||||||
style:
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
Theme.of(context).textTheme.headlineMedium,
|
|
||||||
))),
|
))),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
selectedTileColor:
|
||||||
|
Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||||
|
selected: selectedIds.contains(sortedApps[index].app.id),
|
||||||
|
onLongPress: () {
|
||||||
|
toggleAppSelected(sortedApps[index].app.id);
|
||||||
|
},
|
||||||
title: Text(sortedApps[index].app.name),
|
title: Text(sortedApps[index].app.name),
|
||||||
subtitle: Text('By ${sortedApps[index].app.author}'),
|
subtitle: Text('By ${sortedApps[index].app.author}'),
|
||||||
trailing: sortedApps[index].downloadProgress != null
|
trailing: sortedApps[index].downloadProgress != null
|
||||||
@ -184,26 +158,262 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
: Text(sortedApps[index].app.installedVersion ??
|
: Text(sortedApps[index].app.installedVersion ??
|
||||||
'Not Installed')),
|
'Not Installed')),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (selectedIds.isNotEmpty) {
|
||||||
|
toggleAppSelected(sortedApps[index].app.id);
|
||||||
|
} else {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
AppPage(appId: sortedApps[index].app.id)),
|
AppPage(appId: sortedApps[index].app.id)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, childCount: sortedApps.length))
|
}, childCount: sortedApps.length))
|
||||||
])));
|
])),
|
||||||
|
persistentFooterButtons: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
selectedIds.isEmpty
|
||||||
|
? selectThese(sortedApps.map((e) => e.app.id).toList())
|
||||||
|
: clearSelected();
|
||||||
|
},
|
||||||
|
icon: Icon(selectedIds.isEmpty
|
||||||
|
? Icons.select_all_outlined
|
||||||
|
: Icons.deselect_outlined),
|
||||||
|
label: Text(selectedIds.isEmpty
|
||||||
|
? 'Select All'
|
||||||
|
: 'Deselect ${selectedIds.length.toString()}')),
|
||||||
|
const VerticalDivider(),
|
||||||
|
Expanded(
|
||||||
|
child: selectedIds.isEmpty
|
||||||
|
? Container()
|
||||||
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
showDialog<List<String>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: 'Remove Selected Apps?',
|
||||||
|
items: const [],
|
||||||
|
defaultValues: const [],
|
||||||
|
initValid: true,
|
||||||
|
message:
|
||||||
|
'${selectedIds.length} App${selectedIds.length == 1 ? '' : 's'} will be removed from Obtainium but remain installed. You still need to uninstall ${selectedIds.length == 1 ? 'it' : 'them'} manually.',
|
||||||
|
);
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
appsProvider.removeApps(selectedIds.toList());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Remove Selected Apps',
|
||||||
|
icon: const Icon(Icons.delete_outline_outlined),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: appsProvider.areDownloadsRunning() ||
|
||||||
|
selectedIds
|
||||||
|
.where((id) =>
|
||||||
|
appsProvider.apps[id]!.app
|
||||||
|
.installedVersion !=
|
||||||
|
appsProvider
|
||||||
|
.apps[id]!.app.latestVersion)
|
||||||
|
.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
var existingUpdateIdsSelected =
|
||||||
|
appsProvider
|
||||||
|
.getExistingUpdates(
|
||||||
|
installedOnly: true)
|
||||||
|
.where((element) =>
|
||||||
|
selectedIds.contains(element))
|
||||||
|
.toList();
|
||||||
|
var newInstallIdsSelected = appsProvider
|
||||||
|
.getExistingUpdates(
|
||||||
|
nonInstalledOnly: true)
|
||||||
|
.where((element) =>
|
||||||
|
selectedIds.contains(element))
|
||||||
|
.toList();
|
||||||
|
List<List<GeneratedFormItem>> formInputs =
|
||||||
|
[];
|
||||||
|
if (existingUpdateIdsSelected
|
||||||
|
.isNotEmpty &&
|
||||||
|
newInstallIdsSelected.isNotEmpty) {
|
||||||
|
formInputs.add([
|
||||||
|
GeneratedFormItem(
|
||||||
|
label:
|
||||||
|
'Update ${existingUpdateIdsSelected.length} Apps?',
|
||||||
|
type: FormItemType.bool)
|
||||||
|
]);
|
||||||
|
formInputs.add([
|
||||||
|
GeneratedFormItem(
|
||||||
|
label:
|
||||||
|
'Install ${newInstallIdsSelected.length} new Apps?',
|
||||||
|
type: FormItemType.bool)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
showDialog<List<String>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: 'Install Selected Apps?',
|
||||||
|
message:
|
||||||
|
'${existingUpdateIdsSelected.length} update${existingUpdateIdsSelected.length == 1 ? '' : 's'} and ${newInstallIdsSelected.length} new install${newInstallIdsSelected.length == 1 ? '' : 's'}.',
|
||||||
|
items: formInputs,
|
||||||
|
defaultValues: const [
|
||||||
|
'true',
|
||||||
|
'true'
|
||||||
|
],
|
||||||
|
initValid: true,
|
||||||
|
);
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
bool shouldInstallUpdates =
|
||||||
|
values.length < 2 ||
|
||||||
|
values[0] == 'true';
|
||||||
|
bool shouldInstallNew =
|
||||||
|
values.length < 2 ||
|
||||||
|
values[1] == 'true';
|
||||||
|
settingsProvider
|
||||||
|
.getInstallPermission()
|
||||||
|
.then((_) {
|
||||||
|
List<String> toInstall = [];
|
||||||
|
if (shouldInstallUpdates) {
|
||||||
|
toInstall.addAll(
|
||||||
|
existingUpdateIdsSelected);
|
||||||
|
}
|
||||||
|
if (shouldInstallNew) {
|
||||||
|
toInstall.addAll(
|
||||||
|
newInstallIdsSelected);
|
||||||
|
}
|
||||||
|
appsProvider
|
||||||
|
.downloadAndInstallLatestApp(
|
||||||
|
toInstall, context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Install/Update Selected Apps',
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.file_download_outlined,
|
||||||
|
)),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
String urls = '';
|
||||||
|
for (var id in selectedIds) {
|
||||||
|
urls += '${appsProvider.apps[id]!.app.url}\n';
|
||||||
|
}
|
||||||
|
urls = urls.substring(0, urls.length - 1);
|
||||||
|
Share.share(urls,
|
||||||
|
subject: 'Selected App URLs from Obtainium');
|
||||||
|
},
|
||||||
|
tooltip: 'Share Selected App URLs',
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
const VerticalDivider(),
|
||||||
|
appsProvider.apps.isEmpty
|
||||||
|
? const SizedBox()
|
||||||
|
: TextButton.icon(
|
||||||
|
label: Text(
|
||||||
|
filter == null ? 'Filter' : 'Filter *',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: filter == null
|
||||||
|
? FontWeight.normal
|
||||||
|
: FontWeight.bold),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog<List<String>?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: 'Filter Apps',
|
||||||
|
items: [
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'App Name', required: false),
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Author', required: false)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Up to Date Apps',
|
||||||
|
type: FormItemType.bool)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'Non-Installed Apps',
|
||||||
|
type: FormItemType.bool)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
defaultValues: filter == null
|
||||||
|
? AppsFilter().toValuesArray()
|
||||||
|
: filter!.toValuesArray());
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
setState(() {
|
||||||
|
filter = AppsFilter.fromValuesArray(values);
|
||||||
|
if (AppsFilter().isIdenticalTo(filter!)) {
|
||||||
|
filter = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
filter = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.filter_list_rounded))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppsFilter {
|
class AppsFilter {
|
||||||
late String nameFilter;
|
late String nameFilter;
|
||||||
late String authorFilter;
|
late String authorFilter;
|
||||||
late bool onlyNonLatest;
|
late bool includeUptodate;
|
||||||
|
late bool includeNonInstalled;
|
||||||
|
|
||||||
AppsFilter(
|
AppsFilter(
|
||||||
{this.nameFilter = "",
|
{this.nameFilter = '',
|
||||||
this.authorFilter = "",
|
this.authorFilter = '',
|
||||||
this.onlyNonLatest = false});
|
this.includeUptodate = true,
|
||||||
|
this.includeNonInstalled = true});
|
||||||
|
|
||||||
|
List<String> toValuesArray() {
|
||||||
|
return [
|
||||||
|
nameFilter,
|
||||||
|
authorFilter,
|
||||||
|
includeUptodate ? 'true' : '',
|
||||||
|
includeNonInstalled ? 'true' : ''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
AppsFilter.fromValuesArray(List<String> values) {
|
||||||
|
nameFilter = values[0];
|
||||||
|
authorFilter = values[1];
|
||||||
|
includeUptodate = values[2] == 'true';
|
||||||
|
includeNonInstalled = values[3] == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIdenticalTo(AppsFilter other) =>
|
||||||
|
authorFilter.trim() == other.authorFilter.trim() &&
|
||||||
|
nameFilter.trim() == other.nameFilter.trim() &&
|
||||||
|
includeUptodate == other.includeUptodate &&
|
||||||
|
includeNonInstalled == other.includeNonInstalled;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,8 @@ class _HomePageState extends State<HomePage> {
|
|||||||
List<int> selectedIndexHistory = [];
|
List<int> selectedIndexHistory = [];
|
||||||
|
|
||||||
List<NavigationPageItem> pages = [
|
List<NavigationPageItem> pages = [
|
||||||
NavigationPageItem('Apps', Icons.apps, const AppsPage()),
|
NavigationPageItem(
|
||||||
|
'Apps', Icons.apps, AppsPage(key: GlobalKey<AppsPageState>())),
|
||||||
NavigationPageItem('Add App', Icons.add, const AddAppPage()),
|
NavigationPageItem('Add App', Icons.add, const AddAppPage()),
|
||||||
NavigationPageItem(
|
NavigationPageItem(
|
||||||
'Import/Export', Icons.import_export, const ImportExportPage()),
|
'Import/Export', Icons.import_export, const ImportExportPage()),
|
||||||
@ -88,7 +89,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return !(pages[0].widget.key as GlobalKey<AppsPageState>)
|
||||||
|
.currentState
|
||||||
|
?.clearSelected();
|
||||||
|
// return !appsPageKey.currentState?.clearSelected();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
if (appsProvider.apps.containsKey(app.id)) {
|
if (appsProvider.apps.containsKey(app.id)) {
|
||||||
errorsMap.addAll({app.id: 'App already added'});
|
errorsMap.addAll({app.id: 'App already added'});
|
||||||
} else {
|
} else {
|
||||||
await appsProvider.saveApp(app);
|
await appsProvider.saveApps([app]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<List<String>> errors =
|
List<List<String>> errors =
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
|
||||||
|
|
||||||
class TestPage extends StatefulWidget {
|
|
||||||
const TestPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TestPage> createState() => _TestPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TestPageState extends State<TestPage> {
|
|
||||||
List<String?>? sourceSpecificData;
|
|
||||||
bool valid = false;
|
|
||||||
|
|
||||||
List<List<GeneratedFormItem>> sourceSpecificInputs = [
|
|
||||||
[GeneratedFormItem(label: 'Test Item 1')],
|
|
||||||
[
|
|
||||||
GeneratedFormItem(label: 'Test Item 2', required: false),
|
|
||||||
GeneratedFormItem(label: 'Test Item 3')
|
|
||||||
],
|
|
||||||
[GeneratedFormItem(label: 'Test Item 4', type: FormItemType.bool)]
|
|
||||||
];
|
|
||||||
|
|
||||||
List<String> defaultInputValues = ["ABC"];
|
|
||||||
|
|
||||||
void onSourceSpecificDataChanges(
|
|
||||||
List<String?> valuesFromForm, bool formValid) {
|
|
||||||
setState(() {
|
|
||||||
sourceSpecificData = valuesFromForm;
|
|
||||||
valid = formValid;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Test Page')),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Column(children: [
|
|
||||||
GeneratedForm(
|
|
||||||
items: sourceSpecificInputs,
|
|
||||||
onValueChanges: onSourceSpecificDataChanges,
|
|
||||||
defaultValues: defaultInputValues,
|
|
||||||
),
|
|
||||||
...(sourceSpecificData != null
|
|
||||||
? (sourceSpecificData as List<String?>)
|
|
||||||
.map((e) => Text(e ?? ""))
|
|
||||||
: [Container()])
|
|
||||||
])));
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
@ -13,7 +14,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
import 'package:flutter_install_app/flutter_install_app.dart';
|
||||||
|
|
||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
@ -96,30 +97,65 @@ class AppsProvider with ChangeNotifier {
|
|||||||
.where((element) => element.downloadProgress != null)
|
.where((element) => element.downloadProgress != null)
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
// Given an AppId, uses stored info about the app to download an APK (with user input if needed) and install it
|
Future<bool> canInstallSilently(App app) async {
|
||||||
// Installs can only be done in the foreground, so a notification is sent to get the user's attention if needed
|
// TODO: This is unreliable - try to get from OS in the future
|
||||||
// Returns upon successful download, regardless of installation result
|
var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
Future<bool> downloadAndInstallLatestApp(
|
return app.installedVersion != null &&
|
||||||
List<String> appIds, BuildContext context) async {
|
osInfo.version.sdkInt! >= 30 &&
|
||||||
|
osInfo.version.release!.compareTo('12') >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> askUserToReturnToForeground(BuildContext context,
|
||||||
|
{bool waitForFG = false}) async {
|
||||||
NotificationsProvider notificationsProvider =
|
NotificationsProvider notificationsProvider =
|
||||||
context.read<NotificationsProvider>();
|
context.read<NotificationsProvider>();
|
||||||
|
if (!isForeground) {
|
||||||
|
await notificationsProvider.notify(completeInstallationNotification,
|
||||||
|
cancelExisting: true);
|
||||||
|
if (waitForFG) {
|
||||||
|
await FGBGEvents.stream.first == FGBGType.foreground;
|
||||||
|
await notificationsProvider.cancel(completeInstallationNotification.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
||||||
|
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
||||||
|
// If appropriate criteria are met, the update (never a fresh install) happens silently in the background
|
||||||
|
// But even then, we don't know if it actually succeeded
|
||||||
|
Future<void> installApk(ApkFile file) async {
|
||||||
|
await AppInstaller.installApk(file.file.path, actionRequired: false);
|
||||||
|
apps[file.appId]!.app.installedVersion =
|
||||||
|
apps[file.appId]!.app.latestVersion;
|
||||||
|
await saveApps([apps[file.appId]!.app]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
||||||
|
// If the APKs can be installed silently, they are
|
||||||
|
// If no BuildContext is provided, apps that require user interaction are ignored
|
||||||
|
// If user input is needed and the App is in the background, a notification is sent to get the user's attention
|
||||||
|
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
|
||||||
|
Future<List<String>> downloadAndInstallLatestApp(
|
||||||
|
List<String> appIds, BuildContext? context) async {
|
||||||
Map<String, String> appsToInstall = {};
|
Map<String, String> appsToInstall = {};
|
||||||
for (var id in appIds) {
|
for (var id in appIds) {
|
||||||
if (apps[id] == null) {
|
if (apps[id] == null) {
|
||||||
throw 'App not found';
|
throw 'App not found';
|
||||||
}
|
}
|
||||||
// If the App has more than one APK, the user should pick one
|
|
||||||
|
// If the App has more than one APK, the user should pick one (if context provided)
|
||||||
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
|
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
|
||||||
if (apps[id]!.app.apkUrls.length > 1) {
|
if (apps[id]!.app.apkUrls.length > 1 && context != null) {
|
||||||
apkUrl = await showDialog(
|
apkUrl = await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return APKPicker(app: apps[id]!.app, initVal: apkUrl);
|
return APKPicker(app: apps[id]!.app, initVal: apkUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If the picked APK comes from an origin different from the source, get user confirmation
|
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||||
if (apkUrl != null &&
|
if (apkUrl != null &&
|
||||||
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin) {
|
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin &&
|
||||||
|
context != null) {
|
||||||
if (await showDialog(
|
if (await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
@ -134,35 +170,45 @@ class AppsProvider with ChangeNotifier {
|
|||||||
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
int urlInd = apps[id]!.app.apkUrls.indexOf(apkUrl);
|
||||||
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
if (urlInd != apps[id]!.app.preferredApkIndex) {
|
||||||
apps[id]!.app.preferredApkIndex = urlInd;
|
apps[id]!.app.preferredApkIndex = urlInd;
|
||||||
await saveApp(apps[id]!.app);
|
await saveApps([apps[id]!.app]);
|
||||||
}
|
}
|
||||||
|
if (context != null ||
|
||||||
|
(await canInstallSilently(apps[id]!.app) &&
|
||||||
|
apps[id]!.app.apkUrls.length == 1)) {
|
||||||
appsToInstall.putIfAbsent(id, () => apkUrl!);
|
appsToInstall.putIfAbsent(id, () => apkUrl!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
|
List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
|
||||||
.map((entry) => downloadApp(entry.value, entry.key)));
|
.map((entry) => downloadApp(entry.value, entry.key)));
|
||||||
|
|
||||||
if (!isForeground) {
|
List<ApkFile> silentUpdates = [];
|
||||||
await notificationsProvider.notify(completeInstallationNotification,
|
List<ApkFile> regularInstalls = [];
|
||||||
cancelExisting: true);
|
|
||||||
await FGBGEvents.stream.first == FGBGType.foreground;
|
|
||||||
await notificationsProvider.cancel(completeInstallationNotification.id);
|
|
||||||
// We need to wait for the App to come to the foreground to install it
|
|
||||||
// Can't try to call install plugin in a background isolate (may not have worked anyways) because of:
|
|
||||||
// https://github.com/flutter/flutter/issues/13937
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
|
||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
|
||||||
// This also does not use the 'session-based' installer API, so background/silent updates are impossible
|
|
||||||
for (var f in downloadedFiles) {
|
for (var f in downloadedFiles) {
|
||||||
await InstallPlugin.installApk(f.file.path, 'dev.imranr.obtainium');
|
bool willBeSilent = await canInstallSilently(apps[f.appId]!.app);
|
||||||
apps[f.appId]!.app.installedVersion = apps[f.appId]!.app.latestVersion;
|
if (willBeSilent) {
|
||||||
await saveApp(apps[f.appId]!.app);
|
silentUpdates.add(f);
|
||||||
|
} else {
|
||||||
|
regularInstalls.add(f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return downloadedFiles.isNotEmpty;
|
for (var u in silentUpdates) {
|
||||||
|
await installApk(u);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context != null) {
|
||||||
|
if (regularInstalls.isNotEmpty) {
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
await askUserToReturnToForeground(context);
|
||||||
|
}
|
||||||
|
for (var i in regularInstalls) {
|
||||||
|
await installApk(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadedFiles.map((e) => e.appId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> getAppsDir() async {
|
Future<Directory> getAppsDir() async {
|
||||||
@ -200,15 +246,19 @@ class AppsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveApp(App app) async {
|
Future<void> saveApps(List<App> apps) async {
|
||||||
|
for (var app in apps) {
|
||||||
File('${(await getAppsDir()).path}/${app.id}.json')
|
File('${(await getAppsDir()).path}/${app.id}.json')
|
||||||
.writeAsStringSync(jsonEncode(app.toJson()));
|
.writeAsStringSync(jsonEncode(app.toJson()));
|
||||||
apps.update(app.id, (value) => AppInMemory(app, value.downloadProgress),
|
this.apps.update(
|
||||||
|
app.id, (value) => AppInMemory(app, value.downloadProgress),
|
||||||
ifAbsent: () => AppInMemory(app, null));
|
ifAbsent: () => AppInMemory(app, null));
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeApp(String appId) async {
|
Future<void> removeApps(List<String> appIds) async {
|
||||||
|
for (var appId in appIds) {
|
||||||
File file = File('${(await getAppsDir()).path}/$appId.json');
|
File file = File('${(await getAppsDir()).path}/$appId.json');
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
file.deleteSync();
|
file.deleteSync();
|
||||||
@ -216,8 +266,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (apps.containsKey(appId)) {
|
if (apps.containsKey(appId)) {
|
||||||
apps.remove(appId);
|
apps.remove(appId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (appIds.isNotEmpty) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool checkAppObjectForUpdate(App app) {
|
bool checkAppObjectForUpdate(App app) {
|
||||||
if (!apps.containsKey(app.id)) {
|
if (!apps.containsKey(app.id)) {
|
||||||
@ -238,7 +291,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||||
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
||||||
}
|
}
|
||||||
await saveApp(newApp);
|
await saveApps([newApp]);
|
||||||
return newApp;
|
return newApp;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -261,15 +314,22 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> getExistingUpdates() {
|
List<String> getExistingUpdates(
|
||||||
|
{bool installedOnly = false, bool nonInstalledOnly = false}) {
|
||||||
List<String> updateAppIds = [];
|
List<String> updateAppIds = [];
|
||||||
List<String> appIds = apps.keys.toList();
|
List<String> appIds = apps.keys.toList();
|
||||||
for (int i = 0; i < appIds.length; i++) {
|
for (int i = 0; i < appIds.length; i++) {
|
||||||
App? app = apps[appIds[i]]!.app;
|
App? app = apps[appIds[i]]!.app;
|
||||||
if (app.installedVersion != app.latestVersion) {
|
if (app.installedVersion != app.latestVersion &&
|
||||||
|
(!installedOnly || !nonInstalledOnly)) {
|
||||||
|
if ((app.installedVersion == null &&
|
||||||
|
(nonInstalledOnly || !installedOnly) ||
|
||||||
|
(app.installedVersion != null &&
|
||||||
|
(installedOnly || !nonInstalledOnly)))) {
|
||||||
updateAppIds.add(app.id);
|
updateAppIds.add(app.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return updateAppIds;
|
return updateAppIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +355,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
for (App a in importedApps) {
|
for (App a in importedApps) {
|
||||||
a.installedVersion =
|
a.installedVersion =
|
||||||
apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null;
|
apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null;
|
||||||
await saveApp(a);
|
await saveApps([a]);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return importedApps.length;
|
return importedApps.length;
|
||||||
|
@ -33,6 +33,22 @@ class UpdateNotification extends ObtainiumNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SilentUpdateNotification extends ObtainiumNotification {
|
||||||
|
SilentUpdateNotification(List<App> updates)
|
||||||
|
: super(
|
||||||
|
3,
|
||||||
|
'Apps Updated',
|
||||||
|
'',
|
||||||
|
'APPS_UPDATED',
|
||||||
|
'Apps Updated',
|
||||||
|
'Notifies the user that updates to one or more Apps were applied in the background',
|
||||||
|
Importance.defaultImportance) {
|
||||||
|
message = updates.length == 1
|
||||||
|
? '${updates[0].name} was updated to ${updates[0].latestVersion}.'
|
||||||
|
: '${(updates.length == 2 ? '${updates[0].name} and ${updates[1].name}' : '${updates[0].name} and ${updates.length - 1} more apps')} were updated.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
||||||
ErrorCheckingUpdatesNotification(String error)
|
ErrorCheckingUpdatesNotification(String error)
|
||||||
: super(
|
: super(
|
||||||
|
@ -10,6 +10,7 @@ import 'package:obtainium/app_sources/gitlab.dart';
|
|||||||
import 'package:obtainium/app_sources/izzyondroid.dart';
|
import 'package:obtainium/app_sources/izzyondroid.dart';
|
||||||
import 'package:obtainium/app_sources/mullvad.dart';
|
import 'package:obtainium/app_sources/mullvad.dart';
|
||||||
import 'package:obtainium/app_sources/signal.dart';
|
import 'package:obtainium/app_sources/signal.dart';
|
||||||
|
import 'package:obtainium/app_sources/sourceforge.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
import 'package:obtainium/mass_app_sources/githubstars.dart';
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ class App {
|
|||||||
|
|
||||||
escapeRegEx(String s) {
|
escapeRegEx(String s) {
|
||||||
return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
||||||
return "\\${x[0]}";
|
return '\\${x[0]}';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +145,8 @@ class SourceProvider {
|
|||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
IzzyOnDroid(),
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal()
|
Signal(),
|
||||||
|
SourceForge()
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add more mass source classes here so they are available via the service
|
// Add more mass source classes here so they are available via the service
|
||||||
@ -176,8 +178,8 @@ class SourceProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<App> getApp(
|
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
||||||
AppSource source, String url, List<String> additionalData) async {
|
{String customName = ''}) async {
|
||||||
String standardUrl = source.standardizeURL(makeUrlHttps(url));
|
String standardUrl = source.standardizeURL(makeUrlHttps(url));
|
||||||
AppNames names = source.getAppNames(standardUrl);
|
AppNames names = source.getAppNames(standardUrl);
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
@ -186,7 +188,9 @@ class SourceProvider {
|
|||||||
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}',
|
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}',
|
||||||
standardUrl,
|
standardUrl,
|
||||||
names.author[0].toUpperCase() + names.author.substring(1),
|
names.author[0].toUpperCase() + names.author.substring(1),
|
||||||
names.name[0].toUpperCase() + names.name.substring(1),
|
customName.trim().isNotEmpty
|
||||||
|
? customName
|
||||||
|
: names.name[0].toUpperCase() + names.name.substring(1),
|
||||||
null,
|
null,
|
||||||
apk.version,
|
apk.version,
|
||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
|
83
pubspec.lock
83
pubspec.lock
@ -7,7 +7,7 @@ packages:
|
|||||||
name: animations
|
name: animations
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.5"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -175,7 +175,7 @@ packages:
|
|||||||
name: file_picker
|
name: file_picker
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.2.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -188,6 +188,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
|
flutter_install_app:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_install_app
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -208,7 +215,7 @@ packages:
|
|||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "11.0.1"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -275,13 +282,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
install_plugin_v2:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: install_plugin_v2
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -295,7 +295,7 @@ packages:
|
|||||||
name: json_annotation
|
name: json_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.6.0"
|
version: "4.7.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -324,6 +324,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -393,7 +400,7 @@ packages:
|
|||||||
name: permission_handler
|
name: permission_handler
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "10.0.1"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -414,7 +421,7 @@ packages:
|
|||||||
name: permission_handler_platform_interface
|
name: permission_handler_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.7.0"
|
version: "3.7.1"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -457,6 +464,48 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.3"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.4.0"
|
||||||
|
share_plus_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
share_plus_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
share_plus_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
|
share_plus_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -538,7 +587,7 @@ packages:
|
|||||||
name: stream_channel
|
name: stream_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -566,7 +615,7 @@ packages:
|
|||||||
name: timezone
|
name: timezone
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.9.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -636,7 +685,7 @@ packages:
|
|||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.4"
|
||||||
webview_flutter:
|
webview_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -650,7 +699,7 @@ packages:
|
|||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.1"
|
version: "2.10.2"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
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.3.2+18 # When changing this, update the tag in main() accordingly
|
version: 0.4.0+19 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.19.0-79.0.dev <3.0.0'
|
sdk: '>=2.19.0-79.0.dev <3.0.0'
|
||||||
@ -38,13 +38,12 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.5
|
cupertino_icons: ^1.0.5
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
flutter_fgbg: ^0.2.0 # Try removing reliance on this
|
flutter_fgbg: ^0.2.0 # Try removing reliance on this
|
||||||
flutter_local_notifications: ^10.0.0
|
flutter_local_notifications: ^11.0.1
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
webview_flutter: ^3.0.4
|
webview_flutter: ^3.0.4
|
||||||
workmanager: ^0.5.0
|
workmanager: ^0.5.0
|
||||||
dynamic_color: ^1.5.4
|
dynamic_color: ^1.5.4
|
||||||
install_plugin_v2: ^1.0.0 # Try replacing this
|
|
||||||
html: ^0.15.0
|
html: ^0.15.0
|
||||||
shared_preferences: ^2.0.15
|
shared_preferences: ^2.0.15
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
@ -53,6 +52,8 @@ dependencies:
|
|||||||
device_info_plus: ^4.1.2
|
device_info_plus: ^4.1.2
|
||||||
file_picker: ^5.1.0
|
file_picker: ^5.1.0
|
||||||
animations: ^2.0.4
|
animations: ^2.0.4
|
||||||
|
flutter_install_app: ^1.3.0
|
||||||
|
share_plus: ^4.4.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user