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,232 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/navigation_provider.dart';
import '../pages/desktop/home_page.dart';
import '../pages/mobile/mobile_home_page.dart';
import '../pages/search/search_page.dart';
import '../pages/library/library_page.dart';
import '../widgets/common/mini_player.dart';
import '../widgets/desktop/desktop_sidebar.dart';
import '../widgets/desktop/desktop_top_bar.dart';
/// Adaptive Layout - Desktop or Mobile based on screen width
class AdaptiveLayout extends ConsumerWidget {
const AdaptiveLayout({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return LayoutBuilder(
builder: (context, constraints) {
// Breakpoint at 800px
if (constraints.maxWidth >= 800) {
return const DesktopLayout();
} else {
return const MobileLayout();
}
},
);
}
}
/// Desktop Layout - 3 columns
class DesktopLayout extends ConsumerWidget {
const DesktopLayout({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentPage = ref.watch(currentPageProvider);
return Scaffold(
body: Row(
children: [
// Sidebar (240px fixed)
const DesktopSidebar(
width: 240,
),
// Main content
Expanded(
child: Column(
children: [
// Top bar
const DesktopTopBar(),
// Content area
Expanded(
child: _buildCurrentPage(currentPage),
),
],
),
),
// Right panel (320px) - Queue/Now Playing
// TODO: Implement RightPanel
// const SizedBox(width: 320, child: RightPanel()),
],
),
// Persistent mini player at bottom
bottomNavigationBar: const MiniPlayer(),
);
}
Widget _buildCurrentPage(String page) {
switch (page) {
case 'home':
return const HomePage();
case 'search':
return const SearchPage();
case 'library':
return const LibraryPage();
case 'settings':
// TODO: Implement SettingsPage
return const _PlaceholderPage(title: 'Settings');
default:
return const HomePage();
}
}
}
/// Mobile Layout - Bottom nav
class MobileLayout extends ConsumerWidget {
const MobileLayout({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentPage = ref.watch(currentPageProvider);
final navigationNotifier = ref.read(navigationProvider.notifier);
return Scaffold(
body: Column(
children: [
// Top bar
// TODO: Implement MobileTopBar
const SizedBox(height: 60),
// Content area
Expanded(
child: _buildCurrentPage(currentPage),
),
// Mini player (sticky)
const MiniPlayer(),
// Bottom navigation
NavigationBar(
height: 56,
selectedIndex: _navItems.indexWhere(
(item) => item.page == currentPage,
),
onDestinationSelected: (index) {
navigationNotifier.navigateTo(_navItems[index].page);
},
destinations: _navItems
.map(
(item) => NavigationDestination(
icon: Icon(item.icon),
label: Text(item.label),
selectedIcon: Icon(item.selectedIcon ?? item.icon),
),
)
.toList(),
),
],
),
);
}
Widget _buildCurrentPage(String page) {
switch (page) {
case 'home':
return const MobileHomePage();
case 'search':
return const SearchPage();
case 'library':
return const LibraryPage();
case 'settings':
return const _PlaceholderPage(title: 'Settings');
default:
return const MobileHomePage();
}
}
}
/// Navigation items
class _NavItem {
final String page;
final String label;
final IconData icon;
final IconData? selectedIcon;
const _NavItem({
required this.page,
required this.label,
required this.icon,
this.selectedIcon,
});
}
final List<_NavItem> _navItems = const [
_NavItem(
page: 'home',
label: 'Home',
icon: Icons.home_outlined,
selectedIcon: Icons.home,
),
_NavItem(
page: 'search',
label: 'Search',
icon: Icons.search_outlined,
selectedIcon: Icons.search,
),
_NavItem(
page: 'library',
label: 'Library',
icon: Icons.library_music_outlined,
selectedIcon: Icons.library_music,
),
_NavItem(
page: 'settings',
label: 'Settings',
icon: Icons.settings_outlined,
selectedIcon: Icons.settings,
),
];
/// Placeholder page for unimplemented pages
class _PlaceholderPage extends StatelessWidget {
final String title;
const _PlaceholderPage({required this.title});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.construction,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
title,
style: Theme.of(context).textTheme.displaySmall,
),
const SizedBox(height: 8),
Text(
'Coming soon...',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6),
),
),
],
),
);
}
}