diff --git a/lib/components/custom_app_bar.dart b/lib/components/custom_app_bar.dart new file mode 100644 index 0000000..47e08d2 --- /dev/null +++ b/lib/components/custom_app_bar.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class CustomAppBar extends StatefulWidget { + const CustomAppBar({super.key, required this.title}); + + final String title; + + @override + State createState() => _CustomAppBarState(); +} + +class _CustomAppBarState extends State { + @override + Widget build(BuildContext context) { + return SliverAppBar( + pinned: true, + automaticallyImplyLeading: false, + expandedHeight: 100, + flexibleSpace: FlexibleSpaceBar( + titlePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + title: Text( + widget.title, + style: + TextStyle(color: Theme.of(context).textTheme.bodyMedium!.color), + ), + ), + ); + } +} diff --git a/lib/components/generated_form_modal.dart b/lib/components/generated_form_modal.dart index c45ea4b..e1644be 100644 --- a/lib/components/generated_form_modal.dart +++ b/lib/components/generated_form_modal.dart @@ -59,14 +59,13 @@ class _GeneratedFormModalState extends State { actions: [ TextButton( onPressed: () { - HapticFeedback.lightImpact(); Navigator.of(context).pop(null); }, child: const Text('Cancel')), TextButton( onPressed: () { if (_formKey.currentState?.validate() == true) { - HapticFeedback.heavyImpact(); + HapticFeedback.selectionClick(); Navigator.of(context).pop(formInputs .map((e) => (e[0] as TextEditingController).value.text) .toList()); diff --git a/lib/main.dart b/lib/main.dart index 9b8e067..4d952a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; const String currentReleaseTag = - 'v0.2.1-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES + 'v0.2.2-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES @pragma('vm:entry-point') void bgTaskCallback() { diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index fab23d4..72a086a 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/pages/app.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; @@ -22,113 +23,125 @@ class _AddAppPageState extends State { @override Widget build(BuildContext context) { SourceProvider sourceProvider = SourceProvider(); - return Center( - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container(), - Padding( - padding: const EdgeInsets.all(16), + return CustomScrollView(slivers: [ + const CustomAppBar(title: 'Add App'), + SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Form( + key: _formKey, child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - TextFormField( - decoration: const InputDecoration( - hintText: 'https://github.com/Author/Project', - helperText: 'Enter the App source URL'), - controller: urlInputController, - validator: (value) { - if (value == null || - value.isEmpty || - Uri.tryParse(value) == null) { - return 'Please enter a supported source URL'; - } - return null; - }, - ), + Container(), Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ElevatedButton( - onPressed: gettingAppInfo - ? null - : () { - HapticFeedback.mediumImpact(); - if (_formKey.currentState!.validate()) { - setState(() { - gettingAppInfo = true; - }); - sourceProvider - .getApp(urlInputController.value.text) - .then((app) { - var appsProvider = - context.read(); - var settingsProvider = - context.read(); - if (appsProvider.apps.containsKey(app.id)) { - throw 'App already added'; - } - settingsProvider - .getInstallPermission() - .then((_) { - appsProvider.saveApp(app).then((_) { - urlInputController.clear(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AppPage(appId: app.id))); - }); - }); - }).catchError((e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); - }).whenComplete(() { - setState(() { - gettingAppInfo = false; - }); - }); - } - }, - child: const Text('Add'), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + decoration: const InputDecoration( + hintText: 'https://github.com/Author/Project', + helperText: 'Enter the App source URL'), + 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: gettingAppInfo + ? null + : () { + HapticFeedback.selectionClick(); + if (_formKey.currentState!.validate()) { + setState(() { + gettingAppInfo = true; + }); + sourceProvider + .getApp( + urlInputController.value.text) + .then((app) { + var appsProvider = + context.read(); + var settingsProvider = + context.read(); + if (appsProvider.apps + .containsKey(app.id)) { + throw 'App already added'; + } + settingsProvider + .getInstallPermission() + .then((_) { + appsProvider.saveApp(app).then((_) { + urlInputController.clear(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AppPage( + appId: app.id))); + }); + }); + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text(e.toString())), + ); + }).whenComplete(() { + setState(() { + gettingAppInfo = false; + }); + }); + } + }, + child: const Text('Add'), + ), + ), + ], ), ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Supported Sources:', + // style: TextStyle(fontWeight: FontWeight.bold), + // style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox( + height: 8, + ), + ...sourceProvider + .getSourceHosts() + .map((e) => GestureDetector( + onTap: () { + launchUrlString('https://$e', + mode: LaunchMode.externalApplication); + }, + child: Text( + e, + style: const TextStyle( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic), + ))) + .toList() + ]), + if (gettingAppInfo) + const LinearProgressIndicator() + else + Container(), ], - ), - ), - Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text( - 'Supported Sources:', - // style: TextStyle(fontWeight: FontWeight.bold), - // style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox( - height: 8, - ), - ...sourceProvider - .getSourceHosts() - .map((e) => GestureDetector( - onTap: () { - launchUrlString('https://$e', - mode: LaunchMode.externalApplication); - }, - child: Text( - e, - style: const TextStyle( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic), - ))) - .toList() - ]), - if (gettingAppInfo) - const LinearProgressIndicator() - else - Container(), - ], - )), - ); + )), + )) + ]); } } diff --git a/lib/pages/app.dart b/lib/pages/app.dart index 6c1c6be..8fefe9d 100644 --- a/lib/pages/app.dart +++ b/lib/pages/app.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -25,61 +26,64 @@ class _AppPageState extends State { appsProvider.getUpdate(app!.app.id); } return Scaffold( - appBar: AppBar( - title: Text('${app?.app.author}/${app?.app.name}'), - ), - body: settingsProvider.showAppWebpage - ? WebView( - initialUrl: app?.app.url, - javascriptMode: JavascriptMode.unrestricted, - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - app?.app.name ?? 'App', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.displayLarge, - ), - Text( - 'By ${app?.app.author ?? 'Unknown'}', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox( - height: 32, - ), - GestureDetector( - onTap: () { - if (app?.app.url != null) { - launchUrlString(app?.app.url ?? '', - mode: LaunchMode.externalApplication); - } - }, - child: Text( - app?.app.url ?? '', + backgroundColor: Theme.of(context).colorScheme.surface, + body: CustomScrollView(slivers: [ + CustomAppBar(title: '${app?.app.name}'), + SliverFillRemaining( + child: settingsProvider.showAppWebpage + ? WebView( + initialUrl: app?.app.url, + javascriptMode: JavascriptMode.unrestricted, + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + app?.app.name ?? 'App', textAlign: TextAlign.center, - style: const TextStyle( - decoration: TextDecoration.underline, - fontStyle: FontStyle.italic, - fontSize: 12), - )), - const SizedBox( - height: 32, + style: Theme.of(context).textTheme.displayLarge, + ), + Text( + 'By ${app?.app.author ?? 'Unknown'}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox( + height: 32, + ), + GestureDetector( + onTap: () { + if (app?.app.url != null) { + launchUrlString(app?.app.url ?? '', + mode: LaunchMode.externalApplication); + } + }, + child: Text( + app?.app.url ?? '', + textAlign: TextAlign.center, + style: const TextStyle( + decoration: TextDecoration.underline, + fontStyle: FontStyle.italic, + fontSize: 12), + )), + const SizedBox( + height: 32, + ), + Text( + 'Latest Version: ${app?.app.latestVersion ?? 'Unknown'}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'Installed Version: ${app?.app.installedVersion ?? 'None'}', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], ), - Text( - 'Latest Version: ${app?.app.latestVersion ?? 'Unknown'}', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'Installed Version: ${app?.app.installedVersion ?? 'None'}', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - ), + ), + ]), bottomSheet: Padding( padding: EdgeInsets.fromLTRB( 0, 0, 0, MediaQuery.of(context).padding.bottom), @@ -91,15 +95,15 @@ class _AppPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (app?.app.installedVersion == null) + if (app?.app.installedVersion != app?.app.latestVersion) IconButton( onPressed: () { showDialog( context: context, builder: (BuildContext ctx) { return AlertDialog( - title: const Text( - 'App Already Installed?'), + title: Text( + 'App Already ${app?.app.installedVersion == null ? 'Installed' : 'Updated'}?'), actions: [ TextButton( onPressed: () { @@ -108,6 +112,7 @@ class _AppPageState extends State { child: const Text('No')), TextButton( onPressed: () { + HapticFeedback.selectionClick(); var updatedApp = app?.app; if (updatedApp != null) { updatedApp.installedVersion = @@ -124,9 +129,42 @@ class _AppPageState extends State { }); }, tooltip: 'Mark as Installed', - icon: const Icon(Icons.done)), - if (app?.app.installedVersion == null) - const SizedBox(width: 16.0), + icon: const Icon(Icons.done)) + else + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext ctx) { + return AlertDialog( + title: const Text('App Not Installed?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('No')), + TextButton( + onPressed: () { + HapticFeedback.selectionClick(); + var updatedApp = app?.app; + if (updatedApp != null) { + updatedApp.installedVersion = + null; + appsProvider + .saveApp(updatedApp); + } + Navigator.of(context).pop(); + }, + child: const Text( + 'Yes, Mark as Not Installed')) + ], + ); + }); + }, + tooltip: 'Mark as Not Installed', + icon: const Icon(Icons.no_cell_outlined)), + const SizedBox(width: 16.0), Expanded( child: ElevatedButton( onPressed: (app?.app.installedVersion == null || @@ -154,7 +192,6 @@ class _AppPageState extends State { onPressed: app?.downloadProgress != null ? null : () { - HapticFeedback.lightImpact(); showDialog( context: context, builder: (BuildContext ctx) { @@ -165,7 +202,8 @@ class _AppPageState extends State { actions: [ TextButton( onPressed: () { - HapticFeedback.heavyImpact(); + HapticFeedback + .selectionClick(); appsProvider .removeApp(app!.app.id) .then((_) { @@ -178,7 +216,6 @@ class _AppPageState extends State { child: const Text('Remove')), TextButton( onPressed: () { - HapticFeedback.lightImpact(); Navigator.of(context).pop(); }, child: const Text('Cancel')) diff --git a/lib/pages/apps.dart b/lib/pages/apps.dart index 7ccacfd..6f93c7c 100644 --- a/lib/pages/apps.dart +++ b/lib/pages/apps.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/pages/app.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; @@ -35,6 +36,7 @@ class _AppsPageState extends State { } return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, floatingActionButton: existingUpdateAppIds.isEmpty ? null : ElevatedButton.icon( @@ -47,49 +49,51 @@ class _AppsPageState extends State { existingUpdateAppIds, context); }); }, - icon: const Icon(Icons.update), - label: const Text('Update All')), - body: Center( - child: appsProvider.loadingApps - ? const CircularProgressIndicator() - : appsProvider.apps.isEmpty - ? Text( - 'No Apps', - style: Theme.of(context).textTheme.headlineMedium, - ) - : RefreshIndicator( - onRefresh: () { - HapticFeedback.lightImpact(); - return appsProvider.checkUpdates(); - }, - child: ListView( - children: sortedApps - .map( - (e) => ListTile( - title: Text('${e.app.author}/${e.app.name}'), - subtitle: Text( - e.app.installedVersion ?? 'Not Installed'), - trailing: e.downloadProgress != null - ? Text( - 'Downloading - ${e.downloadProgress?.toInt()}%') - : (e.app.installedVersion != null && - e.app.installedVersion != - e.app.latestVersion - ? const Text('Update Available') - : null), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AppPage(appId: e.app.id)), - ); - }, - ), - ) - .toList(), - ), - ), - )); + icon: const Icon(Icons.install_mobile_outlined), + label: const Text('Install All')), + body: RefreshIndicator( + onRefresh: () { + HapticFeedback.lightImpact(); + return appsProvider.checkUpdates(); + }, + child: CustomScrollView(slivers: [ + const CustomAppBar(title: 'Apps'), + if (appsProvider.loadingApps || appsProvider.apps.isEmpty) + SliverFillRemaining( + child: Center( + child: appsProvider.loadingApps + ? const CircularProgressIndicator() + : Text( + 'No Apps', + style: + Theme.of(context).textTheme.headlineMedium, + ))), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return ListTile( + title: Text( + '${sortedApps[index].app.author}/${sortedApps[index].app.name}'), + subtitle: Text(sortedApps[index].app.installedVersion ?? + 'Not Installed'), + trailing: sortedApps[index].downloadProgress != null + ? Text( + 'Downloading - ${sortedApps[index].downloadProgress?.toInt()}%') + : (sortedApps[index].app.installedVersion != null && + sortedApps[index].app.installedVersion != + sortedApps[index].app.latestVersion + ? const Text('Update Available') + : null), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AppPage(appId: sortedApps[index].app.id)), + ); + }, + ); + }, childCount: sortedApps.length)) + ]))); } } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 252dde1..96d5683 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,3 +1,4 @@ +import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:obtainium/pages/add_app.dart'; @@ -12,33 +13,56 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } +class NavigationPageItem { + late String title; + late IconData icon; + late Widget widget; + + NavigationPageItem(this.title, this.icon, this.widget); +} + class _HomePageState extends State { List selectedIndexHistory = []; - List pages = [ - const AppsPage(), - const AddAppPage(), - const ImportExportPage(), - const SettingsPage() + + List pages = [ + NavigationPageItem('Apps', Icons.apps, const AppsPage()), + NavigationPageItem('Add App', Icons.add, const AddAppPage()), + NavigationPageItem( + 'Import/Export', Icons.import_export, const ImportExportPage()), + NavigationPageItem('Settings', Icons.settings, const SettingsPage()) ]; @override Widget build(BuildContext context) { return WillPopScope( child: Scaffold( - appBar: AppBar(title: const Text('Obtainium')), - body: pages.elementAt( - selectedIndexHistory.isEmpty ? 0 : selectedIndexHistory.last), + backgroundColor: Theme.of(context).colorScheme.surface, + body: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + child: child, + ); + }, + child: pages + .elementAt(selectedIndexHistory.isEmpty + ? 0 + : selectedIndexHistory.last) + .widget, + ), bottomNavigationBar: NavigationBar( - destinations: const [ - NavigationDestination(icon: Icon(Icons.apps), label: 'Apps'), - NavigationDestination(icon: Icon(Icons.add), label: 'Add App'), - NavigationDestination( - icon: Icon(Icons.import_export), label: 'Import/Export'), - NavigationDestination( - icon: Icon(Icons.settings), label: 'Settings'), - ], + destinations: pages + .map((e) => + NavigationDestination(icon: Icon(e.icon), label: e.title)) + .toList(), onDestinationSelected: (int index) { - HapticFeedback.lightImpact(); + HapticFeedback.selectionClick(); setState(() { if (index == 0) { selectedIndexHistory.clear(); diff --git a/lib/pages/import_export.dart b/lib/pages/import_export.dart index 831252c..9591df9 100644 --- a/lib/pages/import_export.dart +++ b/lib/pages/import_export.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; @@ -25,6 +26,16 @@ class _ImportExportPageState extends State { SourceProvider sourceProvider = SourceProvider(); var settingsProvider = context.read(); var appsProvider = context.read(); + var outlineButtonStyle = ButtonStyle( + shape: MaterialStateProperty.all( + StadiumBorder( + side: BorderSide( + width: 1, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); Future>> addApps(List urls) async { await settingsProvider.getInstallPermission(); @@ -43,192 +54,227 @@ class _ImportExportPageState extends State { return errors; } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton( - onPressed: appsProvider.apps.isEmpty || importInProgress - ? null - : () { - HapticFeedback.lightImpact(); - appsProvider.exportApps().then((String path) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Exported to $path')), - ); - }); - }, - child: const Text('Obtainium Export')), - const SizedBox( - height: 8, - ), - ElevatedButton( - onPressed: importInProgress - ? null - : () { - HapticFeedback.lightImpact(); - FilePicker.platform.pickFiles().then((result) { - setState(() { - importInProgress = true; - }); - if (result != null) { - String data = File(result.files.single.path!) - .readAsStringSync(); - try { - jsonDecode(data); - } catch (e) { - throw 'Invalid input'; - } - appsProvider.importApps(data).then((value) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '$value App${value == 1 ? '' : 's'} Imported')), - ); - }); - } else { - // User canceled the picker - } - }).catchError((e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); - }, - child: const Text('Obtainium Import')), - if (importInProgress) - Column( - children: const [ - SizedBox( - height: 14, - ), - LinearProgressIndicator(), - SizedBox( - height: 14, - ), - ], - ) - else - const Divider( - height: 32, - ), - TextButton( - onPressed: importInProgress - ? null - : () { - showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: 'Import from URL List', - items: [ - GeneratedFormItem('App URL List', true, 7) - ], - ); - }).then((values) { - if (values != null) { - var urls = (values[0] as String).split('\n'); - setState(() { - importInProgress = true; - }); - addApps(urls).then((errors) { - if (errors.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Imported ${urls.length} Apps')), - ); - } else { - showDialog( - context: context, - builder: (BuildContext ctx) { - return ImportErrorDialog( - urlsLength: urls.length, - errors: errors); - }); - } - }).catchError((e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); - } - }); - }, - child: const Text('Import from URL List')), - ...sourceProvider.massSources - .map((source) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 8), - TextButton( + return CustomScrollView(slivers: [ + const CustomAppBar(title: 'Import/Export'), + SliverFillRemaining( + hasScrollBody: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: TextButton( + style: outlineButtonStyle, + onPressed: appsProvider.apps.isEmpty || + importInProgress + ? null + : () { + HapticFeedback.selectionClick(); + appsProvider + .exportApps() + .then((String path) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: + Text('Exported to $path')), + ); + }); + }, + child: const Text('Obtainium Export'))), + const SizedBox( + width: 16, + ), + Expanded( + child: TextButton( + style: outlineButtonStyle, onPressed: importInProgress ? null : () { - showDialog( - context: context, - builder: (BuildContext ctx) { - return GeneratedFormModal( - title: 'Import ${source.name}', - items: source.requiredArgs - .map((e) => - GeneratedFormItem( - e, true, 1)) - .toList()); - }).then((values) { - if (values != null) { - source.getUrls(values).then((urls) { - setState(() { - importInProgress = true; - }); - addApps(urls).then((errors) { - if (errors.isEmpty) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - 'Imported ${urls.length} Apps')), - ); - } else { - showDialog( - context: context, - builder: - (BuildContext ctx) { - return ImportErrorDialog( - urlsLength: - urls.length, - errors: errors); - }); - } - }).whenComplete(() { - setState(() { - importInProgress = false; - }); - }); - }).catchError((e) { + HapticFeedback.selectionClick(); + FilePicker.platform + .pickFiles() + .then((result) { + setState(() { + importInProgress = true; + }); + if (result != null) { + String data = + File(result.files.single.path!) + .readAsStringSync(); + try { + jsonDecode(data); + } catch (e) { + throw 'Invalid input'; + } + appsProvider + .importApps(data) + .then((value) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( - content: Text(e.toString())), + content: Text( + '$value App${value == 1 ? '' : 's'} Imported')), ); }); + } else { + // User canceled the picker } + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar(content: Text(e.toString())), + ); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); }); }, - child: Text('Import ${source.name}')) - ])) - .toList() - ], - )); + child: const Text('Obtainium Import'))) + ], + ), + if (importInProgress) + Column( + children: const [ + SizedBox( + height: 14, + ), + LinearProgressIndicator(), + SizedBox( + height: 14, + ), + ], + ) + else + const Divider( + height: 32, + ), + TextButton( + onPressed: importInProgress + ? null + : () { + showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: 'Import from URL List', + items: [ + GeneratedFormItem( + 'App URL List', true, 7) + ], + ); + }).then((values) { + if (values != null) { + var urls = (values[0] as String).split('\n'); + setState(() { + importInProgress = true; + }); + addApps(urls).then((errors) { + if (errors.isEmpty) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + 'Imported ${urls.length} Apps')), + ); + } else { + showDialog( + context: context, + builder: (BuildContext ctx) { + return ImportErrorDialog( + urlsLength: urls.length, + errors: errors); + }); + } + }).catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + }).whenComplete(() { + setState(() { + importInProgress = false; + }); + }); + } + }); + }, + child: const Text( + 'Import from URL List', + )), + ...sourceProvider.massSources + .map((source) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + TextButton( + onPressed: importInProgress + ? null + : () { + showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: + 'Import ${source.name}', + items: source.requiredArgs + .map((e) => + GeneratedFormItem( + e, true, 1)) + .toList()); + }).then((values) { + if (values != null) { + source + .getUrls(values) + .then((urls) { + setState(() { + importInProgress = true; + }); + addApps(urls).then((errors) { + if (errors.isEmpty) { + ScaffoldMessenger.of( + context) + .showSnackBar( + SnackBar( + content: Text( + 'Imported ${urls.length} Apps')), + ); + } else { + showDialog( + context: context, + builder: (BuildContext + ctx) { + return ImportErrorDialog( + urlsLength: + urls.length, + errors: errors); + }); + } + }).whenComplete(() { + setState(() { + importInProgress = false; + }); + }); + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: + Text(e.toString())), + ); + }); + } + }); + }, + child: Text('Import ${source.name}')) + ])) + .toList() + ], + ))) + ]); } } @@ -278,7 +324,6 @@ class _ImportErrorDialogState extends State { actions: [ TextButton( onPressed: () { - HapticFeedback.lightImpact(); Navigator.of(context).pop(null); }, child: const Text('Okay')) diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 6322c60..0129f46 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -18,185 +19,214 @@ class _SettingsPageState extends State { if (settingsProvider.prefs == null) { settingsProvider.initializeSettings(); } - return Padding( - padding: const EdgeInsets.all(16), - child: settingsProvider.prefs == null - ? Container() - : Column( - children: [ - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Theme'), - value: settingsProvider.theme, - items: const [ - DropdownMenuItem( - value: ThemeSettings.dark, - child: Text('Dark'), + return CustomScrollView(slivers: [ + const CustomAppBar(title: 'Add App'), + SliverFillRemaining( + hasScrollBody: true, + child: Padding( + padding: const EdgeInsets.all(16), + child: settingsProvider.prefs == null + ? Container() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Appearance', + style: TextStyle( + color: Theme.of(context).colorScheme.primary), ), - DropdownMenuItem( - value: ThemeSettings.light, - child: Text('Light'), + DropdownButtonFormField( + decoration: + const InputDecoration(labelText: 'Theme'), + value: settingsProvider.theme, + items: const [ + DropdownMenuItem( + value: ThemeSettings.dark, + child: Text('Dark'), + ), + DropdownMenuItem( + value: ThemeSettings.light, + child: Text('Light'), + ), + DropdownMenuItem( + value: ThemeSettings.system, + child: Text('Follow System'), + ) + ], + onChanged: (value) { + if (value != null) { + settingsProvider.theme = value; + } + }), + const SizedBox( + height: 16, ), - DropdownMenuItem( - value: ThemeSettings.system, - child: Text('Follow System'), - ) - ], - onChanged: (value) { - if (value != null) { - settingsProvider.theme = value; - } - }), - const SizedBox( - height: 16, - ), - DropdownButtonFormField( - decoration: const InputDecoration(labelText: 'Colour'), - value: settingsProvider.colour, - items: const [ - DropdownMenuItem( - value: ColourSettings.basic, - child: Text('Obtainium'), + DropdownButtonFormField( + decoration: + const InputDecoration(labelText: 'Colour'), + value: settingsProvider.colour, + items: const [ + DropdownMenuItem( + value: ColourSettings.basic, + child: Text('Obtainium'), + ), + DropdownMenuItem( + value: ColourSettings.materialYou, + child: Text('Material You'), + ) + ], + onChanged: (value) { + if (value != null) { + settingsProvider.colour = value; + } + }), + const SizedBox( + height: 16, ), - DropdownMenuItem( - value: ColourSettings.materialYou, - child: Text('Material You'), - ) - ], - onChanged: (value) { - if (value != null) { - settingsProvider.colour = value; - } - }), - const SizedBox( - height: 16, - ), - DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Background Update Checking Interval'), - value: settingsProvider.updateInterval, - items: const [ - DropdownMenuItem( - value: 15, - child: Text('15 Minutes'), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'App Sort By'), + value: settingsProvider.sortColumn, + items: const [ + DropdownMenuItem( + value: SortColumnSettings.authorName, + child: Text('Author/Name'), + ), + DropdownMenuItem( + value: SortColumnSettings.nameAuthor, + child: Text('Name/Author'), + ), + DropdownMenuItem( + value: SortColumnSettings.added, + child: Text('As Added'), + ) + ], + onChanged: (value) { + if (value != null) { + settingsProvider.sortColumn = value; + } + })), + const SizedBox( + width: 16, + ), + Expanded( + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'App Sort Order'), + value: settingsProvider.sortOrder, + items: const [ + DropdownMenuItem( + value: SortOrderSettings.ascending, + child: Text('Ascending'), + ), + DropdownMenuItem( + value: SortOrderSettings.descending, + child: Text('Descending'), + ), + ], + onChanged: (value) { + if (value != null) { + settingsProvider.sortOrder = value; + } + })), + ], ), - DropdownMenuItem( - value: 30, - child: Text('30 Minutes'), + const SizedBox( + height: 16, ), - DropdownMenuItem( - value: 60, - child: Text('1 Hour'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Show Source Webpage in App View'), + Switch( + value: settingsProvider.showAppWebpage, + onChanged: (value) { + settingsProvider.showAppWebpage = value; + }) + ], ), - DropdownMenuItem( - value: 360, - child: Text('6 Hours'), + const Divider( + height: 16, ), - DropdownMenuItem( - value: 720, - child: Text('12 Hours'), + const SizedBox( + height: 16, ), - DropdownMenuItem( - value: 1440, - child: Text('1 Day'), + Text( + 'More', + style: TextStyle( + color: Theme.of(context).colorScheme.primary), ), - DropdownMenuItem( - value: 0, - child: Text('Never - Manual Only'), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: + 'Background Update Checking Interval'), + value: settingsProvider.updateInterval, + items: const [ + DropdownMenuItem( + value: 15, + child: Text('15 Minutes'), + ), + DropdownMenuItem( + value: 30, + child: Text('30 Minutes'), + ), + DropdownMenuItem( + value: 60, + child: Text('1 Hour'), + ), + DropdownMenuItem( + value: 360, + child: Text('6 Hours'), + ), + DropdownMenuItem( + value: 720, + child: Text('12 Hours'), + ), + DropdownMenuItem( + value: 1440, + child: Text('1 Day'), + ), + DropdownMenuItem( + value: 0, + child: Text('Never - Manual Only'), + ), + ], + onChanged: (value) { + if (value != null) { + settingsProvider.updateInterval = value; + } + }), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton.icon( + style: ButtonStyle( + foregroundColor: + MaterialStateProperty.resolveWith( + (Set states) { + return Colors.grey; + }), + ), + onPressed: () { + launchUrlString(settingsProvider.sourceUrl, + mode: LaunchMode.externalApplication); + }, + icon: const Icon(Icons.code), + label: Text( + 'Source', + style: Theme.of(context).textTheme.bodySmall, + ), + ) + ], ), ], - onChanged: (value) { - if (value != null) { - settingsProvider.updateInterval = value; - } - }), - const SizedBox( - height: 16, - ), - DropdownButtonFormField( - decoration: - const InputDecoration(labelText: 'App Sort By'), - value: settingsProvider.sortColumn, - items: const [ - DropdownMenuItem( - value: SortColumnSettings.authorName, - child: Text('Author/Name'), - ), - DropdownMenuItem( - value: SortColumnSettings.nameAuthor, - child: Text('Name/Author'), - ), - DropdownMenuItem( - value: SortColumnSettings.added, - child: Text('As Added'), - ) - ], - onChanged: (value) { - if (value != null) { - settingsProvider.sortColumn = value; - } - }), - const SizedBox( - height: 16, - ), - DropdownButtonFormField( - decoration: - const InputDecoration(labelText: 'App Sort Order'), - value: settingsProvider.sortOrder, - items: const [ - DropdownMenuItem( - value: SortOrderSettings.ascending, - child: Text('Ascending'), - ), - DropdownMenuItem( - value: SortOrderSettings.descending, - child: Text('Descending'), - ), - ], - onChanged: (value) { - if (value != null) { - settingsProvider.sortOrder = value; - } - }), - const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Show Source Webpage in App View'), - Switch( - value: settingsProvider.showAppWebpage, - onChanged: (value) { - settingsProvider.showAppWebpage = value; - }) - ], - ), - const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton.icon( - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.resolveWith( - (Set states) { - return Colors.grey; - }), - ), - onPressed: () { - HapticFeedback.lightImpact(); - launchUrlString(settingsProvider.sourceUrl, - mode: LaunchMode.externalApplication); - }, - icon: const Icon(Icons.code), - label: Text( - 'Source', - style: Theme.of(context).textTheme.bodySmall, - ), - ) - ], - ), - ], - )); + ))) + ]); } } diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index e92dfa4..638e3ee 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -339,13 +339,12 @@ class _APKPickerState extends State { actions: [ TextButton( onPressed: () { - HapticFeedback.lightImpact(); Navigator.of(context).pop(null); }, child: const Text('Cancel')), TextButton( onPressed: () { - HapticFeedback.heavyImpact(); + HapticFeedback.selectionClick(); Navigator.of(context).pop(apkUrl); }, child: const Text('Continue')) @@ -376,13 +375,12 @@ class _APKOriginWarningDialogState extends State { actions: [ TextButton( onPressed: () { - HapticFeedback.lightImpact(); Navigator.of(context).pop(null); }, child: const Text('Cancel')), TextButton( onPressed: () { - HapticFeedback.heavyImpact(); + HapticFeedback.selectionClick(); Navigator.of(context).pop(true); }, child: const Text('Continue')) diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 1a3a058..83e1d70 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -399,9 +399,9 @@ class SourceProvider { GitHub(), GitLab(), FDroid(), + IzzyOnDroid(), Mullvad(), - Signal(), - IzzyOnDroid() + Signal() ]; List massSources = [GitHubStars()]; diff --git a/pubspec.lock b/pubspec.lock index 2d0d9c9..64faa17 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + animations: + dependency: "direct main" + description: + name: animations + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" archive: dependency: transitive description: @@ -201,21 +208,21 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "9.9.1" + version: "10.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux url: "https://pub.dartlang.org" source: hosted - version: "0.5.1" + version: "1.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0de0f31..198dc56 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 0.2.1+12 # When changing this, update the tag in main() accordingly +version: 0.2.2+13 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.19.0-79.0.dev <3.0.0' @@ -38,7 +38,7 @@ dependencies: cupertino_icons: ^1.0.5 path_provider: ^2.0.11 flutter_fgbg: ^0.2.0 # Try removing reliance on this - flutter_local_notifications: ^9.9.1 + flutter_local_notifications: ^10.0.0 provider: ^6.0.3 http: ^0.13.5 webview_flutter: ^3.0.4 @@ -52,6 +52,7 @@ dependencies: fluttertoast: ^8.0.9 device_info_plus: ^4.1.2 file_picker: ^5.1.0 + animations: ^2.0.4 dev_dependencies: