mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-30 13:03:28 +01:00 
			
		
		
		
	Progress on basic UI for testing
This commit is contained in:
		
							
								
								
									
										145
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,4 +1,5 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:obtainium/pages/apps.dart'; | ||||
| import 'package:obtainium/services/apps_provider.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:toast/toast.dart'; | ||||
| @@ -11,91 +12,77 @@ 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 { | ||||
|   const MyApp({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return MaterialApp( | ||||
|       title: 'Obtainium', | ||||
|       theme: ThemeData( | ||||
|         primarySwatch: Colors.blue, | ||||
|       ), | ||||
|       home: const MyHomePage(title: 'Obtainium'), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MyHomePage extends StatefulWidget { | ||||
|   const MyHomePage({super.key, required this.title}); | ||||
|  | ||||
|   final String title; | ||||
|  | ||||
|   @override | ||||
|   State<MyHomePage> createState() => _MyHomePageState(); | ||||
| } | ||||
|  | ||||
| class _MyHomePageState extends State<MyHomePage> { | ||||
|   int ind = 0; | ||||
|   List<String> urls = [ | ||||
|     'https://github.com/Ashinch/ReadYou/releases/download', // Should work | ||||
|     'http://github.com/syncthing/syncthing-android/releases/tag/1.20.4', // Should work | ||||
|     'https://github.com/videolan/vlc' // Should not | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     ToastContext().init(context); | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(widget.title), | ||||
|       ), | ||||
|       body: Center( | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: <Widget>[ | ||||
|             Text( | ||||
|               urls[ind], | ||||
|               style: Theme.of(context).textTheme.headline4, | ||||
|             ), | ||||
|           ], | ||||
|         title: 'Obtainium', | ||||
|         theme: ThemeData( | ||||
|           primarySwatch: Colors.blue, | ||||
|         ), | ||||
|       ), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         onPressed: () { | ||||
|           context.read<AppsProvider>().installApp(urls[ind]).then((_) { | ||||
|             setState(() { | ||||
|               ind = ind == (urls.length - 1) ? 0 : ind + 1; | ||||
|             }); | ||||
|           }).catchError((err) { | ||||
|             if (err is! String) { | ||||
|               err = "Unknown Error"; | ||||
|             } | ||||
|             Toast.show(err); | ||||
|           }); | ||||
|         }, | ||||
|         tooltip: 'Increment', | ||||
|         child: const Icon(Icons.add), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|         // home: const MyHomePage(title: 'Obtainium'), | ||||
|         home: const AppsPage()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // class MyHomePage extends StatefulWidget { | ||||
| //   const MyHomePage({super.key, required this.title}); | ||||
|  | ||||
| //   final String title; | ||||
|  | ||||
| //   @override | ||||
| //   State<MyHomePage> createState() => _MyHomePageState(); | ||||
| // } | ||||
|  | ||||
| // class _MyHomePageState extends State<MyHomePage> { | ||||
| //   int ind = 0; | ||||
| //   List<String> urls = [ | ||||
| //     'https://github.com/Ashinch/ReadYou/releases/download', // Should work | ||||
| //     'http://github.com/syncthing/syncthing-android/releases/tag/1.20.4', // Should work | ||||
| //     'https://github.com/videolan/vlc' // Should not | ||||
| //   ]; | ||||
|  | ||||
| //   @override | ||||
| //   Widget build(BuildContext context) { | ||||
| //     ToastContext().init(context); | ||||
| //     return Scaffold( | ||||
| //       appBar: AppBar( | ||||
| //         title: Text(widget.title), | ||||
| //       ), | ||||
| //       body: Center( | ||||
| //         child: Column( | ||||
| //           mainAxisAlignment: MainAxisAlignment.center, | ||||
| //           children: <Widget>[ | ||||
| //             Text( | ||||
| //               urls[ind], | ||||
| //               style: Theme.of(context).textTheme.headline4, | ||||
| //             ), | ||||
| //           ], | ||||
| //         ), | ||||
| //       ), | ||||
| //       floatingActionButton: FloatingActionButton( | ||||
| //         onPressed: () { | ||||
| //           context.read<AppsProvider>().installApp(urls[ind]).then((_) { | ||||
| //             setState(() { | ||||
| //               ind = ind == (urls.length - 1) ? 0 : ind + 1; | ||||
| //             }); | ||||
| //           }).catchError((err) { | ||||
| //             if (err is! String) { | ||||
| //               err = "Unknown Error"; | ||||
| //             } | ||||
| //             Toast.show(err); | ||||
| //           }); | ||||
| //         }, | ||||
| //         tooltip: 'Increment', | ||||
| //         child: const Icon(Icons.add), | ||||
| //       ), | ||||
| //     ); | ||||
| //   } | ||||
|  | ||||
| //   @override | ||||
| //   void 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 { | ||||
|     Directory apkDir = Directory( | ||||
|         '${(await getExternalStorageDirectory())?.path as String}/apks/${app.id}'); | ||||
| @@ -153,7 +153,7 @@ class AppsProvider with ChangeNotifier { | ||||
|  | ||||
|   Future<void> saveApp(App app) async { | ||||
|     File('${(await getAppsDir()).path}/${app.id}.json') | ||||
|         .writeAsStringSync(jsonEncode(app)); | ||||
|         .writeAsStringSync(jsonEncode(app.toJson())); | ||||
|     apps.update(app.id, (value) => app, ifAbsent: () => app); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| @@ -165,11 +165,6 @@ class AppsProvider with ChangeNotifier { | ||||
|     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 { | ||||
|     List<App> updates = []; | ||||
|     List<String> appIds = apps.keys.toList(); | ||||
| @@ -190,7 +185,7 @@ class AppsProvider with ChangeNotifier { | ||||
|     for (int i = 0; i < appIds.length; i++) { | ||||
|       App? app = apps[appIds[i]]; | ||||
|       if (app!.installedVersion != app.latestVersion) { | ||||
|         await installApp(app.apkUrl); | ||||
|         await backgroundDownloadAndInstallApp(app); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -33,25 +33,25 @@ abstract class AppSource { | ||||
| class App { | ||||
|   late String id; | ||||
|   late String url; | ||||
|   late String author; | ||||
|   late String name; | ||||
|   String? installedVersion; | ||||
|   late String latestVersion; | ||||
|   late String apkUrl; | ||||
|   String? currentDownloadId; | ||||
|   App(this.id, this.url, this.installedVersion, this.latestVersion, this.apkUrl, | ||||
|       this.currentDownloadId); | ||||
|   App(this.id, this.url, this.author, this.name, this.installedVersion, | ||||
|       this.latestVersion, this.apkUrl, this.currentDownloadId); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'ID: $id URL: $url INSTALLED: $installedVersion LATEST: $latestVersion APK: $apkUrl'; | ||||
|   } | ||||
|  | ||||
|   factory App.fromJson(Map<String, dynamic> json) => _appFromJson(json); | ||||
| } | ||||
|  | ||||
| App _appFromJson(Map<String, dynamic> json) { | ||||
|   return App( | ||||
|   factory App.fromJson(Map<String, dynamic> json) => App( | ||||
|       json['id'] as String, | ||||
|       json['url'] as String, | ||||
|       json['author'] as String, | ||||
|       json['name'] as String, | ||||
|       json['installedVersion'] == null | ||||
|           ? null | ||||
|           : json['installedVersion'] as String, | ||||
| @@ -60,6 +60,17 @@ App _appFromJson(Map<String, dynamic> json) { | ||||
|       json['currentDownloadId'] == null | ||||
|           ? null | ||||
|           : 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 | ||||
| @@ -121,11 +132,22 @@ class SourceService { | ||||
|   } | ||||
|  | ||||
|   Future<App> getApp(String url) async { | ||||
|     if (url.toLowerCase().indexOf('http://') != 0 && | ||||
|         url.toLowerCase().indexOf('https://') != 0) { | ||||
|       url = 'https://$url'; | ||||
|     } | ||||
|     AppSource source = getSource(url); | ||||
|     String standardUrl = source.standardizeURL(url); | ||||
|     AppNames names = source.getAppNames(standardUrl); | ||||
|     APKDetails apk = await source.getLatestAPKUrl(standardUrl); | ||||
|     return App('${names.author}_${names.name}', standardUrl, null, apk.version, | ||||
|         apk.downloadUrl, null); | ||||
|     return App( | ||||
|         '${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" | ||||
|     source: hosted | ||||
|     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: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -43,6 +43,7 @@ dependencies: | ||||
|   provider: ^6.0.3 | ||||
|   http: ^0.13.5 | ||||
|   toast: ^0.3.0 | ||||
|   webview_flutter: ^3.0.4 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user