mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 21:36:42 +02:00
Compare commits
15 Commits
v0.4.0-bet
...
v0.5.2-bet
Author | SHA1 | Date | |
---|---|---|---|
49b9a65053 | |||
aebc8aed76 | |||
3958425c22 | |||
0a560871cb | |||
fbe4f0b49e | |||
e2440a38c4 | |||
496a10a444 | |||
b8bb8d1f4b | |||
af033f42cb | |||
e706661062 | |||
1a68b8abe6 | |||
15c0ed04d1 | |||
dd193d62f2 | |||
77e1768f3b | |||
da9e5aed5e |
@ -9,8 +9,14 @@ class FDroid implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String standardizeURL(String url) {
|
String standardizeURL(String url) {
|
||||||
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/packages/[^/]+');
|
RegExp standardUrlRegExB =
|
||||||
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
|
||||||
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
|
||||||
|
if (match != null) {
|
||||||
|
url = 'https://$host/packages/${Uri.parse(url).pathSegments.last}';
|
||||||
|
}
|
||||||
|
RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
|
||||||
|
match = standardUrlRegExA.firstMatch(url.toLowerCase());
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
throw notValidURL(runtimeType.toString());
|
throw notValidURL(runtimeType.toString());
|
||||||
}
|
}
|
||||||
@ -54,4 +60,7 @@ class FDroid implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = [];
|
List<String> additionalDataDefaults = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/components/generated_form.dart';
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class GitHub implements AppSource {
|
class GitHub implements AppSource {
|
||||||
@override
|
@override
|
||||||
@ -17,6 +21,14 @@ class GitHub implements AppSource {
|
|||||||
return url.substring(0, match.end);
|
return url.substring(0, match.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> getCredentialPrefixIfAny() async {
|
||||||
|
SettingsProvider settingsProvider = SettingsProvider();
|
||||||
|
await settingsProvider.initializeSettings();
|
||||||
|
String? creds =
|
||||||
|
settingsProvider.getSettingString(moreSourceSettingsFormItems[0].id);
|
||||||
|
return creds != null && creds.isNotEmpty ? '$creds@' : '';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
@ -28,7 +40,7 @@ class GitHub implements AppSource {
|
|||||||
? additionalData[2]
|
? additionalData[2]
|
||||||
: null;
|
: null;
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = jsonDecode(res.body) as List<dynamic>;
|
var releases = jsonDecode(res.body) as List<dynamic>;
|
||||||
|
|
||||||
@ -76,7 +88,10 @@ class GitHub implements AppSource {
|
|||||||
return APKDetails(version, targetRelease['apkUrls']);
|
return APKDetails(version, targetRelease['apkUrls']);
|
||||||
} else {
|
} else {
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
throw 'Rate limit reached - try again in ${(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000).toString()} minutes';
|
throw RateLimitError(
|
||||||
|
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
||||||
|
60000000)
|
||||||
|
.round());
|
||||||
}
|
}
|
||||||
|
|
||||||
throw couldNotFindReleases;
|
throw couldNotFindReleases;
|
||||||
@ -120,4 +135,43 @@ class GitHub implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = ['true', 'true', ''];
|
List<String> additionalDataDefaults = ['true', 'true', ''];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'GitHub Personal Access Token (Increases Rate Limit)',
|
||||||
|
id: 'github-creds',
|
||||||
|
required: false,
|
||||||
|
additionalValidators: [
|
||||||
|
(value) {
|
||||||
|
if (value != null && value.trim().isNotEmpty) {
|
||||||
|
if (value
|
||||||
|
.split(':')
|
||||||
|
.where((element) => element.trim().isNotEmpty)
|
||||||
|
.length !=
|
||||||
|
2) {
|
||||||
|
return 'PAT must be in this format: username:token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hint: 'username:token',
|
||||||
|
belowWidgets: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token',
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'About GitHub PATs',
|
||||||
|
style: TextStyle(
|
||||||
|
decoration: TextDecoration.underline, fontSize: 12),
|
||||||
|
))
|
||||||
|
])
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -68,4 +68,7 @@ class GitLab implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = [];
|
List<String> additionalDataDefaults = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
}
|
}
|
||||||
|
@ -62,4 +62,7 @@ class IzzyOnDroid implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = [];
|
List<String> additionalDataDefaults = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
}
|
}
|
||||||
|
@ -48,4 +48,7 @@ class Mullvad implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = [];
|
List<String> additionalDataDefaults = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
}
|
}
|
||||||
|
@ -41,4 +41,7 @@ class Signal implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = [];
|
List<String> additionalDataDefaults = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
}
|
}
|
||||||
|
@ -65,4 +65,7 @@ class SourceForge implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> additionalDataDefaults = [];
|
List<String> additionalDataDefaults = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<GeneratedFormItem> moreSourceSettingsFormItems = [];
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,19 @@ class GeneratedFormItem {
|
|||||||
late bool required;
|
late bool required;
|
||||||
late int max;
|
late int max;
|
||||||
late List<String? Function(String? value)> additionalValidators;
|
late List<String? Function(String? value)> additionalValidators;
|
||||||
|
late String id;
|
||||||
|
late List<Widget> belowWidgets;
|
||||||
|
late String? hint;
|
||||||
|
|
||||||
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,
|
||||||
this.additionalValidators = const []});
|
this.additionalValidators = const [],
|
||||||
|
this.id = 'input',
|
||||||
|
this.belowWidgets = const [],
|
||||||
|
this.hint});
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeneratedForm extends StatefulWidget {
|
class GeneratedForm extends StatefulWidget {
|
||||||
@ -89,7 +95,8 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
helperText: e.value.label + (e.value.required ? ' *' : '')),
|
helperText: e.value.label + (e.value.required ? ' *' : ''),
|
||||||
|
hintText: e.value.hint),
|
||||||
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) {
|
||||||
@ -155,7 +162,13 @@ class _GeneratedFormState extends State<GeneratedForm> {
|
|||||||
width: 20,
|
width: 20,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
rowItems.add(Expanded(child: rowInput.value));
|
rowItems.add(Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
rowInput.value,
|
||||||
|
...widget.items[rowInputs.key][rowInput.key].belowWidgets
|
||||||
|
])));
|
||||||
});
|
});
|
||||||
rows.add(rowItems);
|
rows.add(rowItems);
|
||||||
});
|
});
|
||||||
|
8
lib/custom_errors.dart
Normal file
8
lib/custom_errors.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class RateLimitError {
|
||||||
|
late int remainingMinutes;
|
||||||
|
RateLimitError(this.remainingMinutes);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'Too many requests (rate limited) - try again in $remainingMinutes minutes';
|
||||||
|
}
|
132
lib/main.dart
132
lib/main.dart
@ -1,6 +1,7 @@
|
|||||||
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/app_sources/github.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';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
@ -13,47 +14,75 @@ 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.4.0-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v0.5.2-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
|
const String bgUpdateCheckTaskName = 'bg-update-check';
|
||||||
|
|
||||||
|
bgUpdateCheck(int? ignoreAfterMicroseconds) async {
|
||||||
|
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
||||||
|
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
||||||
|
: null;
|
||||||
|
var notificationsProvider = NotificationsProvider();
|
||||||
|
await notificationsProvider.notify(checkingUpdatesNotification);
|
||||||
|
try {
|
||||||
|
var appsProvider = AppsProvider();
|
||||||
|
await notificationsProvider.cancel(ErrorCheckingUpdatesNotification('').id);
|
||||||
|
await appsProvider.loadApps();
|
||||||
|
// List<String> existingUpdateIds = // TODO: Uncomment this and below when it works
|
||||||
|
// appsProvider.getExistingUpdates(installedOnly: true);
|
||||||
|
List<String> existingUpdateIds =
|
||||||
|
appsProvider.getExistingUpdates(installedOnly: true);
|
||||||
|
DateTime nextIgnoreAfter = DateTime.now();
|
||||||
|
try {
|
||||||
|
await appsProvider.checkUpdates(ignoreAfter: ignoreAfter);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is RateLimitError) {
|
||||||
|
String nextTaskName =
|
||||||
|
'$bgUpdateCheckTaskName-${nextIgnoreAfter.microsecondsSinceEpoch.toString()}';
|
||||||
|
Workmanager().registerOneOffTask(nextTaskName, nextTaskName,
|
||||||
|
constraints: Constraints(networkType: NetworkType.connected),
|
||||||
|
initialDelay: Duration(minutes: e.remainingMinutes),
|
||||||
|
inputData: {'ignoreAfter': nextIgnoreAfter.microsecondsSinceEpoch});
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<App> newUpdates = appsProvider
|
||||||
|
.getExistingUpdates(installedOnly: true)
|
||||||
|
.where((id) => !existingUpdateIds.contains(id))
|
||||||
|
.map((e) => appsProvider.apps[e]!.app)
|
||||||
|
.toList();
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
return Future.value(true);
|
||||||
|
} catch (e) {
|
||||||
|
notificationsProvider.notify(ErrorCheckingUpdatesNotification(e.toString()),
|
||||||
|
cancelExisting: true);
|
||||||
|
return Future.error(false);
|
||||||
|
} finally {
|
||||||
|
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void bgTaskCallback() {
|
void bgTaskCallback() {
|
||||||
// Background update checking process
|
// Background process callback
|
||||||
Workmanager().executeTask((task, taskName) async {
|
Workmanager().executeTask((task, inputData) async {
|
||||||
var notificationsProvider = NotificationsProvider();
|
return await bgUpdateCheck(inputData?['ignoreAfter']);
|
||||||
await notificationsProvider.notify(checkingUpdatesNotification);
|
|
||||||
try {
|
|
||||||
var appsProvider = AppsProvider();
|
|
||||||
await notificationsProvider
|
|
||||||
.cancel(ErrorCheckingUpdatesNotification('').id);
|
|
||||||
await appsProvider.loadApps();
|
|
||||||
// List<String> existingUpdateIds = // TODO: Uncomment this and below when it works
|
|
||||||
// appsProvider.getExistingUpdates(installedOnly: true);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return Future.value(true);
|
|
||||||
} catch (e) {
|
|
||||||
notificationsProvider.notify(
|
|
||||||
ErrorCheckingUpdatesNotification(e.toString()),
|
|
||||||
cancelExisting: true);
|
|
||||||
return Future.value(false);
|
|
||||||
} finally {
|
|
||||||
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,16 +124,6 @@ class MyApp extends StatelessWidget {
|
|||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) {
|
||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
} else {
|
} else {
|
||||||
// Register the background update task according to the user's setting
|
|
||||||
if (settingsProvider.updateInterval > 0) {
|
|
||||||
Workmanager().registerPeriodicTask('bg-update-check', 'bg-update-check',
|
|
||||||
frequency: Duration(minutes: settingsProvider.updateInterval),
|
|
||||||
initialDelay: Duration(minutes: settingsProvider.updateInterval),
|
|
||||||
constraints: Constraints(networkType: NetworkType.connected),
|
|
||||||
existingWorkPolicy: ExistingWorkPolicy.replace);
|
|
||||||
} else {
|
|
||||||
Workmanager().cancelByUniqueName('bg-update-check');
|
|
||||||
}
|
|
||||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||||
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
|
||||||
@ -119,9 +138,24 @@ class MyApp extends StatelessWidget {
|
|||||||
currentReleaseTag,
|
currentReleaseTag,
|
||||||
[],
|
[],
|
||||||
0,
|
0,
|
||||||
['true'])
|
['true'],
|
||||||
|
null)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
// Register the background update task according to the user's setting
|
||||||
|
if (settingsProvider.updateInterval == 0) {
|
||||||
|
Workmanager().cancelByUniqueName(bgUpdateCheckTaskName);
|
||||||
|
} else {
|
||||||
|
Workmanager().registerPeriodicTask(
|
||||||
|
bgUpdateCheckTaskName, bgUpdateCheckTaskName,
|
||||||
|
frequency: Duration(minutes: settingsProvider.updateInterval),
|
||||||
|
initialDelay: Duration(minutes: settingsProvider.updateInterval),
|
||||||
|
constraints: Constraints(networkType: NetworkType.connected),
|
||||||
|
existingWorkPolicy: ExistingWorkPolicy.keep,
|
||||||
|
backoffPolicy: BackoffPolicy.linear,
|
||||||
|
backoffPolicyDelay:
|
||||||
|
const Duration(minutes: minUpdateIntervalMinutes));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
class GitHubStars implements MassAppSource {
|
class GitHubStars implements MassAppSource {
|
||||||
@ -10,23 +12,40 @@ class GitHubStars implements MassAppSource {
|
|||||||
@override
|
@override
|
||||||
late List<String> requiredArgs = ['Username'];
|
late List<String> requiredArgs = ['Username'];
|
||||||
|
|
||||||
@override
|
Future<List<String>> getOnePageOfUserStarredUrls(
|
||||||
Future<List<String>> getUrls(List<String> args) async {
|
String username, int page) async {
|
||||||
if (args.length != requiredArgs.length) {
|
Response res = await get(Uri.parse(
|
||||||
throw 'Wrong number of arguments provided';
|
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
||||||
}
|
|
||||||
Response res =
|
|
||||||
await get(Uri.parse('https://api.github.com/users/${args[0]}/starred'));
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
return (jsonDecode(res.body) as List<dynamic>)
|
return (jsonDecode(res.body) as List<dynamic>)
|
||||||
.map((e) => e['html_url'] as String)
|
.map((e) => e['html_url'] as String)
|
||||||
.toList();
|
.toList();
|
||||||
} else {
|
} else {
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
throw 'Rate limit reached - try again in ${(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / 60000000).toString()} minutes';
|
throw RateLimitError(
|
||||||
|
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
||||||
|
60000000)
|
||||||
|
.round());
|
||||||
}
|
}
|
||||||
|
|
||||||
throw 'Unable to find user\'s starred repos';
|
throw 'Unable to find user\'s starred repos';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getUrls(List<String> args) async {
|
||||||
|
if (args.length != requiredArgs.length) {
|
||||||
|
throw 'Wrong number of arguments provided';
|
||||||
|
}
|
||||||
|
List<String> urls = [];
|
||||||
|
var page = 1;
|
||||||
|
while (true) {
|
||||||
|
var pageUrls = await getOnePageOfUserStarredUrls(args[0], page++);
|
||||||
|
urls.addAll(pageUrls);
|
||||||
|
if (pageUrls.length < 100) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
sourceProvider
|
sourceProvider
|
||||||
.getSource(value ?? '')
|
.getSource(value ?? '')
|
||||||
.standardizeURL(
|
.standardizeURL(
|
||||||
makeUrlHttps(
|
preStandardizeUrl(
|
||||||
value ?? ''));
|
value ?? ''));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e is String
|
return e is String
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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';
|
||||||
@ -35,58 +36,84 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
appBar: settingsProvider.showAppWebpage ? AppBar() : null,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: settingsProvider.showAppWebpage
|
body: RefreshIndicator(
|
||||||
? WebView(
|
child: settingsProvider.showAppWebpage
|
||||||
initialUrl: app?.app.url,
|
? WebView(
|
||||||
javascriptMode: JavascriptMode.unrestricted,
|
initialUrl: app?.app.url,
|
||||||
)
|
javascriptMode: JavascriptMode.unrestricted,
|
||||||
: Column(
|
)
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
: CustomScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
slivers: [
|
||||||
children: [
|
SliverFillRemaining(
|
||||||
Text(
|
child: Column(
|
||||||
app?.app.name ?? 'App',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
textAlign: TextAlign.center,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
style: Theme.of(context).textTheme.displayLarge,
|
children: [
|
||||||
),
|
Text(
|
||||||
Text(
|
app?.app.name ?? 'App',
|
||||||
'By ${app?.app.author ?? 'Unknown'}',
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
style: Theme.of(context).textTheme.displayLarge,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
),
|
||||||
),
|
Text(
|
||||||
const SizedBox(
|
'By ${app?.app.author ?? 'Unknown'}',
|
||||||
height: 32,
|
textAlign: TextAlign.center,
|
||||||
),
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
GestureDetector(
|
),
|
||||||
onTap: () {
|
const SizedBox(
|
||||||
if (app?.app.url != null) {
|
height: 32,
|
||||||
launchUrlString(app?.app.url ?? '',
|
),
|
||||||
mode: LaunchMode.externalApplication);
|
GestureDetector(
|
||||||
}
|
onTap: () {
|
||||||
},
|
if (app?.app.url != null) {
|
||||||
child: Text(
|
launchUrlString(app?.app.url ?? '',
|
||||||
app?.app.url ?? '',
|
mode: LaunchMode.externalApplication);
|
||||||
textAlign: TextAlign.center,
|
}
|
||||||
style: const TextStyle(
|
},
|
||||||
decoration: TextDecoration.underline,
|
child: Text(
|
||||||
fontStyle: FontStyle.italic,
|
app?.app.url ?? '',
|
||||||
fontSize: 12),
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 12),
|
||||||
|
)),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Latest Version: ${app?.app.latestVersion ?? 'Unknown'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Installed Version: ${app?.app.installedVersion ?? 'None'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Last Update Check: ${app?.app.lastUpdateCheck == null ? 'Never' : '\n${app?.app.lastUpdateCheck?.toLocal()}'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
|
)
|
||||||
|
],
|
||||||
)),
|
)),
|
||||||
const SizedBox(
|
],
|
||||||
height: 32,
|
|
||||||
),
|
),
|
||||||
Text(
|
onRefresh: () async {
|
||||||
'Latest Version: ${app?.app.latestVersion ?? 'Unknown'}',
|
if (app != null) {
|
||||||
textAlign: TextAlign.center,
|
try {
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
await appsProvider.getUpdate(app.app.id);
|
||||||
),
|
} catch (e) {
|
||||||
Text(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
'Installed Version: ${app?.app.installedVersion ?? 'None'}',
|
SnackBar(content: Text(e.toString())),
|
||||||
textAlign: TextAlign.center,
|
);
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
}
|
||||||
),
|
}
|
||||||
],
|
}),
|
||||||
),
|
|
||||||
bottomSheet: Padding(
|
bottomSheet: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
0, 0, 0, MediaQuery.of(context).padding.bottom),
|
0, 0, 0, MediaQuery.of(context).padding.bottom),
|
||||||
@ -187,20 +214,35 @@ class _AppPageState extends State<AppPage> {
|
|||||||
onPressed: app?.downloadProgress != null
|
onPressed: app?.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog(
|
showDialog<List<String>>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: 'Additional Options',
|
title: 'Additional Options',
|
||||||
items: source
|
items: [
|
||||||
.additionalDataFormItems,
|
...source
|
||||||
|
.additionalDataFormItems,
|
||||||
|
[
|
||||||
|
GeneratedFormItem(
|
||||||
|
label: 'App Name',
|
||||||
|
required: true)
|
||||||
|
]
|
||||||
|
],
|
||||||
defaultValues: app != null
|
defaultValues: app != null
|
||||||
? app.app.additionalData
|
? [
|
||||||
: source
|
...app
|
||||||
.additionalDataDefaults);
|
.app.additionalData,
|
||||||
|
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([changedApp]);
|
appsProvider.saveApps([changedApp]);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ class AppsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class AppsPageState extends State<AppsPage> {
|
class AppsPageState extends State<AppsPage> {
|
||||||
AppsFilter? filter;
|
AppsFilter? filter;
|
||||||
|
var updatesOnlyFilter =
|
||||||
|
AppsFilter(includeUptodate: false, includeNonInstalled: false);
|
||||||
Set<String> selectedIds = {};
|
Set<String> selectedIds = {};
|
||||||
|
|
||||||
clearSelected() {
|
clearSelected() {
|
||||||
@ -45,6 +47,8 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
var settingsProvider = context.watch<SettingsProvider>();
|
var settingsProvider = context.watch<SettingsProvider>();
|
||||||
var sortedApps = appsProvider.apps.values.toList();
|
var sortedApps = appsProvider.apps.values.toList();
|
||||||
|
var currentFilterIsUpdatesOnly =
|
||||||
|
filter?.isIdenticalTo(updatesOnlyFilter) ?? false;
|
||||||
|
|
||||||
selectedIds = selectedIds
|
selectedIds = selectedIds
|
||||||
.where((element) => sortedApps.map((e) => e.app.id).contains(element))
|
.where((element) => sortedApps.map((e) => e.app.id).contains(element))
|
||||||
@ -112,6 +116,19 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
sortedApps = sortedApps.reversed.toList();
|
sortedApps = sortedApps.reversed.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingUpdateIdsAllOrSelected = appsProvider
|
||||||
|
.getExistingUpdates(installedOnly: true)
|
||||||
|
.where((element) => selectedIds.isEmpty
|
||||||
|
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
|
: selectedIds.contains(element))
|
||||||
|
.toList();
|
||||||
|
var newInstallIdsAllOrSelected = appsProvider
|
||||||
|
.getExistingUpdates(nonInstalledOnly: true)
|
||||||
|
.where((element) => selectedIds.isEmpty
|
||||||
|
? sortedApps.where((a) => a.app.id == element).isNotEmpty
|
||||||
|
: selectedIds.contains(element))
|
||||||
|
.toList();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
@ -133,8 +150,9 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
: Text(
|
: Text(
|
||||||
appsProvider.apps.isEmpty
|
appsProvider.apps.isEmpty
|
||||||
? 'No Apps'
|
? 'No Apps'
|
||||||
: 'No Search Results',
|
: 'No Apps for Filter',
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
))),
|
))),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
@ -175,154 +193,156 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
persistentFooterButtons: [
|
persistentFooterButtons: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
selectedIds.isEmpty
|
selectedIds.isEmpty
|
||||||
? selectThese(sortedApps.map((e) => e.app.id).toList())
|
? selectThese(sortedApps.map((e) => e.app.id).toList())
|
||||||
: clearSelected();
|
: clearSelected();
|
||||||
},
|
},
|
||||||
icon: Icon(selectedIds.isEmpty
|
icon: Icon(
|
||||||
? Icons.select_all_outlined
|
selectedIds.isEmpty
|
||||||
: Icons.deselect_outlined),
|
? Icons.select_all_outlined
|
||||||
label: Text(selectedIds.isEmpty
|
: Icons.deselect_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
tooltip: selectedIds.isEmpty
|
||||||
? 'Select All'
|
? 'Select All'
|
||||||
: 'Deselect ${selectedIds.length.toString()}')),
|
: 'Deselect ${selectedIds.length.toString()}'),
|
||||||
const VerticalDivider(),
|
const VerticalDivider(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: selectedIds.isEmpty
|
child: Row(
|
||||||
? Container()
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
: Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
selectedIds.isEmpty
|
||||||
children: [
|
? const SizedBox()
|
||||||
IconButton(
|
: IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog<List<String>?>(
|
showDialog<List<String>?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext ctx) {
|
builder: (BuildContext ctx) {
|
||||||
return GeneratedFormModal(
|
return GeneratedFormModal(
|
||||||
title: 'Remove Selected Apps?',
|
title: 'Remove Selected Apps?',
|
||||||
items: const [],
|
items: const [],
|
||||||
defaultValues: const [],
|
defaultValues: const [],
|
||||||
initValid: true,
|
initValid: true,
|
||||||
message:
|
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.',
|
'${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) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
appsProvider.removeApps(selectedIds.toList());
|
appsProvider.removeApps(selectedIds.toList());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tooltip: 'Remove Selected Apps',
|
tooltip: 'Remove Selected Apps',
|
||||||
icon: const Icon(Icons.delete_outline_outlined),
|
icon: const Icon(Icons.delete_outline_outlined),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
onPressed: appsProvider.areDownloadsRunning() ||
|
onPressed: appsProvider.areDownloadsRunning() ||
|
||||||
selectedIds
|
(existingUpdateIdsAllOrSelected.isEmpty &&
|
||||||
.where((id) =>
|
newInstallIdsAllOrSelected.isEmpty)
|
||||||
appsProvider.apps[id]!.app
|
? null
|
||||||
.installedVersion !=
|
: () {
|
||||||
appsProvider
|
HapticFeedback.heavyImpact();
|
||||||
.apps[id]!.app.latestVersion)
|
List<List<GeneratedFormItem>> formInputs = [];
|
||||||
.isEmpty
|
if (existingUpdateIdsAllOrSelected.isNotEmpty &&
|
||||||
? null
|
newInstallIdsAllOrSelected.isNotEmpty) {
|
||||||
: () {
|
formInputs.add([
|
||||||
HapticFeedback.heavyImpact();
|
GeneratedFormItem(
|
||||||
var existingUpdateIdsSelected =
|
label:
|
||||||
appsProvider
|
'Update ${existingUpdateIdsAllOrSelected.length} App${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'}',
|
||||||
.getExistingUpdates(
|
type: FormItemType.bool)
|
||||||
installedOnly: true)
|
]);
|
||||||
.where((element) =>
|
formInputs.add([
|
||||||
selectedIds.contains(element))
|
GeneratedFormItem(
|
||||||
.toList();
|
label:
|
||||||
var newInstallIdsSelected = appsProvider
|
'Install ${newInstallIdsAllOrSelected.length} new App${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}',
|
||||||
.getExistingUpdates(
|
type: FormItemType.bool)
|
||||||
nonInstalledOnly: true)
|
]);
|
||||||
.where((element) =>
|
}
|
||||||
selectedIds.contains(element))
|
showDialog<List<String>?>(
|
||||||
.toList();
|
context: context,
|
||||||
List<List<GeneratedFormItem>> formInputs =
|
builder: (BuildContext ctx) {
|
||||||
[];
|
return GeneratedFormModal(
|
||||||
if (existingUpdateIdsSelected
|
title:
|
||||||
.isNotEmpty &&
|
'Install${selectedIds.isEmpty ? ' ' : ' Selected '}Apps?',
|
||||||
newInstallIdsSelected.isNotEmpty) {
|
message:
|
||||||
formInputs.add([
|
'${existingUpdateIdsAllOrSelected.length} update${existingUpdateIdsAllOrSelected.length == 1 ? '' : 's'} and ${newInstallIdsAllOrSelected.length} new install${newInstallIdsAllOrSelected.length == 1 ? '' : 's'}.',
|
||||||
GeneratedFormItem(
|
items: formInputs,
|
||||||
label:
|
defaultValues: const ['true', 'true'],
|
||||||
'Update ${existingUpdateIdsSelected.length} Apps?',
|
initValid: true,
|
||||||
type: FormItemType.bool)
|
);
|
||||||
]);
|
}).then((values) {
|
||||||
formInputs.add([
|
if (values != null) {
|
||||||
GeneratedFormItem(
|
bool shouldInstallUpdates =
|
||||||
label:
|
values.length < 2 || values[0] == 'true';
|
||||||
'Install ${newInstallIdsSelected.length} new Apps?',
|
bool shouldInstallNew =
|
||||||
type: FormItemType.bool)
|
values.length < 2 || values[1] == 'true';
|
||||||
]);
|
settingsProvider
|
||||||
}
|
.getInstallPermission()
|
||||||
showDialog<List<String>?>(
|
.then((_) {
|
||||||
context: context,
|
List<String> toInstall = [];
|
||||||
builder: (BuildContext ctx) {
|
if (shouldInstallUpdates) {
|
||||||
return GeneratedFormModal(
|
toInstall
|
||||||
title: 'Install Selected Apps?',
|
.addAll(existingUpdateIdsAllOrSelected);
|
||||||
message:
|
}
|
||||||
'${existingUpdateIdsSelected.length} update${existingUpdateIdsSelected.length == 1 ? '' : 's'} and ${newInstallIdsSelected.length} new install${newInstallIdsSelected.length == 1 ? '' : 's'}.',
|
if (shouldInstallNew) {
|
||||||
items: formInputs,
|
toInstall
|
||||||
defaultValues: const [
|
.addAll(newInstallIdsAllOrSelected);
|
||||||
'true',
|
}
|
||||||
'true'
|
appsProvider.downloadAndInstallLatestApp(
|
||||||
],
|
toInstall, context);
|
||||||
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:
|
||||||
},
|
'Install/Update${selectedIds.isEmpty ? ' ' : ' Selected '}Apps',
|
||||||
tooltip: 'Share Selected App URLs',
|
icon: const Icon(
|
||||||
icon: const Icon(Icons.share),
|
Icons.file_download_outlined,
|
||||||
),
|
)),
|
||||||
],
|
selectedIds.isEmpty
|
||||||
)),
|
? const SizedBox()
|
||||||
|
: 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(),
|
const VerticalDivider(),
|
||||||
|
IconButton(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
if (currentFilterIsUpdatesOnly) {
|
||||||
|
filter = null;
|
||||||
|
} else {
|
||||||
|
filter = updatesOnlyFilter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: currentFilterIsUpdatesOnly
|
||||||
|
? 'Remove Out-of-Date App Filter'
|
||||||
|
: 'Show Out-of-Date Apps Only',
|
||||||
|
icon: Icon(
|
||||||
|
currentFilterIsUpdatesOnly
|
||||||
|
? Icons.update_disabled_rounded
|
||||||
|
: Icons.update_rounded,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
appsProvider.apps.isEmpty
|
appsProvider.apps.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: TextButton.icon(
|
: TextButton.icon(
|
||||||
@ -368,10 +388,6 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
filter = null;
|
filter = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
filter = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -40,7 +40,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
|
|
||||||
Future<List<List<String>>> addApps(List<String> urls) async {
|
Future<List<List<String>>> addApps(List<String> urls) async {
|
||||||
await settingsProvider.getInstallPermission();
|
await settingsProvider.getInstallPermission();
|
||||||
List<dynamic> results = await sourceProvider.getApps(urls);
|
List<dynamic> results = await sourceProvider.getApps(urls,
|
||||||
|
ignoreUrls: appsProvider.apps.values.map((e) => e.app.url).toList());
|
||||||
List<App> apps = results[0];
|
List<App> apps = results[0];
|
||||||
Map<String, dynamic> errorsMap = results[1];
|
Map<String, dynamic> errorsMap = results[1];
|
||||||
for (var app in apps) {
|
for (var app in apps) {
|
||||||
@ -266,42 +267,67 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
);
|
);
|
||||||
}).then((values) {
|
}).then((values) {
|
||||||
if (values != null) {
|
if (values != null) {
|
||||||
|
setState(() {
|
||||||
|
importInProgress = true;
|
||||||
|
});
|
||||||
source
|
source
|
||||||
.getUrls(values)
|
.getUrls(values)
|
||||||
.then((urls) {
|
.then((urls) {
|
||||||
setState(() {
|
showDialog<List<String>?>(
|
||||||
importInProgress = true;
|
|
||||||
});
|
|
||||||
addApps(urls)
|
|
||||||
.then((errors) {
|
|
||||||
if (errors.isEmpty) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context)
|
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Imported ${urls.length} Apps')),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
context: context,
|
||||||
builder:
|
builder:
|
||||||
(BuildContext
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
return ImportErrorDialog(
|
return UrlSelectionModal(
|
||||||
urlsLength: urls
|
urls: urls);
|
||||||
.length,
|
})
|
||||||
errors:
|
.then((selectedUrls) {
|
||||||
errors);
|
if (selectedUrls !=
|
||||||
});
|
null) {
|
||||||
|
addApps(selectedUrls)
|
||||||
|
.then((errors) {
|
||||||
|
if (errors
|
||||||
|
.isEmpty) {
|
||||||
|
ScaffoldMessenger
|
||||||
|
.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Imported ${selectedUrls.length} Apps')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showDialog(
|
||||||
|
context:
|
||||||
|
context,
|
||||||
|
builder:
|
||||||
|
(BuildContext
|
||||||
|
ctx) {
|
||||||
|
return ImportErrorDialog(
|
||||||
|
urlsLength:
|
||||||
|
selectedUrls
|
||||||
|
.length,
|
||||||
|
errors:
|
||||||
|
errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).whenComplete(() {
|
||||||
|
setState(() {
|
||||||
|
importInProgress =
|
||||||
|
false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
importInProgress =
|
||||||
|
false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).whenComplete(() {
|
|
||||||
setState(() {
|
|
||||||
importInProgress =
|
|
||||||
false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
|
setState(() {
|
||||||
|
importInProgress =
|
||||||
|
false;
|
||||||
|
});
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context)
|
context)
|
||||||
.showSnackBar(
|
.showSnackBar(
|
||||||
@ -375,3 +401,67 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class UrlSelectionModal extends StatefulWidget {
|
||||||
|
UrlSelectionModal({super.key, required this.urls});
|
||||||
|
|
||||||
|
List<String> urls;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UrlSelectionModal> createState() => _UrlSelectionModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||||
|
Map<String, bool> urlSelections = {};
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
for (var url in widget.urls) {
|
||||||
|
urlSelections.putIfAbsent(url, () => true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: const Text('Select URLs to Import'),
|
||||||
|
content: Column(children: [
|
||||||
|
...urlSelections.keys.map((url) {
|
||||||
|
return Row(children: [
|
||||||
|
Checkbox(
|
||||||
|
value: urlSelections[url],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
urlSelections[url] = value ?? false;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
Uri.parse(url).path.substring(1),
|
||||||
|
))
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(urlSelections.keys
|
||||||
|
.where((url) => urlSelections[url] ?? false)
|
||||||
|
.toList());
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Import ${urlSelections.values.where((b) => b).length} URLs'))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/components/custom_app_bar.dart';
|
import 'package:obtainium/components/custom_app_bar.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -15,6 +17,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
|
SourceProvider sourceProvider = SourceProvider();
|
||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) {
|
||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
}
|
}
|
||||||
@ -22,8 +25,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
body: CustomScrollView(slivers: <Widget>[
|
body: CustomScrollView(slivers: <Widget>[
|
||||||
const CustomAppBar(title: 'Settings'),
|
const CustomAppBar(title: 'Settings'),
|
||||||
SliverFillRemaining(
|
SliverToBoxAdapter(
|
||||||
hasScrollBody: true,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: settingsProvider.prefs == null
|
child: settingsProvider.prefs == null
|
||||||
@ -160,7 +162,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
height: 16,
|
height: 16,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'More',
|
'Updates',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.primary),
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
@ -169,68 +171,108 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
labelText:
|
labelText:
|
||||||
'Background Update Checking Interval'),
|
'Background Update Checking Interval'),
|
||||||
value: settingsProvider.updateInterval,
|
value: settingsProvider.updateInterval,
|
||||||
items: const [
|
items: updateIntervals.map((e) {
|
||||||
DropdownMenuItem(
|
int displayNum = (e < 60
|
||||||
value: 15,
|
? e
|
||||||
child: Text('15 Minutes'),
|
: e < 1440
|
||||||
),
|
? e / 60
|
||||||
DropdownMenuItem(
|
: e / 1440)
|
||||||
value: 30,
|
.round();
|
||||||
child: Text('30 Minutes'),
|
var displayUnit = (e < 60
|
||||||
),
|
? 'Minute'
|
||||||
DropdownMenuItem(
|
: e < 1440
|
||||||
value: 60,
|
? 'Hour'
|
||||||
child: Text('1 Hour'),
|
: 'Day');
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
String display = e == 0
|
||||||
value: 360,
|
? 'Never - Manual Only'
|
||||||
child: Text('6 Hours'),
|
: '$displayNum $displayUnit${displayNum == 1 ? '' : 's'}';
|
||||||
),
|
return DropdownMenuItem(
|
||||||
DropdownMenuItem(
|
value: e, child: Text(display));
|
||||||
value: 720,
|
}).toList(),
|
||||||
child: Text('12 Hours'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 1440,
|
|
||||||
child: Text('1 Day'),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 0,
|
|
||||||
child: Text('Never - Manual Only'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
settingsProvider.updateInterval = value;
|
settingsProvider.updateInterval = value;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
const Spacer(),
|
const SizedBox(
|
||||||
Row(
|
height: 8,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
TextButton.icon(
|
|
||||||
style: ButtonStyle(
|
|
||||||
foregroundColor:
|
|
||||||
MaterialStateProperty.resolveWith<
|
|
||||||
Color>((Set<MaterialState> states) {
|
|
||||||
return Colors.grey;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
launchUrlString(settingsProvider.sourceUrl,
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.code),
|
|
||||||
label: Text(
|
|
||||||
'Source',
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
'Longer intervals recommended for large App collections',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelMedium!
|
||||||
|
.merge(const TextStyle(
|
||||||
|
fontStyle: FontStyle.italic)),
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Source-Specific',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
|
),
|
||||||
|
...sourceProvider.sources.map((e) {
|
||||||
|
if (e.moreSourceSettingsFormItems.isNotEmpty) {
|
||||||
|
return GeneratedForm(
|
||||||
|
items: e.moreSourceSettingsFormItems
|
||||||
|
.map((e) => [e])
|
||||||
|
.toList(),
|
||||||
|
onValueChanges: (values, valid) {
|
||||||
|
if (valid) {
|
||||||
|
for (var i = 0;
|
||||||
|
i < values.length;
|
||||||
|
i++) {
|
||||||
|
settingsProvider.setSettingString(
|
||||||
|
e.moreSourceSettingsFormItems[i]
|
||||||
|
.id,
|
||||||
|
values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultValues:
|
||||||
|
e.moreSourceSettingsFormItems.map((e) {
|
||||||
|
return settingsProvider
|
||||||
|
.getSettingString(e.id) ??
|
||||||
|
'';
|
||||||
|
}).toList());
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
)))
|
))),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
style: ButtonStyle(
|
||||||
|
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(Set<MaterialState> states) {
|
||||||
|
return Colors.grey;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
launchUrlString(settingsProvider.sourceUrl,
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.code),
|
||||||
|
label: Text(
|
||||||
|
'Source',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,16 +293,32 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
await saveApps([newApp]);
|
await saveApps([newApp]);
|
||||||
return newApp;
|
return newApp;
|
||||||
|
} else if ((newApp.lastUpdateCheck?.microsecondsSinceEpoch ?? 0) -
|
||||||
|
(currentApp.lastUpdateCheck?.microsecondsSinceEpoch ?? 0) >
|
||||||
|
5000000) {
|
||||||
|
currentApp.lastUpdateCheck = newApp.lastUpdateCheck;
|
||||||
|
await saveApps([currentApp]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<App>> checkUpdates() async {
|
Future<List<App>> checkUpdates({DateTime? ignoreAfter}) async {
|
||||||
List<App> updates = [];
|
List<App> updates = [];
|
||||||
if (!gettingUpdates) {
|
if (!gettingUpdates) {
|
||||||
gettingUpdates = true;
|
gettingUpdates = true;
|
||||||
|
|
||||||
List<String> appIds = apps.keys.toList();
|
List<String> appIds = apps.keys.toList();
|
||||||
|
if (ignoreAfter != null) {
|
||||||
|
appIds = appIds
|
||||||
|
.where((id) =>
|
||||||
|
apps[id]!.app.lastUpdateCheck == null ||
|
||||||
|
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++) {
|
for (int i = 0; i < appIds.length; i++) {
|
||||||
App? newApp = await getUpdate(appIds[i]);
|
App? newApp = await getUpdate(appIds[i]);
|
||||||
if (newApp != null) {
|
if (newApp != null) {
|
||||||
|
@ -13,6 +13,16 @@ enum SortColumnSettings { added, nameAuthor, authorName }
|
|||||||
|
|
||||||
enum SortOrderSettings { ascending, descending }
|
enum SortOrderSettings { ascending, descending }
|
||||||
|
|
||||||
|
const maxAPIRateLimitMinutes = 30;
|
||||||
|
const minUpdateIntervalMinutes = maxAPIRateLimitMinutes + 30;
|
||||||
|
const maxUpdateIntervalMinutes = 4320;
|
||||||
|
List<int> updateIntervals = [15, 30, 60, 120, 180, 360, 720, 1440, 4320, 0]
|
||||||
|
.where((element) =>
|
||||||
|
(element >= minUpdateIntervalMinutes &&
|
||||||
|
element <= maxUpdateIntervalMinutes) ||
|
||||||
|
element == 0)
|
||||||
|
.toList();
|
||||||
|
|
||||||
class SettingsProvider with ChangeNotifier {
|
class SettingsProvider with ChangeNotifier {
|
||||||
SharedPreferences? prefs;
|
SharedPreferences? prefs;
|
||||||
|
|
||||||
@ -45,7 +55,17 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int get updateInterval {
|
int get updateInterval {
|
||||||
return prefs?.getInt('updateInterval') ?? 1440;
|
var min = prefs?.getInt('updateInterval') ?? 180;
|
||||||
|
if (!updateIntervals.contains(min)) {
|
||||||
|
var temp = updateIntervals[0];
|
||||||
|
for (var i in updateIntervals) {
|
||||||
|
if (min > i && i != 0) {
|
||||||
|
temp = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
min = temp;
|
||||||
|
}
|
||||||
|
return min;
|
||||||
}
|
}
|
||||||
|
|
||||||
set updateInterval(int min) {
|
set updateInterval(int min) {
|
||||||
@ -95,11 +115,19 @@ class SettingsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get showAppWebpage {
|
bool get showAppWebpage {
|
||||||
return prefs?.getBool('showAppWebpage') ?? true;
|
return prefs?.getBool('showAppWebpage') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set showAppWebpage(bool show) {
|
set showAppWebpage(bool show) {
|
||||||
prefs?.setBool('showAppWebpage', show);
|
prefs?.setBool('showAppWebpage', show);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? getSettingString(String settingId) {
|
||||||
|
return prefs?.getString(settingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSettingString(String settingId, String value) {
|
||||||
|
prefs?.setString(settingId, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ class App {
|
|||||||
List<String> apkUrls = [];
|
List<String> apkUrls = [];
|
||||||
late int preferredApkIndex;
|
late int preferredApkIndex;
|
||||||
late List<String> additionalData;
|
late List<String> additionalData;
|
||||||
|
late DateTime? lastUpdateCheck;
|
||||||
App(
|
App(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
@ -47,7 +48,8 @@ class App {
|
|||||||
this.latestVersion,
|
this.latestVersion,
|
||||||
this.apkUrls,
|
this.apkUrls,
|
||||||
this.preferredApkIndex,
|
this.preferredApkIndex,
|
||||||
this.additionalData);
|
this.additionalData,
|
||||||
|
this.lastUpdateCheck);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -69,7 +71,10 @@ class App {
|
|||||||
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
|
json['preferredApkIndex'] == null ? 0 : json['preferredApkIndex'] as int,
|
||||||
json['additionalData'] == null
|
json['additionalData'] == null
|
||||||
? SourceProvider().getSource(json['url']).additionalDataDefaults
|
? SourceProvider().getSource(json['url']).additionalDataDefaults
|
||||||
: List<String>.from(jsonDecode(json['additionalData'])));
|
: List<String>.from(jsonDecode(json['additionalData'])),
|
||||||
|
json['lastUpdateCheck'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']));
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
@ -80,7 +85,8 @@ class App {
|
|||||||
'latestVersion': latestVersion,
|
'latestVersion': latestVersion,
|
||||||
'apkUrls': jsonEncode(apkUrls),
|
'apkUrls': jsonEncode(apkUrls),
|
||||||
'preferredApkIndex': preferredApkIndex,
|
'preferredApkIndex': preferredApkIndex,
|
||||||
'additionalData': jsonEncode(additionalData)
|
'additionalData': jsonEncode(additionalData),
|
||||||
|
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +96,7 @@ escapeRegEx(String s) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeUrlHttps(String url) {
|
preStandardizeUrl(String url) {
|
||||||
if (url.toLowerCase().indexOf('http://') != 0 &&
|
if (url.toLowerCase().indexOf('http://') != 0 &&
|
||||||
url.toLowerCase().indexOf('https://') != 0) {
|
url.toLowerCase().indexOf('https://') != 0) {
|
||||||
url = 'https://$url';
|
url = 'https://$url';
|
||||||
@ -129,6 +135,7 @@ abstract class AppSource {
|
|||||||
AppNames getAppNames(String standardUrl);
|
AppNames getAppNames(String standardUrl);
|
||||||
late List<List<GeneratedFormItem>> additionalDataFormItems;
|
late List<List<GeneratedFormItem>> additionalDataFormItems;
|
||||||
late List<String> additionalDataDefaults;
|
late List<String> additionalDataDefaults;
|
||||||
|
late List<GeneratedFormItem> moreSourceSettingsFormItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MassAppSource {
|
abstract class MassAppSource {
|
||||||
@ -153,7 +160,7 @@ class SourceProvider {
|
|||||||
List<MassAppSource> massSources = [GitHubStars()];
|
List<MassAppSource> massSources = [GitHubStars()];
|
||||||
|
|
||||||
AppSource getSource(String url) {
|
AppSource getSource(String url) {
|
||||||
url = makeUrlHttps(url);
|
url = preStandardizeUrl(url);
|
||||||
AppSource? source;
|
AppSource? source;
|
||||||
for (var s in sources) {
|
for (var s in sources) {
|
||||||
if (url.toLowerCase().contains('://${s.host}')) {
|
if (url.toLowerCase().contains('://${s.host}')) {
|
||||||
@ -180,7 +187,7 @@ class SourceProvider {
|
|||||||
|
|
||||||
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
Future<App> getApp(AppSource source, String url, List<String> additionalData,
|
||||||
{String customName = ''}) async {
|
{String customName = ''}) async {
|
||||||
String standardUrl = source.standardizeURL(makeUrlHttps(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);
|
||||||
@ -195,15 +202,17 @@ class SourceProvider {
|
|||||||
apk.version,
|
apk.version,
|
||||||
apk.apkUrls,
|
apk.apkUrls,
|
||||||
apk.apkUrls.length - 1,
|
apk.apkUrls.length - 1,
|
||||||
additionalData);
|
additionalData,
|
||||||
|
DateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a length 2 list, where the first element is a list of Apps and
|
/// Returns a length 2 list, where the first element is a list of Apps and
|
||||||
/// the second is a Map<String, dynamic> of URLs and errors
|
/// the second is a Map<String, dynamic> of URLs and errors
|
||||||
Future<List<dynamic>> getApps(List<String> urls) async {
|
Future<List<dynamic>> getApps(List<String> urls,
|
||||||
|
{List<String> ignoreUrls = const []}) async {
|
||||||
List<App> apps = [];
|
List<App> apps = [];
|
||||||
Map<String, dynamic> errors = {};
|
Map<String, dynamic> errors = {};
|
||||||
for (var url in urls) {
|
for (var url in urls.where((element) => !ignoreUrls.contains(element))) {
|
||||||
try {
|
try {
|
||||||
var source = getSource(url);
|
var source = getSource(url);
|
||||||
apps.add(await getApp(source, url, source.additionalDataDefaults));
|
apps.add(await getApp(source, url, source.additionalDataDefaults));
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -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.2.0"
|
version: "5.2.0+1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -386,7 +386,7 @@ packages:
|
|||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.5"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -400,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.1"
|
version: "10.0.2"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -421,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.1"
|
version: "3.8.0"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -629,7 +629,7 @@ packages:
|
|||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5"
|
version: "6.1.6"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -664,7 +664,7 @@ packages:
|
|||||||
name: url_launcher_platform_interface
|
name: url_launcher_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
url_launcher_web:
|
url_launcher_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -699,21 +699,21 @@ 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.2"
|
version: "2.10.3"
|
||||||
webview_flutter_platform_interface:
|
webview_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_platform_interface
|
name: webview_flutter_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.3"
|
version: "1.9.5"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.4"
|
version: "2.9.5"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.4.0+19 # When changing this, update the tag in main() accordingly
|
version: 0.5.2+23 # 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'
|
||||||
|
Reference in New Issue
Block a user