mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 21:13:28 +01:00 
			
		
		
		
	Progress on basic UI for testing
This commit is contained in:
		
							
								
								
									
										127
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,4 +1,5 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:obtainium/pages/apps.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'; | ||||||
| import 'package:toast/toast.dart'; | import 'package:toast/toast.dart'; | ||||||
| @@ -11,20 +12,6 @@ void main() async { | |||||||
|   )); |   )); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Extract a GitHub project name and author account name from a GitHub URL (can be any sub-URL of the project) |  | ||||||
| Map<String, String>? getAppNamesFromGitHubURL(String url) { |  | ||||||
|   RegExp regex = RegExp(r'://github.com/[^/]*/[^/]*'); |  | ||||||
|   RegExpMatch? match = regex.firstMatch(url.toLowerCase()); |  | ||||||
|   if (match != null) { |  | ||||||
|     String uri = url.substring(match.start + 14, match.end); |  | ||||||
|     int slashIndex = uri.indexOf('/'); |  | ||||||
|     String author = uri.substring(0, slashIndex); |  | ||||||
|     String appName = uri.substring(slashIndex + 1); |  | ||||||
|     return {'author': author, 'appName': appName}; |  | ||||||
|   } |  | ||||||
|   return null; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class MyApp extends StatelessWidget { | class MyApp extends StatelessWidget { | ||||||
|   const MyApp({super.key}); |   const MyApp({super.key}); | ||||||
|  |  | ||||||
| @@ -35,67 +22,67 @@ class MyApp extends StatelessWidget { | |||||||
|         theme: ThemeData( |         theme: ThemeData( | ||||||
|           primarySwatch: Colors.blue, |           primarySwatch: Colors.blue, | ||||||
|         ), |         ), | ||||||
|       home: const MyHomePage(title: 'Obtainium'), |         // home: const MyHomePage(title: 'Obtainium'), | ||||||
|     ); |         home: const AppsPage()); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class MyHomePage extends StatefulWidget { | // class MyHomePage extends StatefulWidget { | ||||||
|   const MyHomePage({super.key, required this.title}); | //   const MyHomePage({super.key, required this.title}); | ||||||
|  |  | ||||||
|   final String title; | //   final String title; | ||||||
|  |  | ||||||
|   @override | //   @override | ||||||
|   State<MyHomePage> createState() => _MyHomePageState(); | //   State<MyHomePage> createState() => _MyHomePageState(); | ||||||
| } | // } | ||||||
|  |  | ||||||
| class _MyHomePageState extends State<MyHomePage> { | // class _MyHomePageState extends State<MyHomePage> { | ||||||
|   int ind = 0; | //   int ind = 0; | ||||||
|   List<String> urls = [ | //   List<String> urls = [ | ||||||
|     'https://github.com/Ashinch/ReadYou/releases/download', // Should work | //     'https://github.com/Ashinch/ReadYou/releases/download', // Should work | ||||||
|     'http://github.com/syncthing/syncthing-android/releases/tag/1.20.4', // Should work | //     'http://github.com/syncthing/syncthing-android/releases/tag/1.20.4', // Should work | ||||||
|     'https://github.com/videolan/vlc' // Should not | //     'https://github.com/videolan/vlc' // Should not | ||||||
|   ]; | //   ]; | ||||||
|  |  | ||||||
|   @override | //   @override | ||||||
|   Widget build(BuildContext context) { | //   Widget build(BuildContext context) { | ||||||
|     ToastContext().init(context); | //     ToastContext().init(context); | ||||||
|     return Scaffold( | //     return Scaffold( | ||||||
|       appBar: AppBar( | //       appBar: AppBar( | ||||||
|         title: Text(widget.title), | //         title: Text(widget.title), | ||||||
|       ), | //       ), | ||||||
|       body: Center( | //       body: Center( | ||||||
|         child: Column( | //         child: Column( | ||||||
|           mainAxisAlignment: MainAxisAlignment.center, | //           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|           children: <Widget>[ | //           children: <Widget>[ | ||||||
|             Text( | //             Text( | ||||||
|               urls[ind], | //               urls[ind], | ||||||
|               style: Theme.of(context).textTheme.headline4, | //               style: Theme.of(context).textTheme.headline4, | ||||||
|             ), | //             ), | ||||||
|           ], | //           ], | ||||||
|         ), | //         ), | ||||||
|       ), | //       ), | ||||||
|       floatingActionButton: FloatingActionButton( | //       floatingActionButton: FloatingActionButton( | ||||||
|         onPressed: () { | //         onPressed: () { | ||||||
|           context.read<AppsProvider>().installApp(urls[ind]).then((_) { | //           context.read<AppsProvider>().installApp(urls[ind]).then((_) { | ||||||
|             setState(() { | //             setState(() { | ||||||
|               ind = ind == (urls.length - 1) ? 0 : ind + 1; | //               ind = ind == (urls.length - 1) ? 0 : ind + 1; | ||||||
|             }); | //             }); | ||||||
|           }).catchError((err) { | //           }).catchError((err) { | ||||||
|             if (err is! String) { | //             if (err is! String) { | ||||||
|               err = "Unknown Error"; | //               err = "Unknown Error"; | ||||||
|             } | //             } | ||||||
|             Toast.show(err); | //             Toast.show(err); | ||||||
|           }); | //           }); | ||||||
|         }, | //         }, | ||||||
|         tooltip: 'Increment', | //         tooltip: 'Increment', | ||||||
|         child: const Icon(Icons.add), | //         child: const Icon(Icons.add), | ||||||
|       ), | //       ), | ||||||
|     ); | //     ); | ||||||
|   } | //   } | ||||||
|  |  | ||||||
|   @override | //   @override | ||||||
|   void dispose() { | //   void dispose() { | ||||||
|     super.dispose(); | //     super.dispose(); | ||||||
|   } | //   } | ||||||
| } | // } | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								lib/pages/add_app.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								lib/pages/add_app.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:obtainium/pages/app.dart'; | ||||||
|  | import 'package:obtainium/services/apps_provider.dart'; | ||||||
|  | import 'package:obtainium/services/source_service.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  |  | ||||||
|  | class AddAppPage extends StatefulWidget { | ||||||
|  |   const AddAppPage({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<AddAppPage> createState() => _AddAppPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AddAppPageState extends State<AddAppPage> { | ||||||
|  |   final _formKey = GlobalKey<FormState>(); | ||||||
|  |   final urlInputController = TextEditingController(); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Scaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         title: const Text('Obtainium - Add App'), | ||||||
|  |       ), | ||||||
|  |       body: Center( | ||||||
|  |           child: Form( | ||||||
|  |         key: _formKey, | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           children: [ | ||||||
|  |             TextFormField( | ||||||
|  |               controller: urlInputController, | ||||||
|  |               validator: (value) { | ||||||
|  |                 if (value == null || | ||||||
|  |                     value.isEmpty || | ||||||
|  |                     Uri.tryParse(value) == null) { | ||||||
|  |                   return 'Please enter a supported source URL'; | ||||||
|  |                 } | ||||||
|  |                 return null; | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |             Padding( | ||||||
|  |               padding: const EdgeInsets.symmetric(vertical: 16.0), | ||||||
|  |               child: ElevatedButton( | ||||||
|  |                 onPressed: () { | ||||||
|  |                   if (_formKey.currentState!.validate()) { | ||||||
|  |                     SourceService() | ||||||
|  |                         .getApp(urlInputController.value.text) | ||||||
|  |                         .then((app) { | ||||||
|  |                       var appsProvider = context.read<AppsProvider>(); | ||||||
|  |                       appsProvider.saveApp(app).then((_) { | ||||||
|  |                         Navigator.push( | ||||||
|  |                             context, | ||||||
|  |                             MaterialPageRoute( | ||||||
|  |                                 builder: (context) => AppPage(appId: app.id))); | ||||||
|  |                       }); | ||||||
|  |                     }).catchError((e) { | ||||||
|  |                       ScaffoldMessenger.of(context).showSnackBar( | ||||||
|  |                         SnackBar(content: Text(e.toString())), | ||||||
|  |                       ); | ||||||
|  |                     }); | ||||||
|  |                   } | ||||||
|  |                 }, | ||||||
|  |                 child: const Text('Add'), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       )), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								lib/pages/app.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/pages/app.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:obtainium/services/apps_provider.dart'; | ||||||
|  | import 'package:obtainium/services/source_service.dart'; | ||||||
|  | import 'package:webview_flutter/webview_flutter.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  |  | ||||||
|  | class AppPage extends StatefulWidget { | ||||||
|  |   const AppPage({super.key, required this.appId}); | ||||||
|  |  | ||||||
|  |   final String appId; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<AppPage> createState() => _AppPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AppPageState extends State<AppPage> { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     var appsProvider = context.watch<AppsProvider>(); | ||||||
|  |     App? app = appsProvider.apps[widget.appId]; | ||||||
|  |     if (app == null) { | ||||||
|  |       Navigator.pop(context); | ||||||
|  |     } | ||||||
|  |     return Scaffold( | ||||||
|  |         appBar: AppBar( | ||||||
|  |           title: Text('App - ${app?.name} - ${app?.author}'), | ||||||
|  |         ), | ||||||
|  |         body: WebView( | ||||||
|  |           initialUrl: app?.url, | ||||||
|  |         )); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								lib/pages/apps.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/pages/apps.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:obtainium/pages/add_app.dart'; | ||||||
|  | import 'package:obtainium/services/apps_provider.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  |  | ||||||
|  | class AppsPage extends StatefulWidget { | ||||||
|  |   const AppsPage({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<AppsPage> createState() => _AppsPageState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _AppsPageState extends State<AppsPage> { | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Scaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         title: const Text('Obtainium - Apps'), | ||||||
|  |       ), | ||||||
|  |       body: Center( | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |           children: () { | ||||||
|  |             var appsProvider = context.watch<AppsProvider>(); | ||||||
|  |             if (appsProvider.loadingApps) { | ||||||
|  |               return [const Text('Loading Apps...')]; | ||||||
|  |             } else if (appsProvider.apps.isEmpty) { | ||||||
|  |               return [const Text('No Apps Yet.')]; | ||||||
|  |             } else { | ||||||
|  |               return appsProvider.apps.values.map((e) => Text(e.id)).toList(); | ||||||
|  |             } | ||||||
|  |           }(), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       floatingActionButton: FloatingActionButton( | ||||||
|  |         onPressed: () { | ||||||
|  |           Navigator.push( | ||||||
|  |             context, | ||||||
|  |             MaterialPageRoute(builder: (context) => const AddAppPage()), | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |         tooltip: 'Add App', | ||||||
|  |         child: const Icon(Icons.add), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -105,7 +105,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Given a URL (assumed valid), initiate an APK download (will trigger install callback when complete) |   // Given a App (assumed valid), initiate an APK download (will trigger install callback when complete) | ||||||
|   Future<void> backgroundDownloadAndInstallApp(App app) async { |   Future<void> backgroundDownloadAndInstallApp(App app) async { | ||||||
|     Directory apkDir = Directory( |     Directory apkDir = Directory( | ||||||
|         '${(await getExternalStorageDirectory())?.path as String}/apks/${app.id}'); |         '${(await getExternalStorageDirectory())?.path as String}/apks/${app.id}'); | ||||||
| @@ -153,7 +153,7 @@ 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)); |         .writeAsStringSync(jsonEncode(app.toJson())); | ||||||
|     apps.update(app.id, (value) => app, ifAbsent: () => app); |     apps.update(app.id, (value) => app, ifAbsent: () => app); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
| @@ -165,11 +165,6 @@ class AppsProvider with ChangeNotifier { | |||||||
|     return app.latestVersion != apps[app.id]?.installedVersion; |     return app.latestVersion != apps[app.id]?.installedVersion; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<void> installApp(String url) async { |  | ||||||
|     App app = await SourceService().getApp(url); |  | ||||||
|     await backgroundDownloadAndInstallApp(app); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<List<App>> checkUpdates() async { |   Future<List<App>> checkUpdates() async { | ||||||
|     List<App> updates = []; |     List<App> updates = []; | ||||||
|     List<String> appIds = apps.keys.toList(); |     List<String> appIds = apps.keys.toList(); | ||||||
| @@ -190,7 +185,7 @@ class AppsProvider with ChangeNotifier { | |||||||
|     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]]; | ||||||
|       if (app!.installedVersion != app.latestVersion) { |       if (app!.installedVersion != app.latestVersion) { | ||||||
|         await installApp(app.apkUrl); |         await backgroundDownloadAndInstallApp(app); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -33,25 +33,25 @@ abstract class AppSource { | |||||||
| class App { | class App { | ||||||
|   late String id; |   late String id; | ||||||
|   late String url; |   late String url; | ||||||
|  |   late String author; | ||||||
|  |   late String name; | ||||||
|   String? installedVersion; |   String? installedVersion; | ||||||
|   late String latestVersion; |   late String latestVersion; | ||||||
|   late String apkUrl; |   late String apkUrl; | ||||||
|   String? currentDownloadId; |   String? currentDownloadId; | ||||||
|   App(this.id, this.url, this.installedVersion, this.latestVersion, this.apkUrl, |   App(this.id, this.url, this.author, this.name, this.installedVersion, | ||||||
|       this.currentDownloadId); |       this.latestVersion, this.apkUrl, this.currentDownloadId); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|     return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrl'; |     return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrl'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   factory App.fromJson(Map<String, dynamic> json) => _appFromJson(json); |   factory App.fromJson(Map<String, dynamic> json) => App( | ||||||
| } |  | ||||||
|  |  | ||||||
| App _appFromJson(Map<String, dynamic> json) { |  | ||||||
|   return App( |  | ||||||
|       json['id'] as String, |       json['id'] as String, | ||||||
|       json['url'] as String, |       json['url'] as String, | ||||||
|  |       json['author'] as String, | ||||||
|  |       json['name'] as String, | ||||||
|       json['installedVersion'] == null |       json['installedVersion'] == null | ||||||
|           ? null |           ? null | ||||||
|           : json['installedVersion'] as String, |           : json['installedVersion'] as String, | ||||||
| @@ -60,6 +60,17 @@ App _appFromJson(Map<String, dynamic> json) { | |||||||
|       json['currentDownloadId'] == null |       json['currentDownloadId'] == null | ||||||
|           ? null |           ? null | ||||||
|           : json['currentDownloadId'] as String); |           : json['currentDownloadId'] as String); | ||||||
|  |  | ||||||
|  |   Map<String, dynamic> toJson() => { | ||||||
|  |         'id': id, | ||||||
|  |         'url': url, | ||||||
|  |         'author': author, | ||||||
|  |         'name': name, | ||||||
|  |         'installedVersion': installedVersion, | ||||||
|  |         'latestVersion': latestVersion, | ||||||
|  |         'apkUrl': apkUrl, | ||||||
|  |         'currentDownloadId': currentDownloadId | ||||||
|  |       }; | ||||||
| } | } | ||||||
|  |  | ||||||
| // Specific App Source classes | // Specific App Source classes | ||||||
| @@ -121,11 +132,22 @@ class SourceService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<App> getApp(String url) async { |   Future<App> getApp(String url) async { | ||||||
|  |     if (url.toLowerCase().indexOf('http://') != 0 && | ||||||
|  |         url.toLowerCase().indexOf('https://') != 0) { | ||||||
|  |       url = 'https://$url'; | ||||||
|  |     } | ||||||
|     AppSource source = getSource(url); |     AppSource source = getSource(url); | ||||||
|     String standardUrl = source.standardizeURL(url); |     String standardUrl = source.standardizeURL(url); | ||||||
|     AppNames names = source.getAppNames(standardUrl); |     AppNames names = source.getAppNames(standardUrl); | ||||||
|     APKDetails apk = await source.getLatestAPKUrl(standardUrl); |     APKDetails apk = await source.getLatestAPKUrl(standardUrl); | ||||||
|     return App('${names.author}_${names.name}', standardUrl, null, apk.version, |     return App( | ||||||
|         apk.downloadUrl, null); |         '${names.author}_${names.name}', | ||||||
|  |         standardUrl, | ||||||
|  |         names.author[0].toUpperCase() + names.author.substring(1), | ||||||
|  |         names.name[0].toUpperCase() + names.name.substring(1), | ||||||
|  |         null, | ||||||
|  |         apk.version, | ||||||
|  |         apk.downloadUrl, | ||||||
|  |         null); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -357,6 +357,34 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.2" |     version: "2.1.2" | ||||||
|  |   webview_flutter: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: webview_flutter | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.0.4" | ||||||
|  |   webview_flutter_android: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: webview_flutter_android | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.9.5" | ||||||
|  |   webview_flutter_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: webview_flutter_platform_interface | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "1.9.1" | ||||||
|  |   webview_flutter_wkwebview: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: webview_flutter_wkwebview | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "2.9.3" | ||||||
|   win32: |   win32: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ dependencies: | |||||||
|   provider: ^6.0.3 |   provider: ^6.0.3 | ||||||
|   http: ^0.13.5 |   http: ^0.13.5 | ||||||
|   toast: ^0.3.0 |   toast: ^0.3.0 | ||||||
|  |   webview_flutter: ^3.0.4 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user