🎉 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,261 @@
# Settings Page - Visual Preview & Features
## Visual Design
### Overall Theme
- **Background**: Deep dark blue (#0A0E27) with neon cyan accents
- **Cards**: Semi-transparent surfaces with cyan glow borders
- **Typography**: Outfit font family with neon color highlights
- **Effects**: Subtle gradients, glow shadows, smooth transitions
## Section Breakdown
### 1. Profile Section (Top)
```
┌─────────────────────────────────────────────────┐
│ ┌────┐ John Doe [PREMIUM] │
│ │ 👤 │ john.doe@email.com │
│ └────┘ @johndoe │
│ │
│ [ Edit Profile ] │
└─────────────────────────────────────────────────┘
```
Features:
- Circular avatar with gradient glow
- Premium badge with violet/rose gradient
- Display name, email, username
- Edit Profile button (cyan outlined)
### 2. Audio Quality Section
```
┌─────────────────────────────────────────────────┐
│ AUDIO │
├─────────────────────────────────────────────────┤
│ 🎵 Audio Quality │
│ Higher quality uses more data │
│ ──────────────────────────────────────────── │
│ Low [96 kbps] Best for... │
│ Medium [160 kbps] Good... │
│ High [320 kbps] Best... ✓ │
│ Lossless [FLAC] Requires... [🔒]│
└─────────────────────────────────────────────────┘
```
Features:
- Radio-style selection
- Bitrate badges
- Quality descriptions
- Premium lock on Lossless
- Selection indicator (cyan checkmark)
### 3. Playback Section
```
┌─────────────────────────────────────────────────┐
│ PLAYBACK │
├─────────────────────────────────────────────────┤
│ 🎚️ Crossfade [○] │
│ Smooth transition between tracks │
│ │
│ Crossfade Duration: 5s │
│ ━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━ │
│ │
│ ──────────────────────────────────────────── │
│ ♾️ Gapless Playback [●] │
│ No gap between tracks │
│ │
│ ──────────────────────────────────────────── │
│ 🔊 Normalize Volume [○] │
│ Set same volume for all tracks │
└─────────────────────────────────────────────────┘
```
Features:
- Toggle switches with cyan active color
- Crossfade duration slider (1-12 seconds)
- Descriptive subtitles
- Icon indicators
### 4. Downloads Section
```
┌─────────────────────────────────────────────────┐
│ DOWNLOADS │
├─────────────────────────────────────────────────┤
│ 📥 Download on Mobile Data [○] │
│ May use extra data │
│ │
│ ──────────────────────────────────────────── │
│ 🔞 Show Explicit Content [●] │
│ Display explicit content in search │
└─────────────────────────────────────────────────┘
```
### 5. Storage Section
```
┌─────────────────────────────────────────────────┐
│ 💾 Storage │
│ Cache and offline data │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Cache Size 📁 │ │
│ │ 245.3 MB │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [ Clear Cache ] │
└─────────────────────────────────────────────────┘
```
Features:
- Large cache size display (cyan)
- Folder icon
- Clear cache button (rose outlined)
- Confirmation dialog
### 6. About Section
```
┌─────────────────────────────────────────────────┐
│ ABOUT │
├─────────────────────────────────────────────────┤
️ App Version │
│ 1.0.0+1 │
│ │
│ ──────────────────────────────────────────── │
│ 📄 Licenses │
│ Open source licenses │
└─────────────────────────────────────────────────┘
```
### 7. Logout Button
```
┌─────────────────────────────────────────────────┐
│ [ 🚪 Log Out ] │
└─────────────────────────────────────────────────┘
```
Features:
- Rose outlined button
- Confirmation dialog
- Logout icon
## Color Palette
### Primary Colors
- **Cyan**: #00F0FF (primary accent)
- **Violet**: #BF00FF (secondary accent)
- **Rose**: #FF006E (error/danger)
- **Green**: #39FF14 (success)
### Backgrounds
- **Primary**: #0A0E27 (main background)
- **Surface**: #1A1F3A (cards)
- **Surface Variant**: #252B4A (elevated)
### Text Colors
- **On Background**: #E0E6FF (primary text)
- **On Surface**: #B0B8D4 (secondary text)
- **Muted**: #6A7294 (disabled/hints)
## Interactive Elements
### Toggle Switches
- Active: Cyan with glow
- Inactive: Grey
- Smooth animations
### Buttons
- **Elevated**: Cyan gradient with glow shadow
- **Outlined**: Cyan/rose border with transparent bg
- **Text**: Cyan with underline effect
### Cards
- 1px cyan border (15% opacity)
- Subtle glow shadow
- 16px border radius
- Smooth hover effects
## Animations
### Page Transitions
- Smooth slide-in from right
- Fade-in for content
- Staggered section animations
### Micro-interactions
- Ripple effects on taps
- Scale animations on buttons
- Color transitions on toggles
- Slide-up dialogs
## Dialogs
### Edit Profile Dialog
- Centered, rounded corners
- Avatar with camera overlay
- Text input with cyan border
- Save/Cancel buttons
### Clear Cache Dialog
- Warning icon (rose)
- Confirmation text
- Cancel/Clear buttons
### Logout Dialog
- Logout icon (rose)
- Confirmation message
- Cancel/Logout buttons
## Snackbar Notifications
### Success
- Green background
- White text
- Checkmark icon
- 3 second duration
### Error
- Red background
- White text
- Error icon
- Auto-dismiss
### Info
- Cyan background
- White text
- Info icon
- Extended duration for tips
## Responsive Design
### Mobile (< 600px)
- Full-width cards
- 16px horizontal padding
- Bottom navigation or drawer
- Compact spacing
### Tablet (600-900px)
- Centered content (max 600px)
- Larger touch targets
- Side navigation optional
### Desktop (> 900px)
- Centered column (max 800px)
- Larger fonts
- Side navigation
- More spacing
## Accessibility
- High contrast ratios (WCAG AA)
- Large touch targets (44px min)
- Clear visual hierarchy
- Screen reader labels
- Keyboard navigation support
- Focus indicators
## Performance
- Lazy loading for images
- Efficient state management
- Optimized rebuilds with Riverpod
- Smooth 60fps animations
- Minimal memory usage
@@ -0,0 +1,358 @@
/// Settings Page
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../core/theme/colors.dart';
import '../../../core/theme/text_styles.dart';
import '../../providers/settings_provider.dart';
import '../../providers/auth_provider.dart';
import '../../widgets/settings/profile_section.dart';
import '../../widgets/settings/audio_quality_selector.dart';
import '../../widgets/settings/cache_management_tile.dart';
import '../../widgets/settings/settings_tile.dart';
/// Settings page
class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({super.key});
@override
ConsumerState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends ConsumerState<SettingsPage> {
String _appVersion = '1.0.0';
@override
void initState() {
super.initState();
_loadAppVersion();
// Load settings on init
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(settingsProvider.notifier).loadSettings();
});
}
Future<void> _loadAppVersion() async {
final info = await PackageInfo.fromPlatform();
setState(() {
_appVersion = '${info.version}+${info.buildNumber}';
});
}
@override
Widget build(BuildContext context) {
final settingsState = ref.watch(settingsProvider);
final authState = ref.watch(authProvider);
return Scaffold(
body: CustomScrollView(
slivers: [
// App Bar
SliverAppBar(
floating: true,
pinned: true,
elevation: 0,
backgroundColor: AppColors.primary.withOpacity(0.8),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () => Navigator.pop(context),
color: AppColors.onBackground,
),
title: Text(
'Settings',
style: AppTextStyles.h2.copyWith(
color: AppColors.onBackground,
fontWeight: FontWeight.w700,
),
),
),
// Content
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
// Profile Section
const ProfileSection(),
const SizedBox(height: 24),
// Audio Quality Section
const SettingsSectionHeader(title: 'Audio'),
const AudioQualitySelector(),
// Playback Section
const SettingsSectionHeader(title: 'Playback'),
SettingsCard(
children: [
SettingsToggleTile(
title: 'Crossfade',
subtitle: 'Smooth transition between tracks',
leading: const Icon(Icons.fade_out),
value: settingsState.crossfadeEnabled,
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.toggleCrossfade(value);
},
),
if (settingsState.crossfadeEnabled)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Crossfade Duration: ${settingsState.crossfadeDuration.toInt()}s',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.muted,
),
),
Slider(
value: settingsState.crossfadeDuration,
min: 1,
max: 12,
divisions: 11,
activeColor: AppColors.cyan,
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.setCrossfadeDuration(value);
},
),
],
),
),
const Divider(height: 1, color: AppColors.surfaceVariant),
SettingsToggleTile(
title: 'Gapless Playback',
subtitle: 'No gap between tracks',
leading: const Icon(Icons.all_inclusive),
value: settingsState.gaplessPlayback,
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.toggleGaplessPlayback(value);
},
),
const Divider(height: 1, color: AppColors.surfaceVariant),
SettingsToggleTile(
title: 'Normalize Volume',
subtitle: 'Set same volume level for all tracks',
leading: const Icon(Icons.volume_up),
value: settingsState.normalizeVolume,
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.toggleNormalizeVolume(value);
},
),
],
),
// Downloads Section
const SettingsSectionHeader(title: 'Downloads'),
SettingsCard(
children: [
SettingsToggleTile(
title: 'Download on Mobile Data',
subtitle: 'May use extra data',
leading: const Icon(Icons.download_done),
value: settingsState.downloadOnMobileData,
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.toggleDownloadOnMobileData(value);
},
),
const Divider(height: 1, color: AppColors.surfaceVariant),
SettingsToggleTile(
title: 'Show Explicit Content',
subtitle: 'Display explicit content in search',
leading: const Icon(Icons.explicit),
value: settingsState.showExplicitContent,
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.toggleShowExplicitContent(value);
},
),
],
),
const SizedBox(height: 8),
// Cache Management
const CacheManagementTile(),
const SizedBox(height: 24),
// About Section
const SettingsSectionHeader(title: 'About'),
SettingsCard(
children: [
SettingsTile(
title: 'App Version',
subtitle: _appVersion,
leading: const Icon(Icons.info_outline),
),
const Divider(height: 1, color: AppColors.surfaceVariant),
SettingsTile(
title: 'Licenses',
subtitle: 'Open source licenses',
leading: const Icon(Icons.description_outlined),
onTap: () {
showLicensePage(
context: context,
applicationName: 'Spotify Le 2',
applicationVersion: _appVersion,
applicationLegalese: '© 2025 Spotify Le 2',
);
},
),
],
),
const SizedBox(height: 24),
// Logout Button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _showLogoutDialog(context, ref),
icon: const Icon(Icons.logout, size: 18),
label: const Text('Log Out'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(
color: AppColors.rose.withOpacity(0.5),
width: 1.5,
),
foregroundColor: AppColors.rose,
),
),
),
),
const SizedBox(height: 32),
// Error message
if (settingsState.error != null)
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withOpacity(0.3),
),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
),
const SizedBox(width: 12),
Expanded(
child: Text(
settingsState.error!,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.error,
),
),
),
IconButton(
onPressed: () {
ref
.read(settingsProvider.notifier)
.copyWith(error: null);
},
icon: const Icon(Icons.close, size: 20),
color: AppColors.error,
),
],
),
),
const SizedBox(height: 24),
],
),
),
],
),
);
}
void _showLogoutDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: AppColors.cyan.withOpacity(0.2),
width: 1,
),
),
title: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.rose.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.logout,
color: AppColors.rose,
size: 20,
),
),
const SizedBox(width: 12),
Text(
'Log Out',
style: AppTextStyles.h3.copyWith(
color: AppColors.onBackground,
),
),
],
),
content: Text(
'Are you sure you want to log out?',
style: AppTextStyles.body.copyWith(
color: AppColors.onSurface,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel',
style: AppTextStyles.button.copyWith(
color: AppColors.muted,
),
),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
await ref.read(authProvider.notifier).logout();
if (context.mounted) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rose,
foregroundColor: Colors.white,
),
child: Text(
'Log Out',
style: AppTextStyles.button,
),
),
],
),
);
}
}
@@ -0,0 +1,144 @@
/// Example: How to integrate Settings Page into your app
import 'package:flutter/material.dart';
import 'package:spotify_le_2/presentation/pages/settings/settings_page.dart';
// Example 1: Navigate from home page
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage(),
),
);
},
),
],
),
body: const Center(child: Text('Home Page')),
);
}
}
// Example 2: Using Go Router
/*
In your router configuration:
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsPage(),
),
],
);
// Then navigate:
context.push('/settings');
*/
// Example 3: Bottom navigation bar
class MainNavigation extends StatefulWidget {
const MainNavigation({super.key});
@override
State<MainNavigation> createState() => _MainNavigationState();
}
class _MainNavigationState extends State<MainNavigation> {
int _currentIndex = 0;
final List<Widget> _pages = [
const HomePage(),
const SearchPage(),
const LibraryPage(),
const SettingsPage(), // Settings as a main tab
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: NavigationBar(
currentIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() {
_currentIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.search),
label: 'Search',
),
NavigationDestination(
icon: Icon(Icons.library_music),
label: 'Library',
),
NavigationDestination(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
);
}
}
// Example 4: From a profile button in player widget
class PlayerWidget extends StatelessWidget {
const PlayerWidget({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _navigateToSettings(context),
child: const CircleAvatar(
child: Icon(Icons.person),
),
);
}
void _navigateToSettings(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage(),
),
);
}
}
// Placeholder classes for example
class SearchPage extends StatelessWidget {
const SearchPage({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Search')));
}
class LibraryPage extends StatelessWidget {
const LibraryPage({super.key});
@override
Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Library')));
}