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:
@@ -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')));
|
||||
}
|
||||
Reference in New Issue
Block a user