Files
AudiOhm/frontend/lib/presentation/pages/settings/settings_page.dart
T
root a89c7894cf Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube
Backend:
- FastAPI avec PostgreSQL et Redis
- Authentification JWT complète
- API REST pour musique, playlists, recherche
- Streaming audio via yt-dlp
- SQLAlchemy 2.0 async

Frontend:
- Flutter avec thème néon cyberpunk
- State management Riverpod
- Layout adaptatif desktop/mobile
- Lecteur audio avec mini-player

Infrastructure:
- Docker Compose (PostgreSQL + Redis)
- Scripts d'installation automatisés
- Scripts de build pour exécutables

Fichiers ajoutés:
- BUILD_CLIENT_*.bat/sh: Scripts de compilation
- BUILD_CLIENT_README.md: Documentation compilation
- CHECK_FLUTTER.sh: Vérificateur d'environnement
- requirements.txt mis à jour pour Python 3.13
- Modèles SQLAlchemy corrigés (metadata -> extra_metadata)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 20:08:36 +00:00

359 lines
13 KiB
Dart

/// 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,
),
),
],
),
);
}
}