页面层级与返回逻辑实现完成,并且去掉web相关代码支持
This commit is contained in:
+91
-162
@@ -1,4 +1,3 @@
|
|||||||
// lib/main.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
@@ -9,6 +8,8 @@ import 'pages/player_page.dart';
|
|||||||
import 'pages/playlist_page.dart';
|
import 'pages/playlist_page.dart';
|
||||||
import 'widgets/global_mini_player.dart';
|
import 'widgets/global_mini_player.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -21,13 +22,15 @@ void main() {
|
|||||||
initSuccess = true;
|
initSuccess = true;
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
initError = '初始化失败: $e';
|
initError = '初始化失败: $e';
|
||||||
print('❌ $initError');
|
debugPrint('❌ $initError');
|
||||||
print(stack);
|
debugPrint(stack.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ChangeNotifierProvider(
|
MultiProvider(
|
||||||
create: (_) => AudioService(),
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => AudioService()),
|
||||||
|
],
|
||||||
child: QTPlayerApp(
|
child: QTPlayerApp(
|
||||||
initSuccess: initSuccess,
|
initSuccess: initSuccess,
|
||||||
initError: initError,
|
initError: initError,
|
||||||
@@ -36,7 +39,60 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class QTPlayerApp extends StatefulWidget {
|
// ═══════════════════════════════════════════════════
|
||||||
|
// RouteManager - 路由状态管理
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
class RouteManager extends ChangeNotifier {
|
||||||
|
static final RouteManager _instance = RouteManager._internal();
|
||||||
|
factory RouteManager() => _instance;
|
||||||
|
RouteManager._internal();
|
||||||
|
|
||||||
|
String _currentRoute = '/';
|
||||||
|
|
||||||
|
String get currentRoute => _currentRoute;
|
||||||
|
|
||||||
|
bool get showMiniPlayer =>
|
||||||
|
_currentRoute != '/player' && _currentRoute != '/playlist';
|
||||||
|
|
||||||
|
void updateRoute(String route) {
|
||||||
|
if (_currentRoute != route) {
|
||||||
|
_currentRoute = route;
|
||||||
|
debugPrint('📌 路由更新: $route');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// 导航观察者 - 监听路由变化
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
||||||
|
void _updateRoute(Route? route) {
|
||||||
|
final name = route?.settings.name ?? '/';
|
||||||
|
RouteManager().updateRoute(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPush(Route route, Route? previousRoute) {
|
||||||
|
_updateRoute(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPop(Route route, Route? previousRoute) {
|
||||||
|
// ⭐ 关键:用 previousRoute 而不是 navigatorKey.currentContext
|
||||||
|
_updateRoute(previousRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didReplace({Route? newRoute, Route? oldRoute}) {
|
||||||
|
_updateRoute(newRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
// 主应用
|
||||||
|
// ═══════════════════════════════════════════════════
|
||||||
|
class QTPlayerApp extends StatelessWidget {
|
||||||
final bool initSuccess;
|
final bool initSuccess;
|
||||||
final String? initError;
|
final String? initError;
|
||||||
|
|
||||||
@@ -46,99 +102,9 @@ class QTPlayerApp extends StatefulWidget {
|
|||||||
required this.initError,
|
required this.initError,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<QTPlayerApp> createState() => _QTPlayerAppState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _QTPlayerAppState extends State<QTPlayerApp>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
bool _showPlayerPage = false;
|
|
||||||
bool _showPlaylist = false;
|
|
||||||
|
|
||||||
late final AnimationController _animationController;
|
|
||||||
late final Animation<double> _slideAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
GlobalMiniPlayer.onOpenPlayerPage = _openPlayerPage;
|
|
||||||
GlobalMiniPlayer.onOpenPlaylistPage = _openPlaylist;
|
|
||||||
|
|
||||||
_animationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
|
|
||||||
_slideAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _animationController,
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openPlayerPage() {
|
|
||||||
setState(() {
|
|
||||||
_showPlayerPage = true;
|
|
||||||
_showPlaylist = false;
|
|
||||||
});
|
|
||||||
_animationController.forward(from: 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closePlayerPage() {
|
|
||||||
_animationController.reverse().then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_showPlayerPage = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openPlaylist() {
|
|
||||||
setState(() {
|
|
||||||
_showPlaylist = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closePlaylist() {
|
|
||||||
setState(() {
|
|
||||||
_showPlaylist = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// ✅ PopScope 的 onPopInvokedWithResult
|
|
||||||
// ============================================================
|
|
||||||
void _onPopInvokedWithResult(bool didPop, Object? result) {
|
|
||||||
if (didPop) return;
|
|
||||||
|
|
||||||
// 1️⃣ 优先关闭播放列表(最高层级)
|
|
||||||
if (_showPlaylist) {
|
|
||||||
_closePlaylist();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2️⃣ 其次关闭播放页
|
|
||||||
if (_showPlayerPage) {
|
|
||||||
_closePlayerPage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3️⃣ 没有覆盖层,允许退出(由 Navigator 处理)
|
|
||||||
// 注意:这里不能直接退出,需要让 Navigator 处理
|
|
||||||
// 但 canPop 会根据状态返回 true/false
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!widget.initSuccess) {
|
if (!initSuccess) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
backgroundColor: const Color(0xFF0E1211),
|
backgroundColor: const Color(0xFF0E1211),
|
||||||
@@ -154,7 +120,7 @@ class _QTPlayerAppState extends State<QTPlayerApp>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
widget.initError ?? '未知错误',
|
initError ?? '未知错误',
|
||||||
style: TextStyle(color: Colors.grey[400], fontSize: 14),
|
style: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -178,72 +144,35 @@ class _QTPlayerAppState extends State<QTPlayerApp>
|
|||||||
),
|
),
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: const HomePage(),
|
navigatorKey: navigatorKey,
|
||||||
|
navigatorObservers: [MiniPlayerNavigatorObserver()],
|
||||||
|
routes: {
|
||||||
|
'/': (context) => const HomePage(),
|
||||||
|
'/player': (context) => const PlayerPage(),
|
||||||
|
'/playlist': (context) => const PlaylistPage(),
|
||||||
|
},
|
||||||
|
// ⭐ 核心改动:删除手写 Overlay,直接用 Stack
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
// ✅ 暴力测试:canPop 写死为 false
|
return Stack(
|
||||||
return PopScope(
|
fit: StackFit.expand,
|
||||||
canPop: false,
|
children: [
|
||||||
onPopInvokedWithResult: (didPop, result) {
|
// 1. Navigator(所有路由页面)
|
||||||
debugPrint('🔥🔥🔥 BACK FIRED: didPop=$didPop');
|
if (child != null) child,
|
||||||
debugPrint('🔥 playlist=$_showPlaylist, player=$_showPlayerPage');
|
|
||||||
|
|
||||||
if (_showPlaylist) {
|
// 2. MiniPlayer(由路由状态控制显隐)
|
||||||
debugPrint('🔥 closing playlist');
|
AnimatedBuilder(
|
||||||
_closePlaylist();
|
animation: RouteManager(),
|
||||||
} else if (_showPlayerPage) {
|
builder: (context, _) {
|
||||||
debugPrint('🔥 closing player');
|
if (!RouteManager().showMiniPlayer) {
|
||||||
_closePlayerPage();
|
return const SizedBox.shrink();
|
||||||
} else {
|
}
|
||||||
debugPrint('🔥 no overlay, allowing exit');
|
return const Align(
|
||||||
// 注意:这里 canPop=false,需要手动退出
|
alignment: Alignment.bottomCenter,
|
||||||
// 用 SystemNavigator.pop() 或 Navigator.pop(context)
|
child: GlobalMiniPlayer(),
|
||||||
}
|
);
|
||||||
},
|
},
|
||||||
child: Overlay(
|
),
|
||||||
initialEntries: [
|
],
|
||||||
OverlayEntry(
|
|
||||||
builder: (context) => Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(child: child!),
|
|
||||||
const Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: GlobalMiniPlayer(),
|
|
||||||
),
|
|
||||||
if (_showPlayerPage)
|
|
||||||
Positioned.fill(
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: _animationController,
|
|
||||||
builder: (context, child) {
|
|
||||||
final progress = _slideAnimation.value;
|
|
||||||
final offsetY =
|
|
||||||
MediaQuery.of(context).size.height * progress;
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(0, offsetY),
|
|
||||||
child: Opacity(
|
|
||||||
opacity: 1.0 - progress * 0.3,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: PlayerPage(
|
|
||||||
onClose: _closePlayerPage,
|
|
||||||
onOpenPlaylist: _openPlaylist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_showPlaylist)
|
|
||||||
Positioned.fill(
|
|
||||||
child: PlaylistPage(
|
|
||||||
onClose: _closePlaylist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// lib/pages/home_page.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
@@ -7,7 +6,6 @@ import '../services/playback_service.dart';
|
|||||||
import '../widgets/magnetic_scroll_physics.dart';
|
import '../widgets/magnetic_scroll_physics.dart';
|
||||||
import 'webdav_setup_page.dart';
|
import 'webdav_setup_page.dart';
|
||||||
import 'webdav_file_list_page.dart';
|
import 'webdav_file_list_page.dart';
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
// ==================================================
|
// ==================================================
|
||||||
// 歌曲数据模型
|
// 歌曲数据模型
|
||||||
@@ -171,7 +169,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
return (offset / _mediaLibraryHeight).clamp(0.0, 1.0);
|
return (offset / _mediaLibraryHeight).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 媒体库透明度(快速淡出)
|
|
||||||
double get _mediaOpacity {
|
double get _mediaOpacity {
|
||||||
final p = _progress;
|
final p = _progress;
|
||||||
if (p <= 0.3) {
|
if (p <= 0.3) {
|
||||||
@@ -181,7 +178,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列表展开程度
|
|
||||||
double get _listReveal {
|
double get _listReveal {
|
||||||
if (_progress <= 0.7) return 0.0;
|
if (_progress <= 0.7) return 0.0;
|
||||||
return (_progress - 0.7) / 0.3;
|
return (_progress - 0.7) / 0.3;
|
||||||
@@ -204,7 +200,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 高度测量(带重试限制) ==========
|
|
||||||
void _measureMediaLibraryHeight() {
|
void _measureMediaLibraryHeight() {
|
||||||
final renderBox =
|
final renderBox =
|
||||||
_mediaLibraryKey.currentContext?.findRenderObject() as RenderBox?;
|
_mediaLibraryKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
@@ -231,12 +226,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 滚动监听 ==========
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 初始化 WebDAV ==========
|
|
||||||
Future<void> _initWebDAV() async {
|
Future<void> _initWebDAV() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
@@ -249,14 +242,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// ⭐ 核心:播放方法(UI 优先响应,然后真正播放)
|
|
||||||
// ============================================================
|
|
||||||
void _playSong(SongItem song) async {
|
void _playSong(SongItem song) async {
|
||||||
try {
|
try {
|
||||||
final url = WebDAVService.instance.getFileUrl(song.path);
|
final url = WebDAVService.instance.getFileUrl(song.path);
|
||||||
|
|
||||||
// 1️⃣ 先更新 UI(立即响应)
|
|
||||||
final audioService = context.read<AudioService>();
|
final audioService = context.read<AudioService>();
|
||||||
audioService.playSong(Song(
|
audioService.playSong(Song(
|
||||||
id: song.path,
|
id: song.path,
|
||||||
@@ -265,11 +254,9 @@ class _HomePageState extends State<HomePage> {
|
|||||||
url: url,
|
url: url,
|
||||||
));
|
));
|
||||||
|
|
||||||
// 2️⃣ 动态获取认证头
|
|
||||||
final headers = await WebDAVService.instance.getAuthHeaders();
|
final headers = await WebDAVService.instance.getAuthHeaders();
|
||||||
|
|
||||||
if (headers.isEmpty) {
|
if (headers.isEmpty) {
|
||||||
print('⚠️ 未获取到认证头,请检查 WebDAV 登录状态');
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
@@ -281,10 +268,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print('🎵 [收藏列表] 播放 URL: $url');
|
|
||||||
print('📋 [收藏列表] 认证头已设置: ${headers.keys}');
|
|
||||||
|
|
||||||
// 3️⃣ 播放
|
|
||||||
await PlaybackService().play(url, headers: headers);
|
await PlaybackService().play(url, headers: headers);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -298,7 +281,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 重置磁吸状态 ==========
|
|
||||||
void _resetSnapState() {
|
void _resetSnapState() {
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
_scrollController.jumpTo(0.0);
|
_scrollController.jumpTo(0.0);
|
||||||
@@ -306,21 +288,15 @@ class _HomePageState extends State<HomePage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Build
|
|
||||||
// ================================================================
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isConnected = WebDAVService.instance.isConnected;
|
final isConnected = WebDAVService.instance.isConnected;
|
||||||
final username = WebDAVService.instance.username ?? '点击连接';
|
final username = WebDAVService.instance.username ?? '点击连接';
|
||||||
final audioService = context.watch<AudioService>();
|
|
||||||
final showMiniBar = audioService.currentSong != null;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0E1211),
|
backgroundColor: const Color(0xFF0E1211),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// ---- 固定标题 "清听" ----
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -344,8 +320,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ---- 滚动区域 ----
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
@@ -356,7 +330,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
)
|
)
|
||||||
: const ClampingScrollPhysics(),
|
: const ClampingScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
// ---- 媒体库 ----
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _mediaOpacity,
|
opacity: _mediaOpacity,
|
||||||
@@ -551,8 +524,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ---- "我的收藏" Sticky Header ----
|
|
||||||
SliverPersistentHeader(
|
SliverPersistentHeader(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
delegate: _StickyHeaderDelegate(
|
delegate: _StickyHeaderDelegate(
|
||||||
@@ -578,13 +549,11 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ---- 收藏列表 ----
|
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
bottom: showMiniBar ? 80.0 : 20.0,
|
bottom: 20,
|
||||||
),
|
),
|
||||||
sliver: _isLoading
|
sliver: _isLoading
|
||||||
? const SliverFillRemaining(
|
? const SliverFillRemaining(
|
||||||
|
|||||||
+28
-24
@@ -1,19 +1,10 @@
|
|||||||
// lib/pages/player_page.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import 'playlist_page.dart';
|
|
||||||
|
|
||||||
class PlayerPage extends StatefulWidget {
|
class PlayerPage extends StatefulWidget {
|
||||||
final VoidCallback onClose;
|
const PlayerPage({super.key});
|
||||||
final VoidCallback onOpenPlaylist;
|
|
||||||
|
|
||||||
const PlayerPage({
|
|
||||||
super.key,
|
|
||||||
required this.onClose,
|
|
||||||
required this.onOpenPlaylist,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerPage> createState() => _PlayerPageState();
|
State<PlayerPage> createState() => _PlayerPageState();
|
||||||
@@ -37,22 +28,33 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
if (song == null) {
|
if (song == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0E1211),
|
backgroundColor: const Color(0xFF0E1211),
|
||||||
body: Center(
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'正在播放',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Icon(Icons.music_off, size: 64, color: Colors.grey),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
'没有正在播放的歌曲',
|
'没有正在播放的歌曲',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
ElevatedButton(
|
Text(
|
||||||
onPressed: widget.onClose,
|
'请先在首页点击一首歌曲',
|
||||||
style: ElevatedButton.styleFrom(
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
backgroundColor: const Color(0xFFB8D4D0),
|
|
||||||
foregroundColor: Colors.black87,
|
|
||||||
),
|
|
||||||
child: const Text('返回'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -83,7 +85,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||||
onPressed: widget.onClose,
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'正在播放',
|
'正在播放',
|
||||||
@@ -279,8 +281,8 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: const Color(0xFFB8D4D0),
|
color: Color(0xFFB8D4D0),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -301,7 +303,9 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: widget.onOpenPlaylist,
|
onPressed: () {
|
||||||
|
navigatorKey.currentState?.pushNamed('/playlist');
|
||||||
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.playlist_play_outlined,
|
Icons.playlist_play_outlined,
|
||||||
color: Color(0xFFB8D4D0),
|
color: Color(0xFFB8D4D0),
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
// lib/pages/playlist_page.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
|
|
||||||
class PlaylistPage extends StatelessWidget {
|
class PlaylistPage extends StatelessWidget {
|
||||||
final VoidCallback onClose;
|
const PlaylistPage({super.key});
|
||||||
|
|
||||||
const PlaylistPage({
|
|
||||||
super.key,
|
|
||||||
required this.onClose,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -33,7 +27,7 @@ class PlaylistPage extends StatelessWidget {
|
|||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new),
|
icon: const Icon(Icons.arrow_back_ios_new),
|
||||||
onPressed: onClose,
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -60,7 +54,7 @@ class PlaylistPage extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
service.clearQueue();
|
service.clearQueue();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
onClose();
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'清空',
|
'清空',
|
||||||
@@ -146,7 +140,7 @@ class PlaylistPage extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
service.setQueue(queue, startIndex: index);
|
service.setQueue(queue, startIndex: index);
|
||||||
onClose();
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
// lib/pages/shell_page.dart
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import '../services/audio_service.dart';
|
|
||||||
import '../widgets/global_mini_player.dart';
|
|
||||||
import 'home_page.dart';
|
|
||||||
import 'player_page.dart';
|
|
||||||
import 'playlist_page.dart';
|
|
||||||
|
|
||||||
class ShellPage extends StatefulWidget {
|
|
||||||
const ShellPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ShellPage> createState() => _ShellPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShellPageState extends State<ShellPage> {
|
|
||||||
// ✅ 从 PlayerPage 打开 PlaylistPage
|
|
||||||
void _openPlaylistFromPlayer() {
|
|
||||||
Navigator.pushNamed(context, '/playlist');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 关闭 PlayerPage
|
|
||||||
void _closePlayer() {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 关闭 PlaylistPage
|
|
||||||
void _closePlaylist() {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: const Color(0xFF0E1211),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
// ---- 页面内容(Navigator) ----
|
|
||||||
Navigator(
|
|
||||||
initialRoute: '/',
|
|
||||||
onGenerateRoute: (settings) {
|
|
||||||
switch (settings.name) {
|
|
||||||
case '/':
|
|
||||||
return _buildPageRoute(
|
|
||||||
const HomePage(),
|
|
||||||
settings: settings,
|
|
||||||
);
|
|
||||||
case '/player':
|
|
||||||
return _buildPageRoute(
|
|
||||||
PlayerPage(
|
|
||||||
onClose: _closePlayer,
|
|
||||||
onOpenPlaylist: _openPlaylistFromPlayer,
|
|
||||||
),
|
|
||||||
settings: settings,
|
|
||||||
fromBottom: true,
|
|
||||||
);
|
|
||||||
case '/playlist':
|
|
||||||
return _buildPageRoute(
|
|
||||||
PlaylistPage(
|
|
||||||
onClose: _closePlaylist,
|
|
||||||
),
|
|
||||||
settings: settings,
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// ---- MiniPlayer(覆盖在最上层) ----
|
|
||||||
const Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: GlobalMiniPlayer(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PageRouteBuilder _buildPageRoute(
|
|
||||||
Widget page, {
|
|
||||||
required RouteSettings settings,
|
|
||||||
bool fromBottom = false,
|
|
||||||
}) {
|
|
||||||
return PageRouteBuilder(
|
|
||||||
settings: settings,
|
|
||||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
if (fromBottom) {
|
|
||||||
// 播放页:从底部滑入
|
|
||||||
final offset = Tween<Offset>(
|
|
||||||
begin: const Offset(0, 1),
|
|
||||||
end: Offset.zero,
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: animation,
|
|
||||||
curve: Curves.easeOutCubic,
|
|
||||||
));
|
|
||||||
return SlideTransition(position: offset, child: child);
|
|
||||||
} else {
|
|
||||||
// 其他页面:淡入
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
transitionDuration: const Duration(milliseconds: 300),
|
|
||||||
reverseTransitionDuration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+117
-102
@@ -1,128 +1,143 @@
|
|||||||
// lib/widgets/global_mini_player.dart
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/audio_service.dart';
|
import '../services/audio_service.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
|
||||||
class GlobalMiniPlayer extends StatelessWidget {
|
class GlobalMiniPlayer extends StatelessWidget {
|
||||||
const GlobalMiniPlayer({super.key});
|
const GlobalMiniPlayer({super.key});
|
||||||
|
|
||||||
// ✅ 保留静态回调
|
|
||||||
static VoidCallback? onOpenPlayerPage;
|
|
||||||
static VoidCallback? onOpenPlaylistPage;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final service = context.watch<AudioService>();
|
final audioService = context.watch<AudioService>();
|
||||||
final song = service.currentSong;
|
final song = audioService.currentSong;
|
||||||
|
|
||||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
return GestureDetector(
|
return Stack(
|
||||||
onTap: () {
|
clipBehavior: Clip.none,
|
||||||
if (song != null && onOpenPlayerPage != null) {
|
children: [
|
||||||
onOpenPlayerPage!();
|
// 底层遮罩
|
||||||
}
|
Positioned(
|
||||||
},
|
left: 0,
|
||||||
child: Container(
|
right: 0,
|
||||||
height: 56 + bottomPadding,
|
bottom: 0,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: const Color(0xFF1A1F1E),
|
height: 56 + bottomPadding,
|
||||||
boxShadow: [
|
color: const Color(0xFF1A1F1E),
|
||||||
BoxShadow(
|
),
|
||||||
color: Colors.black.withOpacity(0.4),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, -4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
// 上层主体
|
||||||
top: false,
|
Positioned(
|
||||||
bottom: true,
|
left: 0,
|
||||||
child: Padding(
|
right: 0,
|
||||||
padding: const EdgeInsets.only(bottom: 0),
|
bottom: bottomPadding,
|
||||||
child: Row(
|
child: ClipRRect(
|
||||||
children: [
|
borderRadius: const BorderRadius.vertical(
|
||||||
const SizedBox(width: 12),
|
bottom: Radius.circular(16),
|
||||||
Container(
|
),
|
||||||
width: 40,
|
child: Container(
|
||||||
height: 40,
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
color: const Color(0xFF1A1F1E),
|
||||||
color: song != null
|
// ⭐ 用 Material 包裹 InkWell,获得更好的点击反馈
|
||||||
? const Color(0xFF2A3332)
|
child: Material(
|
||||||
: Colors.grey[800],
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(4),
|
child: InkWell(
|
||||||
),
|
onTap: () {
|
||||||
child: Icon(
|
if (song != null) {
|
||||||
song != null ? Icons.music_note : Icons.music_off,
|
navigatorKey.currentState?.pushNamed('/player');
|
||||||
color: song != null ? Colors.white38 : Colors.grey[600],
|
}
|
||||||
size: 20,
|
},
|
||||||
),
|
// ⭐ 让整个区域都响应点击,包括空白部分
|
||||||
),
|
highlightColor: Colors.white.withOpacity(0.05),
|
||||||
const SizedBox(width: 12),
|
splashColor: Colors.white.withOpacity(0.1),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const SizedBox(width: 12),
|
||||||
song != null ? song.title : '清听',
|
Container(
|
||||||
style: const TextStyle(
|
width: 40,
|
||||||
fontSize: 14,
|
height: 40,
|
||||||
fontWeight: FontWeight.w500,
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
|
||||||
decoration: TextDecoration.none,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
song != null ? song.artist : '未播放',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: song != null
|
color: song != null
|
||||||
? Colors.grey[400]
|
? const Color(0xFF2A3332)
|
||||||
: Colors.grey[600],
|
: Colors.grey[800],
|
||||||
decoration: TextDecoration.none,
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
song != null ? Icons.music_note : Icons.music_off,
|
||||||
|
color:
|
||||||
|
song != null ? Colors.white38 : Colors.grey[600],
|
||||||
|
size: 20,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
song != null ? song.title : '清听',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
// ⭐ 强制去掉下划线
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
song != null ? song.artist : '未播放',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: song != null
|
||||||
|
? Colors.grey[400]
|
||||||
|
: Colors.grey[600],
|
||||||
|
// ⭐ 强制去掉下划线
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
song != null && audioService.isPlaying
|
||||||
|
? Icons.pause
|
||||||
|
: Icons.play_arrow,
|
||||||
|
color: song != null ? Colors.white : Colors.grey[600],
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (song != null) {
|
||||||
|
audioService.togglePlay();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.playlist_play_outlined,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (song != null) {
|
||||||
|
navigatorKey.currentState?.pushNamed('/playlist');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
),
|
||||||
icon: Icon(
|
|
||||||
song != null && service.isPlaying
|
|
||||||
? Icons.pause
|
|
||||||
: Icons.play_arrow,
|
|
||||||
color: song != null ? Colors.white : Colors.grey[600],
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (song != null) {
|
|
||||||
service.togglePlay();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.playlist_play_outlined,
|
|
||||||
color: Colors.grey[500],
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
// ✅ 使用静态回调,而不是 Navigator.pushNamed
|
|
||||||
if (onOpenPlaylistPage != null) {
|
|
||||||
onOpenPlaylistPage!();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 917 B |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,46 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!--
|
|
||||||
If you are serving your web app in a path other than the root, change the
|
|
||||||
href value below to reflect the base path you are serving from.
|
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
|
||||||
it to work correctly.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
|
||||||
the `--base-href` argument provided to `flutter build`.
|
|
||||||
-->
|
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
||||||
<meta name="description" content="A new Flutter project.">
|
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
||||||
<meta name="apple-mobile-web-app-title" content="qt_player">
|
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
|
||||||
|
|
||||||
<!-- Favicon -->
|
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
|
||||||
|
|
||||||
<title>qt_player</title>
|
|
||||||
<link rel="manifest" href="manifest.json">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!--
|
|
||||||
You can customize the "flutter_bootstrap.js" script.
|
|
||||||
This is useful to provide a custom configuration to the Flutter loader
|
|
||||||
or to give the user feedback during the initialization process.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://docs.flutter.dev/platform-integration/web/initialization
|
|
||||||
-->
|
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "qt_player",
|
|
||||||
"short_name": "qt_player",
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#0175C2",
|
|
||||||
"theme_color": "#0175C2",
|
|
||||||
"description": "A new Flutter project.",
|
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"prefer_related_applications": false,
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-maskable-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-maskable-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user