只保留Android支持

This commit is contained in:
2026-08-14 21:39:39 +08:00
parent 36e434b8b5
commit b997391500
52 changed files with 633 additions and 1328 deletions
+26 -107
View File
@@ -1,122 +1,41 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'services/audio_service.dart';
import 'pages/home_page.dart';
void main() {
runApp(const MyApp());
runApp(
ChangeNotifierProvider(
create: (_) => AudioService(),
child: const QTPlayerApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
class QTPlayerApp extends StatelessWidget {
const QTPlayerApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: '清听',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
brightness: Brightness.dark, // 深色底色,护眼且显质感
primaryColor: const Color(0xFF7C9A9E), // 清冷的灰绿/青瓷色
colorScheme: const ColorScheme.dark(
primary: Color(0xFF7C9A9E),
secondary: Color(0xFFB8D4D0),
surface: Color(0xFF1A1F1E),
background: Color(0xFF0E1211),
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1A1F1E),
elevation: 0,
centerTitle: true,
),
useMaterial3: true,
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
home: const HomePage(),
);
}
}
+16
View File
@@ -0,0 +1,16 @@
class Song {
final String id;
final String title;
final String artist;
final String? url; // 以后放 content:// 或 https://
Song({required this.id, required this.title, required this.artist, this.url});
// 示例占位数据
static List<Song> get placeholderSongs => [
Song(id: '1', title: '晴天', artist: '周杰伦'),
Song(id: '2', title: '起风了', artist: '买辣椒也用券'),
Song(id: '3', title: 'Flower Dance', artist: 'DJ Okawari'),
Song(id: '4', title: '夜曲', artist: '周杰伦'),
];
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../widgets/mini_player_bar.dart';
import 'playlist_page.dart';
import 'settings_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
PlaylistPage(),
SettingsPage(),
];
@override
Widget build(BuildContext context) {
// 监听播放状态,只要有歌就显示迷你条
final audioService = context.watch<AudioService>();
final showMiniBar = audioService.currentSong != null;
return Scaffold(
body: Column(
children: [
Expanded(
child: IndexedStack(
index: _currentIndex,
children: _pages,
),
),
if (showMiniBar) const MiniPlayerBar(), // 底部迷你播放器
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
backgroundColor: const Color(0xFF1A1F1E),
selectedItemColor: const Color(0xFFB8D4D0),
unselectedItemColor: Colors.grey[600],
onTap: (index) => setState(() => _currentIndex = index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.music_note), label: '歌单'),
BottomNavigationBarItem(
icon: Icon(Icons.settings_outlined), label: '设置'),
],
),
);
}
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/song_model.dart';
import '../services/audio_service.dart';
import '../widgets/full_player_page.dart';
class PlaylistPage extends StatelessWidget {
const PlaylistPage({super.key});
@override
Widget build(BuildContext context) {
final songs = Song.placeholderSongs; // 后续替换成 WebDAV/SAF 列表
return Scaffold(
appBar: AppBar(
title: const Text('我的清听'),
actions: [
IconButton(
icon: const Icon(Icons.shuffle),
onPressed: () {}, // 以后实现随机
),
],
),
body: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: songs.length,
itemBuilder: (context, index) {
final song = songs[index];
return ListTile(
leading: const CircleAvatar(
backgroundColor: Color(0xFF2A3332),
child: Icon(Icons.audiotrack, size: 20, color: Colors.white54),
),
title: Text(song.title,
style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(song.artist,
style: TextStyle(color: Colors.grey[400], fontSize: 13)),
trailing: const Icon(Icons.more_vert, color: Colors.grey),
onTap: () {
// 1. 先更新服务,显示迷你条
context.read<AudioService>().playSong(song);
// 2. 再跳转全屏播放器
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FullPlayerPage()),
);
},
);
},
),
);
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
const SizedBox(height: 20),
_buildTile(
icon: Icons.folder_open_outlined,
title: '本地文件 (SAF)',
subtitle: '通过系统文件选择器导入',
onTap: () {
// 🟢 这里粘贴你旧项目的 SAF 逻辑
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('待接入 SAF 逻辑')),
);
},
),
_buildTile(
icon: Icons.cloud_outlined,
title: 'WebDAV 云盘',
subtitle: '连接你的 NAS 或网盘',
onTap: () {
// 🟢 这里调用你现成的 WebDAV 通道
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('待接入 WebDAV 逻辑')),
);
},
),
_buildTile(
icon: Icons.equalizer_outlined,
title: '均衡器',
subtitle: '简单低音增强 (后续)',
onTap: () {},
),
],
),
);
}
}
Widget _buildTile(
{required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap}) {
return ListTile(
leading: Icon(icon, color: const Color(0xFF7C9A9E)),
title: Text(title),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)),
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
onTap: onTap,
);
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import '../models/song_model.dart';
// 这是一个 ChangeNotifier,以后用来刷新底部的迷你播放条
class AudioService extends ChangeNotifier {
Song? _currentSong;
bool _isPlaying = false;
Song? get currentSong => _currentSong;
bool get isPlaying => _isPlaying;
// 占位方法:以后这里会调用 media_kit
void playSong(Song song) {
_currentSong = song;
_isPlaying = true;
notifyListeners(); // 刷新 UI
print('🎵 准备播放: ${song.title} - ${song.artist}');
}
void togglePlay() {
_isPlaying = !_isPlaying;
notifyListeners();
}
}
+116
View File
@@ -0,0 +1,116 @@
// lib/widgets/full_player_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
class FullPlayerPage extends StatelessWidget {
const FullPlayerPage({super.key});
@override
Widget build(BuildContext context) {
final service = context.watch<AudioService>();
final song = service.currentSong!;
return Scaffold(
backgroundColor: const Color(0xFF0E1211),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.playlist_play_outlined),
onPressed: () {},
),
],
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF2A3332),
boxShadow: [
BoxShadow(
color: const Color(0xFF7C9A9E).withOpacity(0.2),
blurRadius: 60,
spreadRadius: 10,
),
],
),
child:
const Icon(Icons.music_note, size: 80, color: Colors.white24),
),
const SizedBox(height: 60),
Text(
song.title,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
song.artist,
style: TextStyle(fontSize: 16, color: Colors.grey[400]),
),
const SizedBox(height: 40),
Slider(
value: 0.3,
onChanged: (v) {},
activeColor: const Color(0xFFB8D4D0),
inactiveColor: Colors.grey[800],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.skip_previous, size: 32),
color: Colors.white60,
),
const SizedBox(width: 20),
CircleAvatar(
radius: 32,
backgroundColor: const Color(0xFF7C9A9E),
child: IconButton(
icon: Icon(
service.isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.black87,
size: 32,
),
onPressed: () => context.read<AudioService>().togglePlay(),
),
),
const SizedBox(width: 20),
IconButton(
onPressed: () {},
icon: const Icon(Icons.skip_next, size: 32),
color: Colors.white60,
),
],
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.volume_up_outlined),
color: Colors.white38,
),
],
)
],
),
),
);
}
}
+61
View File
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import 'full_player_page.dart';
class MiniPlayerBar extends StatelessWidget {
const MiniPlayerBar({super.key});
@override
Widget build(BuildContext context) {
final service = context.watch<AudioService>();
final song = service.currentSong!;
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FullPlayerPage()),
),
child: Container(
height: 64,
decoration: const BoxDecoration(
color: Color(0xFF1A1F1E),
border: Border(top: BorderSide(color: Colors.white10, width: 0.5)),
),
child: Row(
children: [
const SizedBox(width: 12),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF2A3332),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(Icons.music_note, color: Colors.white38),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(song.title,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.w500)),
Text(song.artist,
style: TextStyle(fontSize: 12, color: Colors.grey[400])),
],
),
),
IconButton(
icon: Icon(service.isPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () => context.read<AudioService>().togglePlay(),
),
const SizedBox(width: 8),
],
),
),
);
}
}