From 10cfe1c87225c92fa96bf673387b09558f5227f4 Mon Sep 17 00:00:00 2001 From: Rivers Cuomo Date: Tue, 6 Aug 2024 06:41:39 -0700 Subject: [PATCH] backup --- lib/main.dart | 26 ++--- lib/screens/home_screen.dart | 129 +++++++++++-------------- lib/screens/job_management_screen.dart | 75 ++++++++++++++ lib/screens/settings_screen.dart | 64 +++++++++--- lib/services/backup_service.dart | 85 ++++++++++++++++ lib/services/services.dart | 1 + pubspec.lock | 8 ++ pubspec.yaml | 1 + 8 files changed, 296 insertions(+), 93 deletions(-) create mode 100644 lib/screens/job_management_screen.dart create mode 100644 lib/services/backup_service.dart diff --git a/lib/main.dart b/lib/main.dart index ce1703c..2fddd98 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:spotkin_flutter/app_core.dart'; @@ -99,24 +100,27 @@ final spotifyThemeData = ThemeData( textTheme: ButtonTextTheme.primary, // padding: const EdgeInsets.all(16.0), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25), + borderRadius: BorderRadius.circular(20), ), ), expansionTileTheme: ExpansionTileThemeData( backgroundColor: spotifyWidgetColor, collapsedBackgroundColor: spotifyWidgetColor, - textColor: Colors.white, - collapsedTextColor: Colors.white, - iconColor: Colors.white, - collapsedIconColor: Colors.white, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(4), - // // side: BorderSide(color: Colors.white24), - // ), + collapsedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + // textColor: Colors.white, + // collapsedTextColor: Colors.white, + // iconColor: Colors.white, + // collapsedIconColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + // side: BorderSide(color: Colors.white24), + ), ), textTheme: TextTheme( - titleLarge: - const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + titleLarge: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold, fontSize: 28), bodyMedium: const TextStyle(color: Colors.white70), // titleMedium: TextStyle(color: Colors.white54), labelMedium: TextStyle(color: Colors.grey[400]), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 57b07d4..3f234ea 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -169,6 +169,7 @@ class _HomeScreenState extends State { return Scaffold( appBar: AppBar( title: const Text('Spotkin'), + titleTextStyle: Theme.of(context).textTheme.titleLarge, automaticallyImplyLeading: false, actions: [ IconButton( @@ -195,89 +196,77 @@ class _HomeScreenState extends State { children: [ Column( children: [ - Material( - elevation: 1, - borderRadius: BorderRadius.circular(12.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(12.0), - child: ExpansionTile( - key: _expansionTileKey, - title: Column(children: [ - PlaylistImageIcon( - playlist: targetPlaylist, - size: 160, - ), - const SizedBox(height: 16), - Column( + ExpansionTile( + key: _expansionTileKey, + title: Column(children: [ + PlaylistImageIcon( + playlist: targetPlaylist, + size: 160, + ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, + PlaylistTitle(context, targetPlaylist, + style: Theme.of(context) + .textTheme + .titleLarge), + const SizedBox(height: 5), + Row( children: [ - PlaylistTitle(context, targetPlaylist, + playlistSubtitle(targetPlaylist, context), + const SizedBox(width: 10), + if (jobResults.isNotEmpty) + Text( + jobResults[0]['result'], style: Theme.of(context) .textTheme - .titleLarge), - const SizedBox(height: 5), - Row( - children: [ - playlistSubtitle( - targetPlaylist, context), - const SizedBox(width: 10), - if (jobResults.isNotEmpty) - Text( - jobResults[0]['result'], - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - fontStyle: - FontStyle.italic), - ), - const SizedBox(width: 10), - if (jobResults.isNotEmpty) - Icon( - size: 14, - jobResults[0]['status'] == - 'Success' - ? Icons.check_circle - : Icons.error, - color: jobResults[0]['status'] == - 'Success' - ? Colors.green - : Colors.red, - ), - ], - ), + .labelMedium! + .copyWith( + fontStyle: FontStyle.italic), + ), + const SizedBox(width: 10), + if (jobResults.isNotEmpty) + Icon( + size: 14, + jobResults[0]['status'] == 'Success' + ? Icons.check_circle + : Icons.error, + color: jobResults[0]['status'] == + 'Success' + ? Colors.green + : Colors.red, + ), ], ), - SpotifyButton( - isProcessing: isProcessing, - processJobs: _processJobs), ], - ) + ), + SpotifyButton( + isProcessing: isProcessing, + processJobs: _processJobs), ], ) - ]), - // leading: , - // subtitle: , - initiallyExpanded: _isExpanded, - onExpansionChanged: (expanded) { - setState(() { - _isExpanded = expanded; - }); - }, - children: [ - buildTargetPlaylistSelectionOptions(), ], - ), - ), + ) + ]), + // leading: , + // subtitle: , + initiallyExpanded: _isExpanded, + onExpansionChanged: (expanded) { + setState(() { + _isExpanded = expanded; + }); + }, + children: [ + buildTargetPlaylistSelectionOptions(), + ], ), SizedBox(height: widgetPadding), ...jobs.asMap().entries.map((entry) { diff --git a/lib/screens/job_management_screen.dart b/lib/screens/job_management_screen.dart new file mode 100644 index 0000000..e886378 --- /dev/null +++ b/lib/screens/job_management_screen.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:spotkin_flutter/app_core.dart'; +import 'package:spotkin_flutter/services/backup_service.dart'; + +class JobManagementScreen extends StatefulWidget { + const JobManagementScreen({super.key}); + + @override + _JobManagementScreenState createState() => _JobManagementScreenState(); +} + +class _JobManagementScreenState extends State { + final StorageService _storageService = StorageService(); + late BackupService _backupService; + List _jobs = []; + + @override + void initState() { + super.initState(); + _backupService = BackupService(_storageService); + _loadJobs(); + } + + void _loadJobs() { + setState(() { + _jobs = _storageService.getJobs(); + }); + } + + void _createBackup() { + _backupService.createBackup(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Backup file created. Check your downloads.')), + ); + } + + Future _importBackup() async { + await _backupService.importBackup(); + _loadJobs(); // Reload jobs after import + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Backup imported and jobs updated.')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Job Management')), + body: Column( + children: [ + ElevatedButton( + onPressed: _createBackup, + child: const Text('Create Backup File'), + ), + ElevatedButton( + onPressed: _importBackup, + child: const Text('Import Backup File'), + ), + Expanded( + child: ListView.builder( + itemCount: _jobs.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(_jobs[index].targetPlaylist.name ?? 'Untitled'), + subtitle: Text('Recipe count: ${_jobs[index].recipe.length}'), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 022859f..099b652 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -4,29 +4,69 @@ import 'package:spotkin_flutter/app_core.dart'; class SettingsScreen extends StatelessWidget { final List jobs; final Function(int, Job) updateJob; + final StorageService storageService = StorageService(); + late final BackupService backupService; - const SettingsScreen({ + SettingsScreen({ Key? key, required this.jobs, required this.updateJob, - }) : super(key: key); + }) : super(key: key) { + backupService = BackupService(storageService); + } + + void _createBackup(BuildContext context) { + backupService.createBackup(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Backup file created. Check your downloads.')), + ); + } + + Future _importBackup(BuildContext context) async { + await backupService.importBackup(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Backup imported and jobs updated.')), + ); + // You might want to refresh the jobs list here or in the parent widget + } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Settings'), - actions: [InfoButton()], + actions: const [InfoButton()], ), - body: ListView.builder( - itemCount: jobs.length, - itemBuilder: (context, index) { - return SettingsCard( - index: index, - job: jobs[index], - updateJob: updateJob, - ); - }, + body: Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: jobs.length, + itemBuilder: (context, index) { + return SettingsCard( + index: index, + job: jobs[index], + updateJob: updateJob, + ); + }, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => _createBackup(context), + child: const Text('Create Backup'), + ), + ElevatedButton( + onPressed: () => _importBackup(context), + child: const Text('Import Backup'), + ), + ], + ), + const SizedBox(height: 16), + ], ), ); } diff --git a/lib/services/backup_service.dart b/lib/services/backup_service.dart new file mode 100644 index 0000000..89f1286 --- /dev/null +++ b/lib/services/backup_service.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'dart:html' as html; +import 'package:intl/intl.dart'; +import 'package:spotkin_flutter/app_core.dart'; + +class BackupService { + final StorageService _storageService; + + BackupService(this._storageService); + + void createBackup() { + List jobs = _storageService.getJobs(); + final jobsJson = jobs.map((job) => job.toJson()).toList(); + final jsonString = jsonEncode(jobsJson); + final bytes = utf8.encode(jsonString); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + + // Generate filename with current date + final now = DateTime.now(); + final formatter = DateFormat('yyyy-MM-dd_HH-mm-ss'); + final String fileName = 'spotkin_jobs_backup_${formatter.format(now)}.json'; + + final anchor = html.document.createElement('a') as html.AnchorElement + ..href = url + ..style.display = 'none' + ..download = fileName; + html.document.body!.children.add(anchor); + + // Trigger download + anchor.click(); + + // Cleanup + html.document.body!.children.remove(anchor); + html.Url.revokeObjectUrl(url); + + print('Backup file "$fileName" created and download initiated.'); + } + + Future importBackup() async { + final uploadInput = html.FileUploadInputElement(); + uploadInput.accept = '.json'; + uploadInput.click(); + + await uploadInput.onChange.first; + + if (uploadInput.files!.isNotEmpty) { + final file = uploadInput.files![0]; + final reader = html.FileReader(); + reader.readAsText(file); + + await reader.onLoad.first; + + final contents = reader.result as String; + try { + final List jsonList = jsonDecode(contents); + List importedJobs = + jsonList.map((json) => Job.fromJson(json)).toList(); + + // Merge imported jobs with existing jobs + List existingJobs = _storageService.getJobs(); + for (var importedJob in importedJobs) { + int existingIndex = existingJobs.indexWhere( + (job) => job.targetPlaylist.id == importedJob.targetPlaylist.id); + if (existingIndex != -1) { + // Update existing job + existingJobs[existingIndex] = importedJob; + } else { + // Add new job + existingJobs.add(importedJob); + } + } + + // Save merged jobs + _storageService.saveJobs(existingJobs); + print( + 'Imported and merged ${importedJobs.length} jobs from ${file.name}.'); + } catch (e) { + print('Error importing jobs from ${file.name}: $e'); + } + } else { + print('No file selected for import.'); + } + } +} diff --git a/lib/services/services.dart b/lib/services/services.dart index a683815..6fbefd1 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -1,4 +1,5 @@ export 'api_service.dart'; +export 'backup_service.dart'; export 'service_locator.dart'; export 'storage_service.dart'; export 'spotify_service.dart'; diff --git a/pubspec.lock b/pubspec.lock index 6861472..1ed6589 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -168,6 +168,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f753b5..a642687 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: flutter_dotenv: ^5.0.2 get_it: ^7.7.0 http: ^1.2.0 + intl: ^0.17.0 oauth2: ^2.0.0 provider: ^6.1.2 spotify: ^0.13.7