mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-16 19:58:09 +02:00
Began moving to different download/install process
Previous download plgin was buggy, overcomplicated, and unmaintained. Began switch to a different approach (not done - installs fail). Also, the GitHub API is no longer used (rate limit) - web scraping instead.
This commit is contained in:
@@ -30,15 +30,6 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<provider
|
|
||||||
android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
|
|
||||||
android:authorities="${applicationId}.flutter_downloader.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
@@ -17,16 +17,16 @@ class _AppPageState extends State<AppPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var appsProvider = context.watch<AppsProvider>();
|
var appsProvider = context.watch<AppsProvider>();
|
||||||
App? app = appsProvider.apps[widget.appId];
|
AppInMemory? app = appsProvider.apps[widget.appId];
|
||||||
if (app?.installedVersion != null) {
|
if (app?.app.installedVersion != null) {
|
||||||
appsProvider.getUpdate(app!.id);
|
appsProvider.getUpdate(app!.app.id);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('${app?.author}/${app?.name}'),
|
title: Text('${app!.app.author}/${app.app.name}'),
|
||||||
),
|
),
|
||||||
body: WebView(
|
body: WebView(
|
||||||
initialUrl: app?.url,
|
initialUrl: app.app.url,
|
||||||
),
|
),
|
||||||
bottomSheet: Column(
|
bottomSheet: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -39,21 +39,21 @@ class _AppPageState extends State<AppPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: (app?.installedVersion == null ||
|
onPressed: (app.app.installedVersion == null ||
|
||||||
appsProvider
|
appsProvider.checkAppObjectForUpdate(
|
||||||
.checkAppObjectForUpdate(app!)) &&
|
app.app)) &&
|
||||||
app?.currentDownloadId == null
|
app.downloadProgress == null
|
||||||
? () {
|
? () {
|
||||||
appsProvider
|
appsProvider.downloadAndInstallLatestApp(
|
||||||
.backgroundDownloadAndInstallApp(app!);
|
app.app.id);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(app?.installedVersion == null
|
child: Text(app.app.installedVersion == null
|
||||||
? 'Install'
|
? 'Install'
|
||||||
: 'Update'))),
|
: 'Update'))),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: app?.currentDownloadId != null
|
onPressed: app.downloadProgress != null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -62,12 +62,12 @@ class _AppPageState extends State<AppPage> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Remove App?'),
|
title: const Text('Remove App?'),
|
||||||
content: Text(
|
content: Text(
|
||||||
'This will remove \'${app?.name}\' from Obtainium.${app?.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
|
'This will remove \'${app.app.name}\' from Obtainium.${app.app.installedVersion != null ? '\n\nNote that while Obtainium will no longer track its updates, the App will remain installed.' : ''}'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
appsProvider
|
appsProvider
|
||||||
.removeApp(app!.id)
|
.removeApp(app.app.id)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
Navigator.of(context).popUntil(
|
Navigator.of(context).popUntil(
|
||||||
@@ -90,7 +90,8 @@ class _AppPageState extends State<AppPage> {
|
|||||||
child: const Text('Remove'),
|
child: const Text('Remove'),
|
||||||
),
|
),
|
||||||
])),
|
])),
|
||||||
if (app?.currentDownloadId != null) const LinearProgressIndicator()
|
if (app.downloadProgress != null)
|
||||||
|
LinearProgressIndicator(value: app.downloadProgress)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:obtainium/pages/add_app.dart';
|
|
||||||
import 'package:obtainium/pages/app.dart';
|
import 'package:obtainium/pages/app.dart';
|
||||||
import 'package:obtainium/services/apps_provider.dart';
|
import 'package:obtainium/services/apps_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -31,18 +30,23 @@ class _AppsPageState extends State<AppsPage> {
|
|||||||
children: appsProvider.apps.values
|
children: appsProvider.apps.values
|
||||||
.map(
|
.map(
|
||||||
(e) => ListTile(
|
(e) => ListTile(
|
||||||
title: Text('${e.author}/${e.name}'),
|
title: Text('${e.app.author}/${e.app.name}'),
|
||||||
subtitle:
|
subtitle:
|
||||||
Text(e.installedVersion ?? 'Not Installed'),
|
Text(e.app.installedVersion ?? 'Not Installed'),
|
||||||
trailing: e.installedVersion != null &&
|
trailing: e.downloadProgress != null
|
||||||
e.installedVersion != e.latestVersion
|
? Text(
|
||||||
|
'Downloading - ${e.downloadProgress!.toInt()}%')
|
||||||
|
: (e.app.installedVersion != null &&
|
||||||
|
e.app.installedVersion !=
|
||||||
|
e.app.latestVersion
|
||||||
? const Text('Update Available')
|
? const Text('Update Available')
|
||||||
: null,
|
: null),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => AppPage(appId: e.id)),
|
builder: (context) =>
|
||||||
|
AppPage(appId: e.app.id)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@@ -3,64 +3,45 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
|
||||||
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
import 'package:flutter_fgbg/flutter_fgbg.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:obtainium/services/source_service.dart';
|
import 'package:obtainium/services/source_service.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:install_plugin_v2/install_plugin_v2.dart';
|
||||||
|
|
||||||
|
class AppInMemory {
|
||||||
|
late App app;
|
||||||
|
double? downloadProgress;
|
||||||
|
|
||||||
|
AppInMemory(this.app, this.downloadProgress);
|
||||||
|
}
|
||||||
|
|
||||||
class AppsProvider with ChangeNotifier {
|
class AppsProvider with ChangeNotifier {
|
||||||
// In memory App state (should always be kept in sync with local storage versions)
|
// In memory App state (should always be kept in sync with local storage versions)
|
||||||
Map<String, App> apps = {};
|
Map<String, AppInMemory> apps = {};
|
||||||
bool loadingApps = false;
|
bool loadingApps = false;
|
||||||
bool gettingUpdates = false;
|
bool gettingUpdates = false;
|
||||||
|
|
||||||
AppsProvider({bool bg = false}) {
|
|
||||||
initializeNotifs();
|
|
||||||
loadApps().then((_) {
|
|
||||||
clearDownloadStates();
|
|
||||||
});
|
|
||||||
if (!bg) {
|
|
||||||
initializeDownloader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notifications plugin for downloads
|
// Notifications plugin for downloads
|
||||||
FlutterLocalNotificationsPlugin downloaderNotifications =
|
FlutterLocalNotificationsPlugin downloaderNotifications =
|
||||||
FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
// Port for FlutterDownloader background/foreground communication
|
|
||||||
final ReceivePort _port = ReceivePort();
|
|
||||||
|
|
||||||
// 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;
|
||||||
StreamSubscription<FGBGType>? foregroundSubscription;
|
StreamSubscription<FGBGType>? foregroundSubscription;
|
||||||
|
|
||||||
// Setup the FlutterDownloader plugin (call only once)
|
AppsProvider({bool bg = false}) {
|
||||||
Future<void> initializeDownloader() async {
|
initializeNotifs();
|
||||||
// Make sure FlutterDownloader can be used
|
|
||||||
await FlutterDownloader.initialize();
|
|
||||||
// Set up the status update callback for FlutterDownloader
|
|
||||||
FlutterDownloader.registerCallback(downloadCallbackBackground);
|
|
||||||
// The actual callback is in the background isolate
|
|
||||||
// So setup a port to pass the data to a foreground callback
|
|
||||||
IsolateNameServer.registerPortWithName(
|
|
||||||
_port.sendPort, 'downloader_send_port');
|
|
||||||
_port.listen((dynamic data) {
|
|
||||||
String id = data[0];
|
|
||||||
DownloadTaskStatus status = data[1];
|
|
||||||
int progress = data[2];
|
|
||||||
downloadCallbackForeground(id, status, progress);
|
|
||||||
});
|
|
||||||
// Subscribe to changes in the app foreground status
|
// Subscribe to changes in the app foreground status
|
||||||
foregroundSubscription = FGBGEvents.stream.listen((event) async {
|
foregroundSubscription = FGBGEvents.stream.listen((event) async {
|
||||||
isForeground = event == FGBGType.foreground;
|
isForeground = event == FGBGType.foreground;
|
||||||
if (isForeground) await loadApps();
|
if (isForeground) await loadApps();
|
||||||
});
|
});
|
||||||
|
loadApps();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initializeNotifs() async {
|
Future<void> initializeNotifs() async {
|
||||||
@@ -69,15 +50,6 @@ class AppsProvider with ChangeNotifier {
|
|||||||
android: AndroidInitializationSettings('ic_notification')));
|
android: AndroidInitializationSettings('ic_notification')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback that receives FlutterDownloader status and forwards to a foreground function
|
|
||||||
@pragma('vm:entry-point')
|
|
||||||
static void downloadCallbackBackground(
|
|
||||||
String id, DownloadTaskStatus status, int progress) {
|
|
||||||
final SendPort? send =
|
|
||||||
IsolateNameServer.lookupPortByName('downloader_send_port');
|
|
||||||
send!.send([id, status, progress]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> notify(int id, String title, String message, String channelCode,
|
Future<void> notify(int id, String title, String message, String channelCode,
|
||||||
String channelName, String channelDescription) {
|
String channelName, String channelDescription) {
|
||||||
return downloaderNotifications.show(
|
return downloaderNotifications.show(
|
||||||
@@ -92,63 +64,44 @@ class AppsProvider with ChangeNotifier {
|
|||||||
groupKey: 'dev.imranr.obtainium.$channelCode')));
|
groupKey: 'dev.imranr.obtainium.$channelCode')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foreground function to act on FlutterDownloader status updates (install downloaded APK)
|
// Given a App (assumed valid), initiate an APK download (will trigger install callback when complete)
|
||||||
void downloadCallbackForeground(
|
Future<void> downloadAndInstallLatestApp(String appId) async {
|
||||||
String id, DownloadTaskStatus status, int progress) async {
|
if (apps[appId] == null) {
|
||||||
if (status == DownloadTaskStatus.complete) {
|
throw 'App not found';
|
||||||
// Wait for app to come to the foreground if not already, and notify the user
|
|
||||||
while (!isForeground) {
|
|
||||||
await notify(
|
|
||||||
1,
|
|
||||||
'Complete App Installation',
|
|
||||||
'Obtainium must be open to install Apps',
|
|
||||||
'COMPLETE_INSTALL',
|
|
||||||
'Complete App Installation',
|
|
||||||
'Asks the user to return to Obtanium to finish installing an App');
|
|
||||||
if (await FGBGEvents.stream.first == FGBGType.foreground) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Install the App (and remove warning notification if any)
|
|
||||||
FlutterDownloader.open(taskId: id);
|
|
||||||
downloaderNotifications.cancel(1);
|
|
||||||
}
|
|
||||||
// Change App status based on result (we assume user accepts install - no way to tell programatically)
|
|
||||||
if (status == DownloadTaskStatus.complete ||
|
|
||||||
status == DownloadTaskStatus.failed ||
|
|
||||||
status == DownloadTaskStatus.canceled) {
|
|
||||||
App? foundApp;
|
|
||||||
apps.forEach((appId, app) {
|
|
||||||
if (app.currentDownloadId == id) {
|
|
||||||
foundApp = apps[appId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
foundApp!.currentDownloadId = null;
|
|
||||||
if (status == DownloadTaskStatus.complete) {
|
|
||||||
foundApp!.installedVersion = foundApp!.latestVersion;
|
|
||||||
}
|
|
||||||
saveApp(foundApp!);
|
|
||||||
}
|
}
|
||||||
|
StreamedResponse response =
|
||||||
|
await Client().send(Request('GET', Uri.parse(apps[appId]!.app.apkUrl)));
|
||||||
|
File downloadFile =
|
||||||
|
File('${(await getTemporaryDirectory()).path}/$appId.apk');
|
||||||
|
var length = response.contentLength;
|
||||||
|
var received = 0;
|
||||||
|
var sink = downloadFile.openWrite();
|
||||||
|
|
||||||
|
await response.stream.map((s) {
|
||||||
|
received += s.length;
|
||||||
|
apps[appId]!.downloadProgress =
|
||||||
|
(length != null ? received / length * 100 : 30);
|
||||||
|
notifyListeners();
|
||||||
|
return s;
|
||||||
|
}).pipe(sink);
|
||||||
|
|
||||||
|
await sink.close();
|
||||||
|
apps[appId]!.downloadProgress = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
downloadFile.deleteSync();
|
||||||
|
throw response.reasonPhrase ?? 'Unknown Error';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a App (assumed valid), initiate an APK download (will trigger install callback when complete)
|
var res = await InstallPlugin.installApk(
|
||||||
Future<void> backgroundDownloadAndInstallApp(App app) async {
|
downloadFile.path, 'dev.imranr.obtainium');
|
||||||
Directory apkDir = Directory(
|
print(res);
|
||||||
'${(await getExternalStorageDirectory())?.path as String}/apks/${app.id}');
|
|
||||||
if (apkDir.existsSync()) apkDir.deleteSync(recursive: true);
|
apps[appId]!.app.installedVersion = apps[appId]!.app.latestVersion;
|
||||||
apkDir.createSync(recursive: true);
|
saveApp(apps[appId]!.app);
|
||||||
String? downloadId = await FlutterDownloader.enqueue(
|
|
||||||
url: app.apkUrl,
|
downloadFile.deleteSync();
|
||||||
savedDir: apkDir.path,
|
|
||||||
showNotification: true,
|
|
||||||
openFileFromNotification: false,
|
|
||||||
);
|
|
||||||
if (downloadId != null) {
|
|
||||||
app.currentDownloadId = downloadId;
|
|
||||||
saveApp(app);
|
|
||||||
} else {
|
|
||||||
throw "Could not start download";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> getAppsDir() async {
|
Future<Directory> getAppsDir() async {
|
||||||
@@ -171,7 +124,7 @@ class AppsProvider with ChangeNotifier {
|
|||||||
for (int i = 0; i < appFiles.length; i++) {
|
for (int i = 0; i < appFiles.length; i++) {
|
||||||
App app =
|
App app =
|
||||||
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
|
App.fromJson(jsonDecode(File(appFiles[i].path).readAsStringSync()));
|
||||||
apps.putIfAbsent(app.id, () => app);
|
apps.putIfAbsent(app.id, () => AppInMemory(app, null));
|
||||||
}
|
}
|
||||||
loadingApps = false;
|
loadingApps = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -180,25 +133,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
Future<void> saveApp(App app) async {
|
Future<void> saveApp(App app) async {
|
||||||
File('${(await getAppsDir()).path}/${app.id}.json')
|
File('${(await getAppsDir()).path}/${app.id}.json')
|
||||||
.writeAsStringSync(jsonEncode(app.toJson()));
|
.writeAsStringSync(jsonEncode(app.toJson()));
|
||||||
apps.update(app.id, (value) => app, ifAbsent: () => app);
|
apps.update(app.id, (value) => AppInMemory(app, value.downloadProgress),
|
||||||
|
ifAbsent: () => AppInMemory(app, null));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearDownloadStates() async {
|
|
||||||
var appList = apps.values.toList();
|
|
||||||
int count = 0;
|
|
||||||
for (int i = 0; i < appList.length; i++) {
|
|
||||||
if (appList[i].currentDownloadId != null) {
|
|
||||||
apps[appList[i].id]?.currentDownloadId = null;
|
|
||||||
await saveApp(apps[appList[i].id]!);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (count > 0) {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeApp(String appId) async {
|
Future<void> removeApp(String appId) async {
|
||||||
File file = File('${(await getAppsDir()).path}/$appId.json');
|
File file = File('${(await getAppsDir()).path}/$appId.json');
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
@@ -214,11 +153,11 @@ class AppsProvider with ChangeNotifier {
|
|||||||
if (!apps.containsKey(app.id)) {
|
if (!apps.containsKey(app.id)) {
|
||||||
throw 'App not found';
|
throw 'App not found';
|
||||||
}
|
}
|
||||||
return app.latestVersion != apps[app.id]?.installedVersion;
|
return app.latestVersion != apps[app.id]?.app.installedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<App?> getUpdate(String appId) async {
|
Future<App?> getUpdate(String appId) async {
|
||||||
App? currentApp = apps[appId];
|
App? currentApp = apps[appId]!.app;
|
||||||
App newApp = await SourceService().getApp(currentApp!.url);
|
App newApp = await SourceService().getApp(currentApp!.url);
|
||||||
if (newApp.latestVersion != currentApp.latestVersion) {
|
if (newApp.latestVersion != currentApp.latestVersion) {
|
||||||
newApp.installedVersion = currentApp.installedVersion;
|
newApp.installedVersion = currentApp.installedVersion;
|
||||||
@@ -248,9 +187,9 @@ class AppsProvider with ChangeNotifier {
|
|||||||
Future<void> installUpdates() async {
|
Future<void> installUpdates() async {
|
||||||
List<String> appIds = apps.keys.toList();
|
List<String> appIds = apps.keys.toList();
|
||||||
for (int i = 0; i < appIds.length; i++) {
|
for (int i = 0; i < appIds.length; i++) {
|
||||||
App? app = apps[appIds[i]];
|
App? app = apps[appIds[i]]!.app;
|
||||||
if (app!.installedVersion != app.latestVersion) {
|
if (app!.installedVersion != app.latestVersion) {
|
||||||
await backgroundDownloadAndInstallApp(app);
|
await downloadAndInstallLatestApp(app.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
|
||||||
// Sub-classes used in App Source
|
// Sub-classes used in App Source
|
||||||
|
|
||||||
@@ -28,6 +29,12 @@ abstract class AppSource {
|
|||||||
AppNames getAppNames(String standardUrl);
|
AppNames getAppNames(String standardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
escapeRegEx(String s) {
|
||||||
|
return s.replaceAllMapped(RegExp(r'[.*+?^${}()|[\]\\]'), (x) {
|
||||||
|
return "\\${x[0]}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// App class
|
// App class
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
@@ -38,9 +45,8 @@ class App {
|
|||||||
String? installedVersion;
|
String? installedVersion;
|
||||||
late String latestVersion;
|
late String latestVersion;
|
||||||
late String apkUrl;
|
late String apkUrl;
|
||||||
String? currentDownloadId;
|
|
||||||
App(this.id, this.url, this.author, this.name, this.installedVersion,
|
App(this.id, this.url, this.author, this.name, this.installedVersion,
|
||||||
this.latestVersion, this.apkUrl, this.currentDownloadId);
|
this.latestVersion, this.apkUrl);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -56,10 +62,7 @@ class App {
|
|||||||
? null
|
? null
|
||||||
: json['installedVersion'] as String,
|
: json['installedVersion'] as String,
|
||||||
json['latestVersion'] as String,
|
json['latestVersion'] as String,
|
||||||
json['apkUrl'] as String,
|
json['apkUrl'] as String);
|
||||||
json['currentDownloadId'] == null
|
|
||||||
? null
|
|
||||||
: json['currentDownloadId'] as String);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
@@ -69,7 +72,6 @@ class App {
|
|||||||
'installedVersion': installedVersion,
|
'installedVersion': installedVersion,
|
||||||
'latestVersion': latestVersion,
|
'latestVersion': latestVersion,
|
||||||
'apkUrl': apkUrl,
|
'apkUrl': apkUrl,
|
||||||
'currentDownloadId': currentDownloadId
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,20 +96,22 @@ class GitHub implements AppSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<APKDetails> getLatestAPKUrl(String standardUrl) async {
|
Future<APKDetails> getLatestAPKUrl(String standardUrl) async {
|
||||||
Response res = await get(Uri.parse(
|
Response res = await get(Uri.parse('$standardUrl/releases/latest'));
|
||||||
'${convertURL(standardUrl, 'api.github.com/repos')}/releases/latest'));
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
dynamic release = jsonDecode(res.body);
|
var standardUri = Uri.parse(standardUrl);
|
||||||
for (int i = 0; i < release['assets'].length; i++) {
|
var parsedHtml = parse(res.body);
|
||||||
if (release['assets'][i]['name']
|
var apkUrlList = parsedHtml.querySelectorAll('a').where((element) {
|
||||||
.toString()
|
return RegExp(
|
||||||
.toLowerCase()
|
'^${escapeRegEx(standardUri.path)}/releases/download/*/(?!/).*.apk\$',
|
||||||
.endsWith('.apk')) {
|
caseSensitive: false)
|
||||||
return APKDetails(release['tag_name'],
|
.hasMatch(element.attributes['href']!);
|
||||||
release['assets'][i]['browser_download_url']);
|
}).toList();
|
||||||
}
|
String? version = parsedHtml.querySelector('h1')?.innerHtml;
|
||||||
}
|
if (apkUrlList.isEmpty || version == null) {
|
||||||
throw 'No APK found';
|
throw 'No APK found';
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
version, '${standardUri.origin}${apkUrlList[0].attributes['href']!}');
|
||||||
} else {
|
} else {
|
||||||
throw 'Unable to fetch release info';
|
throw 'Unable to fetch release info';
|
||||||
}
|
}
|
||||||
@@ -147,7 +151,6 @@ class SourceService {
|
|||||||
names.name[0].toUpperCase() + names.name.substring(1),
|
names.name[0].toUpperCase() + names.name.substring(1),
|
||||||
null,
|
null,
|
||||||
apk.version,
|
apk.version,
|
||||||
apk.downloadUrl,
|
apk.downloadUrl);
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
pubspec.lock
41
pubspec.lock
@@ -71,6 +71,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -118,13 +125,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_downloader:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_downloader
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.8.1"
|
|
||||||
flutter_fgbg:
|
flutter_fgbg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -172,11 +172,13 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_web_plugins:
|
html:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description: flutter
|
description:
|
||||||
source: sdk
|
name: html
|
||||||
version: "0.0.0"
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -198,13 +200,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
js:
|
install_plugin_v2:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: js
|
name: install_plugin_v2
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.4"
|
version: "1.0.0"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -392,13 +394,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.8.0"
|
||||||
toast:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: toast
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.0"
|
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -37,15 +37,15 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
flutter_downloader: ^1.8.1
|
|
||||||
flutter_fgbg: ^0.2.0
|
flutter_fgbg: ^0.2.0
|
||||||
flutter_local_notifications: ^9.7.0
|
flutter_local_notifications: ^9.7.0
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
toast: ^0.3.0
|
|
||||||
webview_flutter: ^3.0.4
|
webview_flutter: ^3.0.4
|
||||||
workmanager: ^0.5.0
|
workmanager: ^0.5.0
|
||||||
dynamic_color: ^1.5.3
|
dynamic_color: ^1.5.3
|
||||||
|
install_plugin_v2: ^1.0.0
|
||||||
|
html: ^0.15.0
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
Reference in New Issue
Block a user