refined init workflow

This commit is contained in:
Moritz Weber 2023-07-15 20:56:30 +02:00
parent 719f9bb721
commit 1034a88cb5
14 changed files with 279 additions and 60 deletions

View file

@ -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.

View file

@ -5,5 +5,6 @@ abstract class InitRepository {
Future<void> setInitialized();
Future<void> initHomePage(BuildContext context);
Future<void> initSmartlists(BuildContext context);
Future<void> createFavoritesSmartlist(BuildContext context);
Future<void> createNewlyAddedSmartlist(BuildContext context);
}

View file

@ -186,10 +186,10 @@ Future<void> setupGetIt() async {
homeHistory: history,
),
);
getIt.registerFactoryParam<ImportStore, String?, void>(
(String? importPath, _) => ImportStore(
getIt.registerFactory<ImportStore>(
() => ImportStore(
importExportRepository: getIt(),
inputPath: importPath,
initRepository: getIt(),
),
);
getIt.registerFactory<ExportStore>(

View file

@ -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"
}

View file

@ -123,7 +123,6 @@ class _RootPageState extends State<RootPage> {
);
initRepository.initHomePage(context);
initRepository.initSmartlists(context);
}
});

View file

@ -49,6 +49,15 @@ class _ExportPageState extends State<ExportPage> {
),
],
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<ExportPage> {
Future<void> _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) {

View file

@ -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<ImportStore>(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<ImportStore>(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());

View file

@ -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),
),
);
})
],
),
),
);
}
}

View file

@ -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<InitWorkflow> createState() => _InitWorkflowState();
@ -24,9 +25,8 @@ class _InitWorkflowState extends State<InitWorkflow> {
PageController pageController = PageController();
int index = 0;
late final ImportStore importStore;
late final List<Widget> pages = [
InitLibPage(importStore: importStore),
InitLibPage(importStore: widget.importStore),
];
final curve = Curves.easeInOut;
@ -34,22 +34,13 @@ class _InitWorkflowState extends State<InitWorkflow> {
@override
void initState() {
importStore = GetIt.I<ImportStore>(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<InitWorkflow> {
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),
],

View file

@ -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<String?> 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;
}

View file

@ -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}
''';
}
}

View file

@ -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<String, dynamic>? data;
@ -30,6 +30,9 @@ abstract class _ImportStore with Store {
@observable
AppData? appData;
@observable
bool error = false;
String? get allowedExtensions => appData?.allowedExtensions;
List<String>? get blockedFiles => appData?.blockedFiles;
List<String>? get libraryFolders => appData?.libraryFolders;
@ -52,9 +55,19 @@ abstract class _ImportStore with Store {
@observable
ObservableList<bool> importedSmartlists = <bool>[].asObservable();
@observable
bool createdFavorites = false;
@observable
bool createdNewlyAdded = false;
@action
Future<void> readDataFile(String path) async {
final _data = await _importExportRepository.readDataFile(path);
Map<String, dynamic>? _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<void> createFavorites(BuildContext context) async {
_initRepository.createFavoritesSmartlist(context).then((_) => createdFavorites = true);
}
@action
Future<void> createNewlyAdded(BuildContext context) async {
_initRepository.createNewlyAddedSmartlist(context).then((_) => createdNewlyAdded = true);
}
void dispose() {}
}

View file

@ -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<void> createFavorites(BuildContext context) {
return _$createFavoritesAsyncAction
.run(() => super.createFavorites(context));
}
late final _$createNewlyAddedAsyncAction =
AsyncAction('_ImportStore.createNewlyAdded', context: context);
@override
Future<void> 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}
''';
}
}

View file

@ -53,7 +53,7 @@ class InitRepositoryImpl extends InitRepository {
}
@override
Future<void> initSmartlists(BuildContext context) async {
Future<void> 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<void> createNewlyAddedSmartlist(BuildContext context) async {
await _playlistDataSource.insertSmartList(
L10n.of(context)!.newlyAdded,
const Filter(