只保留Android支持
This commit is contained in:
+26
-107
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '周杰伦'),
|
||||
];
|
||||
}
|
||||
@@ -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: '设置'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user