🎉 Initial commit: AudiOhm - Alternative à Spotify avec streaming YouTube

Features:
- Frontend Flutter avec thème néon cyberpunk
- Backend FastAPI avec streaming YouTube
- Base de données PostgreSQL + Redis
- Authentification JWT complète
- Recherche multi-source (DB + YouTube)
- Playlists CRUD avec drag & drop
- Queue management
- Settings avec audio quality
- Interface adaptative (Desktop + Mobile)

Tech Stack:
- Frontend: Flutter 3.2+, Riverpod
- Backend: Python 3.11+, FastAPI
- Database: PostgreSQL 15+
- Cache: Redis 7+
- Streaming: yt-dlp + FFmpeg

🚀 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
feldenr
2026-01-18 17:08:59 +01:00
commit 9c504d2c3d
128 changed files with 22638 additions and 0 deletions
@@ -0,0 +1,290 @@
/// Settings Provider
library;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import '../../../domain/entities/user.dart';
import '../../../infrastructure/datasources/remote/auth_api_service.dart';
/// Audio Quality enum
enum AudioQuality { low, medium, high, lossless }
/// Settings state
class SettingsState {
final User? user;
final AudioQuality audioQuality;
final bool downloadOnMobileData;
final bool showExplicitContent;
final bool crossfadeEnabled;
final double crossfadeDuration;
final bool gaplessPlayback;
final bool normalizeVolume;
final String cacheSize;
final bool isLoading;
final String? error;
const SettingsState({
this.user,
this.audioQuality = AudioQuality.high,
this.downloadOnMobileData = false,
this.showExplicitContent = true,
this.crossfadeEnabled = false,
this.crossfadeDuration = 5.0,
this.gaplessPlayback = true,
this.normalizeVolume = false,
this.cacheSize = '0 MB',
this.isLoading = false,
this.error,
});
SettingsState copyWith({
User? user,
AudioQuality? audioQuality,
bool? downloadOnMobileData,
bool? showExplicitContent,
bool? crossfadeEnabled,
double? crossfadeDuration,
bool? gaplessPlayback,
bool? normalizeVolume,
String? cacheSize,
bool? isLoading,
String? error,
}) {
return SettingsState(
user: user ?? this.user,
audioQuality: audioQuality ?? this.audioQuality,
downloadOnMobileData: downloadOnMobileData ?? this.downloadOnMobileData,
showExplicitContent: showExplicitContent ?? this.showExplicitContent,
crossfadeEnabled: crossfadeEnabled ?? this.crossfadeEnabled,
crossfadeDuration: crossfadeDuration ?? this.crossfadeDuration,
gaplessPlayback: gaplessPlayback ?? this.gaplessPlayback,
normalizeVolume: normalizeVolume ?? this.normalizeVolume,
cacheSize: cacheSize ?? this.cacheSize,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}
/// Settings notifier
class SettingsNotifier extends StateNotifier<SettingsState> {
SettingsNotifier(this._authApiService) : super(const SettingsState()) {
_loadSettingsFromPrefs();
}
final AuthApiService _authApiService;
SharedPreferences? _prefs;
// Keys for shared preferences
static const String _audioQualityKey = 'audio_quality';
static const String _downloadOnMobileDataKey = 'download_on_mobile_data';
static const String _showExplicitContentKey = 'show_explicit_content';
static const String _crossfadeEnabledKey = 'crossfade_enabled';
static const String _crossfadeDurationKey = 'crossfade_duration';
static const String _gaplessPlaybackKey = 'gapless_playback';
static const String _normalizeVolumeKey = 'normalize_volume';
/// Initialize shared preferences and load settings
Future<void> _loadSettingsFromPrefs() async {
_prefs = await SharedPreferences.getInstance();
final audioQualityIndex = _prefs?.getInt(_audioQualityKey) ?? 2;
final downloadOnMobileData = _prefs?.getBool(_downloadOnMobileDataKey) ?? false;
final showExplicitContent = _prefs?.getBool(_showExplicitContentKey) ?? true;
final crossfadeEnabled = _prefs?.getBool(_crossfadeEnabledKey) ?? false;
final crossfadeDuration = _prefs?.getDouble(_crossfadeDurationKey) ?? 5.0;
final gaplessPlayback = _prefs?.getBool(_gaplessPlaybackKey) ?? true;
final normalizeVolume = _prefs?.getBool(_normalizeVolumeKey) ?? false;
state = state.copyWith(
audioQuality: AudioQuality.values[audioQualityIndex],
downloadOnMobileData: downloadOnMobileData,
showExplicitContent: showExplicitContent,
crossfadeEnabled: crossfadeEnabled,
crossfadeDuration: crossfadeDuration,
gaplessPlayback: gaplessPlayback,
normalizeVolume: normalizeVolume,
);
await _calculateCacheSize();
}
/// Load user profile from API
Future<void> loadSettings() async {
state = state.copyWith(isLoading: true, error: null);
try {
final user = await _authApiService.getCurrentUser();
state = state.copyWith(user: user, isLoading: false);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}
/// Update user profile
Future<void> updateProfile({
String? displayName,
String? avatarUrl,
}) async {
state = state.copyWith(isLoading: true, error: null);
try {
final updatedUser = await _authApiService.updateProfile(
displayName: displayName,
avatarUrl: avatarUrl,
);
state = state.copyWith(
user: updatedUser,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
rethrow;
}
}
/// Set audio quality
Future<void> setAudioQuality(AudioQuality quality) async {
await _prefs?.setInt(_audioQualityKey, quality.index);
state = state.copyWith(audioQuality: quality);
}
/// Toggle download on mobile data
Future<void> toggleDownloadOnMobileData(bool value) async {
await _prefs?.setBool(_downloadOnMobileDataKey, value);
state = state.copyWith(downloadOnMobileData: value);
}
/// Toggle explicit content
Future<void> toggleShowExplicitContent(bool value) async {
await _prefs?.setBool(_showExplicitContentKey, value);
state = state.copyWith(showExplicitContent: value);
}
/// Toggle crossfade
Future<void> toggleCrossfade(bool value) async {
await _prefs?.setBool(_crossfadeEnabledKey, value);
state = state.copyWith(crossfadeEnabled: value);
}
/// Set crossfade duration
Future<void> setCrossfadeDuration(double duration) async {
await _prefs?.setDouble(_crossfadeDurationKey, duration);
state = state.copyWith(crossfadeDuration: duration);
}
/// Toggle gapless playback
Future<void> toggleGaplessPlayback(bool value) async {
await _prefs?.setBool(_gaplessPlaybackKey, value);
state = state.copyWith(gaplessPlayback: value);
}
/// Toggle normalize volume
Future<void> toggleNormalizeVolume(bool value) async {
await _prefs?.setBool(_normalizeVolumeKey, value);
state = state.copyWith(normalizeVolume: value);
}
/// Calculate cache size
Future<void> _calculateCacheSize() async {
try {
final tempDir = await getTemporaryDirectory();
final appDocDir = await getApplicationDocumentsDirectory();
final tempSize = _getFolderSize(tempDir);
final docSize = _getFolderSize(appDocDir);
final totalSize = tempSize + docSize;
final cacheSizeStr = _formatBytes(totalSize);
state = state.copyWith(cacheSize: cacheSizeStr);
} catch (e) {
state = state.copyWith(cacheSize: 'Unknown');
}
}
/// Get folder size in bytes
int _getFolderSize(Directory dir) {
int size = 0;
try {
if (dir.existsSync()) {
dir
.listSync(recursive: true, followLinks: false)
.forEach((FileSystemEntity entity) {
if (entity is File) {
size += entity.lengthSync();
}
});
}
} catch (e) {
// Ignore errors
}
return size;
}
/// Format bytes to human readable string
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
/// Clear cache
Future<void> clearCache() async {
state = state.copyWith(isLoading: true);
try {
final tempDir = await getTemporaryDirectory();
if (await tempDir.exists()) {
await _deleteFolderContents(tempDir);
}
await _calculateCacheSize();
state = state.copyWith(isLoading: false);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
rethrow;
}
}
/// Delete folder contents
Future<void> _deleteFolderContents(Directory dir) async {
try {
if (await dir.exists()) {
await for (final entity in dir.list()) {
if (entity is File) {
await entity.delete();
} else if (entity is Directory) {
await entity.delete(recursive: true);
}
}
}
} catch (e) {
// Ignore errors
}
}
}
/// Settings provider
final settingsProvider = StateNotifierProvider<SettingsNotifier, SettingsState>(
(ref) {
final authApiService = ref.watch(authApiServiceProvider);
return SettingsNotifier(authApiService);
},
);