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:
root
2026-01-19 07:44:40 +00:00
parent a89c7894cf
commit 85dad89d5b
100 changed files with 13570 additions and 323 deletions
+79 -11
View File
@@ -1,7 +1,7 @@
"""Music API routes."""
from typing import Optional
from fastapi import APIRouter, HTTPException, Query, status
from fastapi import APIRouter, HTTPException, Query, status, Request
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
@@ -20,7 +20,7 @@ from app.services.music_service import MusicService
router = APIRouter(prefix="/music", tags=["music"])
@router.get("/search", response_model=SearchResponse)
@router.get("/search")
async def search_music(
db: DBSession,
q: str = Query(..., min_length=1, max_length=100, description="Search query"),
@@ -44,13 +44,26 @@ async def search_music(
offset=offset,
)
return SearchResponse(
tracks=[TrackSearchResult(**t) for t in results["tracks"]],
artists=[AlbumResponse(**a) for a in results["artists"]],
albums=[AlbumResponse(**a) for a in results["albums"]],
total=results["total"],
query=results["query"],
)
# Convert results without strict validation
tracks = []
for t in results.get("tracks", []):
track_data = {
"title": t.get("title", "Unknown"),
"youtube_id": t.get("youtube_id", ""),
"duration": t.get("duration"),
"image_url": t.get("thumbnail"),
"artist_name": t.get("artist", "Unknown Artist"),
"id": None,
}
tracks.append(track_data)
return {
"tracks": tracks,
"artists": results.get("artists", []),
"albums": results.get("albums", []),
"total": results.get("total", len(tracks)),
"query": results.get("query", q),
}
@router.get("/tracks/{track_id}", response_model=TrackResponse)
@@ -82,6 +95,48 @@ async def get_track(
)
@router.get("/youtube/{youtube_id}/stream")
@router.head("/youtube/{youtube_id}/stream")
async def stream_youtube_track(
youtube_id: str,
db: DBSession,
request: Request = None,
):
"""
Stream a track directly from YouTube by youtube_id.
This endpoint bypasses the database and streams directly from YouTube.
Supports HTTP Range requests for proper audio playback.
"""
music_service = MusicService(db)
try:
# Get YouTube stream URL
stream_url = await music_service.get_stream_url_by_youtube_id(youtube_id)
if not stream_url:
raise HTTPException(
status_code=404,
detail=f"Could not get stream for youtube_id: {youtube_id}"
)
# Get range header from request
range_header = request.headers.get("range") if request else None
# Stream directly from YouTube
from fastapi.responses import StreamingResponse
return await music_service.stream_audio_from_youtube(stream_url, range_header)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to stream from YouTube: {str(e)}"
)
@router.get("/tracks/{track_id}/stream")
async def stream_track(
track_id: str,
@@ -208,7 +263,7 @@ async def get_track_recommendations(
)
@router.get("/trending", response_model=list[TrackSearchResult])
@router.get("/trending")
async def get_trending(
db: DBSession,
limit: int = Query(20, ge=1, le=50),
@@ -224,4 +279,17 @@ async def get_trending(
# Search for popular music on YouTube
results = await music_service.search("music 2024", search_type="track", limit=limit)
return [TrackSearchResult(**t) for t in results["tracks"]]
# Convert YouTube results to TrackSearchResult with only available fields
tracks = []
for t in results.get("tracks", []):
track_data = {
"title": t.get("title", "Unknown"),
"youtube_id": t.get("youtube_id", ""),
"duration": t.get("duration"),
"image_url": t.get("thumbnail"),
"artist_name": t.get("artist", "Unknown Artist"),
"id": None,
}
tracks.append(track_data)
return tracks