Skip to content

Commit 9c5e919

Browse files
committed
back up related enhancements ++
1 parent 407351d commit 9c5e919

5 files changed

Lines changed: 284 additions & 65 deletions

File tree

lib/main.dart

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,15 @@ Future<void> _handleYoutubeVideoIntent(String url) async {
125125
}
126126

127127
Future<void> importItems(String path) async {
128-
bool res = await ImportExportService.importMediaItem(path);
129-
if (res) {
130-
SnackbarService.showMessage("Media Item Imported");
131-
} else {
132-
res = await ImportExportService.importPlaylist(path);
133-
if (res) {
134-
SnackbarService.showMessage("Playlist Imported");
135-
} else {
136-
SnackbarService.showMessage("Invalid File Format");
137-
}
128+
final context = GlobalRoutes.globalRouterKey.currentContext;
129+
if (context == null) {
130+
WidgetsBinding.instance.addPostFrameCallback((_) {
131+
importItems(path);
132+
});
133+
return;
138134
}
135+
136+
await ImportExportService.handleImportOrRestore(context, path);
139137
}
140138

141139
Future<void> setHighRefreshRate() async {
@@ -252,15 +250,19 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
252250
sharedMedia = media;
253251
});
254252
if (sharedMedia != null) {
255-
processIncomingIntent(sharedMedia!);
253+
WidgetsBinding.instance.addPostFrameCallback((_) {
254+
processIncomingIntent(sharedMedia!);
255+
});
256256
}
257257
});
258258
if (!mounted) return;
259259

260260
setState(() {
261261
// If there's initial shared media, process it
262262
if (sharedMedia != null) {
263-
processIncomingIntent(sharedMedia!);
263+
WidgetsBinding.instance.addPostFrameCallback((_) {
264+
processIncomingIntent(sharedMedia!);
265+
});
264266
}
265267
});
266268
} catch (error, stackTrace) {

lib/screens/screen/home_views/setting_views/storage_setting.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,11 @@ class _BackupLocationDialog extends StatelessWidget {
301301
Future<void> _onRestoreTap(BuildContext context) async {
302302
final l10n = AppLocalizations.of(context)!;
303303
try {
304-
// 1) Ask user to pick a file
304+
// 1) Ask user to pick a file (using FileType.any so that custom extensions like .isar / .db are not grayed out on Android)
305305
final result = await FilePicker.platform.pickFiles(
306306
allowMultiple: false,
307307
withData: false,
308-
type: FileType.custom,
309-
allowedExtensions: ['json', 'isar', 'db'],
308+
type: FileType.any,
310309
);
311310

312311
if (result == null || result.files.isEmpty) {

lib/screens/screen/library_views/import_media_view.dart

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -174,23 +174,14 @@ class _ImportMediaFromPlatformsViewState
174174
FilledButton(
175175
onPressed: () {
176176
Navigator.of(context).pop();
177-
FilePicker.platform.pickFiles().then((value) {
178-
if (value != null && value.files[0].path != null) {
177+
FilePicker.platform.pickFiles(
178+
allowMultiple: false,
179+
type: FileType.any,
180+
).then((value) {
181+
if (value != null && value.files.isNotEmpty && value.files[0].path != null) {
179182
final path = value.files[0].path!;
180-
if (path.endsWith('.blm') || path.endsWith('.json')) {
181-
SnackbarService.showMessage(
182-
AppLocalizations.of(context)!.snackbarImportingMedia);
183-
ImportExportService.importJSON(path).then((imported) {
184-
if (imported) {
185-
SnackbarService.showMessage(
186-
AppLocalizations.of(context)!
187-
.snackbarImportCompleted);
188-
}
189-
});
190-
} else {
191-
log('Invalid File Format', name: 'Import File');
192-
SnackbarService.showMessage(AppLocalizations.of(context)!
193-
.snackbarInvalidFileFormat);
183+
if (context.mounted) {
184+
ImportExportService.handleImportOrRestore(context, path);
194185
}
195186
}
196187
});

lib/services/import_export_service.dart

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import 'dart:convert';
22
import 'dart:developer';
33
import 'dart:io';
4+
import 'package:flutter/material.dart';
45
import 'package:Bloomee/core/models/exported.dart';
6+
import 'package:Bloomee/core/theme/app_theme.dart';
57
import 'package:Bloomee/screens/widgets/snackbar.dart';
68
import 'package:Bloomee/services/db/global_db.dart';
79
import 'package:Bloomee/services/db/db_provider.dart';
810
import 'package:Bloomee/services/db/dao/playlist_dao.dart';
911
import 'package:Bloomee/services/db/legacy/legacy_media_id_mapper.dart';
1012
import 'package:Bloomee/services/db/dao/track_dao.dart';
1113
import 'package:Bloomee/services/m3u_processor.dart';
14+
import 'package:Bloomee/services/storage_backup_service.dart';
1215
import 'package:package_info_plus/package_info_plus.dart';
1316
import 'package:path_provider/path_provider.dart';
17+
import 'package:Bloomee/l10n/app_localizations.dart';
1418

1519
/// Service for importing and exporting playlists and tracks.
1620
///
@@ -293,6 +297,9 @@ class ImportExportService {
293297
} else if (data.containsKey('title') &&
294298
(data.containsKey('mediaId') || data.containsKey('duration'))) {
295299
return await importMediaItem(filePath);
300+
} else if (data.containsKey('playlists') && data.containsKey('media_items')) {
301+
final result = await DBProvider.restoreLegacyJsonBackup(filePath);
302+
return result['success'] == true;
296303
} else {
297304
throw const FormatException("Unrecognized JSON structure.");
298305
}
@@ -426,4 +433,226 @@ class ImportExportService {
426433
return null;
427434
}
428435
}
436+
437+
/// Production-level entry point to handle any shared or picked import/restore file.
438+
/// Handles: Isar snapshots (.isar/.db), legacy full JSON backups, and playlist/song JSON exports.
439+
static Future<void> handleImportOrRestore(BuildContext context, String filePath) async {
440+
final l10n = AppLocalizations.of(context)!;
441+
try {
442+
final file = File(filePath);
443+
if (!await file.exists()) {
444+
SnackbarService.showMessage(l10n.storageRestoreNoFile);
445+
return;
446+
}
447+
448+
final payloadType = await StorageBackupService.detectPayloadType(file);
449+
450+
switch (payloadType) {
451+
case RestorePayloadType.isarSnapshot:
452+
if (!context.mounted) return;
453+
final confirm = await showDialog<bool>(
454+
context: context,
455+
builder: (ctx) => AlertDialog(
456+
backgroundColor: const Color.fromARGB(255, 24, 24, 24),
457+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
458+
title: Text(
459+
l10n.storageRestoreConfirmTitle,
460+
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18),
461+
),
462+
content: Text(
463+
"${l10n.storageRestoreConfirmPrefix}\n\n${l10n.storageRestoreMediaBullet}\n${l10n.storageRestoreHistoryBullet}\n\n${l10n.storageRestoreConfirmSuffix}",
464+
style: const TextStyle(color: Colors.white70, fontSize: 14, height: 1.5),
465+
),
466+
actions: [
467+
TextButton(
468+
onPressed: () => Navigator.pop(ctx, false),
469+
child: Text(l10n.storageRestoreNo, style: const TextStyle(color: Colors.white70)),
470+
),
471+
FilledButton(
472+
style: FilledButton.styleFrom(
473+
backgroundColor: Default_Theme.accentColor2,
474+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
475+
),
476+
onPressed: () => Navigator.pop(ctx, true),
477+
child: Text(l10n.storageRestoreYes, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
478+
),
479+
],
480+
),
481+
);
482+
483+
if (confirm != true) return;
484+
if (!context.mounted) return;
485+
486+
// Show progress dialog
487+
showDialog(
488+
context: context,
489+
barrierDismissible: false,
490+
builder: (ctx) => WillPopScope(
491+
onWillPop: () async => false,
492+
child: Center(
493+
child: Container(
494+
padding: const EdgeInsets.all(24),
495+
decoration: BoxDecoration(
496+
color: const Color.fromARGB(255, 24, 24, 24),
497+
borderRadius: BorderRadius.circular(16),
498+
),
499+
child: Column(
500+
mainAxisSize: MainAxisSize.min,
501+
children: [
502+
const CircularProgressIndicator(color: Default_Theme.accentColor2),
503+
const SizedBox(height: 16),
504+
Text(l10n.storageRestoring, style: const TextStyle(color: Colors.white, decoration: TextDecoration.none, fontSize: 14), textAlign: TextAlign.center),
505+
],
506+
),
507+
),
508+
),
509+
),
510+
);
511+
512+
final result = await StorageBackupService.restoreBackup(filePath);
513+
if (!context.mounted) return;
514+
Navigator.pop(context); // Dismiss progress dialog
515+
516+
final success = result['success'] == true;
517+
showDialog(
518+
context: context,
519+
builder: (ctx) => AlertDialog(
520+
backgroundColor: const Color.fromARGB(255, 24, 24, 24),
521+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
522+
title: Text(
523+
success ? l10n.storageRestoreCompleted : l10n.storageRestoreFailedTitle,
524+
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18),
525+
),
526+
content: Text(
527+
success
528+
? l10n.storageRestoreSuccessMessage
529+
: "${l10n.storageRestoreFailedMessage}\n- ${result['error'] ?? l10n.storageRestoreUnknownError}",
530+
style: const TextStyle(color: Colors.white70, fontSize: 14, height: 1.5),
531+
),
532+
actions: [
533+
FilledButton(
534+
style: FilledButton.styleFrom(
535+
backgroundColor: Default_Theme.accentColor2,
536+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
537+
),
538+
onPressed: () => Navigator.pop(ctx),
539+
child: Text(l10n.buttonOk, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
540+
),
541+
],
542+
),
543+
);
544+
break;
545+
546+
case RestorePayloadType.legacyFullJson:
547+
if (!context.mounted) return;
548+
final confirm = await showDialog<bool>(
549+
context: context,
550+
builder: (ctx) => AlertDialog(
551+
backgroundColor: const Color.fromARGB(255, 24, 24, 24),
552+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
553+
title: Text(
554+
l10n.storageRestoreConfirmTitle,
555+
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18),
556+
),
557+
content: Text(
558+
"${l10n.storageRestoreConfirmPrefix}\n\n${l10n.storageRestoreMediaBullet}\n\n${l10n.storageRestoreConfirmSuffix}",
559+
style: const TextStyle(color: Colors.white70, fontSize: 14, height: 1.5),
560+
),
561+
actions: [
562+
TextButton(
563+
onPressed: () => Navigator.pop(ctx, false),
564+
child: Text(l10n.storageRestoreNo, style: const TextStyle(color: Colors.white70)),
565+
),
566+
FilledButton(
567+
style: FilledButton.styleFrom(
568+
backgroundColor: Default_Theme.accentColor2,
569+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
570+
),
571+
onPressed: () => Navigator.pop(ctx, true),
572+
child: Text(l10n.storageRestoreYes, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
573+
),
574+
],
575+
),
576+
);
577+
578+
if (confirm != true) return;
579+
if (!context.mounted) return;
580+
581+
// Show progress dialog
582+
showDialog(
583+
context: context,
584+
barrierDismissible: false,
585+
builder: (ctx) => WillPopScope(
586+
onWillPop: () async => false,
587+
child: Center(
588+
child: Container(
589+
padding: const EdgeInsets.all(24),
590+
decoration: BoxDecoration(
591+
color: const Color.fromARGB(255, 24, 24, 24),
592+
borderRadius: BorderRadius.circular(16),
593+
),
594+
child: Column(
595+
mainAxisSize: MainAxisSize.min,
596+
children: [
597+
const CircularProgressIndicator(color: Default_Theme.accentColor2),
598+
const SizedBox(height: 16),
599+
Text(l10n.storageRestoring, style: const TextStyle(color: Colors.white, decoration: TextDecoration.none, fontSize: 14), textAlign: TextAlign.center),
600+
],
601+
),
602+
),
603+
),
604+
),
605+
);
606+
607+
final result = await DBProvider.restoreLegacyJsonBackup(filePath);
608+
if (!context.mounted) return;
609+
Navigator.pop(context); // Dismiss progress dialog
610+
611+
final success = result['success'] == true;
612+
showDialog(
613+
context: context,
614+
builder: (ctx) => AlertDialog(
615+
backgroundColor: const Color.fromARGB(255, 24, 24, 24),
616+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
617+
title: Text(
618+
success ? l10n.storageRestoreCompleted : l10n.storageRestoreFailedTitle,
619+
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18),
620+
),
621+
content: Text(
622+
success
623+
? l10n.storageRestoreSuccessMessage
624+
: "${l10n.storageRestoreFailedMessage}\n- ${result['error'] ?? l10n.storageRestoreUnknownError}",
625+
style: const TextStyle(color: Colors.white70, fontSize: 14, height: 1.5),
626+
),
627+
actions: [
628+
FilledButton(
629+
style: FilledButton.styleFrom(
630+
backgroundColor: Default_Theme.accentColor2,
631+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
632+
),
633+
onPressed: () => Navigator.pop(ctx),
634+
child: Text(l10n.buttonOk, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
635+
),
636+
],
637+
),
638+
);
639+
break;
640+
641+
case RestorePayloadType.playlistOrTrackJson:
642+
SnackbarService.showMessage(l10n.snackbarImportingMedia);
643+
final res = await importJSON(filePath);
644+
if (res) {
645+
SnackbarService.showMessage(l10n.snackbarImportCompleted);
646+
}
647+
break;
648+
649+
case RestorePayloadType.unsupported:
650+
SnackbarService.showMessage(l10n.snackbarInvalidFileFormat);
651+
break;
652+
}
653+
} catch (e) {
654+
log("Error during import/restore: $e", name: "ImportExportService");
655+
SnackbarService.showMessage(l10n.storageUnexpectedError);
656+
}
657+
}
429658
}

0 commit comments

Comments
 (0)