Compare commits
14 Commits
v0.5.8-bet
...
v0.6.5-bet
Author | SHA1 | Date | |
---|---|---|---|
76e98feeb7 | |||
03da23f77a | |||
9b99e2b302 | |||
e746ca890a | |||
9c00a7da14 | |||
4df0dd64ad | |||
7cf7ffe0de | |||
b1953435af | |||
fc7d7d11d6 | |||
9ef26b3a4a | |||
27ee6b9e88 | |||
d1a3529036 | |||
a954a627fd | |||
52ce5b19c4 |
@ -13,7 +13,6 @@ Currently supported App sources:
|
|||||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||||
- [Mullvad](https://mullvad.net/en/)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- [Signal](https://signal.org/)
|
- [Signal](https://signal.org/)
|
||||||
- [APKMirror](https://apkmirror.com/)
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
|
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
|
||||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 228 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 170 KiB |
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 188 KiB |
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
@ -34,23 +34,39 @@ class FDroid implements AppSource {
|
|||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
Response res = await get(Uri.parse(standardUrl));
|
Response res = await get(Uri.parse(standardUrl));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var latestReleaseDiv =
|
var releases = parse(res.body).querySelectorAll('.package-version');
|
||||||
parse(res.body).querySelector('#latest.package-version');
|
if (releases.isEmpty) {
|
||||||
var apkUrl = latestReleaseDiv
|
throw couldNotFindReleases;
|
||||||
?.querySelector('.package-version-download a')
|
|
||||||
?.attributes['href'];
|
|
||||||
if (apkUrl == null) {
|
|
||||||
throw noAPKFound;
|
|
||||||
}
|
}
|
||||||
var version = latestReleaseDiv
|
String? latestVersion = releases[0]
|
||||||
?.querySelector('.package-version-header b')
|
.querySelector('.package-version-header b')
|
||||||
?.innerHtml
|
?.innerHtml
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.last;
|
.sublist(1)
|
||||||
if (version == null) {
|
.join(' ');
|
||||||
|
if (latestVersion == null) {
|
||||||
throw couldNotFindLatestVersion;
|
throw couldNotFindLatestVersion;
|
||||||
}
|
}
|
||||||
return APKDetails(version, [apkUrl]);
|
List<String> apkUrls = releases
|
||||||
|
.where((element) =>
|
||||||
|
element
|
||||||
|
.querySelector('.package-version-header b')
|
||||||
|
?.innerHtml
|
||||||
|
.split(' ')
|
||||||
|
.sublist(1)
|
||||||
|
.join(' ') ==
|
||||||
|
latestVersion)
|
||||||
|
.map((e) =>
|
||||||
|
e
|
||||||
|
.querySelector('.package-version-download a')
|
||||||
|
?.attributes['href'] ??
|
||||||
|
'')
|
||||||
|
.where((element) => element.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
if (apkUrls.isEmpty) {
|
||||||
|
throw noAPKFound;
|
||||||
|
}
|
||||||
|
return APKDetails(latestVersion, apkUrls);
|
||||||
} else {
|
} else {
|
||||||
throw couldNotFindReleases;
|
throw couldNotFindReleases;
|
||||||
}
|
}
|
||||||
|
@ -69,9 +69,10 @@ class GitHub implements AppSource {
|
|||||||
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
if (!includePrereleases && releases[i]['prerelease'] == true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (regexFilter != null &&
|
if (regexFilter != null &&
|
||||||
!RegExp(regexFilter)
|
!RegExp(regexFilter)
|
||||||
.hasMatch((releases[i]['name'] as String).trim())) {
|
.hasMatch((releases[i]['tag_name'] as String).trim())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/pages/home.dart';
|
import 'package:obtainium/pages/home.dart';
|
||||||
import 'package:obtainium/providers/apps_provider.dart';
|
import 'package:obtainium/providers/apps_provider.dart';
|
||||||
@ -13,12 +14,14 @@ import 'package:workmanager/workmanager.dart';
|
|||||||
import 'package:dynamic_color/dynamic_color.dart';
|
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 currentVersion = '0.6.5';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v0.5.8-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
const String bgUpdateCheckTaskName = 'bg-update-check';
|
const String bgUpdateCheckTaskName = 'bg-update-check';
|
||||||
|
|
||||||
bgUpdateCheck(int? ignoreAfterMicroseconds) async {
|
bgUpdateCheck(int? ignoreAfterMicroseconds) async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
||||||
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
||||||
: null;
|
: null;
|
||||||
@ -27,22 +30,28 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async {
|
|||||||
try {
|
try {
|
||||||
var appsProvider = AppsProvider();
|
var appsProvider = AppsProvider();
|
||||||
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
|
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
|
||||||
await appsProvider.loadApps();
|
await appsProvider.loadApps(shouldCorrectInstallStatus: false);
|
||||||
List<String> existingUpdateIds =
|
List<String> existingUpdateIds =
|
||||||
appsProvider.getExistingUpdates(installedOnly: true);
|
appsProvider.getExistingUpdates(installedOnly: true);
|
||||||
DateTime nextIgnoreAfter = DateTime.now();
|
DateTime nextIgnoreAfter = DateTime.now();
|
||||||
|
String? err;
|
||||||
try {
|
try {
|
||||||
await appsProvider.checkUpdates(ignoreAfter: ignoreAfter);
|
await appsProvider.checkUpdates(
|
||||||
|
ignoreAfter: ignoreAfter,
|
||||||
|
immediatelyThrowRateLimitError: true,
|
||||||
|
immediatelyThrowSocketError: true,
|
||||||
|
shouldCorrectInstallStatus: false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is RateLimitError) {
|
if (e is RateLimitError || e is SocketException) {
|
||||||
String nextTaskName =
|
String nextTaskName =
|
||||||
'$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}';
|
'$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}';
|
||||||
Workmanager().registerOneOffTask(nextTaskName, nextTaskName,
|
Workmanager().registerOneOffTask(nextTaskName, nextTaskName,
|
||||||
constraints: Constraints(networkType: NetworkType.connected),
|
constraints: Constraints(networkType: NetworkType.connected),
|
||||||
initialDelay: Duration(minutes: e.remainingMinutes),
|
initialDelay: Duration(
|
||||||
|
minutes: e is RateLimitError ? e.remainingMinutes : 15),
|
||||||
inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch});
|
inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch});
|
||||||
} else {
|
} else {
|
||||||
rethrow;
|
err = e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<App> newUpdates = appsProvider
|
List<App> newUpdates = appsProvider
|
||||||
@ -66,13 +75,15 @@ bgUpdateCheck(int? ignoreAfterMicroseconds) async {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty) {
|
if (newUpdates.isNotEmpty) {
|
||||||
notificationsProvider.notify(UpdateNotification(newUpdates),
|
notificationsProvider.notify(UpdateNotification(newUpdates));
|
||||||
cancelExisting: true);
|
}
|
||||||
|
if (err != null) {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()),
|
notificationsProvider
|
||||||
cancelExisting: true);
|
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||||
return Future.error(false);
|
return Future.error(false);
|
||||||
} finally {
|
} finally {
|
||||||
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
||||||
@ -89,7 +100,7 @@ void bgTaskCallback() {
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt! >= 29) {
|
if ((await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) {
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
|
||||||
);
|
);
|
||||||
@ -138,7 +149,7 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
Permission.notification.request();
|
Permission.notification.request();
|
||||||
appsProvider.saveApps([
|
appsProvider.saveApps([
|
||||||
App(
|
App(
|
||||||
'imranr98_obtainium_${GitHub().host}',
|
'dev.imranr.obtainium',
|
||||||
'https://github.com/ImranR98/Obtainium',
|
'https://github.com/ImranR98/Obtainium',
|
||||||
'ImranR98',
|
'ImranR98',
|
||||||
'Obtainium',
|
'Obtainium',
|
||||||
|
@ -22,7 +22,6 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
String userInput = '';
|
String userInput = '';
|
||||||
AppSource? pickedSource;
|
AppSource? pickedSource;
|
||||||
List<String> additionalData = [];
|
List<String> additionalData = [];
|
||||||
String customName = '';
|
|
||||||
bool validAdditionalData = true;
|
bool validAdditionalData = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -80,9 +79,6 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
.doesSourceHaveRequiredAdditionalData(
|
.doesSourceHaveRequiredAdditionalData(
|
||||||
source)
|
source)
|
||||||
: true;
|
: true;
|
||||||
if (source == null) {
|
|
||||||
customName = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -90,59 +86,78 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
gettingAppInfo
|
||||||
onPressed: gettingAppInfo ||
|
? const CircularProgressIndicator()
|
||||||
pickedSource == null ||
|
: ElevatedButton(
|
||||||
(pickedSource!.additionalDataFormItems
|
onPressed: gettingAppInfo ||
|
||||||
.isNotEmpty &&
|
pickedSource == null ||
|
||||||
!validAdditionalData)
|
(pickedSource!.additionalDataFormItems
|
||||||
? null
|
.isNotEmpty &&
|
||||||
: () {
|
!validAdditionalData)
|
||||||
HapticFeedback.selectionClick();
|
? null
|
||||||
setState(() {
|
: () async {
|
||||||
gettingAppInfo = true;
|
setState(() {
|
||||||
});
|
gettingAppInfo = true;
|
||||||
sourceProvider
|
});
|
||||||
.getApp(pickedSource!, userInput,
|
var appsProvider =
|
||||||
additionalData,
|
context.read<AppsProvider>();
|
||||||
customName: customName)
|
var settingsProvider =
|
||||||
.then((app) {
|
context.read<SettingsProvider>();
|
||||||
var appsProvider =
|
() async {
|
||||||
context.read<AppsProvider>();
|
HapticFeedback.selectionClick();
|
||||||
var settingsProvider =
|
App app =
|
||||||
context.read<SettingsProvider>();
|
await sourceProvider.getApp(
|
||||||
if (appsProvider.apps
|
pickedSource!,
|
||||||
.containsKey(app.id)) {
|
userInput,
|
||||||
throw 'App already added';
|
additionalData);
|
||||||
}
|
await settingsProvider
|
||||||
settingsProvider
|
.getInstallPermission();
|
||||||
.getInstallPermission()
|
// ignore: use_build_context_synchronously
|
||||||
.then((_) {
|
var apkUrl = await appsProvider
|
||||||
appsProvider
|
.selectApkUrl(app, context);
|
||||||
.saveApps([app]).then((_) {
|
if (apkUrl == null) {
|
||||||
|
throw 'Cancelled';
|
||||||
|
}
|
||||||
|
app.preferredApkIndex =
|
||||||
|
app.apkUrls.indexOf(apkUrl);
|
||||||
|
var downloadedApk =
|
||||||
|
await appsProvider.downloadApp(
|
||||||
|
app,
|
||||||
|
showOccasionalProgressToast:
|
||||||
|
true);
|
||||||
|
app.id = downloadedApk.appId;
|
||||||
|
if (appsProvider.apps
|
||||||
|
.containsKey(app.id)) {
|
||||||
|
throw 'App already added';
|
||||||
|
}
|
||||||
|
await appsProvider.saveApps([app]);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}()
|
||||||
|
.then((app) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
AppPage(
|
AppPage(
|
||||||
appId: app.id)));
|
appId: app.id)));
|
||||||
|
}).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
gettingAppInfo = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
}).catchError((e) {
|
child: const Text('Add'))
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(content: Text(e.toString())),
|
|
||||||
);
|
|
||||||
}).whenComplete(() {
|
|
||||||
setState(() {
|
|
||||||
gettingAppInfo = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Text('Add'))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (pickedSource != null)
|
if (pickedSource != null &&
|
||||||
|
pickedSource!.additionalDataDefaults.isNotEmpty)
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -174,21 +189,6 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
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
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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/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';
|
||||||
@ -46,6 +45,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
child: settingsProvider.showAppWebpage
|
child: settingsProvider.showAppWebpage
|
||||||
? WebView(
|
? WebView(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
initialUrl: app?.app.url,
|
initialUrl: app?.app.url,
|
||||||
javascriptMode: JavascriptMode.unrestricted,
|
javascriptMode: JavascriptMode.unrestricted,
|
||||||
)
|
)
|
||||||
@ -56,8 +56,22 @@ class _AppPageState extends State<AppPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
app?.installedInfo != null
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Image.memory(
|
||||||
|
app!.installedInfo!.icon!,
|
||||||
|
height: 150,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
: Container(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
app?.app.name ?? 'App',
|
app?.installedInfo?.name ?? app?.app.name ?? 'App',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.displayLarge,
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
),
|
),
|
||||||
@ -126,7 +140,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
if (app?.app.installedVersion != app?.app.latestVersion)
|
if (app?.app.installedVersion != null &&
|
||||||
|
app?.app.installedVersion != app?.app.latestVersion)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
@ -135,8 +150,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: const Text(
|
||||||
'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'),
|
'App Already up to Date?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -161,54 +176,13 @@ class _AppPageState extends State<AppPage> {
|
|||||||
.pop();
|
.pop();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Yes, Mark as Installed'))
|
'Yes, Mark as Updated'))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip: 'Mark as Installed',
|
tooltip: 'Mark as Updated',
|
||||||
icon: const Icon(Icons.done))
|
icon: const Icon(Icons.done)),
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
onPressed: app?.downloadProgress != null
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text(
|
|
||||||
'App Not Installed?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context)
|
|
||||||
.pop();
|
|
||||||
},
|
|
||||||
child: const Text('No')),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
var updatedApp = app?.app;
|
|
||||||
if (updatedApp != null) {
|
|
||||||
updatedApp
|
|
||||||
.installedVersion =
|
|
||||||
null;
|
|
||||||
appsProvider.saveApps(
|
|
||||||
[updatedApp]);
|
|
||||||
}
|
|
||||||
Navigator.of(context)
|
|
||||||
.pop();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'Yes, Mark as Not Installed'))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip: 'Mark as Not Installed',
|
|
||||||
icon: const Icon(Icons.no_cell_outlined)),
|
|
||||||
if (source != null &&
|
if (source != null &&
|
||||||
source.additionalDataFormItems.isNotEmpty)
|
source.additionalDataFormItems.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -220,30 +194,15 @@ class _AppPageState extends State<AppPage> {
|
|||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: 'Additional Options',
|
title: 'Additional Options',
|
||||||
items: [
|
items: source
|
||||||
...source
|
.additionalDataFormItems,
|
||||||
.additionalDataFormItems,
|
|
||||||
[
|
|
||||||
GeneratedFormItem(
|
|
||||||
label: 'App Name',
|
|
||||||
required: true)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
defaultValues: app != null
|
defaultValues: app != null
|
||||||
? [
|
? app.app.additionalData
|
||||||
...app
|
: source
|
||||||
.app.additionalData,
|
.additionalDataDefaults);
|
||||||
app.app.name
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
...source
|
|
||||||
.additionalDataDefaults
|
|
||||||
]);
|
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (app != null && values != null) {
|
if (app != null && values != null) {
|
||||||
var changedApp = app.app;
|
var changedApp = app.app;
|
||||||
var name = values.removeLast();
|
|
||||||
changedApp.name = name;
|
|
||||||
changedApp.additionalData = values;
|
changedApp.additionalData = values;
|
||||||
appsProvider.saveApps(
|
appsProvider.saveApps(
|
||||||
[changedApp]).then((value) {
|
[changedApp]).then((value) {
|
||||||
@ -265,12 +224,18 @@ class _AppPageState extends State<AppPage> {
|
|||||||
? () {
|
? () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
appsProvider
|
appsProvider
|
||||||
.downloadAndInstallLatestApp(
|
.downloadAndInstallLatestApps(
|
||||||
[app!.app.id],
|
[app!.app.id],
|
||||||
context).then((res) {
|
context).then((res) {
|
||||||
if (res.isNotEmpty && mounted) {
|
if (res.isNotEmpty && mounted) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString())),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@ -288,7 +253,7 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Remove App?'),
|
title: const Text('Remove App?'),
|
||||||
content: Text(
|
content: Text(
|
||||||
'This will remove \'${app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
|
'This will remove \'${app?.installedInfo?.name ?? app?.app.name}\' from Obtainium.${app?.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -23,6 +23,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var updatesOnlyFilter =
|
var updatesOnlyFilter =
|
||||||
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
||||||
Set<String> selectedIds = {};
|
Set<String> selectedIds = {};
|
||||||
|
DateTime? refreshingSince;
|
||||||
|
|
||||||
clearSelected() {
|
clearSelected() {
|
||||||
if (selectedIds.isNotEmpty) {
|
if (selectedIds.isNotEmpty) {
|
||||||
@ -89,7 +90,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
for (var t in nameTokens) {
|
for (var t in nameTokens) {
|
||||||
if (!app.app.name.toLowerCase().contains(t.toLowerCase())) {
|
var name = app.installedInfo?.name ?? app.app.name;
|
||||||
|
if (!name.toLowerCase().contains(t.toLowerCase())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,13 +105,13 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortedApps.sort((a, b) {
|
sortedApps.sort((a, b) {
|
||||||
|
var nameA = a.installedInfo?.name ?? a.app.name;
|
||||||
|
var nameB = b.installedInfo?.name ?? b.app.name;
|
||||||
int result = 0;
|
int result = 0;
|
||||||
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
|
if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
|
||||||
result =
|
result = (a.app.author + nameA).compareTo(b.app.author + nameB);
|
||||||
(a.app.author + a.app.name).compareTo(b.app.author + b.app.name);
|
|
||||||
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
} else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
|
||||||
result =
|
result = (nameA + a.app.author).compareTo(nameB + b.app.author);
|
||||||
(a.app.name + a.app.author).compareTo(b.app.name + b.app.author);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@ -118,8 +120,9 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
sortedApps = sortedApps.reversed.toList();
|
sortedApps = sortedApps.reversed.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingUpdateIdsAllOrSelected = appsProvider
|
var existingUpdates = appsProvider.getExistingUpdates(installedOnly: true);
|
||||||
.getExistingUpdates(installedOnly: true)
|
|
||||||
|
var existingUpdateIdsAllOrSelected = existingUpdates
|
||||||
.where((element) => selectedIds.isEmpty
|
.where((element) => selectedIds.isEmpty
|
||||||
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
: selectedIds.contains(element))
|
: selectedIds.contains(element))
|
||||||
@ -131,15 +134,34 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: selectedIds.contains(element))
|
: selectedIds.contains(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
if (settingsProvider.pinUpdates) {
|
||||||
|
var temp = [];
|
||||||
|
sortedApps = sortedApps.where((sa) {
|
||||||
|
if (existingUpdates.contains(sa.app.id)) {
|
||||||
|
temp.add(sa);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
sortedApps = [...temp, ...sortedApps];
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = DateTime.now();
|
||||||
|
});
|
||||||
return appsProvider.checkUpdates().catchError((e) {
|
return appsProvider.checkUpdates().catchError((e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(e.toString())),
|
SnackBar(content: Text(e.toString())),
|
||||||
);
|
);
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
refreshingSince = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: CustomScrollView(slivers: <Widget>[
|
child: CustomScrollView(slivers: <Widget>[
|
||||||
@ -156,6 +178,17 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
))),
|
))),
|
||||||
|
if (refreshingSince != null)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: appsProvider.apps.values
|
||||||
|
.where((element) => !(element.app.lastUpdateCheck
|
||||||
|
?.isBefore(refreshingSince!) ??
|
||||||
|
true))
|
||||||
|
.length /
|
||||||
|
appsProvider.apps.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
@ -166,7 +199,14 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
toggleAppSelected(sortedApps[index].app.id);
|
toggleAppSelected(sortedApps[index].app.id);
|
||||||
},
|
},
|
||||||
title: Text(sortedApps[index].app.name),
|
leading: sortedApps[index].installedInfo != null
|
||||||
|
? Image.memory(
|
||||||
|
sortedApps[index].installedInfo!.icon!,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
title: Text(sortedApps[index].installedInfo?.name ??
|
||||||
|
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
|
||||||
? Text(
|
? Text(
|
||||||
@ -178,7 +218,9 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const Text('Update Available'),
|
Text(appsProvider.areDownloadsRunning()
|
||||||
|
? 'Please Wait...'
|
||||||
|
: 'Update Available'),
|
||||||
SourceProvider()
|
SourceProvider()
|
||||||
.getSource(sortedApps[index].app.url)
|
.getSource(sortedApps[index].app.url)
|
||||||
.changeLogPageFromStandardUrl(
|
.changeLogPageFromStandardUrl(
|
||||||
@ -205,8 +247,15 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Text(sortedApps[index].app.installedVersion ??
|
: SingleChildScrollView(
|
||||||
'Not Installed')),
|
child: SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: Text(
|
||||||
|
sortedApps[index].app.installedVersion ??
|
||||||
|
'Not Installed',
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
)))),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (selectedIds.isNotEmpty) {
|
if (selectedIds.isNotEmpty) {
|
||||||
toggleAppSelected(sortedApps[index].app.id);
|
toggleAppSelected(sortedApps[index].app.id);
|
||||||
@ -303,15 +352,20 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
message:
|
message:
|
||||||
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
|
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
|
||||||
items: formInputs,
|
items: formInputs,
|
||||||
defaultValues: const ['true', 'true'],
|
defaultValues: [
|
||||||
|
'true',
|
||||||
|
existingUpdateIdsAllOrSelected.isEmpty
|
||||||
|
? 'true'
|
||||||
|
: ''
|
||||||
|
],
|
||||||
initValid: true,
|
initValid: true,
|
||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
bool shouldInstallUpdates =
|
bool shouldInstallUpdates =
|
||||||
values.length < 2 || values[0] == 'true';
|
values.isEmpty || values[0] == 'true';
|
||||||
bool shouldInstallNew =
|
bool shouldInstallNew = values.isEmpty ||
|
||||||
values.length < 2 || values[1] == 'true';
|
(values.length >= 2 && values[1] == 'true');
|
||||||
settingsProvider
|
settingsProvider
|
||||||
.getInstallPermission()
|
.getInstallPermission()
|
||||||
.then((_) {
|
.then((_) {
|
||||||
@ -324,8 +378,14 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
toInstall
|
toInstall
|
||||||
.addAll(newInstallIdsAllOrSelected);
|
.addAll(newInstallIdsAllOrSelected);
|
||||||
}
|
}
|
||||||
appsProvider.downloadAndInstallLatestApp(
|
appsProvider
|
||||||
toInstall, context);
|
.downloadAndInstallLatestApps(
|
||||||
|
toInstall, context)
|
||||||
|
.catchError((e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -349,7 +409,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceBetween,
|
MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
@ -364,7 +424,10 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
ctx) {
|
ctx) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
'Mark ${selectedIds.length} Selected Apps as Not Installed?'),
|
'Mark ${selectedIds.length} Selected Apps as Updated?'),
|
||||||
|
content:
|
||||||
|
const Text(
|
||||||
|
'Only applies to installed but out of date Apps.'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
@ -383,8 +446,10 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
.saveApps(selectedIds.map((e) {
|
.saveApps(selectedIds.map((e) {
|
||||||
var a =
|
var a =
|
||||||
appsProvider.apps[e]!.app;
|
appsProvider.apps[e]!.app;
|
||||||
a.installedVersion =
|
if (a.installedVersion !=
|
||||||
null;
|
null) {
|
||||||
|
a.installedVersion = a.latestVersion;
|
||||||
|
}
|
||||||
return a;
|
return a;
|
||||||
}).toList());
|
}).toList());
|
||||||
|
|
||||||
@ -398,57 +463,7 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip:
|
tooltip:
|
||||||
'Mark Selected Apps as Not Installed',
|
'Mark Selected Apps as Updated',
|
||||||
icon: const Icon(
|
|
||||||
Icons.no_cell_outlined)),
|
|
||||||
IconButton(
|
|
||||||
onPressed:
|
|
||||||
appsProvider
|
|
||||||
.areDownloadsRunning()
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(BuildContext
|
|
||||||
ctx) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(
|
|
||||||
'Mark ${selectedIds.length} Selected Apps as Installed/Updated?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
() {
|
|
||||||
Navigator.of(context)
|
|
||||||
.pop();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'No')),
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
() {
|
|
||||||
HapticFeedback
|
|
||||||
.selectionClick();
|
|
||||||
appsProvider
|
|
||||||
.saveApps(selectedIds.map((e) {
|
|
||||||
var a =
|
|
||||||
appsProvider.apps[e]!.app;
|
|
||||||
a.installedVersion =
|
|
||||||
a.latestVersion;
|
|
||||||
return a;
|
|
||||||
}).toList());
|
|
||||||
|
|
||||||
Navigator.of(context)
|
|
||||||
.pop();
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'Yes'))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
tooltip:
|
|
||||||
'Mark Selected Apps as Installed/Updated',
|
|
||||||
icon: const Icon(Icons.done)),
|
icon: const Icon(Icons.done)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -155,6 +155,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Pin Updates to Top of Apps View'),
|
||||||
|
Switch(
|
||||||
|
value: settingsProvider.pinUpdates,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsProvider.pinUpdates = value;
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
|
@ -8,9 +8,14 @@ import 'dart:io';
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
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:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
||||||
|
import 'package:installed_apps/app_info.dart';
|
||||||
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
|
import 'package:package_archive_info/package_archive_info.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||||
@ -20,14 +25,15 @@ import 'package:http/http.dart';
|
|||||||
class AppInMemory {
|
class AppInMemory {
|
||||||
late App app;
|
late App app;
|
||||||
double? downloadProgress;
|
double? downloadProgress;
|
||||||
|
AppInfo? installedInfo; // Also indicates that an App is installed
|
||||||
|
|
||||||
AppInMemory(this.app, this.downloadProgress);
|
AppInMemory(this.app, this.downloadProgress, this.installedInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApkFile {
|
class DownloadedApp {
|
||||||
String appId;
|
String appId;
|
||||||
File file;
|
File file;
|
||||||
ApkFile(this.appId, this.file);
|
DownloadedApp(this.appId, this.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppsProvider with ChangeNotifier {
|
class AppsProvider with ChangeNotifier {
|
||||||
@ -38,24 +44,24 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Variables to keep track of the app foreground status (installs can't run in the background)
|
// Variables to keep track of the app foreground status (installs can't run in the background)
|
||||||
bool isForeground = true;
|
bool isForeground = true;
|
||||||
late Stream<FGBGType> foregroundStream;
|
late Stream<FGBGType>? foregroundStream;
|
||||||
late StreamSubscription<FGBGType> foregroundSubscription;
|
late StreamSubscription<FGBGType>? foregroundSubscription;
|
||||||
|
|
||||||
AppsProvider(
|
AppsProvider(
|
||||||
{bool shouldLoadApps = false,
|
{bool shouldLoadApps = false,
|
||||||
bool shouldCheckUpdatesAfterLoad = false,
|
bool shouldCheckUpdatesAfterLoad = false,
|
||||||
bool shouldDeleteAPKs = false}) {
|
bool shouldDeleteAPKs = false}) {
|
||||||
// Subscribe to changes in the app foreground status
|
|
||||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
|
||||||
foregroundSubscription = foregroundStream.listen((event) async {
|
|
||||||
isForeground = event == FGBGType.foreground;
|
|
||||||
if (isForeground) await loadApps();
|
|
||||||
});
|
|
||||||
if (shouldDeleteAPKs) {
|
|
||||||
deleteSavedAPKs();
|
|
||||||
}
|
|
||||||
if (shouldLoadApps) {
|
if (shouldLoadApps) {
|
||||||
|
// Subscribe to changes in the app foreground status
|
||||||
|
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||||
|
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||||
|
isForeground = event == FGBGType.foreground;
|
||||||
|
if (isForeground) await loadApps();
|
||||||
|
});
|
||||||
loadApps().then((_) {
|
loadApps().then((_) {
|
||||||
|
if (shouldDeleteAPKs) {
|
||||||
|
deleteSavedAPKs();
|
||||||
|
}
|
||||||
if (shouldCheckUpdatesAfterLoad) {
|
if (shouldCheckUpdatesAfterLoad) {
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
}
|
}
|
||||||
@ -63,38 +69,95 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApkFile> downloadApp(String apkUrl, String appId) async {
|
downloadApk(String apkUrl, String fileName, Function? onProgress,
|
||||||
apkUrl = await SourceProvider()
|
Function? urlModifier,
|
||||||
.getSource(apps[appId]!.app.url)
|
{bool useExistingIfExists = true}) async {
|
||||||
.apkUrlPrefetchModifier(apkUrl);
|
var destDir = (await getExternalStorageDirectory())!.path;
|
||||||
|
if (urlModifier != null) {
|
||||||
|
apkUrl = await urlModifier(apkUrl);
|
||||||
|
}
|
||||||
StreamedResponse response =
|
StreamedResponse response =
|
||||||
await Client().send(Request('GET', Uri.parse(apkUrl)));
|
await Client().send(Request('GET', Uri.parse(apkUrl)));
|
||||||
File downloadFile =
|
File downloadFile = File('$destDir/$fileName.apk');
|
||||||
File('${(await getExternalStorageDirectory())!.path}/$appId.apk');
|
var alreadyExists = downloadFile.existsSync();
|
||||||
if (downloadFile.existsSync()) {
|
if (!alreadyExists || !useExistingIfExists) {
|
||||||
downloadFile.deleteSync();
|
if (alreadyExists) {
|
||||||
}
|
downloadFile.deleteSync();
|
||||||
var length = response.contentLength;
|
}
|
||||||
var received = 0;
|
|
||||||
var sink = downloadFile.openWrite();
|
|
||||||
|
|
||||||
await response.stream.map((s) {
|
var length = response.contentLength;
|
||||||
received += s.length;
|
var received = 0;
|
||||||
apps[appId]!.downloadProgress =
|
double? progress;
|
||||||
(length != null ? received / length * 100 : 30);
|
var sink = downloadFile.openWrite();
|
||||||
|
|
||||||
|
await response.stream.map((s) {
|
||||||
|
received += s.length;
|
||||||
|
progress = (length != null ? received / length * 100 : 30);
|
||||||
|
if (onProgress != null) {
|
||||||
|
onProgress(progress);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}).pipe(sink);
|
||||||
|
|
||||||
|
await sink.close();
|
||||||
|
progress = null;
|
||||||
|
if (onProgress != null) {
|
||||||
|
onProgress(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
downloadFile.deleteSync();
|
||||||
|
throw response.reasonPhrase ?? 'Unknown Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return downloadFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloads the App (preferred URL) and returns an ApkFile object
|
||||||
|
// If the app was already saved, updates it's download progress % in memory
|
||||||
|
// But also works for Apps that are not saved
|
||||||
|
Future<DownloadedApp> downloadApp(App app,
|
||||||
|
{bool showOccasionalProgressToast = false}) async {
|
||||||
|
int? prevProg;
|
||||||
|
var fileName = '${app.id}-${app.latestVersion}-${app.preferredApkIndex}';
|
||||||
|
File downloadFile = await downloadApk(app.apkUrls[app.preferredApkIndex],
|
||||||
|
'${app.id}-${app.latestVersion}-${app.preferredApkIndex}',
|
||||||
|
(double? progress) {
|
||||||
|
if (apps[app.id] != null) {
|
||||||
|
apps[app.id]!.downloadProgress = progress;
|
||||||
|
}
|
||||||
|
int? prog = progress?.ceil();
|
||||||
|
if (showOccasionalProgressToast &&
|
||||||
|
(prog == 25 || prog == 50 || prog == 75) &&
|
||||||
|
prevProg != prog) {
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: 'Progress: $prog%', toastLength: Toast.LENGTH_SHORT);
|
||||||
|
}
|
||||||
|
prevProg = prog;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return s;
|
}, SourceProvider().getSource(app.url).apkUrlPrefetchModifier);
|
||||||
}).pipe(sink);
|
// Delete older versions of the APK if any
|
||||||
|
for (var file in downloadFile.parent.listSync()) {
|
||||||
await sink.close();
|
var fn = file.path.split('/').last;
|
||||||
apps[appId]!.downloadProgress = null;
|
if (fn.startsWith('${app.id}-') &&
|
||||||
notifyListeners();
|
fn.endsWith('.apk') &&
|
||||||
|
fn != '$fileName.apk') {
|
||||||
if (response.statusCode != 200) {
|
file.delete();
|
||||||
downloadFile.deleteSync();
|
}
|
||||||
throw response.reasonPhrase ?? 'Unknown Error';
|
|
||||||
}
|
}
|
||||||
return ApkFile(appId, downloadFile);
|
// If the ID has changed (as it should on first download), replace it
|
||||||
|
var newInfo = await PackageArchiveInfo.fromPath(downloadFile.path);
|
||||||
|
if (app.id != newInfo.packageName) {
|
||||||
|
var originalAppId = app.id;
|
||||||
|
app.id = newInfo.packageName;
|
||||||
|
downloadFile = downloadFile.renameSync(
|
||||||
|
'${downloadFile.parent.path}/${app.id}-${app.latestVersion}-${app.preferredApkIndex}.apk');
|
||||||
|
if (apps[originalAppId] != null) {
|
||||||
|
await removeApps([originalAppId]);
|
||||||
|
await saveApps([app]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DownloadedApp(app.id, downloadFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areDownloadsRunning() => apps.values
|
bool areDownloadsRunning() => apps.values
|
||||||
@ -105,8 +168,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// TODO: This is unreliable - try to get from OS in the future
|
// TODO: This is unreliable - try to get from OS in the future
|
||||||
var osInfo = await DeviceInfoPlugin().androidInfo;
|
var osInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
return app.installedVersion != null &&
|
return app.installedVersion != null &&
|
||||||
osInfo.version.sdkInt! >= 30 &&
|
osInfo.version.sdkInt >= 30 &&
|
||||||
osInfo.version.release!.compareTo('12') >= 0;
|
osInfo.version.release.compareTo('12') >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> askUserToReturnToForeground(BuildContext context,
|
Future<void> askUserToReturnToForeground(BuildContext context,
|
||||||
@ -127,11 +190,62 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
// 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
|
// 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
|
// But even then, we don't know if it actually succeeded
|
||||||
Future<void> installApk(ApkFile file) async {
|
Future<void> installApk(DownloadedApp file) async {
|
||||||
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
|
var newInfo = await PackageArchiveInfo.fromPath(file.file.path);
|
||||||
|
AppInfo? appInfo;
|
||||||
|
try {
|
||||||
|
appInfo = await InstalledApps.getAppInfo(apps[file.appId]!.app.id);
|
||||||
|
} catch (e) {
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
if (appInfo != null &&
|
||||||
|
int.parse(newInfo.buildNumber) < appInfo.versionCode!) {
|
||||||
|
throw 'Can\'t install an older version';
|
||||||
|
}
|
||||||
|
if (appInfo == null ||
|
||||||
|
int.parse(newInfo.buildNumber) > appInfo.versionCode!) {
|
||||||
|
await InstallPlugin.installApk(file.file.path, 'dev.imranr.obtainium');
|
||||||
|
}
|
||||||
apps[file.appId]!.app.installedVersion =
|
apps[file.appId]!.app.installedVersion =
|
||||||
apps[file.appId]!.app.latestVersion;
|
apps[file.appId]!.app.latestVersion;
|
||||||
await saveApps([apps[file.appId]!.app]);
|
// Don't correct install status as installation may not be done yet
|
||||||
|
await saveApps([apps[file.appId]!.app], shouldCorrectInstallStatus: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> selectApkUrl(App app, BuildContext? context) async {
|
||||||
|
// If the App has more than one APK, the user should pick one (if context provided)
|
||||||
|
String? apkUrl = app.apkUrls[app.preferredApkIndex];
|
||||||
|
if (app.apkUrls.length > 1 && context != null) {
|
||||||
|
apkUrl = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return APKPicker(app: app, initVal: apkUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||||
|
if (apkUrl != null &&
|
||||||
|
Uri.parse(apkUrl).origin != Uri.parse(app.url).origin &&
|
||||||
|
context != null) {
|
||||||
|
if (await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return APKOriginWarningDialog(
|
||||||
|
sourceUrl: app.url, apkUrl: apkUrl!);
|
||||||
|
}) !=
|
||||||
|
true) {
|
||||||
|
apkUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> addToErrorMap(
|
||||||
|
Map<String, List<String>> errors, String appId, String error) {
|
||||||
|
var tempIds = errors.remove(error);
|
||||||
|
tempIds ??= [];
|
||||||
|
tempIds.add(appId);
|
||||||
|
errors.putIfAbsent(error, () => tempIds!);
|
||||||
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
||||||
@ -139,37 +253,16 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// If no BuildContext is provided, apps that require user interaction are ignored
|
// 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
|
// 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
|
// Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
|
||||||
Future<List<String>> downloadAndInstallLatestApp(
|
Future<List<String>> downloadAndInstallLatestApps(
|
||||||
List<String> appIds, BuildContext? context) async {
|
List<String> appIds, BuildContext? context) async {
|
||||||
Map<String, String> appsToInstall = {};
|
List<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 context provided)
|
String? apkUrl = await selectApkUrl(apps[id]!.app, context);
|
||||||
String? apkUrl = apps[id]!.app.apkUrls[apps[id]!.app.preferredApkIndex];
|
|
||||||
if (apps[id]!.app.apkUrls.length > 1 && context != null) {
|
|
||||||
apkUrl = await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return APKPicker(app: apps[id]!.app, initVal: apkUrl);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
|
||||||
if (apkUrl != null &&
|
|
||||||
Uri.parse(apkUrl).origin != Uri.parse(apps[id]!.app.url).origin &&
|
|
||||||
context != null) {
|
|
||||||
if (await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext ctx) {
|
|
||||||
return APKOriginWarningDialog(
|
|
||||||
sourceUrl: apps[id]!.app.url, apkUrl: apkUrl!);
|
|
||||||
}) !=
|
|
||||||
true) {
|
|
||||||
apkUrl = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (apkUrl != null) {
|
if (apkUrl != null) {
|
||||||
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) {
|
||||||
@ -179,18 +272,28 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (context != null ||
|
if (context != null ||
|
||||||
(await canInstallSilently(apps[id]!.app) &&
|
(await canInstallSilently(apps[id]!.app) &&
|
||||||
apps[id]!.app.apkUrls.length == 1)) {
|
apps[id]!.app.apkUrls.length == 1)) {
|
||||||
appsToInstall.putIfAbsent(id, () => apkUrl!);
|
appsToInstall.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Map<String, List<String>> errors = {};
|
||||||
|
|
||||||
List<ApkFile> downloadedFiles = await Future.wait(appsToInstall.entries
|
List<DownloadedApp?> downloadedFiles =
|
||||||
.map((entry) => downloadApp(entry.value, entry.key)));
|
await Future.wait(appsToInstall.map((id) async {
|
||||||
|
try {
|
||||||
|
return await downloadApp(apps[id]!.app);
|
||||||
|
} catch (e) {
|
||||||
|
addToErrorMap(errors, id, e.toString());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
downloadedFiles =
|
||||||
|
downloadedFiles.where((element) => element != null).toList();
|
||||||
|
|
||||||
List<ApkFile> silentUpdates = [];
|
List<DownloadedApp> silentUpdates = [];
|
||||||
List<ApkFile> regularInstalls = [];
|
List<DownloadedApp> regularInstalls = [];
|
||||||
for (var f in downloadedFiles) {
|
for (var f in downloadedFiles) {
|
||||||
bool willBeSilent = await canInstallSilently(apps[f.appId]!.app);
|
bool willBeSilent = await canInstallSilently(apps[f!.appId]!.app);
|
||||||
if (willBeSilent) {
|
if (willBeSilent) {
|
||||||
silentUpdates.add(f);
|
silentUpdates.add(f);
|
||||||
} else {
|
} else {
|
||||||
@ -199,9 +302,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If Obtainium is being installed, it should be the last one
|
// If Obtainium is being installed, it should be the last one
|
||||||
List<ApkFile> moveObtainiumToEnd(List<ApkFile> items) {
|
List<DownloadedApp> moveObtainiumToStart(List<DownloadedApp> items) {
|
||||||
String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
|
String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
|
||||||
ApkFile? temp;
|
DownloadedApp? temp;
|
||||||
items.removeWhere((element) {
|
items.removeWhere((element) {
|
||||||
bool res = element.appId == obtainiumId;
|
bool res = element.appId == obtainiumId;
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -210,7 +313,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
if (temp != null) {
|
if (temp != null) {
|
||||||
items.add(temp!);
|
items = [temp!, ...items];
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@ -218,8 +321,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// TODO: Remove below line if silentupdates are ever figured out
|
// TODO: Remove below line if silentupdates are ever figured out
|
||||||
regularInstalls.addAll(silentUpdates);
|
regularInstalls.addAll(silentUpdates);
|
||||||
|
|
||||||
silentUpdates = moveObtainiumToEnd(silentUpdates);
|
silentUpdates = moveObtainiumToStart(silentUpdates);
|
||||||
regularInstalls = moveObtainiumToEnd(regularInstalls);
|
regularInstalls = moveObtainiumToStart(regularInstalls);
|
||||||
|
|
||||||
// TODO: Uncomment below if silentupdates are ever figured out
|
// TODO: Uncomment below if silentupdates are ever figured out
|
||||||
// for (var u in silentUpdates) {
|
// for (var u in silentUpdates) {
|
||||||
@ -232,11 +335,23 @@ class AppsProvider with ChangeNotifier {
|
|||||||
await askUserToReturnToForeground(context, waitForFG: true);
|
await askUserToReturnToForeground(context, waitForFG: true);
|
||||||
}
|
}
|
||||||
for (var i in regularInstalls) {
|
for (var i in regularInstalls) {
|
||||||
await installApk(i);
|
try {
|
||||||
|
await installApk(i);
|
||||||
|
} catch (e) {
|
||||||
|
addToErrorMap(errors, i.appId, e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
String finalError = '';
|
||||||
|
for (var e in errors.keys) {
|
||||||
|
finalError +=
|
||||||
|
'$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
|
||||||
|
}
|
||||||
|
throw finalError;
|
||||||
|
}
|
||||||
|
|
||||||
return downloadedFiles.map((e) => e.appId).toList();
|
return downloadedFiles.map((e) => e!.appId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> getAppsDir() async {
|
Future<Directory> getAppsDir() async {
|
||||||
@ -248,16 +363,83 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return appsDir;
|
return appsDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete all stored APKs except those likely to still be needed
|
||||||
Future<void> deleteSavedAPKs() async {
|
Future<void> deleteSavedAPKs() async {
|
||||||
(await getExternalStorageDirectory())
|
List<FileSystemEntity>? apks = (await getExternalStorageDirectory())
|
||||||
?.listSync()
|
?.listSync()
|
||||||
.where((element) => element.path.endsWith('.apk'))
|
.where((element) => element.path.endsWith('.apk'))
|
||||||
.forEach((element) {
|
.toList();
|
||||||
element.deleteSync();
|
if (apks != null && apks.isNotEmpty) {
|
||||||
});
|
for (var apk in apks) {
|
||||||
|
var shouldDelete = true;
|
||||||
|
var temp = apk.path.split('/').last;
|
||||||
|
temp = temp.substring(0, temp.length - 4);
|
||||||
|
var fn = temp.split('-');
|
||||||
|
if (fn.length == 3) {
|
||||||
|
var possibleId = fn[0];
|
||||||
|
var possibleVersion = fn[1];
|
||||||
|
var possibleApkUrlIndex = fn[2];
|
||||||
|
if (apps[possibleId] != null) {
|
||||||
|
if (apps[possibleId] != null &&
|
||||||
|
apps[possibleId]?.app != null &&
|
||||||
|
apps[possibleId]!.app.installedVersion !=
|
||||||
|
apps[possibleId]!.app.latestVersion &&
|
||||||
|
apps[possibleId]!.app.latestVersion == possibleVersion &&
|
||||||
|
apps[possibleId]!.app.preferredApkIndex.toString() ==
|
||||||
|
possibleApkUrlIndex) {
|
||||||
|
shouldDelete = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDelete) apk.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadApps() async {
|
Future<AppInfo?> getInstalledInfo(String? packageName) async {
|
||||||
|
if (packageName != null) {
|
||||||
|
try {
|
||||||
|
return await InstalledApps.getAppInfo(packageName);
|
||||||
|
} catch (e) {
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String standardizeVersionString(String versionString) {
|
||||||
|
return versionString.characters
|
||||||
|
.where((p0) => ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']
|
||||||
|
.contains(p0))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the App says it is installed by installedInfo is null, set it to not installed
|
||||||
|
// If the App says is is not installed but installedInfo exists, try to set it to installed as latest version...
|
||||||
|
// ...if the latestVersion seems to match the version in installedInfo (not guaranteed)
|
||||||
|
App? correctInstallStatus(App app, AppInfo? installedInfo) {
|
||||||
|
var modded = false;
|
||||||
|
if (installedInfo == null && app.installedVersion != null) {
|
||||||
|
app.installedVersion = null;
|
||||||
|
modded = true;
|
||||||
|
}
|
||||||
|
if (installedInfo != null && app.installedVersion == null) {
|
||||||
|
if (standardizeVersionString(app.latestVersion) ==
|
||||||
|
installedInfo.versionName) {
|
||||||
|
app.installedVersion = app.latestVersion;
|
||||||
|
} else {
|
||||||
|
app.installedVersion = installedInfo.versionName;
|
||||||
|
}
|
||||||
|
modded = true;
|
||||||
|
}
|
||||||
|
return modded ? app : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadApps({shouldCorrectInstallStatus = true}) async {
|
||||||
|
while (loadingApps) {
|
||||||
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
|
}
|
||||||
loadingApps = true;
|
loadingApps = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
List<FileSystemEntity> appFiles = (await getAppsDir())
|
List<FileSystemEntity> appFiles = (await getAppsDir())
|
||||||
@ -265,22 +447,54 @@ class AppsProvider with ChangeNotifier {
|
|||||||
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
||||||
.toList();
|
.toList();
|
||||||
apps.clear();
|
apps.clear();
|
||||||
|
var sp = SourceProvider();
|
||||||
|
List<List<String>> errors = [];
|
||||||
for (int i = 0; i < appFiles.length; i++) {
|
for (int i = 0; i < appFiles.length; i++) {
|
||||||
App app =
|
App app =
|
||||||
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
|
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
|
||||||
apps.putIfAbsent(app.id, () => AppInMemory(app, null));
|
var info = await getInstalledInfo(app.id);
|
||||||
|
try {
|
||||||
|
sp.getSource(app.url);
|
||||||
|
apps.putIfAbsent(app.id, () => AppInMemory(app, null, info));
|
||||||
|
} catch (e) {
|
||||||
|
errors.add([app.id, app.name, e.toString()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
removeApps(errors.map((e) => e[0]).toList());
|
||||||
|
NotificationsProvider().notify(
|
||||||
|
AppsRemovedNotification(errors.map((e) => [e[1], e[2]]).toList()));
|
||||||
}
|
}
|
||||||
loadingApps = false;
|
loadingApps = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
// For any that are not installed (by ID == package name), set to not installed if needed
|
||||||
|
if (shouldCorrectInstallStatus) {
|
||||||
|
List<App> modifiedApps = [];
|
||||||
|
for (var app in apps.values) {
|
||||||
|
var moddedApp = correctInstallStatus(app.app, app.installedInfo);
|
||||||
|
if (moddedApp != null) {
|
||||||
|
modifiedApps.add(moddedApp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modifiedApps.isNotEmpty) {
|
||||||
|
await saveApps(modifiedApps, shouldCorrectInstallStatus: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveApps(List<App> apps) async {
|
Future<void> saveApps(List<App> apps,
|
||||||
|
{bool shouldCorrectInstallStatus = true}) async {
|
||||||
for (var app in apps) {
|
for (var app in apps) {
|
||||||
|
AppInfo? info = await getInstalledInfo(app.id);
|
||||||
|
app.name = info?.name ?? app.name;
|
||||||
|
if (shouldCorrectInstallStatus) {
|
||||||
|
app = correctInstallStatus(app, info) ?? app;
|
||||||
|
}
|
||||||
File('${(await getAppsDir()).path}/${app.id}.json')
|
File('${(await getAppsDir()).path}/${app.id}.json')
|
||||||
.writeAsStringSync(jsonEncode(app.toJson()));
|
.writeAsStringSync(jsonEncode(app.toJson()));
|
||||||
this.apps.update(
|
this.apps.update(
|
||||||
app.id, (value) => AppInMemory(app, value.downloadProgress),
|
app.id, (value) => AppInMemory(app, value.downloadProgress, info),
|
||||||
ifAbsent: () => AppInMemory(app, null));
|
ifAbsent: () => AppInMemory(app, null, info));
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -307,46 +521,81 @@ class AppsProvider with ChangeNotifier {
|
|||||||
return app.latestVersion != apps[app.id]?.app.installedVersion;
|
return app.latestVersion != apps[app.id]?.app.installedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<App?> getUpdate(String appId) async {
|
Future<App?> getUpdate(String appId,
|
||||||
|
{bool shouldCorrectInstallStatus = true}) async {
|
||||||
App? currentApp = apps[appId]!.app;
|
App? currentApp = apps[appId]!.app;
|
||||||
SourceProvider sourceProvider = SourceProvider();
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
App newApp = await sourceProvider.getApp(
|
App newApp = await sourceProvider.getApp(
|
||||||
sourceProvider.getSource(currentApp.url),
|
sourceProvider.getSource(currentApp.url),
|
||||||
currentApp.url,
|
currentApp.url,
|
||||||
currentApp.additionalData,
|
currentApp.additionalData,
|
||||||
customName: currentApp.name);
|
name: currentApp.name,
|
||||||
|
id: currentApp.id);
|
||||||
newApp.installedVersion = currentApp.installedVersion;
|
newApp.installedVersion = currentApp.installedVersion;
|
||||||
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
if (currentApp.preferredApkIndex < newApp.apkUrls.length) {
|
||||||
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
newApp.preferredApkIndex = currentApp.preferredApkIndex;
|
||||||
}
|
}
|
||||||
await saveApps([newApp]);
|
await saveApps([newApp],
|
||||||
|
shouldCorrectInstallStatus: shouldCorrectInstallStatus);
|
||||||
return newApp.latestVersion != currentApp.latestVersion ? newApp : null;
|
return newApp.latestVersion != currentApp.latestVersion ? newApp : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<App>> checkUpdates({DateTime? ignoreAfter}) async {
|
Future<List<App>> checkUpdates(
|
||||||
|
{DateTime? ignoreAfter,
|
||||||
|
bool immediatelyThrowRateLimitError = false,
|
||||||
|
bool shouldCorrectInstallStatus = true,
|
||||||
|
bool immediatelyThrowSocketError = false}) async {
|
||||||
List<App> updates = [];
|
List<App> updates = [];
|
||||||
|
Map<String, List<String>> errors = {};
|
||||||
if (!gettingUpdates) {
|
if (!gettingUpdates) {
|
||||||
gettingUpdates = true;
|
gettingUpdates = true;
|
||||||
|
|
||||||
List<String> appIds = apps.keys.toList();
|
try {
|
||||||
if (ignoreAfter != null) {
|
List<String> appIds = apps.keys.toList();
|
||||||
appIds = appIds
|
if (ignoreAfter != null) {
|
||||||
.where((id) =>
|
appIds = appIds
|
||||||
apps[id]!.app.lastUpdateCheck == null ||
|
.where((id) =>
|
||||||
apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
|
apps[id]!.app.lastUpdateCheck == null ||
|
||||||
.toList();
|
apps[id]!.app.lastUpdateCheck!.isBefore(ignoreAfter))
|
||||||
}
|
.toList();
|
||||||
appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
|
|
||||||
DateTime.fromMicrosecondsSinceEpoch(0))
|
|
||||||
.compareTo(apps[b]!.app.lastUpdateCheck ??
|
|
||||||
DateTime.fromMicrosecondsSinceEpoch(0)));
|
|
||||||
for (int i = 0; i < appIds.length; i++) {
|
|
||||||
App? newApp = await getUpdate(appIds[i]);
|
|
||||||
if (newApp != null) {
|
|
||||||
updates.add(newApp);
|
|
||||||
}
|
}
|
||||||
|
appIds.sort((a, b) => (apps[a]!.app.lastUpdateCheck ??
|
||||||
|
DateTime.fromMicrosecondsSinceEpoch(0))
|
||||||
|
.compareTo(apps[b]!.app.lastUpdateCheck ??
|
||||||
|
DateTime.fromMicrosecondsSinceEpoch(0)));
|
||||||
|
|
||||||
|
for (int i = 0; i < appIds.length; i++) {
|
||||||
|
App? newApp;
|
||||||
|
try {
|
||||||
|
newApp = await getUpdate(appIds[i],
|
||||||
|
shouldCorrectInstallStatus: shouldCorrectInstallStatus);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is RateLimitError && immediatelyThrowRateLimitError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
if (e is SocketException && immediatelyThrowSocketError) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
var tempIds = errors.remove(e.toString());
|
||||||
|
tempIds ??= [];
|
||||||
|
tempIds.add(appIds[i]);
|
||||||
|
errors.putIfAbsent(e.toString(), () => tempIds!);
|
||||||
|
}
|
||||||
|
if (newApp != null) {
|
||||||
|
updates.add(newApp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
gettingUpdates = false;
|
||||||
}
|
}
|
||||||
gettingUpdates = false;
|
}
|
||||||
|
if (errors.isNotEmpty) {
|
||||||
|
String finalError = '';
|
||||||
|
for (var e in errors.keys) {
|
||||||
|
finalError +=
|
||||||
|
'$e ${errors[e]!.map((e) => apps[e]!.app.name).toString()}. ';
|
||||||
|
}
|
||||||
|
throw finalError;
|
||||||
}
|
}
|
||||||
return updates;
|
return updates;
|
||||||
}
|
}
|
||||||
@ -389,9 +638,13 @@ class AppsProvider with ChangeNotifier {
|
|||||||
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
|
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
|
||||||
.map((e) => App.fromJson(e))
|
.map((e) => App.fromJson(e))
|
||||||
.toList();
|
.toList();
|
||||||
|
while (loadingApps) {
|
||||||
|
await Future.delayed(const Duration(microseconds: 1));
|
||||||
|
}
|
||||||
for (App a in importedApps) {
|
for (App a in importedApps) {
|
||||||
a.installedVersion =
|
if (apps[a.id]?.app.installedVersion != null) {
|
||||||
apps.containsKey(a.id) ? apps[a]?.app.installedVersion : null;
|
a.installedVersion = apps[a.id]?.app.installedVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await saveApps(importedApps);
|
await saveApps(importedApps);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -400,7 +653,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
foregroundSubscription.cancel();
|
foregroundSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,24 @@ class ErrorCheckingUpdatesNotification extends ObtainiumNotification {
|
|||||||
Importance.high);
|
Importance.high);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AppsRemovedNotification extends ObtainiumNotification {
|
||||||
|
AppsRemovedNotification(List<List<String>> namedReasons)
|
||||||
|
: super(
|
||||||
|
6,
|
||||||
|
'Apps Removed',
|
||||||
|
'',
|
||||||
|
'APPS_REMOVED',
|
||||||
|
'Apps Removed',
|
||||||
|
'Notifies the user that one or more Apps were removed due to errors while loading them',
|
||||||
|
Importance.max) {
|
||||||
|
message = '';
|
||||||
|
for (var r in namedReasons) {
|
||||||
|
message += '${r[0]} was removed due to this error: ${r[1]}. \n';
|
||||||
|
}
|
||||||
|
message = message.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final completeInstallationNotification = ObtainiumNotification(
|
final completeInstallationNotification = ObtainiumNotification(
|
||||||
1,
|
1,
|
||||||
'Complete App Installation',
|
'Complete App Installation',
|
||||||
|
@ -123,6 +123,15 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get pinUpdates {
|
||||||
|
return prefs?.getBool('pinUpdates') ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
set pinUpdates(bool show) {
|
||||||
|
prefs?.setBool('pinUpdates', show);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
String? getSettingString(String settingId) {
|
String? getSettingString(String settingId) {
|
||||||
return prefs?.getString(settingId);
|
return prefs?.getString(settingId);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
|
||||||
import 'package:obtainium/app_sources/fdroid.dart';
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:obtainium/app_sources/gitlab.dart';
|
import 'package:obtainium/app_sources/gitlab.dart';
|
||||||
@ -54,7 +53,7 @@ class App {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls';
|
return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrls PREFERREDAPK: $preferredApkIndex ADDITIONALDATA: ${additionalData.toString()} LASTCHECK: ${lastUpdateCheck.toString()}';
|
||||||
}
|
}
|
||||||
|
|
||||||
factory App.fromJson(Map<String, dynamic> json) => App(
|
factory App.fromJson(Map<String, dynamic> json) => App(
|
||||||
@ -105,6 +104,11 @@ preStandardizeUrl(String url) {
|
|||||||
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
if (url.toLowerCase().indexOf('https://www.') == 0) {
|
||||||
url = 'https://${url.substring(12)}';
|
url = 'https://${url.substring(12)}';
|
||||||
}
|
}
|
||||||
|
url = url
|
||||||
|
.split('/')
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.join('/')
|
||||||
|
.replaceFirst(':/', '://');
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +161,7 @@ class SourceProvider {
|
|||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal(),
|
Signal(),
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
APKMirror()
|
// APKMirror()
|
||||||
];
|
];
|
||||||
|
|
||||||
// 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
|
||||||
@ -189,21 +193,24 @@ class SourceProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String generateTempID(AppNames names, AppSource source) =>
|
||||||
|
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}';
|
||||||
|
|
||||||
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
||||||
{String customName = ''}) async {
|
{String name = '', String? id}) async {
|
||||||
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
String standardUrl = source.standardizeURL(preStandardizeUrl(url));
|
||||||
AppNames names = source.getAppNames(standardUrl);
|
AppNames names = source.getAppNames(standardUrl);
|
||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalData);
|
await source.getLatestAPKDetails(standardUrl, additionalData);
|
||||||
return App(
|
return App(
|
||||||
'${names.author.toLowerCase()}_${names.name.toLowerCase()}_${source.host}',
|
id ?? generateTempID(names, source),
|
||||||
standardUrl,
|
standardUrl,
|
||||||
names.author[0].toUpperCase() + names.author.substring(1),
|
names.author[0].toUpperCase() + names.author.substring(1),
|
||||||
customName.trim().isNotEmpty
|
name.trim().isNotEmpty
|
||||||
? customName
|
? name
|
||||||
: names.name[0].toUpperCase() + names.name.substring(1),
|
: names.name[0].toUpperCase() + names.name.substring(1),
|
||||||
null,
|
null,
|
||||||
apk.version,
|
apk.version.replaceAll('/', '-'),
|
||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
apk.apkUrls.length - 1,
|
apk.apkUrls.length - 1,
|
||||||
additionalData,
|
additionalData,
|
||||||
|
142
pubspec.lock
@ -14,7 +14,7 @@ packages:
|
|||||||
name: archive
|
name: archive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.3.2"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -112,42 +112,14 @@ packages:
|
|||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.5"
|
version: "8.0.0"
|
||||||
device_info_plus_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: device_info_plus_linux
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.2"
|
|
||||||
device_info_plus_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: device_info_plus_macos
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.2"
|
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: device_info_plus_platform_interface
|
name: device_info_plus_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
version: "7.0.0"
|
||||||
device_info_plus_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: device_info_plus_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.2"
|
|
||||||
device_info_plus_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: device_info_plus_windows
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.0.2"
|
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -182,7 +154,7 @@ packages:
|
|||||||
name: file_picker
|
name: file_picker
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.0+1"
|
version: "5.2.2"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -215,14 +187,14 @@ packages:
|
|||||||
name: flutter_local_notifications
|
name: flutter_local_notifications
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.0"
|
version: "12.0.3"
|
||||||
flutter_local_notifications_linux:
|
flutter_local_notifications_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_local_notifications_linux
|
name: flutter_local_notifications_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "2.0.0"
|
||||||
flutter_local_notifications_platform_interface:
|
flutter_local_notifications_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -253,14 +225,14 @@ packages:
|
|||||||
name: fluttertoast
|
name: fluttertoast
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.9"
|
version: "8.1.1"
|
||||||
html:
|
html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: html
|
name: html
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.0"
|
version: "0.15.1"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -274,14 +246,14 @@ packages:
|
|||||||
name: http_parser
|
name: http_parser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
version: "4.0.2"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.2"
|
||||||
install_plugin_v2:
|
install_plugin_v2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -289,6 +261,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
installed_apps:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: installed_apps
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -309,7 +288,7 @@ packages:
|
|||||||
name: lints
|
name: lints
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -323,7 +302,7 @@ packages:
|
|||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.1.5"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -345,6 +324,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
package_archive_info:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: package_archive_info
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
|
package_info:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_info
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -407,21 +400,21 @@ packages:
|
|||||||
name: permission_handler
|
name: permission_handler
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.0"
|
version: "10.2.0"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.1.0"
|
version: "10.2.0"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_apple
|
name: permission_handler_apple
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.6"
|
version: "9.0.7"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -435,14 +428,14 @@ packages:
|
|||||||
name: permission_handler_windows
|
name: permission_handler_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.1"
|
version: "0.1.2"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.1.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -470,49 +463,21 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.4"
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.3"
|
version: "6.1.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:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: share_plus_platform_interface
|
name: share_plus_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.2.0"
|
||||||
share_plus_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: share_plus_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0"
|
|
||||||
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:
|
||||||
@ -526,7 +491,7 @@ packages:
|
|||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.13"
|
version: "2.0.14"
|
||||||
shared_preferences_ios:
|
shared_preferences_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -580,7 +545,7 @@ packages:
|
|||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -594,7 +559,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.1"
|
version: "2.1.0"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -615,7 +580,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.14"
|
version: "0.4.12"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -643,7 +608,7 @@ packages:
|
|||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.19"
|
version: "6.0.21"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -686,13 +651,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.6"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.2"
|
||||||
webview_flutter:
|
webview_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -727,14 +699,14 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.1"
|
||||||
workmanager:
|
workmanager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: workmanager
|
name: workmanager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -757,5 +729,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0-79.0.dev <3.0.0"
|
dart: ">=2.18.2 <3.0.0"
|
||||||
flutter: ">=3.3.0"
|
flutter: ">=3.3.0"
|
||||||
|
10
pubspec.yaml
@ -17,10 +17,10 @@ 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.5.8+29 # When changing this, update the tag in main() accordingly
|
version: 0.6.5+49 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.19.0-79.0.dev <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
@ -49,11 +49,13 @@ dependencies:
|
|||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
permission_handler: ^10.0.0
|
permission_handler: ^10.0.0
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
device_info_plus: ^5.0.5
|
device_info_plus: ^8.0.0
|
||||||
file_picker: ^5.1.0
|
file_picker: ^5.1.0
|
||||||
animations: ^2.0.4
|
animations: ^2.0.4
|
||||||
install_plugin_v2: ^1.0.0
|
install_plugin_v2: ^1.0.0
|
||||||
share_plus: ^4.4.0
|
share_plus: ^6.0.1
|
||||||
|
installed_apps: ^1.3.1
|
||||||
|
package_archive_info: ^0.1.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|