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>
This commit is contained in:
root
2026-01-18 20:08:36 +00:00
commit a89c7894cf
132 changed files with 23178 additions and 0 deletions
@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/colors.dart';
import '../../providers/navigation_provider.dart';
import '../common/mini_player.dart';
/// Desktop Navigation Sidebar
class DesktopSidebar extends ConsumerWidget {
final double width;
const DesktopSidebar({
super.key,
required this.width,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentPage = ref.watch(currentPageProvider);
final navigationNotifier = ref.read(navigationProvider.notifier);
return Container(
width: width,
decoration: BoxDecoration(
color: AppColors.surface,
border: Border(
right: BorderSide(
color: AppColors.cyan.withOpacity(0.1),
width: 1,
),
),
),
child: Column(
children: [
// Logo
const Padding(
padding: EdgeInsets.all(24),
child: Text(
'Spotify Le 2',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
foreground: AppColors.primaryGradient,
),
),
),
const Divider(height: 1, color: AppColors.surfaceVariant),
// Navigation items
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
..._navItems.map(
(item) => _NavItemTile(
icon: item.icon,
label: item.label,
isSelected: currentPage == item.page,
onTap: () => navigationNotifier.navigateTo(item.page),
),
),
],
),
),
// Mini player in sidebar
const Padding(
padding: EdgeInsets.all(16),
child: MiniPlayer(compact: true),
),
],
),
);
}
}
class _NavItem {
final String page;
final String label;
final IconData icon;
const _NavItem({
required this.page,
required this.label,
required this.icon,
});
}
final List<_NavItem> _navItems = const [
_NavItem(page: 'home', label: 'Home', icon: Icons.home_outlined),
_NavItem(page: 'search', label: 'Search', icon: Icons.search_outlined),
_NavItem(page: 'library', label: 'Library', icon: Icons.library_music_outlined),
_NavItem(page: 'settings', label: 'Settings', icon: Icons.settings_outlined),
];
/// Navigation Item Tile
class _NavItemTile extends StatefulWidget {
final IconData icon;
final String label;
final bool isSelected;
final VoidCallback onTap;
const _NavItemTile({
required this.icon,
required this.label,
required this.isSelected,
required this.onTap,
});
@override
State<_NavItemTile> createState() => _NavItemTileState();
}
class _NavItemTileState extends State<_NavItemTile>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.02).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => _animationController.forward(),
onExit: (_) => _animationController.reverse(),
child: ScaleTransition(
scale: _scaleAnimation,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: widget.isSelected
? AppColors.cyan.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.isSelected
? AppColors.cyan.withOpacity(0.3)
: Colors.transparent,
width: 1,
),
),
child: ListTile(
leading: Icon(
widget.icon,
color: widget.isSelected ? AppColors.cyan : AppColors.onSurface,
),
title: Text(
widget.label,
style: TextStyle(
color: widget.isSelected
? AppColors.cyan
: AppColors.onSurface,
fontWeight:
widget.isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
onTap: widget.onTap,
),
),
),
);
}
}
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import '../../../core/theme/colors.dart';
/// Desktop Top Bar
class DesktopTopBar extends StatelessWidget {
const DesktopTopBar({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: AppColors.surface,
border: Border(
bottom: BorderSide(
color: AppColors.cyan.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [
// Search bar
Expanded(
child: _SearchBar(),
),
const SizedBox(width: 16),
// User profile
// TODO: Implement user profile menu
const _UserAvatar(),
],
),
);
}
}
/// Search Bar
class _SearchBar extends StatefulWidget {
@override
State<_SearchBar> createState() => _SearchBarState();
}
class _SearchBarState extends State<_SearchBar> {
final _focusNode = FocusNode();
bool _isFocused = false;
@override
void initState() {
super.initState();
_focusNode.addListener(() {
setState(() {
_isFocused = _focusNode.hasFocus;
});
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _isFocused ? AppColors.cyan : AppColors.cyan.withOpacity(0.2),
width: _isFocused ? 2 : 1,
),
boxShadow: _isFocused
? [
BoxShadow(
color: AppColors.cyan.withOpacity(0.3),
blurRadius: 20,
spreadRadius: 0,
),
]
: null,
),
child: TextField(
focusNode: _focusNode,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 14,
),
decoration: InputDecoration(
hintText: 'Search tracks, artists, albums...',
hintStyle: TextStyle(
color: AppColors.muted,
fontSize: 14,
),
prefixIcon: const Icon(
Icons.search,
color: AppColors.cyan,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
),
),
);
}
}
/// User Avatar
class _UserAvatar extends StatelessWidget {
const _UserAvatar();
@override
Widget build(BuildContext context) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: AppColors.primaryGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: AppColors.cyanGlow,
),
child: const Icon(
Icons.person,
color: AppColors.onBackground,
),
);
}
}