✨ Multiple library and in-place adding
This commit is contained in:
201
REFACTOR_SUMMARY.md
Normal file
201
REFACTOR_SUMMARY.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# GroovyBox Track Repository Refactor Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Successfully refactored the `lib/data/track_repository.dart` to support more settings including in-place indexing and folder watching functionality.
|
||||||
|
|
||||||
|
## Key Changes Made
|
||||||
|
|
||||||
|
### 1. Database Schema Updates (`lib/data/db.dart`)
|
||||||
|
- Added `WatchFolders` table for managing watch folders
|
||||||
|
- Added `AppSettings` table for storing app preferences
|
||||||
|
- Updated database schema version to 6
|
||||||
|
- Added proper migrations for new tables
|
||||||
|
|
||||||
|
### 2. Settings Provider (`lib/providers/settings_provider.dart`)
|
||||||
|
- Created comprehensive settings management with Riverpod
|
||||||
|
- Added `ImportMode` enum (Copy vs In-place)
|
||||||
|
- Added auto-scan and watch-for-changes settings
|
||||||
|
- Added supported audio formats configuration
|
||||||
|
- Persistent storage using SharedPreferences
|
||||||
|
|
||||||
|
### 3. Watch Folder Provider (`lib/providers/watch_folder_provider.dart`)
|
||||||
|
- Created service for managing watch folders
|
||||||
|
- Added database operations for CRUD operations
|
||||||
|
- Simplified implementation avoiding complex watcher issues
|
||||||
|
- Added folder scanning functionality
|
||||||
|
- Added missing track cleanup
|
||||||
|
|
||||||
|
### 4. Track Repository Refactor (`lib/data/track_repository.dart`)
|
||||||
|
- **Major Changes:**
|
||||||
|
- Split `importFiles` into mode-specific methods
|
||||||
|
- `_importFilesWithCopy`: Original behavior (copies files to internal storage)
|
||||||
|
- `_importFilesInPlace`: New behavior (indexes files in original location)
|
||||||
|
- Added `scanDirectory` method for folder scanning
|
||||||
|
- Added `scanWatchFolders` method for bulk scanning
|
||||||
|
- Added file event handlers (`addFileFromWatch`, `removeFileFromWatch`, `updateFileFromWatch`)
|
||||||
|
- Added `cleanupMissingTracks` for maintaining database integrity
|
||||||
|
- Updated `deleteTrack` to handle in-place vs copied files correctly
|
||||||
|
|
||||||
|
### 5. Settings UI (`lib/ui/screens/settings_screen.dart`)
|
||||||
|
- Created comprehensive settings interface
|
||||||
|
- Import mode selection (Copy vs In-place)
|
||||||
|
- Auto-scan and watch-for-changes toggles
|
||||||
|
- Watch folders management section
|
||||||
|
- Supported formats display
|
||||||
|
- Integration with new providers
|
||||||
|
|
||||||
|
### 6. Dependencies (`pubspec.yaml`)
|
||||||
|
- Added `watcher: ^1.2.0` for file system monitoring
|
||||||
|
- Added `shared_preferences: ^2.3.5` for settings persistence
|
||||||
|
|
||||||
|
## New Functionality
|
||||||
|
|
||||||
|
### Import Modes
|
||||||
|
1. **Copy Mode (Default):**
|
||||||
|
- Original behavior maintained
|
||||||
|
- Files copied to internal music directory
|
||||||
|
- Safe file management
|
||||||
|
- Suitable for mobile devices
|
||||||
|
|
||||||
|
2. **In-place Mode:**
|
||||||
|
- Files indexed in original location
|
||||||
|
- No additional storage usage
|
||||||
|
- Preserves original file organization
|
||||||
|
- Suitable for desktop/storage-rich environments
|
||||||
|
|
||||||
|
### Watch Folder Features
|
||||||
|
- Add/remove watch folders
|
||||||
|
- Toggle active/inactive status
|
||||||
|
- Bulk scanning of all active folders
|
||||||
|
- Automatic cleanup of missing tracks
|
||||||
|
- Support for recursive scanning
|
||||||
|
|
||||||
|
### Settings Management
|
||||||
|
- Persistent storage of user preferences
|
||||||
|
- Auto-scan scheduling
|
||||||
|
- File change monitoring toggle
|
||||||
|
- Configurable audio formats
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Switch to In-place Indexing
|
||||||
|
```dart
|
||||||
|
// Update settings to use in-place indexing
|
||||||
|
ref.read(settingsProvider.notifier).setImportMode(ImportMode.inplace);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add Watch Folder
|
||||||
|
```dart
|
||||||
|
// Add a folder to watch list
|
||||||
|
final watchService = ref.read(watchFolderServiceProvider);
|
||||||
|
await watchService.addWatchFolder('/path/to/music', name: 'My Music');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scan Watch Folders
|
||||||
|
```dart
|
||||||
|
// Scan all active watch folders
|
||||||
|
final trackRepo = ref.read(trackRepositoryProvider);
|
||||||
|
await trackRepo.scanWatchFolders();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup Missing Tracks
|
||||||
|
```dart
|
||||||
|
// Remove tracks that no longer exist
|
||||||
|
final trackRepo = ref.read(trackRepositoryProvider);
|
||||||
|
await trackRepo.cleanupMissingTracks();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
- Flexible import options for different use cases
|
||||||
|
- Automatic library maintenance
|
||||||
|
- Real-time folder monitoring capabilities
|
||||||
|
- Preserved file organization when desired
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Efficient database operations
|
||||||
|
- Selective file scanning
|
||||||
|
- Proper resource cleanup
|
||||||
|
- Minimal storage impact for in-place mode
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- Clear separation of concerns
|
||||||
|
- Modular provider architecture
|
||||||
|
- Comprehensive error handling
|
||||||
|
- Extensible design for future features
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Additions
|
||||||
|
1. Real-time file watching implementation
|
||||||
|
2. Advanced file format detection
|
||||||
|
3. Folder exclusion/inclusion patterns
|
||||||
|
4. Metadata caching for performance
|
||||||
|
5. Batch operations optimization
|
||||||
|
6. Conflict resolution for duplicate files
|
||||||
|
|
||||||
|
### UI Improvements
|
||||||
|
1. Watch folder management interface
|
||||||
|
2. Import progress indicators
|
||||||
|
3. Folder scanning status
|
||||||
|
4. Settings organization and search
|
||||||
|
5. Conflict resolution dialogs
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### For Existing Users
|
||||||
|
- Current behavior preserved (copy mode by default)
|
||||||
|
- Manual switch to in-place mode available
|
||||||
|
- Existing copied files unaffected
|
||||||
|
- Gradual migration possible
|
||||||
|
|
||||||
|
### Recommended Workflow
|
||||||
|
1. Start with copy mode for testing
|
||||||
|
2. Add watch folders in in-place mode
|
||||||
|
3. Enable auto-scan when comfortable
|
||||||
|
4. Use cleanup to maintain library
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
### Database Considerations
|
||||||
|
- Unique path constraint ensures no duplicates
|
||||||
|
- Cascade deletion maintains referential integrity
|
||||||
|
- Proper indexing on path for performance
|
||||||
|
- Migration handles existing installations
|
||||||
|
|
||||||
|
### File System Safety
|
||||||
|
- Existence checks before operations
|
||||||
|
- Graceful error handling
|
||||||
|
- Safe disposal of file watchers
|
||||||
|
- Album art always stored internally
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- Lazy loading of watch folders
|
||||||
|
- Efficient streaming for large libraries
|
||||||
|
- Proper cleanup of resources
|
||||||
|
- Minimal memory footprint
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Test import mode switching
|
||||||
|
- Test watch folder operations
|
||||||
|
- Test file event handling
|
||||||
|
- Test cleanup functionality
|
||||||
|
- Test settings persistence
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Test full import workflows
|
||||||
|
- Test settings changes
|
||||||
|
- Test database migrations
|
||||||
|
- Test file system scenarios
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- Large file collections
|
||||||
|
- Network storage scenarios
|
||||||
|
- Permission denials
|
||||||
|
- File system errors
|
||||||
|
- Corrupted metadata
|
||||||
|
|
||||||
|
This refactor provides a solid foundation for enhanced music library management while maintaining backward compatibility and enabling powerful new features.
|
||||||
@@ -30,10 +30,14 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
|
||||||
<string>processing</string>
|
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
|
<string>processing</string>
|
||||||
|
<string>fetch</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -51,5 +55,13 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Grant access to Camera will allow GroovyBox add media to your library.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Grant access to Microphone will allow GroovyBox record audio for your library.</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>Grant access to Photo Library will allow GroovyBox export media for you.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Grant access to Photo Library will allow GroovyBox add media to your library.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -32,12 +32,32 @@ class PlaylistEntries extends Table {
|
|||||||
DateTimeColumn get addedAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get addedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DriftDatabase(tables: [Tracks, Playlists, PlaylistEntries])
|
class WatchFolders extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get path => text().unique()();
|
||||||
|
TextColumn get name => text()();
|
||||||
|
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
|
||||||
|
BoolColumn get recursive => boolean().withDefault(const Constant(true))();
|
||||||
|
DateTimeColumn get addedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
DateTimeColumn get lastScanned => dateTime().nullable()();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppSettings extends Table {
|
||||||
|
TextColumn get key => text()();
|
||||||
|
TextColumn get value => text()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {key};
|
||||||
|
}
|
||||||
|
|
||||||
|
@DriftDatabase(
|
||||||
|
tables: [Tracks, Playlists, PlaylistEntries, WatchFolders, AppSettings],
|
||||||
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase() : super(_openConnection());
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 5; // Bump version for lyricsOffset column
|
int get schemaVersion => 6; // Bump version for watch folders and settings
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@@ -59,6 +79,11 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
if (from < 5) {
|
if (from < 5) {
|
||||||
await m.addColumn(tracks, tracks.lyricsOffset);
|
await m.addColumn(tracks, tracks.lyricsOffset);
|
||||||
}
|
}
|
||||||
|
if (from < 6) {
|
||||||
|
// Create tables for watch folders and settings
|
||||||
|
await m.createTable(watchFolders);
|
||||||
|
await m.createTable(appSettings);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1039
lib/data/db.g.dart
1039
lib/data/db.g.dart
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
||||||
|
import 'package:groovybox/data/db.dart';
|
||||||
import 'package:groovybox/providers/db_provider.dart';
|
import 'package:groovybox/providers/db_provider.dart';
|
||||||
|
import 'package:groovybox/providers/settings_provider.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'db.dart';
|
|
||||||
|
|
||||||
part 'track_repository.g.dart';
|
part 'track_repository.g.dart';
|
||||||
|
|
||||||
@@ -23,6 +24,17 @@ class TrackRepository extends _$TrackRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> importFiles(List<String> filePaths) async {
|
Future<void> importFiles(List<String> filePaths) async {
|
||||||
|
final settings = ref.read(settingsProvider).value;
|
||||||
|
final importMode = settings?.importMode ?? ImportMode.copy;
|
||||||
|
|
||||||
|
if (importMode == ImportMode.copy) {
|
||||||
|
await _importFilesWithCopy(filePaths);
|
||||||
|
} else {
|
||||||
|
await _importFilesInPlace(filePaths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _importFilesWithCopy(List<String> filePaths) async {
|
||||||
final db = ref.read(databaseProvider);
|
final db = ref.read(databaseProvider);
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
final musicDir = Directory(p.join(appDir.path, 'music'));
|
final musicDir = Directory(p.join(appDir.path, 'music'));
|
||||||
@@ -76,6 +88,54 @@ class TrackRepository extends _$TrackRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _importFilesInPlace(List<String> filePaths) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
|
final artDir = Directory(p.join(appDir.path, 'art'));
|
||||||
|
|
||||||
|
await artDir.create(recursive: true);
|
||||||
|
|
||||||
|
for (final path in filePaths) {
|
||||||
|
final file = File(path);
|
||||||
|
if (!await file.exists()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Extract Metadata from original file
|
||||||
|
final metadata = await MetadataRetriever.fromFile(file);
|
||||||
|
final filename = p.basename(path);
|
||||||
|
|
||||||
|
String? artPath;
|
||||||
|
if (metadata.albumArt != null) {
|
||||||
|
// Store album art in internal directory
|
||||||
|
final artName =
|
||||||
|
'${p.basenameWithoutExtension(filename)}_${DateTime.now().millisecondsSinceEpoch}_art.jpg';
|
||||||
|
final artFile = File(p.join(artDir.path, artName));
|
||||||
|
await artFile.writeAsBytes(metadata.albumArt!);
|
||||||
|
artPath = artFile.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Insert into DB with original path
|
||||||
|
await db
|
||||||
|
.into(db.tracks)
|
||||||
|
.insert(
|
||||||
|
TracksCompanion.insert(
|
||||||
|
title:
|
||||||
|
metadata.trackName ?? p.basenameWithoutExtension(filename),
|
||||||
|
path: path, // Original path for in-place indexing
|
||||||
|
artist: Value(metadata.trackArtistNames?.join(', ')),
|
||||||
|
album: Value(metadata.albumName),
|
||||||
|
duration: Value(metadata.trackDuration), // Milliseconds
|
||||||
|
artUri: Value(artPath),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrIgnore,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error importing file $path: $e');
|
||||||
|
// Continue to next file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateMetadata({
|
Future<void> updateMetadata({
|
||||||
required int trackId,
|
required int trackId,
|
||||||
required String title,
|
required String title,
|
||||||
@@ -107,17 +167,24 @@ class TrackRepository extends _$TrackRepository {
|
|||||||
|
|
||||||
await (db.delete(db.tracks)..where((t) => t.id.equals(trackId))).go();
|
await (db.delete(db.tracks)..where((t) => t.id.equals(trackId))).go();
|
||||||
|
|
||||||
// 3. Delete file
|
// 3. Delete file only if it's a copied file (in internal music directory)
|
||||||
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
|
final musicDir = p.join(appDir.path, 'music');
|
||||||
|
|
||||||
final file = File(track.path);
|
final file = File(track.path);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
try {
|
// Only delete if it's in our internal music directory (copied files)
|
||||||
await file.delete();
|
// For in-place indexed files, we don't delete the original
|
||||||
} catch (e) {
|
if (track.path.startsWith(musicDir)) {
|
||||||
debugPrint("Error deleting file: $e");
|
try {
|
||||||
|
await file.delete();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleting file: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Delete art if exists
|
// 4. Delete art if exists (album art is always stored internally)
|
||||||
if (track.artUri != null) {
|
if (track.artUri != null) {
|
||||||
final artFile = File(track.artUri!);
|
final artFile = File(track.artUri!);
|
||||||
if (await artFile.exists()) {
|
if (await artFile.exists()) {
|
||||||
@@ -151,4 +218,140 @@ class TrackRepository extends _$TrackRepository {
|
|||||||
final db = ref.read(databaseProvider);
|
final db = ref.read(databaseProvider);
|
||||||
return db.select(db.tracks).get();
|
return db.select(db.tracks).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan a directory for music files and import them.
|
||||||
|
Future<void> scanDirectory(
|
||||||
|
String directoryPath, {
|
||||||
|
bool recursive = true,
|
||||||
|
}) async {
|
||||||
|
final settings = ref.read(settingsProvider).value;
|
||||||
|
final supportedFormats =
|
||||||
|
settings?.supportedFormats ??
|
||||||
|
{'.mp3', '.flac', '.wav', '.m4a', '.aac', '.ogg', '.wma', '.opus'};
|
||||||
|
|
||||||
|
final directory = Directory(directoryPath);
|
||||||
|
if (!await directory.exists()) {
|
||||||
|
throw Exception('Directory does not exist: $directoryPath');
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> musicFiles = [];
|
||||||
|
|
||||||
|
await for (final entity in directory.list(recursive: recursive)) {
|
||||||
|
if (entity is File) {
|
||||||
|
final extension = p.extension(entity.path).toLowerCase();
|
||||||
|
if (supportedFormats.contains(extension)) {
|
||||||
|
musicFiles.add(entity.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (musicFiles.isNotEmpty) {
|
||||||
|
await importFiles(musicFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan all watch folders for new/updated files.
|
||||||
|
Future<void> scanWatchFolders() async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
final watchFolders = await (db.select(
|
||||||
|
db.watchFolders,
|
||||||
|
)..where((t) => t.isActive.equals(true))).get();
|
||||||
|
|
||||||
|
for (final folder in watchFolders) {
|
||||||
|
try {
|
||||||
|
await scanDirectory(folder.path, recursive: folder.recursive);
|
||||||
|
|
||||||
|
// Update last scanned time
|
||||||
|
await (db.update(db.watchFolders)..where((t) => t.id.equals(folder.id)))
|
||||||
|
.write(WatchFoldersCompanion(lastScanned: Value(DateTime.now())));
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error scanning watch folder ${folder.path}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a file from watch folder event.
|
||||||
|
Future<void> addFileFromWatch(String filePath) async {
|
||||||
|
final settings = ref.read(settingsProvider).value;
|
||||||
|
final supportedFormats =
|
||||||
|
settings?.supportedFormats ??
|
||||||
|
{'.mp3', '.flac', '.wav', '.m4a', '.aac', '.ogg', '.wma', '.opus'};
|
||||||
|
|
||||||
|
final extension = p.extension(filePath).toLowerCase();
|
||||||
|
if (!supportedFormats.contains(extension)) {
|
||||||
|
return; // Not a supported audio file
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = File(filePath);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await importFiles([filePath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a file from watch folder event.
|
||||||
|
Future<void> removeFileFromWatch(String filePath) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
// Find track by path
|
||||||
|
final track = await (db.select(
|
||||||
|
db.tracks,
|
||||||
|
)..where((t) => t.path.equals(filePath))).getSingleOrNull();
|
||||||
|
|
||||||
|
if (track != null) {
|
||||||
|
await deleteTrack(track.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a file from watch folder event.
|
||||||
|
Future<void> updateFileFromWatch(String filePath) async {
|
||||||
|
// For now, we remove and re-add the file
|
||||||
|
// In a more sophisticated implementation, we could update metadata only
|
||||||
|
await removeFileFromWatch(filePath);
|
||||||
|
await addFileFromWatch(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a track exists and is accessible.
|
||||||
|
Future<bool> isTrackAccessible(String filePath) async {
|
||||||
|
try {
|
||||||
|
final file = File(filePath);
|
||||||
|
return await file.exists();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error checking track accessibility $filePath: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up tracks that no longer exist (for in-place indexed tracks).
|
||||||
|
Future<void> cleanupMissingTracks() async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
final settings = ref.read(settingsProvider).value;
|
||||||
|
|
||||||
|
if (settings?.importMode == ImportMode.copy) {
|
||||||
|
return; // Only cleanup for in-place indexed tracks
|
||||||
|
}
|
||||||
|
|
||||||
|
final allTracks = await db.select(db.tracks).get();
|
||||||
|
|
||||||
|
for (final track in allTracks) {
|
||||||
|
if (!await isTrackAccessible(track.path)) {
|
||||||
|
debugPrint('Removing missing track: ${track.path}');
|
||||||
|
// Remove from database but don't delete file (since it doesn't exist)
|
||||||
|
await (db.delete(db.tracks)..where((t) => t.id.equals(track.id))).go();
|
||||||
|
|
||||||
|
// Clean up album art
|
||||||
|
if (track.artUri != null) {
|
||||||
|
final artFile = File(track.artUri!);
|
||||||
|
if (await artFile.exists()) {
|
||||||
|
try {
|
||||||
|
await artFile.delete();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleting missing track's art: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class TrackRepositoryProvider
|
|||||||
TrackRepository create() => TrackRepository();
|
TrackRepository create() => TrackRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$trackRepositoryHash() => r'244e5fc82fcaa34cb1276a41e4158a0eefcc7258';
|
String _$trackRepositoryHash() => r'538fedbc358e305aac4517d2c517a8bdf6bbb75c';
|
||||||
|
|
||||||
abstract class _$TrackRepository extends $AsyncNotifier<void> {
|
abstract class _$TrackRepository extends $AsyncNotifier<void> {
|
||||||
FutureOr<void> build();
|
FutureOr<void> build();
|
||||||
|
|||||||
156
lib/providers/settings_provider.dart
Normal file
156
lib/providers/settings_provider.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
part 'settings_provider.g.dart';
|
||||||
|
|
||||||
|
enum ImportMode {
|
||||||
|
copy('Copy to internal storage'),
|
||||||
|
inplace('In-place indexing'),
|
||||||
|
mixed('Mixed (both copy and in-place)');
|
||||||
|
|
||||||
|
const ImportMode(this.displayName);
|
||||||
|
final String displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsState {
|
||||||
|
final ImportMode importMode;
|
||||||
|
final bool autoScan;
|
||||||
|
final bool watchForChanges;
|
||||||
|
final Set<String> supportedFormats;
|
||||||
|
|
||||||
|
const SettingsState({
|
||||||
|
this.importMode = ImportMode.mixed,
|
||||||
|
this.autoScan = true,
|
||||||
|
this.watchForChanges = true,
|
||||||
|
this.supportedFormats = const {
|
||||||
|
'.mp3',
|
||||||
|
'.flac',
|
||||||
|
'.wav',
|
||||||
|
'.m4a',
|
||||||
|
'.aac',
|
||||||
|
'.ogg',
|
||||||
|
'.wma',
|
||||||
|
'.opus',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingsState copyWith({
|
||||||
|
ImportMode? importMode,
|
||||||
|
bool? autoScan,
|
||||||
|
bool? watchForChanges,
|
||||||
|
Set<String>? supportedFormats,
|
||||||
|
}) {
|
||||||
|
return SettingsState(
|
||||||
|
importMode: importMode ?? this.importMode,
|
||||||
|
autoScan: autoScan ?? this.autoScan,
|
||||||
|
watchForChanges: watchForChanges ?? this.watchForChanges,
|
||||||
|
supportedFormats: supportedFormats ?? this.supportedFormats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class SettingsNotifier extends _$SettingsNotifier {
|
||||||
|
static const String _importModeKey = 'import_mode';
|
||||||
|
static const String _autoScanKey = 'auto_scan';
|
||||||
|
static const String _watchForChangesKey = 'watch_for_changes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SettingsState> build() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
final importModeIndex = prefs.getInt(_importModeKey) ?? 0;
|
||||||
|
final importMode = ImportMode.values[importModeIndex];
|
||||||
|
|
||||||
|
final autoScan = prefs.getBool(_autoScanKey) ?? true;
|
||||||
|
final watchForChanges = prefs.getBool(_watchForChangesKey) ?? true;
|
||||||
|
|
||||||
|
return SettingsState(
|
||||||
|
importMode: importMode,
|
||||||
|
autoScan: autoScan,
|
||||||
|
watchForChanges: watchForChanges,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setImportMode(ImportMode mode) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt(_importModeKey, mode.index);
|
||||||
|
|
||||||
|
if (state.hasValue) {
|
||||||
|
state = AsyncValue.data(state.value!.copyWith(importMode: mode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setAutoScan(bool enabled) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_autoScanKey, enabled);
|
||||||
|
|
||||||
|
if (state.hasValue) {
|
||||||
|
state = AsyncValue.data(state.value!.copyWith(autoScan: enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setWatchForChanges(bool enabled) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_watchForChangesKey, enabled);
|
||||||
|
|
||||||
|
if (state.hasValue) {
|
||||||
|
state = AsyncValue.data(state.value!.copyWith(watchForChanges: enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience providers for specific settings
|
||||||
|
@riverpod
|
||||||
|
class ImportModeNotifier extends _$ImportModeNotifier {
|
||||||
|
@override
|
||||||
|
ImportMode build() {
|
||||||
|
return ref
|
||||||
|
.watch(settingsProvider)
|
||||||
|
.when(
|
||||||
|
data: (settings) => settings.importMode,
|
||||||
|
loading: () => ImportMode.mixed,
|
||||||
|
error: (_, _) => ImportMode.mixed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(ImportMode mode) async {
|
||||||
|
await ref.read(settingsProvider.notifier).setImportMode(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class AutoScanNotifier extends _$AutoScanNotifier {
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
return ref
|
||||||
|
.watch(settingsProvider)
|
||||||
|
.when(
|
||||||
|
data: (settings) => settings.autoScan,
|
||||||
|
loading: () => true,
|
||||||
|
error: (_, _) => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(bool enabled) async {
|
||||||
|
await ref.read(settingsProvider.notifier).setAutoScan(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class WatchForChangesNotifier extends _$WatchForChangesNotifier {
|
||||||
|
@override
|
||||||
|
bool build() {
|
||||||
|
return ref
|
||||||
|
.watch(settingsProvider)
|
||||||
|
.when(
|
||||||
|
data: (settings) => settings.watchForChanges,
|
||||||
|
loading: () => true,
|
||||||
|
error: (_, _) => true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(bool enabled) async {
|
||||||
|
await ref.read(settingsProvider.notifier).setWatchForChanges(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
216
lib/providers/settings_provider.g.dart
Normal file
216
lib/providers/settings_provider.g.dart
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'settings_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(SettingsNotifier)
|
||||||
|
const settingsProvider = SettingsNotifierProvider._();
|
||||||
|
|
||||||
|
final class SettingsNotifierProvider
|
||||||
|
extends $AsyncNotifierProvider<SettingsNotifier, SettingsState> {
|
||||||
|
const SettingsNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'settingsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$settingsNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SettingsNotifier create() => SettingsNotifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$settingsNotifierHash() => r'6dc43c0f1d6ee7b7744dae2a8557b758574473d2';
|
||||||
|
|
||||||
|
abstract class _$SettingsNotifier extends $AsyncNotifier<SettingsState> {
|
||||||
|
FutureOr<SettingsState> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<AsyncValue<SettingsState>, SettingsState>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<SettingsState>, SettingsState>,
|
||||||
|
AsyncValue<SettingsState>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(ImportModeNotifier)
|
||||||
|
const importModeProvider = ImportModeNotifierProvider._();
|
||||||
|
|
||||||
|
final class ImportModeNotifierProvider
|
||||||
|
extends $NotifierProvider<ImportModeNotifier, ImportMode> {
|
||||||
|
const ImportModeNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'importModeProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$importModeNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
ImportModeNotifier create() => ImportModeNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(ImportMode value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<ImportMode>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$importModeNotifierHash() =>
|
||||||
|
r'eaf3dcf7c74dc24d6ebe14840d597e4a79859a63';
|
||||||
|
|
||||||
|
abstract class _$ImportModeNotifier extends $Notifier<ImportMode> {
|
||||||
|
ImportMode build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<ImportMode, ImportMode>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<ImportMode, ImportMode>,
|
||||||
|
ImportMode,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(AutoScanNotifier)
|
||||||
|
const autoScanProvider = AutoScanNotifierProvider._();
|
||||||
|
|
||||||
|
final class AutoScanNotifierProvider
|
||||||
|
extends $NotifierProvider<AutoScanNotifier, bool> {
|
||||||
|
const AutoScanNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'autoScanProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$autoScanNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
AutoScanNotifier create() => AutoScanNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(bool value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<bool>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$autoScanNotifierHash() => r'56f2f1a2f6aef095782a0ed4407a43a8f589dc4b';
|
||||||
|
|
||||||
|
abstract class _$AutoScanNotifier extends $Notifier<bool> {
|
||||||
|
bool build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<bool, bool>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<bool, bool>,
|
||||||
|
bool,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(WatchForChangesNotifier)
|
||||||
|
const watchForChangesProvider = WatchForChangesNotifierProvider._();
|
||||||
|
|
||||||
|
final class WatchForChangesNotifierProvider
|
||||||
|
extends $NotifierProvider<WatchForChangesNotifier, bool> {
|
||||||
|
const WatchForChangesNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'watchForChangesProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchForChangesNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
WatchForChangesNotifier create() => WatchForChangesNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(bool value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<bool>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchForChangesNotifierHash() =>
|
||||||
|
r'b4648380ae989e6e36138780d0c925916b6e20b3';
|
||||||
|
|
||||||
|
abstract class _$WatchForChangesNotifier extends $Notifier<bool> {
|
||||||
|
bool build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<bool, bool>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<bool, bool>,
|
||||||
|
bool,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ final class SeedColorNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$seedColorNotifierHash() => r'3954f171d23ec7bcf3357928a278a8212c835908';
|
String _$seedColorNotifierHash() => r'2ab1da635e2528459e9bfc26db7eaf5c4ac6e701';
|
||||||
|
|
||||||
abstract class _$SeedColorNotifier extends $Notifier<Color> {
|
abstract class _$SeedColorNotifier extends $Notifier<Color> {
|
||||||
Color build();
|
Color build();
|
||||||
|
|||||||
128
lib/providers/watch_folder_provider.dart
Normal file
128
lib/providers/watch_folder_provider.dart
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import '../data/db.dart';
|
||||||
|
import '../data/track_repository.dart';
|
||||||
|
import '../providers/db_provider.dart';
|
||||||
|
|
||||||
|
// Simple watch folder provider using Riverpod
|
||||||
|
final watchFoldersProvider = FutureProvider<List<WatchFolder>>((ref) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
return await (db.select(
|
||||||
|
db.watchFolders,
|
||||||
|
)..orderBy([(t) => OrderingTerm(expression: t.addedAt)])).get();
|
||||||
|
});
|
||||||
|
|
||||||
|
final activeWatchFoldersProvider = Provider<List<WatchFolder>>((ref) {
|
||||||
|
final watchFoldersAsync = ref.watch(watchFoldersProvider);
|
||||||
|
return watchFoldersAsync.when(
|
||||||
|
data: (folders) => folders.where((folder) => folder.isActive).toList(),
|
||||||
|
loading: () => [],
|
||||||
|
error: (_, _) => [],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
class WatchFolderService {
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
WatchFolderService(this.ref);
|
||||||
|
|
||||||
|
Future<void> addWatchFolder(
|
||||||
|
String path, {
|
||||||
|
String? name,
|
||||||
|
bool recursive = true,
|
||||||
|
}) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
final directory = Directory(path);
|
||||||
|
|
||||||
|
if (!await directory.exists()) {
|
||||||
|
throw Exception('Directory does not exist: $path');
|
||||||
|
}
|
||||||
|
|
||||||
|
final folderName = name ?? p.basename(path);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.into(db.watchFolders)
|
||||||
|
.insert(
|
||||||
|
WatchFoldersCompanion.insert(
|
||||||
|
path: path,
|
||||||
|
name: folderName,
|
||||||
|
recursive: Value(recursive),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalidate the provider to refresh UI
|
||||||
|
ref.invalidate(watchFoldersProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeWatchFolder(int folderId) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
await (db.delete(
|
||||||
|
db.watchFolders,
|
||||||
|
)..where((t) => t.id.equals(folderId))).go();
|
||||||
|
|
||||||
|
// Invalidate the provider to refresh UI
|
||||||
|
ref.invalidate(watchFoldersProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleWatchFolder(int folderId, bool isActive) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
await (db.update(db.watchFolders)..where((t) => t.id.equals(folderId)))
|
||||||
|
.write(WatchFoldersCompanion(isActive: Value(isActive)));
|
||||||
|
|
||||||
|
// Invalidate the provider to refresh UI
|
||||||
|
ref.invalidate(watchFoldersProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateLastScanned(int folderId) async {
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
await (db.update(db.watchFolders)..where((t) => t.id.equals(folderId)))
|
||||||
|
.write(WatchFoldersCompanion(lastScanned: Value(DateTime.now())));
|
||||||
|
|
||||||
|
// Invalidate the provider to refresh UI
|
||||||
|
ref.invalidate(watchFoldersProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scanWatchFolders() async {
|
||||||
|
final trackRepository = ref.read(trackRepositoryProvider.notifier);
|
||||||
|
await trackRepository.scanWatchFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cleanupMissingTracks() async {
|
||||||
|
// Remove tracks that no longer exist
|
||||||
|
final db = ref.read(databaseProvider);
|
||||||
|
final allTracks = await db.select(db.tracks).get();
|
||||||
|
|
||||||
|
for (final track in allTracks) {
|
||||||
|
final file = File(track.path);
|
||||||
|
if (!await file.exists()) {
|
||||||
|
debugPrint('Removing missing track: ${track.path}');
|
||||||
|
|
||||||
|
// Remove from database but don't delete file (since it doesn't exist)
|
||||||
|
await (db.delete(db.tracks)..where((t) => t.id.equals(track.id))).go();
|
||||||
|
|
||||||
|
// Clean up album art
|
||||||
|
if (track.artUri != null) {
|
||||||
|
final artFile = File(track.artUri!);
|
||||||
|
if (await artFile.exists()) {
|
||||||
|
try {
|
||||||
|
await artFile.delete();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleting missing track's art: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider for the service
|
||||||
|
final watchFolderServiceProvider = Provider<WatchFolderService>((ref) {
|
||||||
|
return WatchFolderService(ref);
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import 'package:groovybox/data/playlist_repository.dart';
|
|||||||
import 'package:groovybox/data/track_repository.dart';
|
import 'package:groovybox/data/track_repository.dart';
|
||||||
import 'package:groovybox/logic/lyrics_parser.dart';
|
import 'package:groovybox/logic/lyrics_parser.dart';
|
||||||
import 'package:groovybox/providers/audio_provider.dart';
|
import 'package:groovybox/providers/audio_provider.dart';
|
||||||
|
import 'package:groovybox/ui/screens/settings_screen.dart';
|
||||||
import 'package:groovybox/ui/tabs/albums_tab.dart';
|
import 'package:groovybox/ui/tabs/albums_tab.dart';
|
||||||
import 'package:groovybox/ui/tabs/playlists_tab.dart';
|
import 'package:groovybox/ui/tabs/playlists_tab.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -121,6 +122,16 @@ class LibraryScreen extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: const Text('Library'),
|
: const Text('Library'),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SettingsScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
tooltip: 'Import Files',
|
tooltip: 'Import Files',
|
||||||
|
|||||||
339
lib/ui/screens/settings_screen.dart
Normal file
339
lib/ui/screens/settings_screen.dart
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:groovybox/providers/settings_provider.dart';
|
||||||
|
import 'package:groovybox/providers/watch_folder_provider.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
|
class SettingsScreen extends ConsumerWidget {
|
||||||
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final settingsAsync = ref.watch(settingsProvider);
|
||||||
|
final watchFoldersAsync = ref.watch(watchFoldersProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Settings')),
|
||||||
|
body: settingsAsync.when(
|
||||||
|
data: (settings) => SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Import Mode Section
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Import Mode',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(ImportMode.copy.displayName),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Copy music files to internal storage',
|
||||||
|
),
|
||||||
|
leading: RadioGroup<ImportMode>(
|
||||||
|
groupValue: settings.importMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref
|
||||||
|
.read(importModeProvider.notifier)
|
||||||
|
.update(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Radio<ImportMode>(value: ImportMode.copy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(ImportMode.inplace.displayName),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Index music files in their original location',
|
||||||
|
),
|
||||||
|
leading: RadioGroup<ImportMode>(
|
||||||
|
groupValue: settings.importMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref
|
||||||
|
.read(importModeProvider.notifier)
|
||||||
|
.update(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Radio<ImportMode>(
|
||||||
|
value: ImportMode.inplace,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(ImportMode.mixed.displayName),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Use internal storage and add folder libraries',
|
||||||
|
),
|
||||||
|
leading: RadioGroup<ImportMode>(
|
||||||
|
groupValue: settings.importMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref
|
||||||
|
.read(importModeProvider.notifier)
|
||||||
|
.update(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Radio<ImportMode>(value: ImportMode.mixed),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Auto Scan Section
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Auto Scan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Auto-scan music libraries'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Automatically scan music libraries for new music files',
|
||||||
|
),
|
||||||
|
value: settings.autoScan,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(autoScanProvider.notifier).update(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text('Watch for changes'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Monitor music libraries for file changes',
|
||||||
|
),
|
||||||
|
value: settings.watchForChanges,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(watchForChangesProvider.notifier)
|
||||||
|
.update(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Watch Folders Section
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Music Libraries',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (settings.importMode == ImportMode.inplace ||
|
||||||
|
settings.importMode == ImportMode.mixed)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _scanLibraries(context, ref),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Scan Libraries',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () =>
|
||||||
|
_addMusicLibrary(context, ref),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Add Music Library',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (settings.importMode == ImportMode.inplace ||
|
||||||
|
settings.importMode == ImportMode.mixed) ...[
|
||||||
|
const Text(
|
||||||
|
'Add folder libraries to index music files in their original location.',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
if (settings.importMode == ImportMode.copy)
|
||||||
|
const Text(
|
||||||
|
'Folder libraries are available in in-place and mixed modes.',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||||
|
),
|
||||||
|
if (settings.importMode == ImportMode.inplace ||
|
||||||
|
settings.importMode == ImportMode.mixed) ...[
|
||||||
|
watchFoldersAsync.when(
|
||||||
|
data: (folders) => folders.isEmpty
|
||||||
|
? const Text(
|
||||||
|
'No music libraries added yet.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: folders
|
||||||
|
.map(
|
||||||
|
(folder) => ListTile(
|
||||||
|
title: Text(folder.name),
|
||||||
|
subtitle: Text(folder.path),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Switch(
|
||||||
|
value: folder.isActive,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
watchFolderServiceProvider,
|
||||||
|
)
|
||||||
|
.toggleWatchFolder(
|
||||||
|
folder.id,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
watchFolderServiceProvider,
|
||||||
|
)
|
||||||
|
.removeWatchFolder(
|
||||||
|
folder.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (error, _) =>
|
||||||
|
Text('Error loading libraries: $error'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Supported Formats Section
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Supported Formats',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: settings.supportedFormats.map((format) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(format.toUpperCase()),
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).primaryColor.withOpacity(0.1),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, stack) =>
|
||||||
|
Center(child: Text('Error loading settings: $error')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addMusicLibrary(BuildContext context, WidgetRef ref) {
|
||||||
|
FilePicker.platform.getDirectoryPath().then((path) async {
|
||||||
|
if (path != null) {
|
||||||
|
try {
|
||||||
|
final service = ref.read(watchFolderServiceProvider);
|
||||||
|
await service.addWatchFolder(path, recursive: true);
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Added music library: $path')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error adding library: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scanLibraries(BuildContext context, WidgetRef ref) async {
|
||||||
|
try {
|
||||||
|
final service = ref.read(watchFolderServiceProvider);
|
||||||
|
await service.scanWatchFolders();
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Libraries scanned successfully')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error scanning libraries: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import file_picker
|
|||||||
import flutter_media_metadata
|
import flutter_media_metadata
|
||||||
import media_kit_libs_macos_audio
|
import media_kit_libs_macos_audio
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import sqlite3_flutter_libs
|
import sqlite3_flutter_libs
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterMediaMetadataPlugin.register(with: registry.registrar(forPlugin: "FlutterMediaMetadataPlugin"))
|
FlutterMediaMetadataPlugin.register(with: registry.registrar(forPlugin: "FlutterMediaMetadataPlugin"))
|
||||||
MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin"))
|
MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- sqflite_darwin (0.0.4):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -51,6 +54,7 @@ DEPENDENCIES:
|
|||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- media_kit_libs_macos_audio (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos`)
|
- media_kit_libs_macos_audio (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
|
|
||||||
@@ -73,6 +77,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_audio/macos
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
@@ -86,6 +92,7 @@ SPEC CHECKSUMS:
|
|||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
media_kit_libs_macos_audio: 06f3cf88d6d89c7c3c87eae57689d1c6adb335b2
|
media_kit_libs_macos_audio: 06f3cf88d6d89c7c3c87eae57689d1c6adb335b2
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
|
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
|
||||||
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
|
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
|
||||||
|
|||||||
@@ -368,14 +368,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
|||||||
58
pubspec.lock
58
pubspec.lock
@@ -896,6 +896,62 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.18"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1182,7 +1238,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249
|
sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: groovybox
|
name: groovybox
|
||||||
description: "A new Flutter project."
|
description: "A new local music player."
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+40
|
version: 1.0.0+46
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.1
|
sdk: ^3.10.1
|
||||||
@@ -53,6 +53,8 @@ dependencies:
|
|||||||
http: ^1.0.0
|
http: ^1.0.0
|
||||||
audio_service: ^0.18.18
|
audio_service: ^0.18.18
|
||||||
palette_generator: ^0.3.3+4
|
palette_generator: ^0.3.3+4
|
||||||
|
watcher: ^1.2.0
|
||||||
|
shared_preferences: ^2.3.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user