refined init workflow
This commit is contained in:
parent
719f9bb721
commit
1034a88cb5
14 changed files with 279 additions and 60 deletions
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -123,7 +123,6 @@ class _RootPageState extends State<RootPage> {
|
|||
);
|
||||
|
||||
initRepository.initHomePage(context);
|
||||
initRepository.initSmartlists(context);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
64
src/lib/presentation/pages/init/init_smartlists.dart
Normal file
64
src/lib/presentation/pages/init/init_smartlists.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue