diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index d11402f..97e15e2 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -26,8 +26,20 @@ class _PlayerPageState extends BaseState { bool _isFavorite = false; String _album = ''; String _folder = ''; + String _dbTitle = ''; + String _dbArtist = ''; String? _metaSongId; + /// 0 = 文件信息,1 = 播放主页,2 = 歌词 + final PageController _pageController = PageController(initialPage: 1); + int _pageIndex = 1; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + String _formatDuration(Duration d) { if (d.inMilliseconds < 0) return '00:00'; final minutes = d.inMinutes; @@ -88,10 +100,14 @@ class _PlayerPageState extends BaseState { final db = SongDatabase(); String album = ''; + String dbTitle = ''; + String dbArtist = ''; bool favorite = false; try { final row = await db.getSongByPath(song.id); album = (row?['album'] as String?)?.trim() ?? ''; + dbTitle = (row?['title'] as String?)?.trim() ?? ''; + dbArtist = (row?['artist'] as String?)?.trim() ?? ''; favorite = await db.isFavorite(song.id); } catch (_) { // 元数据只是装饰,失败忽略 @@ -113,6 +129,8 @@ class _PlayerPageState extends BaseState { setState(() { _album = album; _folder = folder; + _dbTitle = dbTitle; + _dbArtist = dbArtist; _isFavorite = favorite; }); } @@ -175,6 +193,7 @@ class _PlayerPageState extends BaseState { icon: const Icon(Icons.keyboard_arrow_down, color: Colors.white70), onPressed: _closePage, ), + title: _buildPageIndicator(), actions: [ PopupMenuButton( icon: const Icon(Icons.more_vert, color: Colors.white54), @@ -230,94 +249,232 @@ class _PlayerPageState extends BaseState { ), ], ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ---- 封面 ---- - AspectRatio( - aspectRatio: 1, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: song.artwork != null - ? Image.memory( - song.artwork!, - fit: BoxFit.cover, - gaplessPlayback: true, - ) - : Container( - color: const Color(0xFF2A3332), - child: const Icon( - Icons.music_note, - size: 72, - color: Colors.white24, - ), - ), - ), - ), - const SizedBox(height: 28), + body: PageView( + controller: _pageController, + onPageChanged: (index) => setState(() => _pageIndex = index), + children: [ + _buildInfoPage(song), + _buildMainPage(service, song, isPlaying), + _buildLyricsPage(song), + ], + ), + ); + } - // ---- 标题 / 艺术家 ---- + /// 顶部两点指示器:左点=信息页,右点=歌词页,中间不亮表示在主页 + Widget _buildPageIndicator() { + Widget dot(int index) { + final active = _pageIndex == index; + return GestureDetector( + onTap: () => _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 280), + curve: Curves.easeOutCubic, + ), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 7, + height: 7, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: active ? Colors.white : Colors.white24, + ), + ), + ); + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + dot(0), + Container( + width: 56, + height: 1, + margin: const EdgeInsets.symmetric(horizontal: 8), + color: Colors.white12, + ), + dot(2), + ], + ); + } + + Widget _buildMainPage(AudioService service, Song song, bool isPlaying) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ---- 封面 ---- + AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: song.artwork != null + ? Image.memory( + song.artwork!, + fit: BoxFit.cover, + gaplessPlayback: true, + ) + : Container( + color: const Color(0xFF2A3332), + child: const Icon( + Icons.music_note, + size: 72, + color: Colors.white24, + ), + ), + ), + ), + const SizedBox(height: 28), + + // ---- 标题 / 艺术家 ---- + Text( + song.title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Colors.white, + height: 1.25, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + song.artist, + style: TextStyle(fontSize: 14, color: Colors.grey[400]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + // ---- 信息区(专辑 / 目录)---- + if (_album.isNotEmpty || _folder.isNotEmpty) ...[ + const SizedBox(height: 14), + if (_album.isNotEmpty) Text( - song.title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - color: Colors.white, - height: 1.25, + _album, + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + height: 1.5, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 6), + if (_folder.isNotEmpty) Text( - song.artist, - style: TextStyle(fontSize: 14, color: Colors.grey[400]), + '($_folder)', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + height: 1.5, + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), + ], - // ---- 信息区(专辑 / 目录)---- - if (_album.isNotEmpty || _folder.isNotEmpty) ...[ - const SizedBox(height: 14), - if (_album.isNotEmpty) - Text( - _album, - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], - height: 1.5, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - if (_folder.isNotEmpty) - Text( - '($_folder)', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - height: 1.5, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], + const Spacer(), - const Spacer(), + // ---- 进度 ---- + _buildProgressSection(service), + const SizedBox(height: 18), - // ---- 进度 ---- - _buildProgressSection(service), - const SizedBox(height: 18), + // ---- 控制 ---- + _buildControlButtons(service, song, isPlaying), + const SizedBox(height: 20), + ], + ), + ); + } - // ---- 控制 ---- - _buildControlButtons(service, song, isPlaying), - const SizedBox(height: 20), - ], + /// 左页:文件信息 + Widget _buildInfoPage(Song song) { + final rawName = song.id.split('/').last; + String fileName = rawName; + try { + fileName = Uri.decodeComponent(rawName); + } catch (_) {} + + final dot = fileName.lastIndexOf('.'); + final format = + dot > 0 ? fileName.substring(dot + 1).toUpperCase() : '未知'; + + String decodedPath = song.id; + try { + decodedPath = Uri.decodeComponent(song.id); + } catch (_) {} + + final effectiveArtist = _dbArtist.isNotEmpty + ? _dbArtist + : (song.artist.isEmpty ? '未知' : song.artist); + final effectiveTitle = _dbTitle.isNotEmpty ? _dbTitle : song.title; + + final rows = >[ + MapEntry('标题', effectiveTitle), + MapEntry('歌手', effectiveArtist), + MapEntry('专辑', _album.isEmpty ? '未知' : _album), + MapEntry('文件夹', _folder.isEmpty ? '根目录' : _folder), + MapEntry('文件名', fileName), + MapEntry('格式', format), + MapEntry('路径', decodedPath), + ]; + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '文件信息', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), - ), + const SizedBox(height: 20), + for (final row in rows) ...[ + Text( + row.key, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 4), + Text( + row.value, + style: const TextStyle( + fontSize: 14, + color: Colors.white, + height: 1.4, + ), + ), + const SizedBox(height: 18), + ], + ], + ), + ); + } + + /// 右页:歌词(占位) + Widget _buildLyricsPage(Song song) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.lyrics_outlined, size: 44, color: Colors.grey[700]), + const SizedBox(height: 14), + Text( + '暂无歌词', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 6), + Text( + song.title, + style: TextStyle(fontSize: 12, color: Colors.grey[700]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), ); }