mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
17 Commits
v0.7.0-bet
...
v0.7.4-bet
Author | SHA1 | Date | |
---|---|---|---|
a4bc278e4c | |||
b04986622b | |||
2059e4fd44 | |||
618a1523cf | |||
ba1cdc2c73 | |||
aa2a25fffe | |||
c8ec67aef3 | |||
9576a99a4e | |||
0202224fa6 | |||
631ffd5c34 | |||
feed7ffc0b | |||
296485de8a | |||
d2f226d442 | |||
cbdb449e35 | |||
3100a3a08c | |||
18951d6461 | |||
0e0a39a40f |
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
@ -28,41 +30,24 @@ class FDroid extends AppSource {
|
|||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
String? tryInferringAppId(String standardUrl) {
|
||||||
|
return Uri.parse(standardUrl).pathSegments.last;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Response res, String apkUrlPrefix) {
|
||||||
String standardUrl, List<String> additionalData) async {
|
|
||||||
Response res = await get(Uri.parse(standardUrl));
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var releases = parse(res.body).querySelectorAll('.package-version');
|
List<dynamic> releases = jsonDecode(res.body)['packages'] ?? [];
|
||||||
if (releases.isEmpty) {
|
if (releases.isEmpty) {
|
||||||
throw NoReleasesError();
|
throw NoReleasesError();
|
||||||
}
|
}
|
||||||
String? latestVersion = releases[0]
|
String? latestVersion = releases[0]['versionName'];
|
||||||
.querySelector('.package-version-header b')
|
|
||||||
?.innerHtml
|
|
||||||
.split(' ')
|
|
||||||
.sublist(1)
|
|
||||||
.join(' ');
|
|
||||||
if (latestVersion == null) {
|
if (latestVersion == null) {
|
||||||
throw NoVersionError();
|
throw NoVersionError();
|
||||||
}
|
}
|
||||||
List<String> apkUrls = releases
|
List<String> apkUrls = releases
|
||||||
.where((element) =>
|
.where((element) => element['versionName'] == latestVersion)
|
||||||
element
|
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
|
||||||
.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();
|
.toList();
|
||||||
if (apkUrls.isEmpty) {
|
if (apkUrls.isEmpty) {
|
||||||
throw NoAPKError();
|
throw NoAPKError();
|
||||||
@ -73,6 +58,15 @@ class FDroid extends AppSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl, List<String> additionalData) async {
|
||||||
|
String? appId = tryInferringAppId(standardUrl);
|
||||||
|
return getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
|
await get(Uri.parse('https://f-droid.org/api/v1/packages/$appId')),
|
||||||
|
'https://f-droid.org/repo/$appId');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AppNames getAppNames(String standardUrl) {
|
AppNames getAppNames(String standardUrl) {
|
||||||
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
return AppNames('F-Droid', Uri.parse(standardUrl).pathSegments.last);
|
||||||
|
@ -105,9 +105,6 @@ class GitHub extends AppSource {
|
|||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/releases';
|
'$standardUrl/releases';
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
@ -167,14 +164,8 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
return APKDetails(version, targetRelease['apkUrls']);
|
return APKDetails(version, targetRelease['apkUrls']);
|
||||||
} else {
|
} else {
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
rateLimitErrorCheck(res);
|
||||||
throw RateLimitError(
|
throw getObtainiumHttpError(res);
|
||||||
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
|
||||||
60000000)
|
|
||||||
.round());
|
|
||||||
}
|
|
||||||
|
|
||||||
throw NoReleasesError();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,23 +177,31 @@ class GitHub extends AppSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> search(String query) async {
|
Future<Map<String, String>> search(String query) async {
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100'));
|
'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
return (jsonDecode(res.body)['items'] as List<dynamic>)
|
Map<String, String> urlsWithDescriptions = {};
|
||||||
.map((e) => e['html_url'] as String)
|
for (var e in (jsonDecode(res.body)['items'] as List<dynamic>)) {
|
||||||
.toList();
|
urlsWithDescriptions.addAll({
|
||||||
} else {
|
e['html_url'] as String: e['description'] != null
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
? e['description'] as String
|
||||||
throw RateLimitError(
|
: 'No description'
|
||||||
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
});
|
||||||
60000000)
|
|
||||||
.round());
|
|
||||||
}
|
}
|
||||||
throw ObtainiumError(
|
return urlsWithDescriptions;
|
||||||
res.reasonPhrase ?? 'Error ${res.statusCode.toString()}',
|
} else {
|
||||||
unexpected: true);
|
rateLimitErrorCheck(res);
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimitErrorCheck(Response res) {
|
||||||
|
if (res.headers['x-ratelimit-remaining'] == '0') {
|
||||||
|
throw RateLimitError(
|
||||||
|
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
||||||
|
60000000)
|
||||||
|
.round());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,6 @@ class GitLab extends AppSource {
|
|||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'$standardUrl/-/releases';
|
'$standardUrl/-/releases';
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:obtainium/app_sources/fdroid.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -22,41 +23,18 @@ class IzzyOnDroid extends AppSource {
|
|||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
String? tryInferringAppId(String standardUrl) {
|
||||||
|
return FDroid().tryInferringAppId(standardUrl);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
Response res = await get(Uri.parse(standardUrl));
|
String? appId = tryInferringAppId(standardUrl);
|
||||||
if (res.statusCode == 200) {
|
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
|
||||||
var parsedHtml = parse(res.body);
|
await get(
|
||||||
var multipleVersionApkUrls = parsedHtml
|
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')),
|
||||||
.querySelectorAll('a')
|
'https://android.izzysoft.de/frepo/$appId');
|
||||||
.where((element) =>
|
|
||||||
element.attributes['href']?.toLowerCase().endsWith('.apk') ??
|
|
||||||
false)
|
|
||||||
.map((e) => 'https://$host${e.attributes['href'] ?? ''}')
|
|
||||||
.toList();
|
|
||||||
if (multipleVersionApkUrls.isEmpty) {
|
|
||||||
throw NoAPKError();
|
|
||||||
}
|
|
||||||
var version = parsedHtml
|
|
||||||
.querySelector('#keydata')
|
|
||||||
?.querySelectorAll('b')
|
|
||||||
.where(
|
|
||||||
(element) => element.innerHtml.toLowerCase().contains('version'))
|
|
||||||
.toList()[0]
|
|
||||||
.parentNode
|
|
||||||
?.parentNode
|
|
||||||
?.children[1]
|
|
||||||
.innerHtml;
|
|
||||||
if (version == null) {
|
|
||||||
throw NoVersionError();
|
|
||||||
}
|
|
||||||
return APKDetails(version, [multipleVersionApkUrls[0]]);
|
|
||||||
} else {
|
|
||||||
throw NoReleasesError();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -22,9 +22,6 @@ class Mullvad extends AppSource {
|
|||||||
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
String? changeLogPageFromStandardUrl(String standardUrl) =>
|
||||||
'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md';
|
'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md';
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
|
@ -16,9 +16,6 @@ class Signal extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
|
@ -21,9 +21,6 @@ class SourceForge extends AppSource {
|
|||||||
@override
|
@override
|
||||||
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
String? changeLogPageFromStandardUrl(String standardUrl) => null;
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKDetails(
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
String standardUrl, List<String> additionalData) async {
|
String standardUrl, List<String> additionalData) async {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ObtainiumError {
|
class ObtainiumError {
|
||||||
late String message;
|
late String message;
|
||||||
@ -75,6 +77,8 @@ class MultiAppMultiError extends ObtainiumError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showError(dynamic e, BuildContext context) {
|
showError(dynamic e, BuildContext context) {
|
||||||
|
Provider.of<LogsProvider>(context, listen: false)
|
||||||
|
.add(e.toString(), level: LogLevels.error);
|
||||||
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
if (e is String || (e is ObtainiumError && !e.unexpected)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(e.toString())),
|
SnackBar(content: Text(e.toString())),
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter/services.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';
|
||||||
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
@ -15,7 +16,7 @@ import 'package:dynamic_color/dynamic_color.dart';
|
|||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
|
||||||
|
|
||||||
const String currentVersion = '0.7.0';
|
const String currentVersion = '0.7.4';
|
||||||
const String currentReleaseTag =
|
const String currentReleaseTag =
|
||||||
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
|
||||||
|
|
||||||
@ -23,12 +24,15 @@ const int bgUpdateCheckAlarmId = 666;
|
|||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
||||||
|
LogsProvider logs = LogsProvider();
|
||||||
|
logs.add('Started BG update check task');
|
||||||
int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds'];
|
int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds'];
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await AndroidAlarmManager.initialize();
|
await AndroidAlarmManager.initialize();
|
||||||
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
DateTime? ignoreAfter = ignoreAfterMicroseconds != null
|
||||||
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds)
|
||||||
: null;
|
: null;
|
||||||
|
logs.add('Bg update ignoreAfter is $ignoreAfter');
|
||||||
var notificationsProvider = NotificationsProvider();
|
var notificationsProvider = NotificationsProvider();
|
||||||
await notificationsProvider.notify(checkingUpdatesNotification);
|
await notificationsProvider.notify(checkingUpdatesNotification);
|
||||||
try {
|
try {
|
||||||
@ -40,17 +44,18 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
DateTime nextIgnoreAfter = DateTime.now();
|
DateTime nextIgnoreAfter = DateTime.now();
|
||||||
String? err;
|
String? err;
|
||||||
try {
|
try {
|
||||||
|
logs.add('Started actual BG update checking');
|
||||||
await appsProvider.checkUpdates(
|
await appsProvider.checkUpdates(
|
||||||
ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true);
|
ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is RateLimitError || e is SocketException) {
|
if (e is RateLimitError || e is SocketException) {
|
||||||
AndroidAlarmManager.oneShot(
|
var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15;
|
||||||
Duration(minutes: e is RateLimitError ? e.remainingMinutes : 15),
|
logs.add(
|
||||||
Random().nextInt(pow(2, 31) as int),
|
'BG update checking encountered a ${e.runtimeType}, will schedule a retry check in $remainingMinutes minutes');
|
||||||
bgUpdateCheck,
|
AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes),
|
||||||
params: {
|
Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: {
|
||||||
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
err = e.toString();
|
err = e.toString();
|
||||||
}
|
}
|
||||||
@ -74,7 +79,8 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
// silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()),
|
// silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()),
|
||||||
// cancelExisting: true);
|
// cancelExisting: true);
|
||||||
// }
|
// }
|
||||||
|
logs.add(
|
||||||
|
'BG update checking found ${newUpdates.length} updates - will notify user if needed');
|
||||||
if (newUpdates.isNotEmpty) {
|
if (newUpdates.isNotEmpty) {
|
||||||
notificationsProvider.notify(UpdateNotification(newUpdates));
|
notificationsProvider.notify(UpdateNotification(newUpdates));
|
||||||
}
|
}
|
||||||
@ -85,6 +91,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
|
|||||||
notificationsProvider
|
notificationsProvider
|
||||||
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
.notify(ErrorCheckingUpdatesNotification(e.toString()));
|
||||||
} finally {
|
} finally {
|
||||||
|
logs.add('Finished BG update check task');
|
||||||
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
await notificationsProvider.cancel(checkingUpdatesNotification.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +109,8 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
ChangeNotifierProvider(create: (context) => AppsProvider()),
|
||||||
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
ChangeNotifierProvider(create: (context) => SettingsProvider()),
|
||||||
Provider(create: (context) => NotificationsProvider())
|
Provider(create: (context) => NotificationsProvider()),
|
||||||
|
Provider(create: (context) => LogsProvider())
|
||||||
],
|
],
|
||||||
child: const Obtainium(),
|
child: const Obtainium(),
|
||||||
));
|
));
|
||||||
@ -124,17 +132,19 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
SettingsProvider settingsProvider = context.watch<SettingsProvider>();
|
||||||
AppsProvider appsProvider = context.read<AppsProvider>();
|
AppsProvider appsProvider = context.read<AppsProvider>();
|
||||||
|
LogsProvider logs = context.read<LogsProvider>();
|
||||||
|
|
||||||
if (settingsProvider.prefs == null) {
|
if (settingsProvider.prefs == null) {
|
||||||
settingsProvider.initializeSettings();
|
settingsProvider.initializeSettings();
|
||||||
} else {
|
} else {
|
||||||
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
bool isFirstRun = settingsProvider.checkAndFlipFirstRun();
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
|
logs.add('This is the first ever run of Obtainium');
|
||||||
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
// If this is the first run, ask for notification permissions and add Obtainium to the Apps list
|
||||||
Permission.notification.request();
|
Permission.notification.request();
|
||||||
appsProvider.saveApps([
|
appsProvider.saveApps([
|
||||||
App(
|
App(
|
||||||
'dev.imranr.obtainium',
|
obtainiumId,
|
||||||
'https://github.com/ImranR98/Obtainium',
|
'https://github.com/ImranR98/Obtainium',
|
||||||
'ImranR98',
|
'ImranR98',
|
||||||
'Obtainium',
|
'Obtainium',
|
||||||
@ -149,6 +159,10 @@ class _ObtainiumState extends State<Obtainium> {
|
|||||||
}
|
}
|
||||||
// Register the background update task according to the user's setting
|
// Register the background update task according to the user's setting
|
||||||
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
if (existingUpdateInterval != settingsProvider.updateInterval) {
|
||||||
|
if (existingUpdateInterval != -1) {
|
||||||
|
logs.add(
|
||||||
|
'Setting update interval to ${settingsProvider.updateInterval}');
|
||||||
|
}
|
||||||
existingUpdateInterval = settingsProvider.updateInterval;
|
existingUpdateInterval = settingsProvider.updateInterval;
|
||||||
if (existingUpdateInterval == 0) {
|
if (existingUpdateInterval == 0) {
|
||||||
AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
|
AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
|
||||||
|
@ -12,40 +12,42 @@ class GitHubStars implements MassAppUrlSource {
|
|||||||
@override
|
@override
|
||||||
late List<String> requiredArgs = ['Username'];
|
late List<String> requiredArgs = ['Username'];
|
||||||
|
|
||||||
Future<List<String>> getOnePageOfUserStarredUrls(
|
Future<Map<String, String>> getOnePageOfUserStarredUrlsWithDescriptions(
|
||||||
String username, int page) async {
|
String username, int page) async {
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse(
|
||||||
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
'https://${await GitHub().getCredentialPrefixIfAny()}api.github.com/users/$username/starred?per_page=100&page=$page'));
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
return (jsonDecode(res.body) as List<dynamic>)
|
Map<String, String> urlsWithDescriptions = {};
|
||||||
.map((e) => e['html_url'] as String)
|
for (var e in (jsonDecode(res.body) as List<dynamic>)) {
|
||||||
.toList();
|
urlsWithDescriptions.addAll({
|
||||||
} else {
|
e['html_url'] as String: e['description'] != null
|
||||||
if (res.headers['x-ratelimit-remaining'] == '0') {
|
? e['description'] as String
|
||||||
throw RateLimitError(
|
: 'No description'
|
||||||
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') /
|
});
|
||||||
60000000)
|
|
||||||
.round());
|
|
||||||
}
|
}
|
||||||
|
return urlsWithDescriptions;
|
||||||
throw ObtainiumError('Unable to find user\'s starred repos');
|
} else {
|
||||||
|
var gh = GitHub();
|
||||||
|
gh.rateLimitErrorCheck(res);
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> getUrls(List<String> args) async {
|
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args) async {
|
||||||
if (args.length != requiredArgs.length) {
|
if (args.length != requiredArgs.length) {
|
||||||
throw ObtainiumError('Wrong number of arguments provided');
|
throw ObtainiumError('Wrong number of arguments provided');
|
||||||
}
|
}
|
||||||
List<String> urls = [];
|
Map<String, String> urlsWithDescriptions = {};
|
||||||
var page = 1;
|
var page = 1;
|
||||||
while (true) {
|
while (true) {
|
||||||
var pageUrls = await getOnePageOfUserStarredUrls(args[0], page++);
|
var pageUrls =
|
||||||
urls.addAll(pageUrls);
|
await getOnePageOfUserStarredUrlsWithDescriptions(args[0], page++);
|
||||||
|
urlsWithDescriptions.addAll(pageUrls);
|
||||||
if (pageUrls.length < 100) {
|
if (pageUrls.length < 100) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urls;
|
return urlsWithDescriptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,18 +115,23 @@ class _AddAppPageState extends State<AddAppPage> {
|
|||||||
additionalData);
|
additionalData);
|
||||||
await settingsProvider
|
await settingsProvider
|
||||||
.getInstallPermission();
|
.getInstallPermission();
|
||||||
// ignore: use_build_context_synchronously
|
// Only download the APK here if you need to for the package ID
|
||||||
var apkUrl = await appsProvider
|
if (sourceProvider
|
||||||
.confirmApkUrl(app, context);
|
.isTempId(app.id)) {
|
||||||
if (apkUrl == null) {
|
// ignore: use_build_context_synchronously
|
||||||
throw ObtainiumError('Cancelled');
|
var apkUrl = await appsProvider
|
||||||
|
.confirmApkUrl(app, context);
|
||||||
|
if (apkUrl == null) {
|
||||||
|
throw ObtainiumError(
|
||||||
|
'Cancelled');
|
||||||
|
}
|
||||||
|
app.preferredApkIndex =
|
||||||
|
app.apkUrls.indexOf(apkUrl);
|
||||||
|
var downloadedApk =
|
||||||
|
await appsProvider
|
||||||
|
.downloadApp(app);
|
||||||
|
app.id = downloadedApk.appId;
|
||||||
}
|
}
|
||||||
app.preferredApkIndex =
|
|
||||||
app.apkUrls.indexOf(apkUrl);
|
|
||||||
var downloadedApk =
|
|
||||||
await appsProvider
|
|
||||||
.downloadApp(app);
|
|
||||||
app.id = downloadedApk.appId;
|
|
||||||
if (appsProvider.apps
|
if (appsProvider.apps
|
||||||
.containsKey(app.id)) {
|
.containsKey(app.id)) {
|
||||||
throw ObtainiumError(
|
throw ObtainiumError(
|
||||||
|
@ -528,6 +528,36 @@ class AppsPageState extends State<AppsPage> {
|
|||||||
tooltip: 'Share Selected App URLs',
|
tooltip: 'Share Selected App URLs',
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title:
|
||||||
|
'Reset Install Status for Selected Apps?',
|
||||||
|
items: const [],
|
||||||
|
defaultValues: const [],
|
||||||
|
initValid: true,
|
||||||
|
message:
|
||||||
|
'The install status of ${selectedApps.length} App${selectedApps.length == 1 ? '' : 's'} will be reset.\n\nThis can help when the App version shown in Obtainium is incorrect due to failed updates or other issues.',
|
||||||
|
);
|
||||||
|
}).then((values) {
|
||||||
|
if (values != null) {
|
||||||
|
appsProvider.saveApps(
|
||||||
|
selectedApps.map((e) {
|
||||||
|
e.installedVersion = null;
|
||||||
|
return e;
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
}).whenComplete(() {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Reset Install Status',
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.restore_page_outlined),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,7 @@ import 'package:obtainium/providers/settings_provider.dart';
|
|||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class ImportExportPage extends StatefulWidget {
|
class ImportExportPage extends StatefulWidget {
|
||||||
const ImportExportPage({super.key});
|
const ImportExportPage({super.key});
|
||||||
@ -258,9 +259,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
var urls = await source
|
var urlsWithDescriptions =
|
||||||
.search(values[0]);
|
await source
|
||||||
if (urls.isNotEmpty) {
|
.search(values[0]);
|
||||||
|
if (urlsWithDescriptions
|
||||||
|
.isNotEmpty) {
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
await showDialog<
|
await showDialog<
|
||||||
List<
|
List<
|
||||||
@ -270,7 +273,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
(BuildContext
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
return UrlSelectionModal(
|
return UrlSelectionModal(
|
||||||
urls: urls,
|
urlsWithDescriptions:
|
||||||
|
urlsWithDescriptions,
|
||||||
defaultSelected:
|
defaultSelected:
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@ -353,8 +357,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
importInProgress = true;
|
importInProgress = true;
|
||||||
});
|
});
|
||||||
var urls = await source
|
var urlsWithDescriptions =
|
||||||
.getUrls(values);
|
await source
|
||||||
|
.getUrlsWithDescriptions(
|
||||||
|
values);
|
||||||
var selectedUrls =
|
var selectedUrls =
|
||||||
await showDialog<
|
await showDialog<
|
||||||
List<String>?>(
|
List<String>?>(
|
||||||
@ -363,7 +369,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
|
|||||||
(BuildContext
|
(BuildContext
|
||||||
ctx) {
|
ctx) {
|
||||||
return UrlSelectionModal(
|
return UrlSelectionModal(
|
||||||
urls: urls);
|
urlsWithDescriptions:
|
||||||
|
urlsWithDescriptions);
|
||||||
});
|
});
|
||||||
if (selectedUrls != null) {
|
if (selectedUrls != null) {
|
||||||
var errors =
|
var errors =
|
||||||
@ -477,9 +484,11 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
|
|||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class UrlSelectionModal extends StatefulWidget {
|
class UrlSelectionModal extends StatefulWidget {
|
||||||
UrlSelectionModal(
|
UrlSelectionModal(
|
||||||
{super.key, required this.urls, this.defaultSelected = true});
|
{super.key,
|
||||||
|
required this.urlsWithDescriptions,
|
||||||
|
this.defaultSelected = true});
|
||||||
|
|
||||||
List<String> urls;
|
Map<String, String> urlsWithDescriptions;
|
||||||
bool defaultSelected;
|
bool defaultSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -487,12 +496,13 @@ class UrlSelectionModal extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
||||||
Map<String, bool> urlSelections = {};
|
Map<MapEntry<String, String>, bool> urlWithDescriptionSelections = {};
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
for (var url in widget.urls) {
|
for (var url in widget.urlsWithDescriptions.entries) {
|
||||||
urlSelections.putIfAbsent(url, () => widget.defaultSelected);
|
urlWithDescriptionSelections.putIfAbsent(
|
||||||
|
url, () => widget.defaultSelected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,21 +512,48 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: const Text('Select URLs to Import'),
|
title: const Text('Select URLs to Import'),
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
...urlSelections.keys.map((url) {
|
...urlWithDescriptionSelections.keys.map((urlWithD) {
|
||||||
return Row(children: [
|
return Row(children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: urlSelections[url],
|
value: urlWithDescriptionSelections[urlWithD],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
urlSelections[url] = value ?? false;
|
urlWithDescriptionSelections[urlWithD] = value ?? false;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Column(
|
||||||
Uri.parse(url).path.substring(1),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(urlWithD.key,
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
Uri.parse(urlWithD.key).path.substring(1),
|
||||||
|
style:
|
||||||
|
const TextStyle(decoration: TextDecoration.underline),
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
)),
|
||||||
|
Text(
|
||||||
|
urlWithD.value.length > 128
|
||||||
|
? '${urlWithD.value.substring(0, 128)}...'
|
||||||
|
: urlWithD.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontStyle: FontStyle.italic, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
)
|
||||||
|
],
|
||||||
))
|
))
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
@ -529,12 +566,13 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
|
|||||||
child: const Text('Cancel')),
|
child: const Text('Cancel')),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(urlSelections.keys
|
Navigator.of(context).pop(urlWithDescriptionSelections.entries
|
||||||
.where((url) => urlSelections[url] ?? false)
|
.where((entry) => entry.value)
|
||||||
|
.map((e) => e.key.key)
|
||||||
.toList());
|
.toList());
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Import ${urlSelections.values.where((b) => b).length} URLs'))
|
'Import ${urlWithDescriptionSelections.values.where((b) => b).length} URLs'))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
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/components/generated_form.dart';
|
||||||
|
import 'package:obtainium/components/generated_form_modal.dart';
|
||||||
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/settings_provider.dart';
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatefulWidget {
|
class SettingsPage extends StatefulWidget {
|
||||||
@ -238,23 +242,55 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
height16,
|
const Divider(
|
||||||
TextButton.icon(
|
height: 32,
|
||||||
style: ButtonStyle(
|
),
|
||||||
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
Row(
|
||||||
(Set<MaterialState> states) {
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
return Colors.grey;
|
children: [
|
||||||
}),
|
TextButton.icon(
|
||||||
),
|
onPressed: () {
|
||||||
onPressed: () {
|
launchUrlString(settingsProvider.sourceUrl,
|
||||||
launchUrlString(settingsProvider.sourceUrl,
|
mode: LaunchMode.externalApplication);
|
||||||
mode: LaunchMode.externalApplication);
|
},
|
||||||
},
|
icon: const Icon(Icons.code),
|
||||||
icon: const Icon(Icons.code),
|
label: const Text(
|
||||||
label: Text(
|
'App Source',
|
||||||
'Source',
|
),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
),
|
||||||
),
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<LogsProvider>().get().then((logs) {
|
||||||
|
if (logs.isEmpty) {
|
||||||
|
showError(ObtainiumError('No Logs'), context);
|
||||||
|
} else {
|
||||||
|
String logString =
|
||||||
|
logs.map((e) => e.toString()).join('\n\n');
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext ctx) {
|
||||||
|
return GeneratedFormModal(
|
||||||
|
title: 'Obtainium App Logs',
|
||||||
|
items: const [],
|
||||||
|
defaultValues: const [],
|
||||||
|
message: logString,
|
||||||
|
initValid: true,
|
||||||
|
);
|
||||||
|
}).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
Share.share(
|
||||||
|
logs
|
||||||
|
.map((e) => e.toString())
|
||||||
|
.join('\n\n'),
|
||||||
|
subject: 'Obtainium App Logs');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.bug_report_outlined),
|
||||||
|
label: const Text('App Logs')),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
height16,
|
height16,
|
||||||
],
|
],
|
||||||
|
@ -12,9 +12,10 @@ 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/app_info.dart';
|
||||||
import 'package:installed_apps/installed_apps.dart';
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
import 'package:obtainium/app_sources/github.dart';
|
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
|
import 'package:obtainium/providers/logs_provider.dart';
|
||||||
import 'package:obtainium/providers/notifications_provider.dart';
|
import 'package:obtainium/providers/notifications_provider.dart';
|
||||||
|
import 'package:obtainium/providers/settings_provider.dart';
|
||||||
import 'package:package_archive_info/package_archive_info.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';
|
||||||
@ -42,6 +43,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
bool loadingApps = false;
|
bool loadingApps = false;
|
||||||
bool gettingUpdates = false;
|
bool gettingUpdates = false;
|
||||||
bool forBGTask = false;
|
bool forBGTask = false;
|
||||||
|
LogsProvider logs = LogsProvider();
|
||||||
|
|
||||||
// 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;
|
||||||
@ -63,7 +65,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// Delete existing APKs
|
// Delete existing APKs
|
||||||
(await getExternalStorageDirectory())
|
(await getExternalStorageDirectory())
|
||||||
?.listSync()
|
?.listSync()
|
||||||
.where((element) => element.path.endsWith('.apk'))
|
.where((element) =>
|
||||||
|
element.path.endsWith('.apk') ||
|
||||||
|
element.path.endsWith('.apk.part'))
|
||||||
.forEach((apk) {
|
.forEach((apk) {
|
||||||
apk.delete();
|
apk.delete();
|
||||||
});
|
});
|
||||||
@ -71,38 +75,39 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(String url, String fileName, Function? onProgress) async {
|
downloadFile(String url, String fileName, Function? onProgress,
|
||||||
|
{bool useExisting = true}) async {
|
||||||
var destDir = (await getExternalStorageDirectory())!.path;
|
var destDir = (await getExternalStorageDirectory())!.path;
|
||||||
StreamedResponse response =
|
StreamedResponse response =
|
||||||
await Client().send(Request('GET', Uri.parse(url)));
|
await Client().send(Request('GET', Uri.parse(url)));
|
||||||
File downloadedFile = File('$destDir/$fileName');
|
File downloadedFile = File('$destDir/$fileName');
|
||||||
|
if (!(downloadedFile.existsSync() && useExisting)) {
|
||||||
if (downloadedFile.existsSync()) {
|
File tempDownloadedFile = File('${downloadedFile.path}.part');
|
||||||
downloadedFile.deleteSync();
|
if (tempDownloadedFile.existsSync()) {
|
||||||
}
|
tempDownloadedFile.deleteSync();
|
||||||
var length = response.contentLength;
|
}
|
||||||
var received = 0;
|
var length = response.contentLength;
|
||||||
double? progress;
|
var received = 0;
|
||||||
var sink = downloadedFile.openWrite();
|
double? progress;
|
||||||
|
var sink = tempDownloadedFile.openWrite();
|
||||||
await response.stream.map((s) {
|
await response.stream.map((s) {
|
||||||
received += s.length;
|
received += s.length;
|
||||||
progress = (length != null ? received / length * 100 : 30);
|
progress = (length != null ? received / length * 100 : 30);
|
||||||
|
if (onProgress != null) {
|
||||||
|
onProgress(progress);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}).pipe(sink);
|
||||||
|
await sink.close();
|
||||||
|
progress = null;
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
onProgress(progress);
|
onProgress(progress);
|
||||||
}
|
}
|
||||||
return s;
|
if (response.statusCode != 200) {
|
||||||
}).pipe(sink);
|
tempDownloadedFile.deleteSync();
|
||||||
|
throw response.reasonPhrase ?? 'Unknown Error';
|
||||||
await sink.close();
|
}
|
||||||
progress = null;
|
tempDownloadedFile.renameSync(downloadedFile.path);
|
||||||
if (onProgress != null) {
|
|
||||||
onProgress(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
downloadedFile.deleteSync();
|
|
||||||
throw response.reasonPhrase ?? 'Unknown Error';
|
|
||||||
}
|
}
|
||||||
return downloadedFile;
|
return downloadedFile;
|
||||||
}
|
}
|
||||||
@ -182,6 +187,15 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> canDowngradeApps() async {
|
||||||
|
try {
|
||||||
|
await InstalledApps.getAppInfo('com.berdik.letmedowngrade');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
// Unfortunately this 'await' does not actually wait for the APK to finish installing
|
||||||
// So we only know that the install prompt was shown, but the user could still cancel w/o us knowing
|
// 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
|
||||||
@ -195,7 +209,8 @@ class AppsProvider with ChangeNotifier {
|
|||||||
// OK
|
// OK
|
||||||
}
|
}
|
||||||
if (appInfo != null &&
|
if (appInfo != null &&
|
||||||
int.parse(newInfo.buildNumber) < appInfo.versionCode!) {
|
int.parse(newInfo.buildNumber) < appInfo.versionCode! &&
|
||||||
|
!(await canDowngradeApps())) {
|
||||||
throw DowngradeError();
|
throw DowngradeError();
|
||||||
}
|
}
|
||||||
if (appInfo == null ||
|
if (appInfo == null ||
|
||||||
@ -300,10 +315,10 @@ 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<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
|
List<DownloadedApk> moveObtainiumToStart(List<DownloadedApk> items) {
|
||||||
String obtainiumId = 'imranr98_obtainium_${GitHub().host}';
|
|
||||||
DownloadedApk? temp;
|
DownloadedApk? temp;
|
||||||
items.removeWhere((element) {
|
items.removeWhere((element) {
|
||||||
bool res = element.appId == obtainiumId;
|
bool res =
|
||||||
|
element.appId == obtainiumId || element.appId == obtainiumTempId;
|
||||||
if (res) {
|
if (res) {
|
||||||
temp = element;
|
temp = element;
|
||||||
}
|
}
|
||||||
@ -412,22 +427,28 @@ class AppsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
loadingApps = true;
|
loadingApps = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
List<FileSystemEntity> appFiles = (await getAppsDir())
|
List<App> newApps = (await getAppsDir())
|
||||||
.listSync()
|
.listSync()
|
||||||
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
.where((item) => item.path.toLowerCase().endsWith('.json'))
|
||||||
|
.map((e) => App.fromJson(jsonDecode(File(e.path).readAsStringSync())))
|
||||||
.toList();
|
.toList();
|
||||||
apps.clear();
|
var idsToDelete = apps.values
|
||||||
|
.map((e) => e.app.id)
|
||||||
|
.toSet()
|
||||||
|
.difference(newApps.map((e) => e.id).toSet());
|
||||||
|
for (var id in idsToDelete) {
|
||||||
|
apps.remove(id);
|
||||||
|
}
|
||||||
var sp = SourceProvider();
|
var sp = SourceProvider();
|
||||||
List<List<String>> errors = [];
|
List<List<String>> errors = [];
|
||||||
for (int i = 0; i < appFiles.length; i++) {
|
for (int i = 0; i < newApps.length; i++) {
|
||||||
App app =
|
var info = await getInstalledInfo(newApps[i].id);
|
||||||
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
|
|
||||||
var info = await getInstalledInfo(app.id);
|
|
||||||
try {
|
try {
|
||||||
sp.getSource(app.url);
|
sp.getSource(newApps[i].url);
|
||||||
apps.putIfAbsent(app.id, () => AppInMemory(app, null, info));
|
apps.putIfAbsent(
|
||||||
|
newApps[i].id, () => AppInMemory(newApps[i], null, info));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add([app.id, app.name, e.toString()]);
|
errors.add([newApps[i].id, newApps[i].name, e.toString()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errors.isNotEmpty) {
|
if (errors.isNotEmpty) {
|
||||||
|
109
lib/providers/logs_provider.dart
Normal file
109
lib/providers/logs_provider.dart
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
const String logTable = 'logs';
|
||||||
|
const String idColumn = '_id';
|
||||||
|
const String levelColumn = 'level';
|
||||||
|
const String messageColumn = 'message';
|
||||||
|
const String timestampColumn = 'timestamp';
|
||||||
|
const String dbPath = 'logs.db';
|
||||||
|
|
||||||
|
enum LogLevels { debug, info, warning, error }
|
||||||
|
|
||||||
|
class Log {
|
||||||
|
int? id;
|
||||||
|
late LogLevels level;
|
||||||
|
late String message;
|
||||||
|
DateTime timestamp = DateTime.now();
|
||||||
|
|
||||||
|
Map<String, Object?> toMap() {
|
||||||
|
var map = <String, Object?>{
|
||||||
|
idColumn: id,
|
||||||
|
levelColumn: level.index,
|
||||||
|
messageColumn: message,
|
||||||
|
timestampColumn: timestamp.millisecondsSinceEpoch
|
||||||
|
};
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log(this.message, this.level);
|
||||||
|
|
||||||
|
Log.fromMap(Map<String, Object?> map) {
|
||||||
|
id = map[idColumn] as int;
|
||||||
|
level = LogLevels.values.elementAt(map[levelColumn] as int);
|
||||||
|
message = map[messageColumn] as String;
|
||||||
|
timestamp =
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(map[timestampColumn] as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '${timestamp.toString()}: ${level.name}: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogsProvider {
|
||||||
|
LogsProvider({bool runDefaultClear = true}) {
|
||||||
|
clear(before: DateTime.now().subtract(const Duration(days: 7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Database? db;
|
||||||
|
|
||||||
|
Future<Database> getDB() async {
|
||||||
|
db ??= await openDatabase(dbPath, version: 1,
|
||||||
|
onCreate: (Database db, int version) async {
|
||||||
|
await db.execute('''
|
||||||
|
create table if not exists $logTable (
|
||||||
|
$idColumn integer primary key autoincrement,
|
||||||
|
$levelColumn integer not null,
|
||||||
|
$messageColumn text not null,
|
||||||
|
$timestampColumn integer not null)
|
||||||
|
''');
|
||||||
|
});
|
||||||
|
return db!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Log> add(String message, {LogLevels level = LogLevels.info}) async {
|
||||||
|
Log l = Log(message, level);
|
||||||
|
l.id = await (await getDB()).insert(logTable, l.toMap());
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(l);
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Log>> get({DateTime? before, DateTime? after}) async {
|
||||||
|
var where = getWhereDates(before: before, after: after);
|
||||||
|
return (await (await getDB())
|
||||||
|
.query(logTable, where: where.key, whereArgs: where.value))
|
||||||
|
.map((e) => Log.fromMap(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> clear({DateTime? before, DateTime? after}) async {
|
||||||
|
var where = getWhereDates(before: before, after: after);
|
||||||
|
var res = await (await getDB())
|
||||||
|
.delete(logTable, where: where.key, whereArgs: where.value);
|
||||||
|
if (res > 0) {
|
||||||
|
add('Cleared $res logs (before = $before, after = $after)');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MapEntry<String?, List<int>?> getWhereDates(
|
||||||
|
{DateTime? before, DateTime? after}) {
|
||||||
|
List<String> where = [];
|
||||||
|
List<int> whereArgs = [];
|
||||||
|
if (before != null) {
|
||||||
|
where.add('$timestampColumn < ?');
|
||||||
|
whereArgs.add(before.millisecondsSinceEpoch);
|
||||||
|
}
|
||||||
|
if (after != null) {
|
||||||
|
where.add('$timestampColumn > ?');
|
||||||
|
whereArgs.add(after.millisecondsSinceEpoch);
|
||||||
|
}
|
||||||
|
return whereArgs.isEmpty
|
||||||
|
? const MapEntry(null, null)
|
||||||
|
: MapEntry(where.join(' and '), whereArgs);
|
||||||
|
}
|
@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:obtainium/app_sources/github.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
|
||||||
|
String obtainiumId = 'dev.imranr.obtainium';
|
||||||
|
|
||||||
enum ThemeSettings { system, light, dark }
|
enum ThemeSettings { system, light, dark }
|
||||||
|
|
||||||
enum ColourSettings { basic, materialYou }
|
enum ColourSettings { basic, materialYou }
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:http/http.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';
|
||||||
@ -154,20 +155,29 @@ class AppSource {
|
|||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> apkUrlPrefetchModifier(String apkUrl) {
|
Future<String> apkUrlPrefetchModifier(String apkUrl) async {
|
||||||
throw NotImplementedError();
|
return apkUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canSearch = false;
|
bool canSearch = false;
|
||||||
Future<List<String>> search(String query) {
|
Future<Map<String, String>> search(String query) {
|
||||||
throw NotImplementedError();
|
throw NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? tryInferringAppId(String standardUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ObtainiumError getObtainiumHttpError(Response res) {
|
||||||
|
return ObtainiumError(
|
||||||
|
res.reasonPhrase ?? 'Error ${res.statusCode.toString()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MassAppUrlSource {
|
abstract class MassAppUrlSource {
|
||||||
late String name;
|
late String name;
|
||||||
late List<String> requiredArgs;
|
late List<String> requiredArgs;
|
||||||
Future<List<String>> getUrls(List<String> args);
|
Future<Map<String, String>> getUrlsWithDescriptions(List<String> args);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceProvider {
|
class SourceProvider {
|
||||||
@ -234,7 +244,9 @@ class SourceProvider {
|
|||||||
APKDetails apk =
|
APKDetails apk =
|
||||||
await source.getLatestAPKDetails(standardUrl, additionalData);
|
await source.getLatestAPKDetails(standardUrl, additionalData);
|
||||||
return App(
|
return App(
|
||||||
id ?? generateTempID(names, source),
|
id ??
|
||||||
|
source.tryInferringAppId(standardUrl) ??
|
||||||
|
generateTempID(names, source),
|
||||||
standardUrl,
|
standardUrl,
|
||||||
names.author[0].toUpperCase() + names.author.substring(1),
|
names.author[0].toUpperCase() + names.author.substring(1),
|
||||||
name.trim().isNotEmpty
|
name.trim().isNotEmpty
|
||||||
|
21
pubspec.lock
21
pubspec.lock
@ -567,6 +567,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0+2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -588,6 +602,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0+3"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
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.7.0+56 # When changing this, update the tag in main() accordingly
|
version: 0.7.4+60 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=2.18.2 <3.0.0'
|
||||||
@ -56,6 +56,7 @@ dependencies:
|
|||||||
installed_apps: ^1.3.1
|
installed_apps: ^1.3.1
|
||||||
package_archive_info: ^0.1.0
|
package_archive_info: ^0.1.0
|
||||||
android_alarm_manager_plus: ^2.1.0
|
android_alarm_manager_plus: ^2.1.0
|
||||||
|
sqflite: ^2.2.0+3
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user