Multiple library and in-place adding

This commit is contained in:
2025-12-18 23:31:34 +08:00
parent 6f95c30e90
commit 4f6e5883b7
17 changed files with 2414 additions and 21 deletions

View 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);
}
}

View 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);
}
}

View File

@@ -94,7 +94,7 @@ final class SeedColorNotifierProvider
}
}
String _$seedColorNotifierHash() => r'3954f171d23ec7bcf3357928a278a8212c835908';
String _$seedColorNotifierHash() => r'2ab1da635e2528459e9bfc26db7eaf5c4ac6e701';
abstract class _$SeedColorNotifier extends $Notifier<Color> {
Color build();

View 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);
});