mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-11 02:08:44 +02:00
feat: add local library albums to Albums tab with unified grid and LocalAlbumScreen
- Add local library albums as clickable cards in Albums filter - Merge downloaded and local albums into single unified grid (fix layout gaps) - Create LocalAlbumScreen for viewing local album details with: - Cover art display with dominant color extraction - Album info card with Local badge and quality info - Track list with disc grouping support - Selection mode with delete functionality - UI consistent with DownloadedAlbumScreen (Card + ListTile layout) - Add singles filter support for local library singles - Add extractDominantColorFromFile to PaletteService - Add delete(id) method to LibraryDatabase - Add removeItem(id) method to LocalLibraryNotifier - Update CHANGELOG.md for v3.4.0
This commit is contained in:
1 parent
9c22f41a3e
commit
72d45746a5
14 files changed
+3255
-478
No files matched your search
@@ -14,6 +14,7 @@ class LocalLibraryItem {
|
||||
final String albumName;
|
||||
final String? albumArtist;
|
||||
final String filePath;
|
||||
final String? coverPath; // Path to extracted cover art
|
||||
final DateTime scannedAt;
|
||||
final String? isrc;
|
||||
final int? trackNumber;
|
||||
@@ -32,6 +33,7 @@ class LocalLibraryItem {
|
||||
required this.albumName,
|
||||
this.albumArtist,
|
||||
required this.filePath,
|
||||
this.coverPath,
|
||||
required this.scannedAt,
|
||||
this.isrc,
|
||||
this.trackNumber,
|
||||
@@ -51,6 +53,7 @@ class LocalLibraryItem {
|
||||
'albumName': albumName,
|
||||
'albumArtist': albumArtist,
|
||||
'filePath': filePath,
|
||||
'coverPath': coverPath,
|
||||
'scannedAt': scannedAt.toIso8601String(),
|
||||
'isrc': isrc,
|
||||
'trackNumber': trackNumber,
|
||||
@@ -71,6 +74,7 @@ class LocalLibraryItem {
|
||||
albumName: json['albumName'] as String,
|
||||
albumArtist: json['albumArtist'] as String?,
|
||||
filePath: json['filePath'] as String,
|
||||
coverPath: json['coverPath'] as String?,
|
||||
scannedAt: DateTime.parse(json['scannedAt'] as String),
|
||||
isrc: json['isrc'] as String?,
|
||||
trackNumber: json['trackNumber'] as int?,
|
||||
@@ -109,7 +113,7 @@ class LibraryDatabase {
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 1,
|
||||
version: 2, // Bumped version for cover_path migration
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _upgradeDB,
|
||||
);
|
||||
@@ -126,6 +130,7 @@ class LibraryDatabase {
|
||||
album_name TEXT NOT NULL,
|
||||
album_artist TEXT,
|
||||
file_path TEXT NOT NULL UNIQUE,
|
||||
cover_path TEXT,
|
||||
scanned_at TEXT NOT NULL,
|
||||
isrc TEXT,
|
||||
track_number INTEGER,
|
||||
@@ -150,7 +155,12 @@ class LibraryDatabase {
|
||||
|
||||
Future<void> _upgradeDB(Database db, int oldVersion, int newVersion) async {
|
||||
_log.i('Upgrading library database from v$oldVersion to v$newVersion');
|
||||
// Future migrations go here
|
||||
|
||||
if (oldVersion < 2) {
|
||||
// Add cover_path column
|
||||
await db.execute('ALTER TABLE library ADD COLUMN cover_path TEXT');
|
||||
_log.i('Added cover_path column');
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert JSON format (camelCase) to DB row (snake_case)
|
||||
@@ -162,6 +172,7 @@ class LibraryDatabase {
|
||||
'album_name': json['albumName'],
|
||||
'album_artist': json['albumArtist'],
|
||||
'file_path': json['filePath'],
|
||||
'cover_path': json['coverPath'],
|
||||
'scanned_at': json['scannedAt'],
|
||||
'isrc': json['isrc'],
|
||||
'track_number': json['trackNumber'],
|
||||
@@ -184,6 +195,7 @@ class LibraryDatabase {
|
||||
'albumName': row['album_name'],
|
||||
'albumArtist': row['album_artist'],
|
||||
'filePath': row['file_path'],
|
||||
'coverPath': row['cover_path'],
|
||||
'scannedAt': row['scanned_at'],
|
||||
'isrc': row['isrc'],
|
||||
'trackNumber': row['track_number'],
|
||||
@@ -333,6 +345,12 @@ class LibraryDatabase {
|
||||
await db.delete('library', where: 'file_path = ?', whereArgs: [filePath]);
|
||||
}
|
||||
|
||||
/// Delete by ID
|
||||
Future<void> delete(String id) async {
|
||||
final db = await database;
|
||||
await db.delete('library', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
/// Delete items where file no longer exists
|
||||
Future<int> cleanupMissingFiles() async {
|
||||
final db = await database;
|
||||
|
||||
@@ -145,12 +145,20 @@ class NotificationService {
|
||||
required String artistName,
|
||||
int? completedCount,
|
||||
int? totalCount,
|
||||
bool alreadyInLibrary = false,
|
||||
}) async {
|
||||
if (!_isInitialized) await initialize();
|
||||
|
||||
final title = completedCount != null && totalCount != null
|
||||
? 'Download Complete ($completedCount/$totalCount)'
|
||||
: 'Download Complete';
|
||||
String title;
|
||||
if (alreadyInLibrary) {
|
||||
title = completedCount != null && totalCount != null
|
||||
? 'Already in Library ($completedCount/$totalCount)'
|
||||
: 'Already in Library';
|
||||
} else {
|
||||
title = completedCount != null && totalCount != null
|
||||
? 'Download Complete ($completedCount/$totalCount)'
|
||||
: 'Download Complete';
|
||||
}
|
||||
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
channelId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
@@ -46,6 +47,40 @@ class PaletteService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract dominant color from a local file path
|
||||
Future<Color?> extractDominantColorFromFile(String? filePath) async {
|
||||
if (filePath == null || filePath.isEmpty) return null;
|
||||
|
||||
final cached = _colorCache[filePath];
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) return null;
|
||||
|
||||
final paletteGenerator = await PaletteGenerator.fromImageProvider(
|
||||
FileImage(file),
|
||||
size: const Size(64, 64),
|
||||
maximumColorCount: 8,
|
||||
);
|
||||
|
||||
final color = paletteGenerator.dominantColor?.color ??
|
||||
paletteGenerator.vibrantColor?.color ??
|
||||
paletteGenerator.mutedColor?.color;
|
||||
|
||||
if (color != null) {
|
||||
_colorCache[filePath] = color;
|
||||
}
|
||||
|
||||
return color;
|
||||
} catch (e) {
|
||||
debugPrint('PaletteService file error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the color cache
|
||||
void clearCache() {
|
||||
_colorCache.clear();
|
||||
|
||||
@@ -823,6 +823,14 @@ static Future<Map<String, dynamic>> downloadWithExtensions({
|
||||
|
||||
// ==================== LOCAL LIBRARY SCANNING ====================
|
||||
|
||||
/// Set the directory for caching extracted cover art
|
||||
static Future<void> setLibraryCoverCacheDir(String cacheDir) async {
|
||||
_log.i('setLibraryCoverCacheDir: $cacheDir');
|
||||
await _channel.invokeMethod('setLibraryCoverCacheDir', {
|
||||
'cache_dir': cacheDir,
|
||||
});
|
||||
}
|
||||
|
||||
/// Scan a folder for audio files and read their metadata
|
||||
/// Returns a list of track metadata
|
||||
static Future<List<Map<String, dynamic>>> scanLibraryFolder(String folderPath) async {
|
||||
|
||||
Reference in new issue
Block a user