目前调通了120全局,准备开始介入播放层,在修bug
This commit is contained in:
@@ -34,3 +34,4 @@ desktop.ini
|
||||
# ---------- 临时文件 ----------
|
||||
/tmp/
|
||||
*.tmp
|
||||
.flutter-plugins-dependencies
|
||||
@@ -1,5 +1,71 @@
|
||||
package com.example.qt_player
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val CHANNEL = "com.qt_player/frame_rate"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// 在创建时尝试设置(但可能窗口还未就绪,所以下面在 onResume 再次设置)
|
||||
setHighRefreshRate()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// 每次回到前台时确保高刷生效
|
||||
setHighRefreshRate()
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"setHighRefreshRate" -> {
|
||||
setHighRefreshRate()
|
||||
result.success(true)
|
||||
}
|
||||
"clearRefreshRate" -> {
|
||||
clearRefreshRate()
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setHighRefreshRate() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val params = window.attributes
|
||||
params.preferredRefreshRate = 120f
|
||||
window.attributes = params
|
||||
Log.d("QTPlayer", "✅ 设置高刷 120Hz")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("QTPlayer", "设置高刷失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearRefreshRate() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val params = window.attributes
|
||||
params.preferredRefreshRate = 0f // 0 表示系统自动
|
||||
window.attributes = params
|
||||
Log.d("QTPlayer", "恢复默认刷新率")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("QTPlayer", "恢复默认刷新率失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -1,9 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'services/audio_service.dart';
|
||||
import 'services/playback_service.dart';
|
||||
import 'pages/home_page.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// 初始化 media_kit
|
||||
Player.init();
|
||||
// 初始化播放服务
|
||||
PlaybackService().init();
|
||||
runApp(
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => AudioService(),
|
||||
@@ -20,8 +27,8 @@ class QTPlayerApp extends StatelessWidget {
|
||||
return MaterialApp(
|
||||
title: '清听',
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.dark, // 深色底色,护眼且显质感
|
||||
primaryColor: const Color(0xFF7C9A9E), // 清冷的灰绿/青瓷色
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: const Color(0xFF7C9A9E),
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: Color(0xFF7C9A9E),
|
||||
secondary: Color(0xFFB8D4D0),
|
||||
|
||||
+533
-183
@@ -1,23 +1,236 @@
|
||||
// lib/pages/home_page.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import '../services/webdav_service.dart';
|
||||
import '../services/playback_service.dart';
|
||||
import '../widgets/mini_player_bar.dart';
|
||||
import 'webdav_setup_page.dart';
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
// ==================================================
|
||||
// 歌曲数据模型(含元数据状态)
|
||||
// ==================================================
|
||||
class SongItem {
|
||||
final String path; // WebDAV 完整路径
|
||||
final String fileName; // 文件名
|
||||
final String? title; // 元数据标题
|
||||
final String? artist; // 元数据艺术家
|
||||
final String sourceTag; // 来源标签
|
||||
final String metadataState; // "unknown" | "loading" | "success" | "failed"
|
||||
|
||||
SongItem({
|
||||
required this.path,
|
||||
required this.fileName,
|
||||
this.title,
|
||||
this.artist,
|
||||
required this.sourceTag,
|
||||
this.metadataState = 'unknown',
|
||||
});
|
||||
|
||||
String get displayTitle => (metadataState == 'success' && title != null)
|
||||
? title!
|
||||
: fileName.replaceAll(RegExp(r'\.[^.]*$'), '');
|
||||
|
||||
String get displaySubtitle {
|
||||
if (metadataState == 'success' && artist != null) {
|
||||
return artist!;
|
||||
} else {
|
||||
return sourceTag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// 通用可点击组件(缩放 + 高亮,无涟漪)
|
||||
// ==================================================
|
||||
class _ClickableTile extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ClickableTile({
|
||||
required this.child,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ClickableTile> createState() => _ClickableTileState();
|
||||
}
|
||||
|
||||
class _ClickableTileState extends State<_ClickableTile>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _scale;
|
||||
late final Animation<double> _opacity;
|
||||
|
||||
static const Duration _duration = Duration(milliseconds: 120);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this, duration: _duration);
|
||||
_scale = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
_opacity = Tween<double>(begin: 0.0, end: 0.08).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
_controller.reverse();
|
||||
widget.onTap();
|
||||
}
|
||||
|
||||
void _handleTapCancel() {
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onTapDown: _handleTapDown,
|
||||
onTapUp: _handleTapUp,
|
||||
onTapCancel: _handleTapCancel,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scale.value,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(_opacity.value),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// 主页
|
||||
// ==================================================
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
// 收藏数据(从你的 ASCII 图提取)
|
||||
final List<Map<String, String>> _favorites = const [
|
||||
{'title': '暧昧', 'artist': '王菲', 'format': 'flac', 'tag': 'openlist'},
|
||||
{'title': '清平调(独唱版)', 'artist': '王菲', 'format': 'flac', 'tag': 'openlist'},
|
||||
{
|
||||
'title': 'Rain in the Park',
|
||||
'artist': 'Marika Takeuchi',
|
||||
'format': 'flac',
|
||||
'tag': 'openlist'
|
||||
},
|
||||
];
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
List<SongItem> _favorites = [];
|
||||
bool _isWebDAVConnected = false;
|
||||
String _webDAVUsername = '';
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initWebDAV();
|
||||
}
|
||||
|
||||
Future<void> _initWebDAV() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||
if (hasCred) {
|
||||
_isWebDAVConnected = true;
|
||||
// 从 BaseUrl 中提取用户名(简化展示)
|
||||
final baseUrl = WebDAVService.instance.baseUrl;
|
||||
_webDAVUsername =
|
||||
baseUrl?.replaceAll(RegExp(r'^https?://'), '').split('/').first ??
|
||||
'已连接';
|
||||
await _loadMusicList();
|
||||
} else {
|
||||
_isWebDAVConnected = false;
|
||||
_favorites = [];
|
||||
}
|
||||
} catch (e) {
|
||||
_isWebDAVConnected = false;
|
||||
_favorites = [];
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMusicList() async {
|
||||
try {
|
||||
final files = await WebDAVService.instance.getMusicFiles();
|
||||
setState(() {
|
||||
_favorites = files.map((file) {
|
||||
return SongItem(
|
||||
path: file.path,
|
||||
fileName: file.name,
|
||||
title: file.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
|
||||
artist: null,
|
||||
sourceTag: 'webdav',
|
||||
metadataState: 'unknown',
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
} catch (e) {
|
||||
// 加载失败保持空列表
|
||||
setState(() => _favorites = []);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新列表(从 WebDAV 设置页返回时调用)
|
||||
Future<void> _refreshFromWebDAV() async {
|
||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||
setState(() {
|
||||
_isWebDAVConnected = hasCred;
|
||||
if (hasCred) {
|
||||
final baseUrl = WebDAVService.instance.baseUrl;
|
||||
_webDAVUsername =
|
||||
baseUrl?.replaceAll(RegExp(r'^https?://'), '').split('/').first ??
|
||||
'已连接';
|
||||
}
|
||||
});
|
||||
if (hasCred) {
|
||||
await _loadMusicList();
|
||||
} else {
|
||||
setState(() => _favorites = []);
|
||||
}
|
||||
}
|
||||
|
||||
void _playSong(SongItem song) async {
|
||||
try {
|
||||
final url = WebDAVService.instance.getFileUrl(song.path);
|
||||
await PlaybackService().play(url);
|
||||
// 更新 AudioService 状态
|
||||
context.read<AudioService>().playSong(Song(
|
||||
id: song.path,
|
||||
title: song.displayTitle,
|
||||
artist: song.displaySubtitle,
|
||||
url: url,
|
||||
));
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('播放失败: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -26,18 +239,17 @@ class HomePage extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0E1211),
|
||||
// ---------- 主体:可滚动内容 ----------
|
||||
body: Column(
|
||||
body: Stack(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 48),
|
||||
// ---------- 顶部标题栏 ----------
|
||||
Row(
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
// ---------- 顶部安全区域 + 标题 ----------
|
||||
SliverToBoxAdapter(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
@@ -50,204 +262,342 @@ class HomePage extends StatelessWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu, color: Colors.white54),
|
||||
onPressed: () {
|
||||
// 后续:菜单入口
|
||||
},
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- 【媒体库】区域 ----------
|
||||
const Text(
|
||||
'媒体库',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFFB8D4D0),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ---- WebDAV 连接状态卡片 ----
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A2A2A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF2A4A4A), width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cloud_outlined,
|
||||
color: Color(0xFFB8D4D0), size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
// ---------- 媒体库区块 ----------
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'WebDAV',
|
||||
'媒体库',
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
'username@nas', // 后续替换为真实用户名
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: Colors.grey[400]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4CAF50).withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'已连接',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Color(0xFF4CAF50)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ---- 三个功能入口(横向三列) ----
|
||||
Row(
|
||||
children: [
|
||||
_buildEntryCard(Icons.music_note, '本地音乐'),
|
||||
const SizedBox(width: 12),
|
||||
_buildEntryCard(Icons.history, '最近播放'),
|
||||
const SizedBox(width: 12),
|
||||
_buildEntryCard(Icons.playlist_play, '歌单列表'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// ---------- 【我的收藏】区域 ----------
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.favorite,
|
||||
color: Color(0xFFB8D4D0), size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'我的收藏',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFFB8D4D0),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text(
|
||||
'查看全部',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ---- WebDAV 入口 ----
|
||||
_ClickableTile(
|
||||
onTap: () async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const WebDAVSetupPage(),
|
||||
),
|
||||
);
|
||||
if (result == true) {
|
||||
await _refreshFromWebDAV();
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 8.0),
|
||||
child: Icon(
|
||||
Icons.cloud_outlined,
|
||||
color: Color(0xFFB8D4D0),
|
||||
size: 56,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'WebDAV',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
_isWebDAVConnected ? '● 已连接' : '● 未连接',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _isWebDAVConnected
|
||||
? const Color(0xFF4CAF50)
|
||||
: Colors.grey[500],
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_isWebDAVConnected
|
||||
? _webDAVUsername
|
||||
: '点击连接',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: _isWebDAVConnected
|
||||
? Colors.grey[400]
|
||||
: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ---- 收藏列表 ----
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _favorites.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: Colors.white10,
|
||||
height: 1,
|
||||
// ---- 三个功能入口 ----
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ClickableTile(
|
||||
onTap: () {},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.music_note,
|
||||
size: 24,
|
||||
color: const Color(0xFFB8D4D0)),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'本地音乐',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white70,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _ClickableTile(
|
||||
onTap: () {},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history,
|
||||
size: 24,
|
||||
color: const Color(0xFFB8D4D0)),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'最近播放',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _ClickableTile(
|
||||
onTap: () {},
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.playlist_play,
|
||||
size: 24,
|
||||
color: const Color(0xFFB8D4D0)),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'歌单列表',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- “我的收藏” Sticky Header ----------
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _StickyHeaderDelegate(
|
||||
child: Container(
|
||||
height: 48,
|
||||
color: const Color(0xFF0E1211),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.favorite,
|
||||
color: Color(0xFFB8D4D0), size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'我的收藏',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFFB8D4D0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ---------- 收藏列表 ----------
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: showMiniBar ? 80.0 : 20.0,
|
||||
),
|
||||
sliver: _isLoading
|
||||
? const SliverFillRemaining(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFFB8D4D0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: _favorites.isEmpty
|
||||
? SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.music_note,
|
||||
size: 48,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_isWebDAVConnected
|
||||
? '还没有收藏歌曲\n去媒体库发现音乐'
|
||||
: '请先连接 WebDAV',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[500],
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final song = _favorites[index];
|
||||
return ListTile(
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 6),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2A3332),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Icon(Icons.music_note,
|
||||
color: Colors.white38, size: 20),
|
||||
leading: const Icon(
|
||||
Icons.music_note,
|
||||
color: Colors.white38,
|
||||
size: 20,
|
||||
),
|
||||
title: Text(
|
||||
'${song['title']} - ${song['artist']}.${song['format']}',
|
||||
song.displayTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w400),
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
song['tag']!,
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
song.displaySubtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2A4A4A).withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
child: Text(
|
||||
song['tag']!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Color(0xFF7C9A9E)),
|
||||
onTap: () => _playSong(song),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// 后续:播放该歌曲
|
||||
},
|
||||
);
|
||||
},
|
||||
childCount: _favorites.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 80), // 底部留空,避免被播放栏遮挡
|
||||
],
|
||||
),
|
||||
|
||||
// ---------- 底部 MiniPlayer ----------
|
||||
if (showMiniBar)
|
||||
const Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: MiniPlayerBar(),
|
||||
),
|
||||
),
|
||||
// ---------- 底部全局播放控制栏(固定) ----------
|
||||
if (showMiniBar) const MiniPlayerBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 功能入口卡片(横向三列共用)
|
||||
Widget _buildEntryCard(IconData icon, String label) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1F1E),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[800]!, width: 0.5),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
// 后续跳转
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 28, color: const Color(0xFFB8D4D0)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style:
|
||||
const TextStyle(fontSize: 13, fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// ==================================================
|
||||
// Sticky Header 委托
|
||||
// ==================================================
|
||||
class _StickyHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
final Widget child;
|
||||
|
||||
_StickyHeaderDelegate({required this.child});
|
||||
|
||||
@override
|
||||
double get minExtent => 48;
|
||||
@override
|
||||
double get maxExtent => 48;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_StickyHeaderDelegate oldDelegate) {
|
||||
return child != oldDelegate.child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/webdav_service.dart';
|
||||
|
||||
class WebDAVSetupPage extends StatefulWidget {
|
||||
const WebDAVSetupPage({super.key});
|
||||
|
||||
@override
|
||||
State<WebDAVSetupPage> createState() => _WebDAVSetupPageState();
|
||||
}
|
||||
|
||||
class _WebDAVSetupPageState extends State<WebDAVSetupPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _baseUrlController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _isConnected = false;
|
||||
String _statusText = '未连接';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSavedCredentials();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_baseUrlController.dispose();
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadSavedCredentials() async {
|
||||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||||
setState(() {
|
||||
_isConnected = hasCred;
|
||||
_statusText = hasCred ? '已连接' : '未连接';
|
||||
if (hasCred) {
|
||||
_baseUrlController.text = WebDAVService.instance._baseUrl ?? '';
|
||||
// 用户名不显示,保持隐私
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _connect() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
await WebDAVService.instance.saveCredentials(
|
||||
_baseUrlController.text.trim(),
|
||||
_usernameController.text.trim(),
|
||||
_passwordController.text.trim(),
|
||||
);
|
||||
|
||||
// 尝试列出文件以验证连接
|
||||
await WebDAVService.instance.getMusicFiles();
|
||||
|
||||
setState(() {
|
||||
_isConnected = true;
|
||||
_statusText = '已连接';
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('✅ WebDAV 连接成功'),
|
||||
backgroundColor: Color(0xFF4CAF50),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isConnected = false;
|
||||
_statusText = '连接失败';
|
||||
_isLoading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('❌ 连接失败: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disconnect() async {
|
||||
await WebDAVService.instance.clearCredentials();
|
||||
setState(() {
|
||||
_isConnected = false;
|
||||
_statusText = '未连接';
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('已断开连接'),
|
||||
backgroundColor: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0E1211),
|
||||
appBar: AppBar(
|
||||
title: const Text('WebDAV 设置'),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态显示
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A2A2A),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_isConnected ? Icons.cloud_done : Icons.cloud_off,
|
||||
color:
|
||||
_isConnected ? const Color(0xFF4CAF50) : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'状态: $_statusText',
|
||||
style: TextStyle(
|
||||
color: _isConnected
|
||||
? const Color(0xFF4CAF50)
|
||||
: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_isConnected)
|
||||
TextButton(
|
||||
onPressed: _disconnect,
|
||||
style:
|
||||
TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('断开'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 表单
|
||||
TextFormField(
|
||||
controller: _baseUrlController,
|
||||
enabled: !_isConnected,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'WebDAV 地址',
|
||||
labelStyle: TextStyle(color: Colors.grey),
|
||||
hintText: 'https://nas.local/dav/',
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFFB8D4D0)),
|
||||
),
|
||||
),
|
||||
validator: (v) =>
|
||||
v == null || v.isEmpty ? '请输入 WebDAV 地址' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _usernameController,
|
||||
enabled: !_isConnected,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: '用户名',
|
||||
labelStyle: TextStyle(color: Colors.grey),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFFB8D4D0)),
|
||||
),
|
||||
),
|
||||
validator: (v) => v == null || v.isEmpty ? '请输入用户名' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
enabled: !_isConnected,
|
||||
obscureText: true,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
decoration: const InputDecoration(
|
||||
labelText: '密码',
|
||||
labelStyle: TextStyle(color: Colors.grey),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Color(0xFFB8D4D0)),
|
||||
),
|
||||
),
|
||||
validator: (v) => v == null || v.isEmpty ? '请输入密码' : null,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
_isConnected ? null : (_isLoading ? null : _connect),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFB8D4D0),
|
||||
foregroundColor: Colors.black87,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.black87,
|
||||
),
|
||||
)
|
||||
: Text(_isConnected ? '已连接' : '连接'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit/media_kit.dart' as media_kit;
|
||||
|
||||
class PlaybackService {
|
||||
static final PlaybackService _instance = PlaybackService._();
|
||||
factory PlaybackService() => _instance;
|
||||
PlaybackService._();
|
||||
|
||||
late final Player _player;
|
||||
bool get isInitialized => _player.state.playing;
|
||||
|
||||
void init() {
|
||||
_player = Player();
|
||||
}
|
||||
|
||||
Future<void> play(String url) async {
|
||||
await _player.open(Media(url));
|
||||
await _player.play();
|
||||
}
|
||||
|
||||
void pause() => _player.pause();
|
||||
void resume() => _player.play();
|
||||
void stop() => _player.stop();
|
||||
void dispose() => _player.dispose();
|
||||
|
||||
// 状态流
|
||||
Stream<PlayerState> get stateStream => _player.stream;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
class PlayerService {
|
||||
static final PlayerService _instance = PlayerService._internal();
|
||||
factory PlayerService() => _instance;
|
||||
PlayerService._internal();
|
||||
|
||||
final Player _player = Player();
|
||||
|
||||
void play(String url) {
|
||||
_player.open(Media(url));
|
||||
}
|
||||
|
||||
void pause() => _player.pause();
|
||||
void resume() => _player.play();
|
||||
void stop() => _player.stop();
|
||||
|
||||
void dispose() => _player.dispose();
|
||||
|
||||
// 状态监听...
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:webdav_client/webdav_client.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class WebDAVService {
|
||||
static const String _keyBaseUrl = 'webdav_base_url';
|
||||
static const String _keyUsername = 'webdav_username';
|
||||
static const String _keyPassword = 'webdav_password';
|
||||
|
||||
static WebDAVService? _instance;
|
||||
static WebDAVService get instance => _instance ??= WebDAVService._();
|
||||
|
||||
WebDAVService._();
|
||||
|
||||
WebDAVClient? _client;
|
||||
String? _baseUrl;
|
||||
|
||||
bool get isConnected => _client != null;
|
||||
|
||||
// 保存凭据
|
||||
Future<void> saveCredentials(
|
||||
String baseUrl, String username, String password) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keyBaseUrl, baseUrl);
|
||||
await prefs.setString(_keyUsername, username);
|
||||
await prefs.setString(_keyPassword, password);
|
||||
_baseUrl = baseUrl;
|
||||
_client = WebDAVClient(
|
||||
baseUri: Uri.parse(baseUrl),
|
||||
credentials: '$username:$password',
|
||||
);
|
||||
}
|
||||
|
||||
// 加载已保存的凭据
|
||||
Future<bool> loadCredentials() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final baseUrl = prefs.getString(_keyBaseUrl);
|
||||
final username = prefs.getString(_keyUsername);
|
||||
final password = prefs.getString(_keyPassword);
|
||||
if (baseUrl != null && username != null && password != null) {
|
||||
_baseUrl = baseUrl;
|
||||
_client = WebDAVClient(
|
||||
baseUri: Uri.parse(baseUrl),
|
||||
credentials: '$username:$password',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清除凭据
|
||||
Future<void> clearCredentials() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_keyBaseUrl);
|
||||
await prefs.remove(_keyUsername);
|
||||
await prefs.remove(_keyPassword);
|
||||
_client = null;
|
||||
_baseUrl = null;
|
||||
}
|
||||
|
||||
// 获取音乐文件列表(仅 .mp3 .flac .m4a .ape .wav)
|
||||
Future<List<WebDAVFileItem>> getMusicFiles({String path = '/'}) async {
|
||||
if (_client == null) throw Exception('WebDAV 未连接');
|
||||
|
||||
final items = await _client!.listAll(recursive: true);
|
||||
final musicExtensions = ['.mp3', '.flac', '.m4a', '.ape', '.wav', '.opus'];
|
||||
|
||||
return items
|
||||
.where((item) =>
|
||||
item.isFile &&
|
||||
musicExtensions.any((ext) => item.path.toLowerCase().endsWith(ext)))
|
||||
.map((item) => WebDAVFileItem(
|
||||
path: item.path,
|
||||
name: item.path.split('/').last,
|
||||
size: item.size,
|
||||
modified: item.modified,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 获取文件的完整下载 URL(用于播放)
|
||||
String getFileUrl(String path) {
|
||||
if (_baseUrl == null) throw Exception('WebDAV 未配置');
|
||||
// 确保 baseUrl 末尾有 '/'
|
||||
final base = _baseUrl!.endsWith('/') ? _baseUrl! : '$_baseUrl/';
|
||||
// 去掉路径开头的 '/'
|
||||
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
||||
return '$base$cleanPath';
|
||||
}
|
||||
}
|
||||
|
||||
class WebDAVFileItem {
|
||||
final String path;
|
||||
final String name;
|
||||
final int? size;
|
||||
final DateTime? modified;
|
||||
|
||||
WebDAVFileItem({
|
||||
required this.path,
|
||||
required this.name,
|
||||
this.size,
|
||||
this.modified,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// lib/widgets/mini_player_bar.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/audio_service.dart';
|
||||
@@ -15,31 +14,40 @@ class MiniPlayerBar extends StatelessWidget {
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1F1E),
|
||||
border:
|
||||
const Border(top: BorderSide(color: Colors.white10, width: 0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2)),
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
// 顶部微渐变(轻阴影)
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.1),
|
||||
],
|
||||
stops: const [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 12),
|
||||
// ---- 封面方图 ----
|
||||
// 封面占位(56px)
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2A3332),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child:
|
||||
const Icon(Icons.music_note, color: Colors.white38, size: 22),
|
||||
const Icon(Icons.music_note, color: Colors.white38, size: 24),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// ---- 歌曲信息 ----
|
||||
// 歌曲信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -48,20 +56,26 @@ class MiniPlayerBar extends StatelessWidget {
|
||||
Text(
|
||||
song.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w500),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
song.artist,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ---- 控制按钮:播放/暂停 + 展开列表 ----
|
||||
// 控制按钮
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
service.isPlaying ? Icons.pause : Icons.play_arrow,
|
||||
@@ -72,9 +86,7 @@ class MiniPlayerBar extends StatelessWidget {
|
||||
IconButton(
|
||||
icon:
|
||||
const Icon(Icons.playlist_play_outlined, color: Colors.white54),
|
||||
onPressed: () {
|
||||
// 后续:展开当前播放列表
|
||||
},
|
||||
onPressed: () {},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
|
||||
@@ -65,6 +65,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.11.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -243,6 +259,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ dependencies:
|
||||
media_kit: ^1.1.10
|
||||
media_kit_libs_audio: ^1.0.7
|
||||
provider: ^6.1.2
|
||||
|
||||
dio: ^5.4.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
Reference in New Issue
Block a user