From 1034a88cb579d010cef9279506f64231380b3650 Mon Sep 17 00:00:00 2001 From: Moritz Weber Date: Sat, 15 Jul 2023 20:56:30 +0200 Subject: [PATCH] refined init workflow --- CHANGELOG.md | 6 ++ .../domain/repositories/init_repository.dart | 3 +- src/lib/injection_container.dart | 6 +- src/lib/l10n/app_en.arb | 10 ++- src/lib/main.dart | 1 - src/lib/presentation/pages/export_page.dart | 41 ++++++----- .../presentation/pages/init/init_page.dart | 34 +++++++-- .../pages/init/init_smartlists.dart | 64 +++++++++++++++++ .../pages/init/init_workflow.dart | 35 ++++------ src/lib/presentation/state/export_store.dart | 8 ++- .../presentation/state/export_store.g.dart | 18 ++++- src/lib/presentation/state/import_store.dart | 37 ++++++++-- .../presentation/state/import_store.g.dart | 70 ++++++++++++++++++- .../repositories/init_repository_impl.dart | 6 +- 14 files changed, 279 insertions(+), 60 deletions(-) create mode 100644 src/lib/presentation/pages/init/init_smartlists.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5373463..d61f497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +- Implemented loop mode to stop after every song (#102). +- Implemented share button for songs. +- Refined init workflow. + ## 1.4 - Implemented an onboarding workflow. diff --git a/src/lib/domain/repositories/init_repository.dart b/src/lib/domain/repositories/init_repository.dart index 40c17fa..58fa4e3 100644 --- a/src/lib/domain/repositories/init_repository.dart +++ b/src/lib/domain/repositories/init_repository.dart @@ -5,5 +5,6 @@ abstract class InitRepository { Future setInitialized(); Future initHomePage(BuildContext context); - Future initSmartlists(BuildContext context); + Future createFavoritesSmartlist(BuildContext context); + Future createNewlyAddedSmartlist(BuildContext context); } diff --git a/src/lib/injection_container.dart b/src/lib/injection_container.dart index 20c81dd..3d1c26d 100644 --- a/src/lib/injection_container.dart +++ b/src/lib/injection_container.dart @@ -186,10 +186,10 @@ Future setupGetIt() async { homeHistory: history, ), ); - getIt.registerFactoryParam( - (String? importPath, _) => ImportStore( + getIt.registerFactory( + () => ImportStore( importExportRepository: getIt(), - inputPath: importPath, + initRepository: getIt(), ), ); getIt.registerFactory( diff --git a/src/lib/l10n/app_en.arb b/src/lib/l10n/app_en.arb index 5d088ba..34968b1 100644 --- a/src/lib/l10n/app_en.arb +++ b/src/lib/l10n/app_en.arb @@ -360,6 +360,7 @@ "@exportData": {}, "exportDescription": "Select the data you want to export. By default, everything is exported. When exporting, you can select a folder for the file to be stored.", "@exportDescription": {}, + "exportingData": "Exporting data", "songsAlbumsArtists": "Songs, Albums, and Artists", "@songsAlbumsArtists": {}, "librarySettings": "Library Settings", @@ -387,13 +388,20 @@ "disabledBattery": "Battery optimization is disabled.", "@disabledBattery": {}, "favorites": "Favorites", + "favoritesDesc": "Contains all the songs that you like.", "@favorites": {}, "newlyAdded": "Newly added", + "newlyAddedDesc": "Contains the 100 songs that were added last.", "@newlyAdded": {}, "back": "Back", "@back": {}, "next": "Next", "@next": {}, "finish": "Finish", - "@finish": {} + "@finish": {}, + "errorReadData": "Error reading data file.", + "createSmartlists": "Create Smartlists", + "createSmartlistsDesc": "Create suggested smartlists to enhance your listening experience. You can customize these lists later.", + "create": "Create", + "created": "Created" } diff --git a/src/lib/main.dart b/src/lib/main.dart index fc0b68b..67df7c4 100644 --- a/src/lib/main.dart +++ b/src/lib/main.dart @@ -123,7 +123,6 @@ class _RootPageState extends State { ); initRepository.initHomePage(context); - initRepository.initSmartlists(context); } }); diff --git a/src/lib/presentation/pages/export_page.dart b/src/lib/presentation/pages/export_page.dart index 5261779..add6db0 100644 --- a/src/lib/presentation/pages/export_page.dart +++ b/src/lib/presentation/pages/export_page.dart @@ -49,6 +49,15 @@ class _ExportPageState extends State { ), ], centerTitle: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(4.0), + child: Observer(builder: (_) { + if (_exportStore.isExporting) { + return const LinearProgressIndicator(); + } + return Container(); + }), + ), ), body: ListView( children: [ @@ -110,22 +119,22 @@ class _ExportPageState extends State { Future _exportData(BuildContext context) async { try { - final path = await _exportStore.exportData(await FilePicker.platform.getDirectoryPath()); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - if (path != null) const Icon(Icons.check_circle_rounded, color: GREEN), - if (path == null) const Icon(Icons.warning_rounded, color: RED), - const SizedBox(width: 16.0), - if (path != null) Expanded(child: Text(L10n.of(context)!.dataExportedTo(path))), - if (path == null) Expanded(child: Text(L10n.of(context)!.dataExportFailed)), - ], - ), - duration: const Duration(seconds: 10), - showCloseIcon: true, - ), - ); + final dir = await FilePicker.platform.getDirectoryPath(); + _exportStore.exportData(dir).then((path) => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + if (path != null) const Icon(Icons.check_circle_rounded, color: GREEN), + if (path == null) const Icon(Icons.warning_rounded, color: RED), + const SizedBox(width: 16.0), + if (path != null) Expanded(child: Text(L10n.of(context)!.dataExportedTo(path))), + if (path == null) Expanded(child: Text(L10n.of(context)!.dataExportFailed)), + ], + ), + duration: const Duration(seconds: 10), + showCloseIcon: true, + ), + )); } on PlatformException catch (e) { print('Unsupported operation' + e.toString()); } catch (ex) { diff --git a/src/lib/presentation/pages/init/init_page.dart b/src/lib/presentation/pages/init/init_page.dart index 9d07ee3..a8424ec 100644 --- a/src/lib/presentation/pages/init/init_page.dart +++ b/src/lib/presentation/pages/init/init_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/localizations.dart'; import 'package:get_it/get_it.dart'; +import '../../state/import_store.dart'; import '../../state/navigation_store.dart'; import '../../theming.dart'; import 'init_workflow.dart'; @@ -57,7 +58,9 @@ class InitPage extends StatelessWidget { trailing: const Icon(Icons.chevron_right_rounded), onTap: () => Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => const InitWorkflow(), + builder: (BuildContext context) => InitWorkflow( + importStore: GetIt.I(param1: null), + ), ), ), ), @@ -89,11 +92,30 @@ class InitPage extends StatelessWidget { if (pickResult != null) { final importPath = pickResult.paths.first!; - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => InitWorkflow(importPath: importPath), - ), - ); + final importStore = GetIt.I(param1: importPath); + importStore.readDataFile(importPath).then((_) { + if (importStore.error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.warning_rounded, color: RED), + const SizedBox(width: 16.0), + Expanded(child: Text(L10n.of(context)!.errorReadData)), + ], + ), + duration: const Duration(seconds: 5), + showCloseIcon: true, + ), + ); + } else { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => InitWorkflow(importStore: importStore), + ), + ); + } + }); } } on PlatformException catch (e) { print('Unsupported operation' + e.toString()); diff --git a/src/lib/presentation/pages/init/init_smartlists.dart b/src/lib/presentation/pages/init/init_smartlists.dart new file mode 100644 index 0000000..9e38b4e --- /dev/null +++ b/src/lib/presentation/pages/init/init_smartlists.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/localizations.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +import '../../state/import_store.dart'; +import '../../theming.dart'; +import '../../widgets/info_card.dart'; + +class InitSmartlistsPage extends StatelessWidget { + const InitSmartlistsPage({Key? key, required this.importStore}) : super(key: key); + + final ImportStore importStore; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.createSmartlists, + style: TEXT_HEADER, + ), + centerTitle: true, + automaticallyImplyLeading: false, + ), + body: SafeArea( + child: ListView( + children: [ + InfoCard( + text: L10n.of(context)!.createSmartlistsDesc, + ), + Observer(builder: (context) { + return ListTile( + title: Text(L10n.of(context)!.favorites), + subtitle: Text(L10n.of(context)!.favoritesDesc), + trailing: ElevatedButton( + child: importStore.createdFavorites + ? Text(L10n.of(context)!.created) + : Text(L10n.of(context)!.create), + onPressed: importStore.createdFavorites + ? null + : () => importStore.createFavorites(context), + ), + ); + }), + Observer(builder: (context) { + return ListTile( + title: Text(L10n.of(context)!.newlyAdded), + subtitle: Text(L10n.of(context)!.newlyAddedDesc), + trailing: ElevatedButton( + child: importStore.createdNewlyAdded + ? Text(L10n.of(context)!.created) + : Text(L10n.of(context)!.create), + onPressed: importStore.createdNewlyAdded + ? null + : () => importStore.createNewlyAdded(context), + ), + ); + }) + ], + ), + ), + ); + } +} diff --git a/src/lib/presentation/pages/init/init_workflow.dart b/src/lib/presentation/pages/init/init_workflow.dart index 5a3b2ed..8aea2f3 100644 --- a/src/lib/presentation/pages/init/init_workflow.dart +++ b/src/lib/presentation/pages/init/init_workflow.dart @@ -10,11 +10,12 @@ import '../../theming.dart'; import 'init_battery_page.dart'; import 'init_lib_page.dart'; import 'init_meta_page.dart'; +import 'init_smartlists.dart'; class InitWorkflow extends StatefulWidget { - const InitWorkflow({super.key, this.importPath}); + const InitWorkflow({super.key, required this.importStore}); - final String? importPath; + final ImportStore importStore; @override State createState() => _InitWorkflowState(); @@ -24,9 +25,8 @@ class _InitWorkflowState extends State { PageController pageController = PageController(); int index = 0; - late final ImportStore importStore; late final List pages = [ - InitLibPage(importStore: importStore), + InitLibPage(importStore: widget.importStore), ]; final curve = Curves.easeInOut; @@ -34,22 +34,13 @@ class _InitWorkflowState extends State { @override void initState() { - importStore = GetIt.I(param1: widget.importPath); - if (widget.importPath != null) { - importStore.readDataFile(widget.importPath!).then((_) { - if (importStore.songs?.isNotEmpty ?? false) - setState(() => pages.add(InitMetaPage(importStore: importStore))); - DeviceInfoPlugin().androidInfo.then((info) { - if (info.version.sdkInt > 30) - setState(() => pages.add(InitBatteryPage(importStore: importStore))); - }); - }); - } else { - DeviceInfoPlugin().androidInfo.then((info) { - if (info.version.sdkInt > 30) - setState(() => pages.add(InitBatteryPage(importStore: importStore))); - }); - } + if (widget.importStore.songs?.isNotEmpty ?? false) + setState(() => pages.add(InitMetaPage(importStore: widget.importStore))); + setState(() => pages.add(InitSmartlistsPage(importStore: widget.importStore))); + DeviceInfoPlugin().androidInfo.then((info) { + if (info.version.sdkInt > 30) + setState(() => pages.add(InitBatteryPage(importStore: widget.importStore))); + }); super.initState(); } @@ -123,7 +114,9 @@ class _InitWorkflowState extends State { children: [ const SizedBox(width: 10), Text( - index == pages.length - 1 ? L10n.of(context)!.finish : L10n.of(context)!.next, + index == pages.length - 1 + ? L10n.of(context)!.finish + : L10n.of(context)!.next, ), const Icon(Icons.chevron_right_rounded), ], diff --git a/src/lib/presentation/state/export_store.dart b/src/lib/presentation/state/export_store.dart index 8e740f9..6fd9738 100644 --- a/src/lib/presentation/state/export_store.dart +++ b/src/lib/presentation/state/export_store.dart @@ -21,6 +21,9 @@ abstract class _ExportStore with Store { @readonly DataSelection _selection = DataSelection.all(); + @observable + bool isExporting = false; + @action void setSongsAlbumsArtists(bool selected) { _selection.songsAlbumsArtists = selected; @@ -53,7 +56,10 @@ abstract class _ExportStore with Store { Future exportData(String? path) async { if (path != null) { - return await _importExportRepository.exportData(path, _selection); + isExporting = true; + final exportPath = await _importExportRepository.exportData(path, _selection); + isExporting = false; + return exportPath; } return null; } diff --git a/src/lib/presentation/state/export_store.g.dart b/src/lib/presentation/state/export_store.g.dart index bc59842..65f1f1d 100644 --- a/src/lib/presentation/state/export_store.g.dart +++ b/src/lib/presentation/state/export_store.g.dart @@ -27,6 +27,22 @@ mixin _$ExportStore on _ExportStore, Store { }); } + late final _$isExportingAtom = + Atom(name: '_ExportStore.isExporting', context: context); + + @override + bool get isExporting { + _$isExportingAtom.reportRead(); + return super.isExporting; + } + + @override + set isExporting(bool value) { + _$isExportingAtom.reportWrite(value, super.isExporting, () { + super.isExporting = value; + }); + } + late final _$_ExportStoreActionController = ActionController(name: '_ExportStore', context: context); @@ -88,7 +104,7 @@ mixin _$ExportStore on _ExportStore, Store { @override String toString() { return ''' - +isExporting: ${isExporting} '''; } } diff --git a/src/lib/presentation/state/import_store.dart b/src/lib/presentation/state/import_store.dart index cbaa0a3..5ddac14 100644 --- a/src/lib/presentation/state/import_store.dart +++ b/src/lib/presentation/state/import_store.dart @@ -1,27 +1,27 @@ +import 'package:flutter/material.dart'; import 'package:mobx/mobx.dart'; import '../../domain/entities/app_data.dart'; import '../../domain/repositories/import_export_repository.dart'; +import '../../domain/repositories/init_repository.dart'; part 'import_store.g.dart'; class ImportStore extends _ImportStore with _$ImportStore { ImportStore({ required ImportExportRepository importExportRepository, - String? inputPath, - }) : super(importExportRepository, inputPath); + required InitRepository initRepository, + }) : super(importExportRepository, initRepository); } abstract class _ImportStore with Store { _ImportStore( this._importExportRepository, - this.inputPath, + this._initRepository, ); final ImportExportRepository _importExportRepository; - - /// The path of the file to import data from. - String? inputPath; + final InitRepository _initRepository; /// The raw available data. Map? data; @@ -30,6 +30,9 @@ abstract class _ImportStore with Store { @observable AppData? appData; + @observable + bool error = false; + String? get allowedExtensions => appData?.allowedExtensions; List? get blockedFiles => appData?.blockedFiles; List? get libraryFolders => appData?.libraryFolders; @@ -52,9 +55,19 @@ abstract class _ImportStore with Store { @observable ObservableList importedSmartlists = [].asObservable(); + @observable + bool createdFavorites = false; + @observable + bool createdNewlyAdded = false; + @action Future readDataFile(String path) async { - final _data = await _importExportRepository.readDataFile(path); + Map? _data; + try { + _data = await _importExportRepository.readDataFile(path); + } on FormatException { + error = true; + } if (_data != null) { data = _data; @@ -95,5 +108,15 @@ abstract class _ImportStore with Store { } } + @action + Future createFavorites(BuildContext context) async { + _initRepository.createFavoritesSmartlist(context).then((_) => createdFavorites = true); + } + + @action + Future createNewlyAdded(BuildContext context) async { + _initRepository.createNewlyAddedSmartlist(context).then((_) => createdNewlyAdded = true); + } + void dispose() {} } diff --git a/src/lib/presentation/state/import_store.g.dart b/src/lib/presentation/state/import_store.g.dart index 8c9276e..c9056ab 100644 --- a/src/lib/presentation/state/import_store.g.dart +++ b/src/lib/presentation/state/import_store.g.dart @@ -25,6 +25,21 @@ mixin _$ImportStore on _ImportStore, Store { }); } + late final _$errorAtom = Atom(name: '_ImportStore.error', context: context); + + @override + bool get error { + _$errorAtom.reportRead(); + return super.error; + } + + @override + set error(bool value) { + _$errorAtom.reportWrite(value, super.error, () { + super.error = value; + }); + } + late final _$scannedAtom = Atom(name: '_ImportStore.scanned', context: context); @@ -121,6 +136,38 @@ mixin _$ImportStore on _ImportStore, Store { }); } + late final _$createdFavoritesAtom = + Atom(name: '_ImportStore.createdFavorites', context: context); + + @override + bool get createdFavorites { + _$createdFavoritesAtom.reportRead(); + return super.createdFavorites; + } + + @override + set createdFavorites(bool value) { + _$createdFavoritesAtom.reportWrite(value, super.createdFavorites, () { + super.createdFavorites = value; + }); + } + + late final _$createdNewlyAddedAtom = + Atom(name: '_ImportStore.createdNewlyAdded', context: context); + + @override + bool get createdNewlyAdded { + _$createdNewlyAddedAtom.reportRead(); + return super.createdNewlyAdded; + } + + @override + set createdNewlyAdded(bool value) { + _$createdNewlyAddedAtom.reportWrite(value, super.createdNewlyAdded, () { + super.createdNewlyAdded = value; + }); + } + late final _$readDataFileAsyncAction = AsyncAction('_ImportStore.readDataFile', context: context); @@ -154,16 +201,37 @@ mixin _$ImportStore on _ImportStore, Store { return _$importSmartlistAsyncAction.run(() => super.importSmartlist(i)); } + late final _$createFavoritesAsyncAction = + AsyncAction('_ImportStore.createFavorites', context: context); + + @override + Future createFavorites(BuildContext context) { + return _$createFavoritesAsyncAction + .run(() => super.createFavorites(context)); + } + + late final _$createNewlyAddedAsyncAction = + AsyncAction('_ImportStore.createNewlyAdded', context: context); + + @override + Future createNewlyAdded(BuildContext context) { + return _$createNewlyAddedAsyncAction + .run(() => super.createNewlyAdded(context)); + } + @override String toString() { return ''' appData: ${appData}, +error: ${error}, scanned: ${scanned}, addedLibraryFolders: ${addedLibraryFolders}, importing: ${importing}, importedMetadata: ${importedMetadata}, importedPlaylists: ${importedPlaylists}, -importedSmartlists: ${importedSmartlists} +importedSmartlists: ${importedSmartlists}, +createdFavorites: ${createdFavorites}, +createdNewlyAdded: ${createdNewlyAdded} '''; } } diff --git a/src/lib/system/repositories/init_repository_impl.dart b/src/lib/system/repositories/init_repository_impl.dart index 85699dd..2c01700 100644 --- a/src/lib/system/repositories/init_repository_impl.dart +++ b/src/lib/system/repositories/init_repository_impl.dart @@ -53,7 +53,7 @@ class InitRepositoryImpl extends InitRepository { } @override - Future initSmartlists(BuildContext context) async { + Future createFavoritesSmartlist(BuildContext context) async { await _playlistDataSource.insertSmartList( L10n.of(context)!.favorites, const Filter( @@ -79,6 +79,10 @@ class InitRepositoryImpl extends InitRepository { 'sanguine', ShuffleMode.plus, ); + } + + @override + Future createNewlyAddedSmartlist(BuildContext context) async { await _playlistDataSource.insertSmartList( L10n.of(context)!.newlyAdded, const Filter(