From 8ac679003e1d3ad392fc98ac2b9dd5d2c7e25672 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 1 Jan 2026 22:29:40 +0700 Subject: [PATCH] v1.1.1: UI fixes, MIT license, history persistence improvements --- LICENSE | 21 ++++ lib/main.dart | 20 +++- lib/providers/download_queue_provider.dart | 23 +++- lib/screens/settings_screen.dart | 126 +++++++++++---------- lib/screens/settings_tab.dart | 117 ++++++++++--------- lib/screens/setup_screen.dart | 11 +- pubspec.yaml | 2 +- 7 files changed, 200 insertions(+), 120 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1a27ac3c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 zarzet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/main.dart b/lib/main.dart index 98c3348b..12be91d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/app.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); runApp( - const ProviderScope( - child: SpotiFLACApp(), + ProviderScope( + child: const _EagerInitialization( + child: SpotiFLACApp(), + ), ), ); } + +/// Widget to eagerly initialize providers that need to load data on startup +class _EagerInitialization extends ConsumerWidget { + const _EagerInitialization({required this.child}); + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Eagerly initialize download history provider to load from storage + ref.watch(downloadHistoryProvider); + return child; + } +} diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index cf49e3c6..3dcc963f 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -71,22 +71,35 @@ class DownloadHistoryState { // Download History Notifier (Riverpod 3.x) class DownloadHistoryNotifier extends Notifier { static const _storageKey = 'download_history'; + bool _isLoaded = false; @override DownloadHistoryState build() { // Load history from storage on init - Future.microtask(() => _loadFromStorage()); + _loadFromStorageSync(); return const DownloadHistoryState(); } + /// Synchronously schedule load - ensures it runs before any UI renders + void _loadFromStorageSync() { + if (_isLoaded) return; + Future.microtask(() async { + await _loadFromStorage(); + _isLoaded = true; + }); + } + Future _loadFromStorage() async { try { final prefs = await SharedPreferences.getInstance(); final jsonStr = prefs.getString(_storageKey); - if (jsonStr != null) { + if (jsonStr != null && jsonStr.isNotEmpty) { final List jsonList = jsonDecode(jsonStr); final items = jsonList.map((e) => DownloadHistoryItem.fromJson(e as Map)).toList(); state = state.copyWith(items: items); + print('[DownloadHistory] Loaded ${items.length} items from storage'); + } else { + print('[DownloadHistory] No history found in storage'); } } catch (e) { print('[DownloadHistory] Failed to load history: $e'); @@ -98,11 +111,17 @@ class DownloadHistoryNotifier extends Notifier { final prefs = await SharedPreferences.getInstance(); final jsonList = state.items.map((e) => e.toJson()).toList(); await prefs.setString(_storageKey, jsonEncode(jsonList)); + print('[DownloadHistory] Saved ${state.items.length} items to storage'); } catch (e) { print('[DownloadHistory] Failed to save history: $e'); } } + /// Force reload from storage (useful after app restart) + Future reloadFromStorage() async { + await _loadFromStorage(); + } + void addToHistory(DownloadHistoryItem item) { state = state.copyWith(items: [item, ...state.items]); _saveToStorage(); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index aac1807a..5edd6802 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -55,9 +55,6 @@ class SettingsScreen extends ConsumerWidget { onTap: () => _showColorPicker(context, ref, themeSettings.seedColorValue), ), - // Theme Preview - _buildThemePreview(context, colorScheme), - const Divider(), // Download Section @@ -172,19 +169,64 @@ class SettingsScreen extends ConsumerWidget { ListTile( leading: Icon(Icons.info, color: colorScheme.primary), title: const Text('About'), - subtitle: const Text('SpotiFLAC v1.1.0'), - onTap: () => showAboutDialog( - context: context, - applicationName: 'SpotiFLAC', - applicationVersion: '1.1.0', - applicationLegalese: '© 2024 SpotiFLAC\n\nMobile: zarzet\nOriginal: afkarxyz', - ), + subtitle: const Text('SpotiFLAC v1.1.1'), + onTap: () => _showAboutDialog(context), ), ], ), ); } + void _showAboutDialog(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + Image.asset('assets/images/logo.png', width: 40, height: 40, errorBuilder: (_, __, ___) => Icon(Icons.music_note, size: 40, color: colorScheme.primary)), + const SizedBox(width: 12), + const Text('SpotiFLAC'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAboutRow('Version', '1.1.1', colorScheme), + const SizedBox(height: 8), + _buildAboutRow('Mobile', 'zarzet', colorScheme), + const SizedBox(height: 8), + _buildAboutRow('Original', 'afkarxyz', colorScheme), + const SizedBox(height: 16), + Text( + '© 2026 SpotiFLAC', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + Widget _buildAboutRow(String label, String value, ColorScheme colorScheme) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(color: colorScheme.onSurfaceVariant)), + Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), + ], + ); + } + Widget _buildSectionHeader(BuildContext context, String title, ColorScheme colorScheme) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), @@ -198,51 +240,6 @@ class SettingsScreen extends ConsumerWidget { ); } - Widget _buildThemePreview(BuildContext context, ColorScheme colorScheme) { - return Padding( - padding: const EdgeInsets.all(16), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Theme Preview', - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _buildColorChip('Primary', colorScheme.primary, colorScheme.onPrimary), - _buildColorChip('Secondary', colorScheme.secondary, colorScheme.onSecondary), - _buildColorChip('Tertiary', colorScheme.tertiary, colorScheme.onTertiary), - _buildColorChip('Surface', colorScheme.surface, colorScheme.onSurface), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildColorChip(String label, Color background, Color foreground) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(16), - ), - child: Text( - label, - style: TextStyle(color: foreground, fontSize: 12), - ), - ); - } - String _getThemeModeName(ThemeMode mode) { switch (mode) { case ThemeMode.light: return 'Light'; @@ -478,11 +475,20 @@ class SettingsScreen extends ConsumerWidget { _buildConcurrentOption(context, ref, 2, '2 Parallel', 'Download 2 tracks simultaneously', current, colorScheme), _buildConcurrentOption(context, ref, 3, '3 Parallel', 'Download 3 tracks simultaneously', current, colorScheme), const SizedBox(height: 12), - Text( - '⚠️ Parallel downloads may trigger rate limiting from streaming services.', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.error, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.warning_amber_rounded, size: 16, color: colorScheme.error), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Parallel downloads may trigger rate limiting from streaming services.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.error, + ), + ), + ), + ], ), ], ), diff --git a/lib/screens/settings_tab.dart b/lib/screens/settings_tab.dart index 0e8c2db8..5084649b 100644 --- a/lib/screens/settings_tab.dart +++ b/lib/screens/settings_tab.dart @@ -62,9 +62,6 @@ class _SettingsTabState extends ConsumerState with AutomaticKeepAli onTap: () => _showColorPicker(context, ref, themeSettings.seedColorValue), ), - // Theme Preview - _buildThemePreview(context, colorScheme), - const Divider(), // Download Section @@ -179,13 +176,8 @@ class _SettingsTabState extends ConsumerState with AutomaticKeepAli ListTile( leading: Icon(Icons.info, color: colorScheme.primary), title: const Text('About'), - subtitle: const Text('SpotiFLAC v1.1.0'), - onTap: () => showAboutDialog( - context: context, - applicationName: 'SpotiFLAC', - applicationVersion: '1.1.0', - applicationLegalese: '© 2024 SpotiFLAC\n\nMobile: zarzet\nOriginal: afkarxyz', - ), + subtitle: const Text('SpotiFLAC v1.1.1'), + onTap: () => _showAboutDialog(context), ), // Bottom padding for navigation bar @@ -194,6 +186,56 @@ class _SettingsTabState extends ConsumerState with AutomaticKeepAli ); } + void _showAboutDialog(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + Image.asset('assets/images/logo.png', width: 40, height: 40, errorBuilder: (_, __, ___) => Icon(Icons.music_note, size: 40, color: colorScheme.primary)), + const SizedBox(width: 12), + const Text('SpotiFLAC'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAboutRow('Version', '1.1.1', colorScheme), + const SizedBox(height: 8), + _buildAboutRow('Mobile', 'zarzet', colorScheme), + const SizedBox(height: 8), + _buildAboutRow('Original', 'afkarxyz', colorScheme), + const SizedBox(height: 16), + Text( + '© 2026 SpotiFLAC', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + Widget _buildAboutRow(String label, String value, ColorScheme colorScheme) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(color: colorScheme.onSurfaceVariant)), + Text(value, style: const TextStyle(fontWeight: FontWeight.w500)), + ], + ); + } + Widget _buildSectionHeader(BuildContext context, String title, ColorScheme colorScheme) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), @@ -207,42 +249,6 @@ class _SettingsTabState extends ConsumerState with AutomaticKeepAli ); } - Widget _buildThemePreview(BuildContext context, ColorScheme colorScheme) { - return Padding( - padding: const EdgeInsets.all(16), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Theme Preview', style: Theme.of(context).textTheme.titleSmall), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _buildColorChip('Primary', colorScheme.primary, colorScheme.onPrimary), - _buildColorChip('Secondary', colorScheme.secondary, colorScheme.onSecondary), - _buildColorChip('Tertiary', colorScheme.tertiary, colorScheme.onTertiary), - _buildColorChip('Surface', colorScheme.surface, colorScheme.onSurface), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildColorChip(String label, Color background, Color foreground) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration(color: background, borderRadius: BorderRadius.circular(16)), - child: Text(label, style: TextStyle(color: foreground, fontSize: 12)), - ); - } - String _getThemeModeName(ThemeMode mode) { switch (mode) { case ThemeMode.light: return 'Light'; @@ -447,11 +453,20 @@ class _SettingsTabState extends ConsumerState with AutomaticKeepAli _buildConcurrentOption(context, ref, 2, '2 Parallel', 'Download 2 tracks simultaneously', current, colorScheme), _buildConcurrentOption(context, ref, 3, '3 Parallel', 'Download 3 tracks simultaneously', current, colorScheme), const SizedBox(height: 12), - Text( - '⚠️ Parallel downloads may trigger rate limiting from streaming services.', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.error, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.warning_amber_rounded, size: 16, color: colorScheme.error), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Parallel downloads may trigger rate limiting from streaming services.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.error, + ), + ), + ), + ], ), ], ), diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index 8b5befb9..d6dab7aa 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -314,10 +314,13 @@ class _SetupScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ _buildStepDot(0, 'Permission', colorScheme), - Container( - width: 40, - height: 2, - color: _currentStep >= 1 ? colorScheme.primary : colorScheme.surfaceContainerHighest, + Padding( + padding: const EdgeInsets.only(bottom: 20), // Offset for label height + child: Container( + width: 40, + height: 2, + color: _currentStep >= 1 ? colorScheme.primary : colorScheme.surfaceContainerHighest, + ), ), _buildStepDot(1, 'Folder', colorScheme), ], diff --git a/pubspec.yaml b/pubspec.yaml index 65d2a1f3..5bdab734 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: spotiflac_android description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music publish_to: 'none' -version: 1.1.0+7 +version: 1.1.1+8 environment: sdk: ^3.10.0