✨ Multiple library and in-place adding
This commit is contained in:
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> {
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user