diff --git a/lib/api/api_service_chats.dart b/lib/api/api_service_chats.dart index 175d34a..9184565 100644 --- a/lib/api/api_service_chats.dart +++ b/lib/api/api_service_chats.dart @@ -61,6 +61,7 @@ extension ApiServiceChats on ApiService { "[_sendAuthRequestAfterHandshake] ✅ Профиль и ID пользователя найдены. ID: ${contactProfile['id']}. ЗАПУСКАЕМ АНАЛИТИКУ.", ); _userId = contactProfile['id']; + await prefs.setString('userId', _userId.toString()); _sessionId = DateTime.now().millisecondsSinceEpoch; _lastActionTime = _sessionId; diff --git a/lib/main.dart b/lib/main.dart index 6d2f4f0..0185e8a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -102,6 +102,7 @@ class MyApp extends StatelessWidget { dynamicSchemeVariant: DynamicSchemeVariant.tonalSpot, ), useMaterial3: true, + pageTransitionsTheme: PageTransitionsTheme(builders: {TargetPlatform.android: CupertinoPageTransitionsBuilder()}), appBarTheme: AppBarTheme( titleTextStyle: TextStyle( fontSize: 16, @@ -152,12 +153,14 @@ class MyApp extends StatelessWidget { navigatorKey: navigatorKey, builder: (context, child) { final showHud = themeProvider.debugShowPerformanceOverlay; - if (!showHud) return child ?? const SizedBox.shrink(); - return Stack( - children: [ - if (child != null) child, - const Positioned(top: 8, right: 56, child: _MiniFpsHud()), - ], + return SizedBox.expand( + child: Stack( + children: [ + if (child != null) child, + if (showHud) + const Positioned(top: 8, right: 56, child: _MiniFpsHud()), + ], + ), ); }, theme: baseLightTheme, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 8d0236b..aa43b8d 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -24,6 +24,7 @@ import 'package:gwid/screens/edit_contact_screen.dart'; import 'package:gwid/widgets/contact_name_widget.dart'; import 'package:gwid/widgets/contact_avatar_widget.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:video_player/video_player.dart'; @@ -343,6 +344,7 @@ class _ChatScreenState extends State { Future _initializeChat() async { await _loadCachedContacts(); + final prefs = await SharedPreferences.getInstance(); if (!widget.isGroupChat && !widget.isChannel) { _contactDetailsCache[widget.contact.id] = widget.contact; @@ -357,7 +359,8 @@ class _ChatScreenState extends State { if (contactProfile != null && contactProfile['id'] != null && contactProfile['id'] != 0) { - _actualMyId = contactProfile['id']; + String? idStr = await prefs.getString("userId"); + _actualMyId = idStr!.isNotEmpty ? int.parse(idStr) : contactProfile['id']; print( '✅ [_initializeChat] ID пользователя получен из ApiService: $_actualMyId', ); @@ -374,7 +377,8 @@ class _ChatScreenState extends State { ); } } else if (_actualMyId == null) { - _actualMyId = widget.myId; + final prefs = await SharedPreferences.getInstance(); + _actualMyId = int.parse(await prefs.getString('userId')!);; print( '⚠️ [_initializeChat] ID не найден, используется из виджета: $_actualMyId', ); @@ -1129,13 +1133,13 @@ class _ChatScreenState extends State { final int tempCid = DateTime.now().millisecondsSinceEpoch; final tempMessageJson = { - 'id': 'local_$tempCid', // Временный "локальный" ID + 'id': 'local_$tempCid', 'text': text, 'time': tempCid, 'sender': _actualMyId!, - 'cid': tempCid, // Уникальный ID клиента + 'cid': tempCid, 'type': 'USER', - 'attaches': [], // Оптимистично без вложений (для текста) + 'attaches': [], 'link': _replyingToMessage != null ? { 'type': 'REPLY', @@ -1191,7 +1195,6 @@ class _ChatScreenState extends State { } void _testSlideAnimation() { - print('=== ТЕСТ SLIDE+ АНИМАЦИИ ==='); final myMessage = Message( id: 'test_my_${DateTime.now().millisecondsSinceEpoch}', @@ -1948,12 +1951,12 @@ class _ChatScreenState extends State { for (final contact in chatContacts) { _contactDetailsCache[contact.id] = contact; - if (contact.id == widget.myId && _actualMyId == null) { - _actualMyId = contact.id; + /*if (contact.id == widget.myId && _actualMyId == null) { + //_actualMyId = contact.id; print( '✅ [_loadCachedContacts] Собственный ID восстановлен из кэша: $_actualMyId (${contact.name})', ); - } + }*/ } print( '✅ Загружено ${_contactDetailsCache.length} контактов из кэша чата ${widget.chatId}', @@ -1968,7 +1971,9 @@ class _ChatScreenState extends State { _contactDetailsCache[contact.id] = contact; if (contact.id == widget.myId && _actualMyId == null) { - _actualMyId = contact.id; + final prefs = await SharedPreferences.getInstance(); + + _actualMyId = int.parse(await prefs.getString('userId')!); print( '✅ [_loadCachedContacts] Собственный ID восстановлен из глобального кэша: $_actualMyId (${contact.name})', ); @@ -4364,6 +4369,7 @@ extension BrightnessExtension on Brightness { bool get isDark => this == Brightness.dark; } +//note: unused class GroupProfileDraggableDialog extends StatelessWidget { final Contact contact; @@ -4427,8 +4433,6 @@ class GroupProfileDraggableDialog extends StatelessWidget { IconButton( icon: Icon(Icons.settings, color: colors.primary), onPressed: () async { - final myId = 0; // This should be passed or retrieved - Navigator.of(context).pop(); Navigator.of(context).push( MaterialPageRoute( @@ -4436,7 +4440,7 @@ class GroupProfileDraggableDialog extends StatelessWidget { chatId: -contact .id, // Convert back to positive chatId initialContact: contact, - myId: myId, + myId: 0, ), ), ); diff --git a/lib/screens/chats_screen.dart b/lib/screens/chats_screen.dart index bfe854a..b2e92f0 100644 --- a/lib/screens/chats_screen.dart +++ b/lib/screens/chats_screen.dart @@ -270,9 +270,9 @@ class _ChatsScreenState extends State void _navigateToLogin() { print('Перенаправляем на экран входа из-за недействительного токена'); - Navigator.of(context).pushReplacement( + /*Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const PhoneEntryScreen()), - ); + );*/ } void _showTokenExpiredDialog(String message) { diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index be3d3c4..fe40a7c 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1242,7 +1242,6 @@ class _DesktopLayoutState extends State<_DesktopLayout> { isChannel: _isChannel, participantCount: _participantCount, isDesktopMode: true, - onChatUpdated: () {}, ), ), ], diff --git a/lib/screens/music_library_screen.dart b/lib/screens/music_library_screen.dart index b084163..0b9d5c6 100644 --- a/lib/screens/music_library_screen.dart +++ b/lib/screens/music_library_screen.dart @@ -132,15 +132,21 @@ class _MusicLibraryScreenState extends State { return ValueListenableBuilder( valueListenable: BottomSheetMusicPlayer.isExpandedNotifier, builder: (context, isPlayerExpanded, child) { - return PopScope( - canPop: !isPlayerExpanded, - onPopInvoked: (didPop) { - if (!didPop && isPlayerExpanded) { - BottomSheetMusicPlayer.isExpandedNotifier.value = false; - } - }, - child: Scaffold( - appBar: AppBar(title: const Text('Музыка')), + return ValueListenableBuilder( + valueListenable: BottomSheetMusicPlayer.isFullscreenNotifier, + builder: (context, isFullscreen, _) { + return PopScope( + canPop: !isPlayerExpanded, + onPopInvoked: (didPop) { + if (!didPop && isPlayerExpanded) { + BottomSheetMusicPlayer.isExpandedNotifier.value = false; + BottomSheetMusicPlayer.isFullscreenNotifier.value = false; + } + }, + child: Scaffold( + appBar: isFullscreen + ? null + : AppBar(title: const Text('Музыка')), body: Stack( children: [ _isLoading @@ -321,7 +327,9 @@ class _MusicLibraryScreenState extends State { ), ], ), - ), + ), + ); + }, ); }, ); diff --git a/lib/screens/otp_screen.dart b/lib/screens/otp_screen.dart index 2871b28..b830e52 100644 --- a/lib/screens/otp_screen.dart +++ b/lib/screens/otp_screen.dart @@ -56,7 +56,7 @@ class _OTPScreenState extends State { if (payload != null && payload['tokenAttrs']?['LOGIN']?['token'] != null) { final String finalToken = payload['tokenAttrs']['LOGIN']['token']; - final userId = payload['tokenAttrs']?['LOGIN']?['userId']; + final userId = payload['payload']?['profile']?['contact']?['id']; print('Успешная авторизация! Токен: $finalToken, UserID: $userId'); ApiService.instance diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 6911a6f..05f0566 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -200,18 +200,16 @@ class _SettingsScreenState extends State { } return Scaffold( - appBar: AppBar( - title: const Text("Настройки"), - leading: widget.showBackToChats - ? IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: widget.onBackToChats, - ) - : null, - ), - body: SafeArea( - child: _buildSettingsContent(), - ), + appBar: AppBar( + title: const Text("Настройки"), + /*leading: widget.showBackToChats + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: widget.onBackToChats, + ) + : null,*/ + ), + body: _buildSettingsContent(), ); } @@ -224,8 +222,8 @@ class _SettingsScreenState extends State { final isSmallScreen = screenWidth < 600 || screenHeight < 800; return Scaffold( - backgroundColor: Colors.transparent, - body: Stack( + body: SafeArea( + child: Stack( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), @@ -346,7 +344,7 @@ class _SettingsScreenState extends State { ), ), ], - ), + )), ); } diff --git a/lib/services/music_player_service.dart b/lib/services/music_player_service.dart index 11c88ff..29a3eea 100644 --- a/lib/services/music_player_service.dart +++ b/lib/services/music_player_service.dart @@ -104,6 +104,7 @@ class MusicPlayerService extends ChangeNotifier { StreamSubscription? _positionSubscription; StreamSubscription? _durationSubscription; StreamSubscription? _playerStateSubscription; + bool _wasCompleted = false; MusicTrack? get currentTrack => _currentIndex >= 0 && _currentIndex < _playlist.length @@ -129,10 +130,20 @@ class MusicPlayerService extends ChangeNotifier { }); _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) { + final wasCompleted = _wasCompleted; _isPlaying = state.playing; _isLoading = state.processingState == ProcessingState.loading || state.processingState == ProcessingState.buffering; + + // Detect track completion and auto-play next track + if (state.processingState == ProcessingState.completed && !wasCompleted) { + _wasCompleted = true; + _autoPlayNext(); + } else if (state.processingState != ProcessingState.completed) { + _wasCompleted = false; + } + notifyListeners(); }); @@ -257,6 +268,20 @@ class MusicPlayerService extends ChangeNotifier { await savePlaylist(); } + Future _autoPlayNext() async { + if (_playlist.isEmpty || _playlist.length <= 1) return; + + try { + _currentIndex = (_currentIndex + 1) % _playlist.length; + await _loadAndPlayTrack(_playlist[_currentIndex]); + await savePlaylist(); + } catch (e) { + print('Error auto-playing next track: $e'); + _isLoading = false; + notifyListeners(); + } + } + Future addToPlaylist(MusicTrack track) async { if (!_playlist.any((t) => t.id == track.id)) { _playlist.add(track); diff --git a/lib/widgets/bottom_sheet_music_player.dart b/lib/widgets/bottom_sheet_music_player.dart index 36871d8..128d044 100644 --- a/lib/widgets/bottom_sheet_music_player.dart +++ b/lib/widgets/bottom_sheet_music_player.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../services/music_player_service.dart'; @@ -8,37 +9,74 @@ class BottomSheetMusicPlayer extends StatefulWidget { static final ValueNotifier isExpandedNotifier = ValueNotifier( false, ); + static final ValueNotifier isFullscreenNotifier = ValueNotifier( + false, + ); @override State createState() => _BottomSheetMusicPlayerState(); } +enum _PlayerState { collapsed, expanded, fullscreen } + class _BottomSheetMusicPlayerState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; - bool _isExpanded = false; + late Animation _heightAnimation; + late Animation _opacityAnimation; + _PlayerState _currentState = _PlayerState.collapsed; + static const Duration _animationDuration = Duration(milliseconds: 300); @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 200), + duration: _animationDuration, + ); + _heightAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOutCubic, + ); + _opacityAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, ); BottomSheetMusicPlayer.isExpandedNotifier.addListener(_onExpandedChanged); + BottomSheetMusicPlayer.isFullscreenNotifier.addListener(_onFullscreenChanged); } void _onExpandedChanged() { final shouldBeExpanded = BottomSheetMusicPlayer.isExpandedNotifier.value; - if (shouldBeExpanded != _isExpanded) { + if (shouldBeExpanded && _currentState == _PlayerState.collapsed) { setState(() { - _isExpanded = shouldBeExpanded; - if (_isExpanded) { + _currentState = _PlayerState.expanded; + _animationController.forward(); + }); + } else if (!shouldBeExpanded && _currentState != _PlayerState.collapsed) { + setState(() { + _currentState = _PlayerState.collapsed; + _animationController.reverse(); + BottomSheetMusicPlayer.isFullscreenNotifier.value = false; + }); + } + } + + void _onFullscreenChanged() { + final shouldBeFullscreen = BottomSheetMusicPlayer.isFullscreenNotifier.value; + if (shouldBeFullscreen && _currentState != _PlayerState.fullscreen) { + setState(() { + _currentState = _PlayerState.fullscreen; + if (_animationController.value < 1.0) { _animationController.forward(); - } else { - _animationController.reverse(); } }); + } else if (!shouldBeFullscreen && + _currentState == _PlayerState.fullscreen) { + setState(() { + _currentState = _PlayerState.expanded; + _animationController.value = 1.0; + }); } } @@ -47,18 +85,51 @@ class _BottomSheetMusicPlayerState extends State BottomSheetMusicPlayer.isExpandedNotifier.removeListener( _onExpandedChanged, ); + BottomSheetMusicPlayer.isFullscreenNotifier.removeListener( + _onFullscreenChanged, + ); _animationController.dispose(); super.dispose(); } void _toggleExpand() { + HapticFeedback.lightImpact(); setState(() { - _isExpanded = !_isExpanded; - BottomSheetMusicPlayer.isExpandedNotifier.value = _isExpanded; - if (_isExpanded) { + if (_currentState == _PlayerState.collapsed) { + // Tap on collapsed opens directly to fullscreen + _currentState = _PlayerState.fullscreen; _animationController.forward(); - } else { + BottomSheetMusicPlayer.isExpandedNotifier.value = true; + BottomSheetMusicPlayer.isFullscreenNotifier.value = true; + } else if (_currentState == _PlayerState.fullscreen) { + // From fullscreen, go to collapsed + _currentState = _PlayerState.collapsed; _animationController.reverse(); + BottomSheetMusicPlayer.isExpandedNotifier.value = false; + BottomSheetMusicPlayer.isFullscreenNotifier.value = false; + } else { + // From expanded, go to collapsed + _currentState = _PlayerState.collapsed; + _animationController.reverse(); + BottomSheetMusicPlayer.isExpandedNotifier.value = false; + BottomSheetMusicPlayer.isFullscreenNotifier.value = false; + } + }); + } + + void _toggleFullscreen() { + HapticFeedback.mediumImpact(); + setState(() { + if (_currentState == _PlayerState.fullscreen) { + _currentState = _PlayerState.expanded; + // Keep animation at 1.0 for expanded state + _animationController.value = 1.0; + BottomSheetMusicPlayer.isFullscreenNotifier.value = false; + } else { + // Transitioning from expanded to fullscreen + // Animation is already at 1.0, we'll use AnimatedContainer for smooth transition + _currentState = _PlayerState.fullscreen; + BottomSheetMusicPlayer.isFullscreenNotifier.value = true; } }); } @@ -87,45 +158,78 @@ class _BottomSheetMusicPlayerState extends State } return AnimatedBuilder( - animation: _animationController, + animation: _heightAnimation, builder: (context, child) { final screenHeight = MediaQuery.of(context).size.height; - final collapsedHeight = 100.0; - final expandedHeight = screenHeight * 0.75; - final animationValue = Curves.easeInOut.transform( - _animationController.value, - ); - final currentHeight = - collapsedHeight + - (expandedHeight - collapsedHeight) * animationValue; + final collapsedHeight = 88.0; + final expandedHeight = screenHeight * 0.85; + final fullscreenHeight = screenHeight; + + double targetHeight; + if (_currentState == _PlayerState.fullscreen) { + targetHeight = fullscreenHeight; + } else if (_currentState == _PlayerState.expanded) { + targetHeight = expandedHeight; + } else { + targetHeight = collapsedHeight; + } + + // Interpolate between collapsed and target height + final currentHeight = collapsedHeight + + (targetHeight - collapsedHeight) * _heightAnimation.value; return Material( color: Colors.transparent, + elevation: 0, child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, + duration: _animationDuration, + curve: Curves.easeInOutCubic, height: currentHeight, decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.only( - topLeft: const Radius.circular(24), - topRight: const Radius.circular(24), - bottomLeft: Radius.circular(24 * (1 - animationValue)), - bottomRight: Radius.circular(24 * (1 - animationValue)), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 12, - offset: const Offset(0, -4), - ), - ], + borderRadius: _currentState == _PlayerState.fullscreen + ? BorderRadius.zero + : BorderRadius.only( + topLeft: const Radius.circular(28), + topRight: const Radius.circular(28), + bottomLeft: Radius.circular( + 28 * (1 - _heightAnimation.value), + ), + bottomRight: Radius.circular( + 28 * (1 - _heightAnimation.value), + ), + ), + boxShadow: _currentState == _PlayerState.fullscreen + ? [] + : [ + BoxShadow( + color: Colors.black.withOpacity( + 0.15 * _heightAnimation.value, + ), + blurRadius: 20, + offset: Offset(0, -6 * _heightAnimation.value), + ), + ], ), - child: _buildAnimatedContent( - context, - musicPlayer, - track, - colorScheme, + child: ClipRRect( + borderRadius: _currentState == _PlayerState.fullscreen + ? BorderRadius.zero + : BorderRadius.only( + topLeft: const Radius.circular(28), + topRight: const Radius.circular(28), + bottomLeft: Radius.circular( + 28 * (1 - _heightAnimation.value), + ), + bottomRight: Radius.circular( + 28 * (1 - _heightAnimation.value), + ), + ), + child: _buildAnimatedContent( + context, + musicPlayer, + track, + colorScheme, + ), ), ), ); @@ -140,27 +244,29 @@ class _BottomSheetMusicPlayerState extends State ColorScheme colorScheme, ) { return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, + duration: _animationDuration, + switchInCurve: Curves.easeInOutCubic, + switchOutCurve: Curves.easeInOutCubic, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SlideTransition( - position: - Tween( - begin: const Offset(0.0, 0.1), - end: Offset.zero, - ).animate( - CurvedAnimation(parent: animation, curve: Curves.easeInOut), - ), + position: Tween( + begin: const Offset(0.0, 0.05), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + ), + ), child: child, ), ); }, - child: _isExpanded - ? _buildExpandedView(context, musicPlayer, track, colorScheme) - : _buildCollapsedView(context, musicPlayer, track, colorScheme), + child: _currentState == _PlayerState.collapsed + ? _buildCollapsedView(context, musicPlayer, track, colorScheme) + : _buildExpandedView(context, musicPlayer, track, colorScheme), ); } @@ -173,80 +279,107 @@ class _BottomSheetMusicPlayerState extends State return SafeArea( key: const ValueKey('collapsed'), top: false, - child: GestureDetector( - onTap: _toggleExpand, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - width: 48, - height: 48, - color: colorScheme.primaryContainer, - child: track.albumArtUrl != null - ? Image.network( - track.albumArtUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _buildAlbumArtPlaceholder(colorScheme), - ) - : _buildAlbumArtPlaceholder(colorScheme), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - track.title, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: _toggleExpand, + borderRadius: BorderRadius.circular(28), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Hero( + tag: 'album-art-${track.id}', + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 64, + height: 64, + color: colorScheme.primaryContainer, + child: track.albumArtUrl != null + ? Image.network( + track.albumArtUrl!, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return _buildAlbumArtPlaceholder(colorScheme); + }, + errorBuilder: (context, error, stackTrace) => + _buildAlbumArtPlaceholder(colorScheme), + ) + : _buildAlbumArtPlaceholder(colorScheme), ), - const SizedBox(height: 4), - Text( - track.artist, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurface.withOpacity(0.7), - fontSize: 12, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - if (musicPlayer.isPlaying) { - musicPlayer.pause(); - } else { - musicPlayer.resume(); - } - }, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: colorScheme.primary, - shape: BoxShape.circle, - ), - child: Icon( - musicPlayer.isPlaying ? Icons.pause : Icons.play_arrow, - size: 24, - color: colorScheme.onPrimary, ), ), - ), - ], + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + track.title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + track.artist, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface.withOpacity(0.65), + fontSize: 13, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + HapticFeedback.selectionClick(); + if (musicPlayer.isPlaying) { + musicPlayer.pause(); + } else { + musicPlayer.resume(); + } + }, + borderRadius: BorderRadius.circular(24), + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + ), + child: musicPlayer.isLoading + ? Padding( + padding: const EdgeInsets.all(12), + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation( + colorScheme.onPrimary, + ), + ), + ) + : Icon( + musicPlayer.isPlaying ? Icons.pause : Icons.play_arrow, + size: 26, + color: colorScheme.onPrimary, + ), + ), + ), + ), + ], + ), ), ), ), @@ -261,221 +394,300 @@ class _BottomSheetMusicPlayerState extends State ) { return SafeArea( key: const ValueKey('expanded'), - top: false, + top: _currentState == _PlayerState.fullscreen, child: Column( children: [ - GestureDetector( - onTap: _toggleExpand, - onVerticalDragEnd: (details) { - if (details.primaryVelocity != null && - details.primaryVelocity! > 200) { - _toggleExpand(); - } - }, - child: Container( - margin: const EdgeInsets.only(top: 12), - padding: const EdgeInsets.symmetric(vertical: 12), - width: double.infinity, - child: Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: colorScheme.onSurface.withOpacity(0.3), - borderRadius: BorderRadius.circular(2), + Row( + children: [ + if (_currentState == _PlayerState.fullscreen) + Material( + color: Colors.transparent, + child: InkWell( + onTap: _toggleExpand, + borderRadius: BorderRadius.circular(24), + child: Container( + margin: const EdgeInsets.only(top: 8, right: 16), + padding: const EdgeInsets.all(12), + child: Icon( + Icons.fullscreen_exit_rounded, + color: colorScheme.onSurface, + size: 24, + ), + ), ), ), - ), - ), + ], ), Expanded( - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final albumSize = maxWidth < 400 ? maxWidth : 400.0; - return Container( - width: albumSize, - height: albumSize, - margin: EdgeInsets.symmetric( - horizontal: (maxWidth - albumSize) / 2, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + // Album art with hero animation + LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + final albumSize = maxWidth < 380 ? maxWidth : 380.0; + return Hero( + tag: 'album-art-${track.id}', + child: Container( + width: albumSize, + height: albumSize, + margin: EdgeInsets.symmetric( + horizontal: (maxWidth - albumSize) / 2, ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: track.albumArtUrl != null - ? Image.network( - track.albumArtUrl!, - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => - _buildLargeAlbumArtPlaceholder( - context, - colorScheme, - ), - ) - : _buildLargeAlbumArtPlaceholder( - context, - colorScheme, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 30, + offset: const Offset(0, 12), + spreadRadius: 2, ), - ), - ); - }, - ), - const SizedBox(height: 16), - Text( - track.title, - style: Theme.of(context).textTheme.headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - track.artist, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorScheme.onSurface.withOpacity(0.7), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(32), + child: track.albumArtUrl != null + ? Image.network( + track.albumArtUrl!, + fit: BoxFit.cover, + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return _buildLargeAlbumArtPlaceholder( + context, + colorScheme, + ); + }, + errorBuilder: + (context, error, stackTrace) => + _buildLargeAlbumArtPlaceholder( + context, + colorScheme, + ), + ) + : _buildLargeAlbumArtPlaceholder( + context, + colorScheme, + ), + ), + ), + ); + }, ), - textAlign: TextAlign.center, - ), - if (track.album != null) ...[ - const SizedBox(height: 4), + const SizedBox(height: 32), + // Track info Text( - track.album!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface.withOpacity(0.6), - ), + track.title, + style: Theme.of(context).textTheme.headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: -0.5, + ), textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + Text( + track.artist, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: colorScheme.onSurface.withOpacity(0.75), + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (track.album != null) ...[ + const SizedBox(height: 8), + Text( + track.album!, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurface.withOpacity(0.6), + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 40), + // Progress slider + Column( + children: [ + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: colorScheme.primary, + inactiveTrackColor: + colorScheme.surfaceContainerHigh, + thumbColor: colorScheme.primary, + overlayColor: + colorScheme.primary.withOpacity(0.1), + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + trackHeight: 4, + ), + child: Slider( + value: musicPlayer.duration.inMilliseconds > 0 + ? (musicPlayer.position.inMilliseconds / + musicPlayer.duration.inMilliseconds) + .clamp(0.0, 1.0) + : 0.0, + onChanged: (value) { + HapticFeedback.selectionClick(); + final newPosition = Duration( + milliseconds: (value * + musicPlayer.duration.inMilliseconds) + .round(), + ); + musicPlayer.seek(newPosition); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(musicPlayer.position), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: colorScheme.onSurface + .withOpacity(0.7), + fontWeight: FontWeight.w500, + fontSize: 13, + ), + ), + Text( + _formatDuration(musicPlayer.duration), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: colorScheme.onSurface + .withOpacity(0.7), + fontWeight: FontWeight.w500, + fontSize: 13, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + // Control buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + HapticFeedback.selectionClick(); + musicPlayer.previous(); + }, + borderRadius: BorderRadius.circular(28), + child: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh, + shape: BoxShape.circle, + ), + child: Icon( + Icons.skip_previous_rounded, + size: 28, + color: colorScheme.onSurface, + ), + ), + ), + ), + const SizedBox(width: 24), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + HapticFeedback.mediumImpact(); + if (musicPlayer.isPlaying) { + musicPlayer.pause(); + } else { + musicPlayer.resume(); + } + }, + borderRadius: BorderRadius.circular(40), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + ), + child: musicPlayer.isLoading + ? Padding( + padding: const EdgeInsets.all(24), + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: + AlwaysStoppedAnimation( + colorScheme.onPrimary, + ), + ), + ) + : Icon( + musicPlayer.isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + size: 40, + color: colorScheme.onPrimary, + ), + ), + ), + ), + const SizedBox(width: 24), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + HapticFeedback.selectionClick(); + musicPlayer.next(); + }, + borderRadius: BorderRadius.circular(28), + child: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh, + shape: BoxShape.circle, + ), + child: Icon( + Icons.skip_next_rounded, + size: 28, + color: colorScheme.onSurface, + ), + ), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).padding.bottom + 24, ), ], - const SizedBox(height: 32), - Column( - children: [ - Slider( - value: musicPlayer.duration.inMilliseconds > 0 - ? musicPlayer.position.inMilliseconds / - musicPlayer.duration.inMilliseconds - : 0.0, - onChanged: (value) { - final newPosition = Duration( - milliseconds: - (value * musicPlayer.duration.inMilliseconds) - .round(), - ); - musicPlayer.seek(newPosition); - }, - activeColor: colorScheme.primary, - inactiveColor: colorScheme.surfaceContainerHigh, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _formatDuration(musicPlayer.position), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurface.withOpacity( - 0.7, - ), - ), - ), - Text( - _formatDuration(musicPlayer.duration), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: colorScheme.onSurface.withOpacity( - 0.7, - ), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: musicPlayer.previous, - icon: const Icon(Icons.skip_previous), - iconSize: 32, - style: IconButton.styleFrom( - backgroundColor: colorScheme.surfaceContainerHigh, - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.all(16), - ), - ), - const SizedBox(width: 16), - FilledButton( - onPressed: () { - if (musicPlayer.isPlaying) { - musicPlayer.pause(); - } else { - musicPlayer.resume(); - } - }, - style: FilledButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.all(20), - shape: const CircleBorder(), - minimumSize: const Size(72, 72), - ), - child: musicPlayer.isLoading - ? SizedBox( - width: 32, - height: 32, - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - colorScheme.onPrimary, - ), - ), - ) - : Icon( - musicPlayer.isPlaying - ? Icons.pause - : Icons.play_arrow, - size: 36, - ), - ), - const SizedBox(width: 16), - IconButton( - onPressed: musicPlayer.next, - icon: const Icon(Icons.skip_next), - iconSize: 32, - style: IconButton.styleFrom( - backgroundColor: colorScheme.surfaceContainerHigh, - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.all(16), - ), - ), - ], - ), - const SizedBox(height: 24), - ], + ), ), - ), - ), + ), ], ), @@ -484,11 +696,22 @@ class _BottomSheetMusicPlayerState extends State Widget _buildAlbumArtPlaceholder(ColorScheme colorScheme) { return Container( - color: colorScheme.primaryContainer, - child: Icon( - Icons.music_note, - color: colorScheme.onPrimaryContainer, - size: 28, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primaryContainer, + colorScheme.primaryContainer.withOpacity(0.7), + ], + ), + ), + child: Center( + child: Icon( + Icons.music_note_rounded, + color: colorScheme.onPrimaryContainer.withOpacity(0.7), + size: 32, + ), ), ); } @@ -500,11 +723,22 @@ class _BottomSheetMusicPlayerState extends State return Container( width: double.infinity, height: double.infinity, - color: colorScheme.primaryContainer, - child: Icon( - Icons.music_note, - color: colorScheme.onPrimaryContainer, - size: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primaryContainer, + colorScheme.primaryContainer.withOpacity(0.6), + ], + ), + ), + child: Center( + child: Icon( + Icons.music_note_rounded, + color: colorScheme.onPrimaryContainer.withOpacity(0.6), + size: 100, + ), ), ); }