🎉 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,247 @@
/// Audio Quality Selector Widget
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/colors.dart';
import '../../../core/theme/text_styles.dart';
import '../../providers/settings_provider.dart';
/// Audio quality selector widget
class AudioQualitySelector extends ConsumerWidget {
const AudioQualitySelector({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settingsState = ref.watch(settingsProvider);
final currentQuality = settingsState.audioQuality;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.cyan.withOpacity(0.15),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.cyan.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.high_quality_outlined,
color: AppColors.cyan,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Audio Quality',
style: AppTextStyles.body.copyWith(
color: AppColors.onBackground,
fontWeight: FontWeight.w600,
),
),
Text(
'Higher quality uses more data',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.muted,
),
),
],
),
),
],
),
),
const Divider(height: 1, color: AppColors.surfaceVariant),
// Audio quality options
_buildQualityOption(
context,
ref,
AudioQuality.low,
'Low',
'96 kbps',
'Best for data saving',
currentQuality,
),
_buildQualityOption(
context,
ref,
AudioQuality.medium,
'Medium',
'160 kbps',
'Good balance',
currentQuality,
),
_buildQualityOption(
context,
ref,
AudioQuality.high,
'High',
'320 kbps',
'Best quality',
currentQuality,
),
_buildQualityOption(
context,
ref,
AudioQuality.lossless,
'Lossless',
'FLAC',
'Requires Premium',
currentQuality,
requiresPremium: true,
),
],
),
);
}
Widget _buildQualityOption(
BuildContext context,
WidgetRef ref,
AudioQuality quality,
String title,
String bitrate,
String description,
AudioQuality currentQuality, {
bool requiresPremium = false,
}) {
final isSelected = currentQuality == quality;
final settingsState = ref.watch(settingsProvider);
final isPremium = settingsState.user?.isPremium ?? false;
final isLocked = requiresPremium && !isPremium;
return InkWell(
onTap: isLocked
? null
: () => ref.read(settingsProvider.notifier).setAudioQuality(quality),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isSelected
? AppColors.cyan.withOpacity(0.1)
: Colors.transparent,
border: Border(
left: BorderSide(
color: isSelected ? AppColors.cyan : Colors.transparent,
width: 3,
),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: AppTextStyles.body.copyWith(
color: isLocked
? AppColors.muted
: AppColors.onBackground,
fontWeight:
isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
if (requiresPremium) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.violet.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'PRO',
style: AppTextStyles.caption.copyWith(
color: AppColors.violet,
fontWeight: FontWeight.w700,
fontSize: 10,
),
),
),
],
],
),
const SizedBox(height: 4),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(4),
),
child: Text(
bitrate,
style: AppTextStyles.caption.copyWith(
color: AppColors.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
Text(
description,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.muted,
),
),
],
),
],
),
),
if (isLocked) ...[
Icon(
Icons.lock_outline,
color: AppColors.muted,
size: 20,
),
] else if (isSelected) ...[
Icon(
Icons.check_circle,
color: AppColors.cyan,
size: 24,
),
] else ...[
Icon(
Icons.radio_button_unchecked,
color: AppColors.muted,
size: 24,
),
],
],
),
),
);
}
}
@@ -0,0 +1,259 @@
/// Cache Management Tile Widget
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/colors.dart';
import '../../../core/theme/text_styles.dart';
import '../../providers/settings_provider.dart';
/// Cache management tile widget
class CacheManagementTile extends ConsumerWidget {
const CacheManagementTile({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settingsState = ref.watch(settingsProvider);
final cacheSize = settingsState.cacheSize;
final isLoading = settingsState.isLoading;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.cyan.withOpacity(0.15),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.cyan.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.storage_outlined,
color: AppColors.cyan,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Storage',
style: AppTextStyles.body.copyWith(
color: AppColors.onBackground,
fontWeight: FontWeight.w600,
),
),
Text(
'Cache and offline data',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.muted,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Cache size display
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Cache Size',
style: AppTextStyles.body.copyWith(
color: AppColors.onSurface,
),
),
const SizedBox(height: 4),
Text(
cacheSize,
style: AppTextStyles.h3.copyWith(
color: AppColors.cyan,
fontWeight: FontWeight.w700,
),
),
],
),
Icon(
Icons.folder_outlined,
color: AppColors.muted,
size: 32,
),
],
),
),
const SizedBox(height: 16),
// Clear cache button
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: isLoading
? null
: () => _showClearCacheDialog(context, ref),
icon: isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.cyan,
),
),
)
: const Icon(Icons.delete_outline, size: 18),
label: Text(
isLoading ? 'Clearing...' : 'Clear Cache',
style: AppTextStyles.button,
),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(
color: AppColors.rose.withOpacity(0.5),
width: 1.5,
),
foregroundColor: AppColors.rose,
),
),
),
],
),
),
);
}
void _showClearCacheDialog(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.warning_outlined,
color: AppColors.rose,
size: 20,
),
),
const SizedBox(width: 12),
Text(
'Clear Cache',
style: AppTextStyles.h3.copyWith(
color: AppColors.onBackground,
),
),
],
),
content: Text(
'This will delete all cached data. You may need to re-download content for offline use.\n\nContinue?',
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);
try {
await ref.read(settingsProvider.notifier).clearCache();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Cache cleared successfully',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.vert,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Failed to clear cache: ${e.toString()}',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rose,
foregroundColor: Colors.white,
),
child: Text(
'Clear',
style: AppTextStyles.button,
),
),
],
),
);
}
}
@@ -0,0 +1,386 @@
/// Edit Profile Dialog Widget
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import '../../../core/theme/colors.dart';
import '../../../core/theme/text_styles.dart';
import '../../../domain/entities/user.dart';
import '../../providers/settings_provider.dart';
/// Edit profile dialog
class EditProfileDialog extends ConsumerStatefulWidget {
const EditProfileDialog({
super.key,
required this.user,
});
final User user;
@override
ConsumerState<EditProfileDialog> createState() => _EditProfileDialogState();
}
class _EditProfileDialogState extends ConsumerState<EditProfileDialog> {
late final TextEditingController _displayNameController;
final ImagePicker _imagePicker = ImagePicker();
String? _avatarUrl;
@override
void initState() {
super.initState();
_displayNameController = TextEditingController(
text: widget.user.displayName ?? widget.user.username,
);
_avatarUrl = widget.user.avatarUrl;
}
@override
void dispose() {
_displayNameController.dispose();
super.dispose();
}
Future<void> _pickImage() async {
try {
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.gallery,
maxWidth: 512,
maxHeight: 512,
imageQuality: 85,
);
if (image != null && mounted) {
// For now, just show the selected image
// In production, you would upload this to your server
setState(() {
_avatarUrl = image.path;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Image selected. Note: Avatar upload requires server implementation.',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.info,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Failed to pick image: ${e.toString()}',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
}
Future<void> _saveProfile() async {
final displayName = _displayNameController.text.trim();
if (displayName.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Display name cannot be empty',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
return;
}
Navigator.pop(context);
try {
await ref.read(settingsProvider.notifier).updateProfile(
displayName: displayName,
avatarUrl: _avatarUrl,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Profile updated successfully',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.vert,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Failed to update profile: ${e.toString()}',
style: AppTextStyles.body.copyWith(
color: Colors.white,
),
),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: AppColors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: AppColors.cyan.withOpacity(0.2),
width: 1,
),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Row(
children: [
Text(
'Edit Profile',
style: AppTextStyles.h3.copyWith(
color: AppColors.onBackground,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
color: AppColors.muted,
),
],
),
const SizedBox(height: 24),
// Avatar
Center(
child: GestureDetector(
onTap: _pickImage,
child: Stack(
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: AppColors.primaryGradient,
boxShadow: AppColors.cyanGlow,
),
child: ClipOval(
child: _avatarUrl != null
// Check if it's a network URL or local file path
? (_avatarUrl!.startsWith('http')
? Image.network(
_avatarUrl!,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return _buildDefaultAvatar();
},
)
: Image.file(
// Use File for local path
// ignore: unnecessary_null_comparison
_avatarUrl != null
? _avatarUrl as Object
: Object(),
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
return _buildDefaultAvatar();
},
))
: _buildDefaultAvatar(),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.cyan,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.surface,
width: 3,
),
),
child: const Icon(
Icons.camera_alt_outlined,
color: AppColors.primary,
size: 18,
),
),
),
],
),
),
),
const SizedBox(height: 16),
Center(
child: Text(
'Tap to change photo',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.cyan,
),
),
),
const SizedBox(height: 24),
// Display name field
Text(
'Display Name',
style: AppTextStyles.label.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
TextField(
controller: _displayNameController,
style: AppTextStyles.body.copyWith(
color: AppColors.onBackground,
),
decoration: InputDecoration(
hintText: 'Enter display name',
hintStyle: AppTextStyles.body.copyWith(
color: AppColors.muted,
),
filled: true,
fillColor: AppColors.surfaceVariant.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.cyan.withOpacity(0.2),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.cyan.withOpacity(0.2),
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppColors.cyan,
width: 2,
),
),
),
),
const SizedBox(height: 24),
// Buttons
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(
color: AppColors.muted.withOpacity(0.5),
width: 1.5,
),
foregroundColor: AppColors.muted,
),
child: Text(
'Cancel',
style: AppTextStyles.button,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _saveProfile,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: AppColors.cyan,
foregroundColor: AppColors.primary,
),
child: Text(
'Save',
style: AppTextStyles.button,
),
),
),
],
),
],
),
),
);
}
Widget _buildDefaultAvatar() {
final firstLetter = (widget.user.displayName ?? widget.user.username)
.substring(0, 1)
.toUpperCase();
return Container(
color: AppColors.surfaceVariant,
child: Center(
child: Text(
firstLetter,
style: AppTextStyles.h2.copyWith(
color: AppColors.cyan,
fontWeight: FontWeight.w700,
),
),
),
);
}
}
@@ -0,0 +1,192 @@
/// Profile Section Widget
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/colors.dart';
import '../../../core/theme/text_styles.dart';
import '../../../domain/entities/user.dart';
import '../../providers/settings_provider.dart';
import 'edit_profile_dialog.dart';
/// Profile section widget
class ProfileSection extends ConsumerWidget {
const ProfileSection({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settingsState = ref.watch(settingsProvider);
final user = settingsState.user;
if (user == null) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.surface,
AppColors.surfaceVariant,
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.cyan.withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: AppColors.cyan.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Avatar and name
Row(
children: [
// Avatar
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: AppColors.primaryGradient,
boxShadow: AppColors.cyanGlow,
),
child: ClipOval(
child: user.avatarUrl != null
? Image.network(
user.avatarUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultAvatar(user);
},
)
: _buildDefaultAvatar(user),
),
),
const SizedBox(width: 20),
// Name and email
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
user.displayName ?? user.username,
style: AppTextStyles.h3.copyWith(
color: AppColors.onBackground,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
if (user.isPremium) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
gradient: AppColors.accentGradient,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColors.violet.withOpacity(0.3),
blurRadius: 8,
),
],
),
child: Text(
'PREMIUM',
style: AppTextStyles.caption.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
letterSpacing: 1,
),
),
),
],
],
),
const SizedBox(height: 6),
Text(
user.email,
style: AppTextStyles.body.copyWith(
color: AppColors.muted,
),
),
const SizedBox(height: 6),
Text(
'@${user.username}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
],
),
),
],
),
const SizedBox(height: 20),
// Edit Profile Button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _showEditProfileDialog(context, ref, user),
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Edit Profile'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: AppColors.cyan.withOpacity(0.15),
foregroundColor: AppColors.cyan,
elevation: 0,
side: BorderSide(
color: AppColors.cyan.withOpacity(0.3),
width: 1,
),
),
),
),
],
),
),
);
}
Widget _buildDefaultAvatar(User user) {
return Container(
color: AppColors.surfaceVariant,
child: Center(
child: Text(
(user.displayName ?? user.username)
.substring(0, 1)
.toUpperCase(),
style: AppTextStyles.h2.copyWith(
color: AppColors.cyan,
fontWeight: FontWeight.w700,
),
),
),
);
}
void _showEditProfileDialog(BuildContext context, WidgetRef ref, User user) {
showDialog(
context: context,
builder: (context) => EditProfileDialog(user: user),
);
}
}
@@ -0,0 +1,195 @@
/// Settings Tile - Reusable settings item widget
library;
import 'package:flutter/material.dart';
import '../../../core/theme/colors.dart';
import '../../../core/theme/text_styles.dart';
/// Reusable settings tile widget
class SettingsTile extends StatelessWidget {
const SettingsTile({
super.key,
required this.title,
this.subtitle,
this.leading,
this.trailing,
this.onTap,
this.isEnabled = true,
});
final String title;
final String? subtitle;
final Widget? leading;
final Widget? trailing;
final VoidCallback? onTap;
final bool isEnabled;
@override
Widget build(BuildContext context) {
return Opacity(
opacity: isEnabled ? 1.0 : 0.5,
child: InkWell(
onTap: isEnabled ? onTap : null,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
if (leading != null) ...[
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.cyan.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: IconTheme(
data: IconThemeData(
color: AppColors.cyan,
size: 20,
),
child: leading!,
),
),
const SizedBox(width: 16),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.body.copyWith(
color: AppColors.onBackground,
fontWeight: FontWeight.w500,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.muted,
),
),
],
],
),
),
if (trailing != null) trailing!,
],
),
),
),
);
}
}
/// Settings tile with toggle switch
class SettingsToggleTile extends StatelessWidget {
const SettingsToggleTile({
super.key,
required this.title,
this.subtitle,
this.leading,
required this.value,
required this.onChanged,
this.isEnabled = true,
});
final String title;
final String? subtitle;
final Widget? leading;
final bool value;
final ValueChanged<bool>? onChanged;
final bool isEnabled;
@override
Widget build(BuildContext context) {
return SettingsTile(
title: title,
subtitle: subtitle,
leading: leading,
isEnabled: isEnabled,
trailing: Switch(
value: value,
onChanged: isEnabled ? onChanged : null,
activeColor: AppColors.cyan,
activeTrackColor: AppColors.cyan.withOpacity(0.3),
inactiveTrackColor: AppColors.surfaceVariant,
inactiveThumbColor: AppColors.muted,
),
);
}
}
/// Settings section header
class SettingsSectionHeader extends StatelessWidget {
const SettingsSectionHeader({
super.key,
required this.title,
this.padding = const EdgeInsets.fromLTRB(16, 24, 16, 8),
});
final String title;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding,
child: Text(
title.toUpperCase(),
style: AppTextStyles.label.copyWith(
color: AppColors.cyan,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
);
}
}
/// Settings card container
class SettingsCard extends StatelessWidget {
const SettingsCard({
super.key,
required this.children,
this.padding = const EdgeInsets.all(8),
});
final List<Widget> children;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.cyan.withOpacity(0.15),
width: 1,
),
boxShadow: [
BoxShadow(
color: AppColors.cyan.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: children,
),
),
);
}
}
@@ -0,0 +1,8 @@
/// Settings Widgets Export
library;
export 'profile_section.dart';
export 'audio_quality_selector.dart';
export 'cache_management_tile.dart';
export 'settings_tile.dart';
export 'edit_profile_dialog.dart';