feat: Modernisation UI/UX et configuration Flutter multi-plateforme
Phase 1 - Corrections Critiques: - Fixed memory leaks dans music_provider.dart (stream subscriptions) - Fixed race conditions dans search_provider.dart (stale results) - Fixed token refresh errors dans api_service.dart - Improved error handling avec messages utilisateur - Changed API URL to HTTPS by default Phase 2 - Améliorations UX Desktop: - Ajouté cursor pointers sur tous les éléments cliquables - Implémenté hover states avec effets néon glow (200ms transitions) - Créé skeleton loading states avec shimmer animation - Ajouté widgets: ClickableWrapper, ErrorDisplay, SkeletonLoading - Enhanced visual feedback pour desktop users Phase 3 - Configuration Flutter: - Configuré Android (Gradle 8.1.0, Kotlin 1.9.0, minSdk 21, targetSdk 34) - Créé launcher icons cyberpunk néon (5 densités) - Configuré Windows desktop (structure complète) - Activé Linux desktop support - Ajouté package équatable pour entités de domaine - Corrigé imports (colors.dart, auth_provider.dart) - Fixed Dio API compatibility (RequestOptions) Documentation: - STYLE_GUIDE.md: Guide complet (100+ pages) - DESIGN_IMPLEMENTATION_GUIDE.md: Implémentation Flutter - BUILD_STATUS.md: Status builds + troubleshooting - QUICKSTART_BUILDS.md: Guide rapide - BUILD_INDEX.md: Index documentation - PHASE_1_CORRECTIONS.md: Corrections Phase 1 - PHASE_2_UX_IMPROVEMENTS.md: Améliorations Phase 2 - PR_REVIEW_SUMMARY.md: Revue code complète - CODE_ANALYSIS_AND_PRIORITIES.md: Analyse code Scripts & Builds: - BUILD_ALL.sh: Script automatisé builds multi-plateforme - builds/: Structure avec README par plateforme - design-system/: Système de design complet Backend: - Ajouté streaming HTTP Range pour audio progressif - Enhanced YouTube service avec métadonnées complètes - Improved error handling et validation Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../../../core/theme/colors.dart';
|
||||
|
||||
/// Skeleton loading card for albums/playlists/tracks
|
||||
class ContentCardSkeleton extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
|
||||
const ContentCardSkeleton({
|
||||
super.key,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: AppColors.surfaceVariant,
|
||||
highlightColor: AppColors.surfaceElevated,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image placeholder
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Title placeholder
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Subtitle placeholder
|
||||
Container(
|
||||
width: 100,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton for list items (e.g., track list items)
|
||||
class ListItemSkeleton extends StatelessWidget {
|
||||
final bool showLeading;
|
||||
final bool showTrailing;
|
||||
|
||||
const ListItemSkeleton({
|
||||
super.key,
|
||||
this.showLeading = true,
|
||||
this.showTrailing = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: AppColors.surfaceVariant,
|
||||
highlightColor: AppColors.surfaceElevated,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Leading icon/image
|
||||
if (showLeading) ...[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
// Title and subtitle
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: 150,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Trailing icon
|
||||
if (showTrailing) ...[
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton for search results grid
|
||||
class SearchGridSkeleton extends StatelessWidget {
|
||||
final int itemCount;
|
||||
|
||||
const SearchGridSkeleton({
|
||||
super.key,
|
||||
this.itemCount = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1.2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) => const ContentCardSkeleton(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton for horizontal scrolling lists
|
||||
class HorizontalListSkeleton extends StatelessWidget {
|
||||
final int itemCount;
|
||||
final double itemHeight;
|
||||
final double itemWidth;
|
||||
|
||||
const HorizontalListSkeleton({
|
||||
super.key,
|
||||
this.itemCount = 6,
|
||||
this.itemHeight = 160,
|
||||
this.itemWidth = 120,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: itemHeight,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: itemCount,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) => ContentCardSkeleton(
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full page skeleton with multiple sections
|
||||
class PageSkeleton extends StatelessWidget {
|
||||
final bool showHero;
|
||||
final int sectionCount;
|
||||
|
||||
const PageSkeleton({
|
||||
super.key,
|
||||
this.showHero = true,
|
||||
this.sectionCount = 3,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: AppColors.surfaceVariant,
|
||||
highlightColor: AppColors.surfaceElevated,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Hero section
|
||||
if (showHero) ...[
|
||||
Container(
|
||||
height: 180,
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Sections
|
||||
...List.generate(
|
||||
sectionCount,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section title
|
||||
Container(
|
||||
width: 150,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Horizontal list
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 6,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) => Container(
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Circular loading indicator with theme colors
|
||||
class ThemedCircularProgress extends StatelessWidget {
|
||||
final double? size;
|
||||
final double strokeWidth;
|
||||
|
||||
const ThemedCircularProgress({
|
||||
super.key,
|
||||
this.size,
|
||||
this.strokeWidth = 3.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: strokeWidth,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(AppColors.cyan),
|
||||
backgroundColor: AppColors.surfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user