From 605877f6bfc55bc28f5d38ffe64d3a4c0c0a5d7a Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Thu, 18 Aug 2022 12:24:36 -0400 Subject: [PATCH] Progress on basic UI for testing --- lib/main.dart | 145 ++++++++++++++----------------- lib/pages/add_app.dart | 72 +++++++++++++++ lib/pages/app.dart | 32 +++++++ lib/pages/apps.dart | 47 ++++++++++ lib/services/apps_provider.dart | 11 +-- lib/services/source_service.dart | 40 +++++++-- pubspec.lock | 28 ++++++ pubspec.yaml | 1 + 8 files changed, 280 insertions(+), 96 deletions(-) create mode 100644 lib/pages/add_app.dart create mode 100644 lib/pages/app.dart create mode 100644 lib/pages/apps.dart diff --git a/lib/main.dart b/lib/main.dart index 5ee695b..222d20b 100644 --- a/lib/main.dart +++ b/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? 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 createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int ind = 0; - List 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: [ - Text( - urls[ind], - style: Theme.of(context).textTheme.headline4, - ), - ], + title: 'Obtainium', + theme: ThemeData( + primarySwatch: Colors.blue, ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - context.read().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 createState() => _MyHomePageState(); +// } + +// class _MyHomePageState extends State { +// int ind = 0; +// List 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: [ +// Text( +// urls[ind], +// style: Theme.of(context).textTheme.headline4, +// ), +// ], +// ), +// ), +// floatingActionButton: FloatingActionButton( +// onPressed: () { +// context.read().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(); +// } +// } diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart new file mode 100644 index 0000000..daeed1d --- /dev/null +++ b/lib/pages/add_app.dart @@ -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 createState() => _AddAppPageState(); +} + +class _AddAppPageState extends State { + final _formKey = GlobalKey(); + 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.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'), + ), + ), + ], + ), + )), + ); + } +} diff --git a/lib/pages/app.dart b/lib/pages/app.dart new file mode 100644 index 0000000..5898934 --- /dev/null +++ b/lib/pages/app.dart @@ -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 createState() => _AppPageState(); +} + +class _AppPageState extends State { + @override + Widget build(BuildContext context) { + var appsProvider = context.watch(); + 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, + )); + } +} diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart new file mode 100644 index 0000000..f65643e --- /dev/null +++ b/lib/pages/apps.dart @@ -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 createState() => _AppsPageState(); +} + +class _AppsPageState extends State { + @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(); + 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), + ), + ); + } +} diff --git a/lib/services/apps_provider.dart b/lib/services/apps_provider.dart index 6058d1f..614d813 100644 --- a/lib/services/apps_provider.dart +++ b/lib/services/apps_provider.dart @@ -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 backgroundDownloadAndInstallApp(App app) async { Directory apkDir = Directory( '${(await getExternalStorageDirectory())?.path as String}/apks/${app.id}'); @@ -153,7 +153,7 @@ class AppsProvider with ChangeNotifier { Future 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 installApp(String url) async { - App app = await SourceService().getApp(url); - await backgroundDownloadAndInstallApp(app); - } - Future> checkUpdates() async { List updates = []; List 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); } } } diff --git a/lib/services/source_service.dart b/lib/services/source_service.dart index 60a1bfe..f32336c 100644 --- a/lib/services/source_service.dart +++ b/lib/services/source_service.dart @@ -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 json) => _appFromJson(json); -} - -App _appFromJson(Map json) { - return App( + factory App.fromJson(Map 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 json) { json['currentDownloadId'] == null ? null : json['currentDownloadId'] as String); + + Map 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 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); } } diff --git a/pubspec.lock b/pubspec.lock index 1286af7..751019e 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 1ba57f3..ab670bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: