v1.1.1: UI fixes, MIT license, history persistence improvements

This commit is contained in:
zarzet
2026-01-01 22:29:40 +07:00
parent 6a1265eac3
commit 8ac679003e
7 changed files with 200 additions and 120 deletions
+21
View File
@@ -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.
+18 -2
View File
@@ -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;
}
}
+21 -2
View File
@@ -71,22 +71,35 @@ class DownloadHistoryState {
// Download History Notifier (Riverpod 3.x)
class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
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<void> _loadFromStorage() async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_storageKey);
if (jsonStr != null) {
if (jsonStr != null && jsonStr.isNotEmpty) {
final List<dynamic> jsonList = jsonDecode(jsonStr);
final items = jsonList.map((e) => DownloadHistoryItem.fromJson(e as Map<String, dynamic>)).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<DownloadHistoryState> {
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<void> reloadFromStorage() async {
await _loadFromStorage();
}
void addToHistory(DownloadHistoryItem item) {
state = state.copyWith(items: [item, ...state.items]);
_saveToStorage();
+66 -60
View File
@@ -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,
),
),
),
],
),
],
),
+66 -51
View File
@@ -62,9 +62,6 @@ class _SettingsTabState extends ConsumerState<SettingsTab> 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<SettingsTab> 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<SettingsTab> 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<SettingsTab> 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<SettingsTab> 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,
),
),
),
],
),
],
),
+7 -4
View File
@@ -314,10 +314,13 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
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),
],
+1 -1
View File
@@ -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