85dad89d5b
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>
303 lines
8.3 KiB
Dart
303 lines
8.3 KiB
Dart
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,
|
|
),
|
|
);
|
|
}
|
|
}
|