// lib/pages/settings_page.dart import 'dart:io'; import 'package:android_intent_plus/android_intent.dart'; import 'package:android_intent_plus/flag.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import '../database/song_database.dart'; import '../services/audio_service.dart'; import '../services/webdav_service.dart'; import '../utils/file_provider_utils.dart'; /// 与 pubspec.yaml 的 version 保持同步(发版时一起改) const String kAppVersion = '1.1.0-dev.1'; const int kAppBuild = 2; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { bool _loading = true; int _songRows = 0; int _metadataRows = 0; int _favoriteRows = 0; int _artworkCount = 0; int _artworkBytes = 0; int _tempCount = 0; int _tempBytes = 0; @override void initState() { super.initState(); _loadStats(); } Future _loadStats() async { setState(() => _loading = true); try { final db = await SongDatabase().database; final songRows = (await db.rawQuery('SELECT COUNT(*) AS c FROM songs')).first['c']; final metaRows = (await db.rawQuery( 'SELECT COUNT(*) AS c FROM metadata_cache')) .first['c']; final favRows = (await db.rawQuery('SELECT COUNT(*) AS c FROM favorites')).first['c']; var artworkCount = 0, artworkBytes = 0; final docs = await getApplicationDocumentsDirectory(); final artDir = Directory('${docs.path}/artworks'); if (await artDir.exists()) { await for (final entity in artDir.list()) { if (entity is File) { artworkCount++; artworkBytes += await entity.length(); } } } var tempCount = 0, tempBytes = 0; final tmp = await getTemporaryDirectory(); if (await tmp.exists()) { await for (final entity in tmp.list()) { if (entity is File && entity.path.contains('/metadata_') && entity.path.endsWith('.part')) { tempCount++; tempBytes += await entity.length(); } } } if (!mounted) return; setState(() { _songRows = songRows as int? ?? 0; _metadataRows = metaRows as int? ?? 0; _favoriteRows = favRows as int? ?? 0; _artworkCount = artworkCount; _artworkBytes = artworkBytes; _tempCount = tempCount; _tempBytes = tempBytes; _loading = false; }); } catch (e) { debugPrint('⚠️ [Settings] 统计失败: $e'); if (mounted) setState(() => _loading = false); } } Future _clearMetadataCache() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1A1F1E), title: const Text('清理元数据缓存', style: TextStyle(color: Colors.white)), content: const Text( '会删除歌曲标签缓存和封面文件,歌单与收藏不受影响。\n下次播放时重新抓取。', style: TextStyle(color: Colors.grey), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('清理', style: TextStyle(color: Colors.redAccent)), ), ], ), ); if (confirmed != true) return; try { final db = await SongDatabase().database; await db.delete('songs'); await db.delete('metadata_cache'); final docs = await getApplicationDocumentsDirectory(); final artDir = Directory('${docs.path}/artworks'); if (await artDir.exists()) { await artDir.delete(recursive: true); } final tmp = await getTemporaryDirectory(); if (await tmp.exists()) { await for (final entity in tmp.list()) { if (entity is File && entity.path.contains('/metadata_') && entity.path.endsWith('.part')) { await entity.delete(); } } } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('元数据缓存已清理')), ); } await _loadStats(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('清理失败: $e'))); } } } Future _exportDiagnostics() async { try { final now = DateTime.now(); final docs = await getApplicationDocumentsDirectory(); final file = File('${docs.path}/diagnostics-${now.millisecondsSinceEpoch}.txt'); final song = AudioService().currentSong; final buffer = StringBuffer() ..writeln('清听 诊断信息') ..writeln('时间: ${now.toIso8601String()}') ..writeln('版本: $kAppVersion ($kAppBuild)') ..writeln('平台: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}') ..writeln() ..writeln('[连接]') ..writeln('baseUrl: ${WebDAVService.instance.baseUrl ?? '(未配置)'}') ..writeln('username: ${WebDAVService.instance.username ?? '(未配置)'}') ..writeln('connected: ${WebDAVService.instance.isConnected}') ..writeln() ..writeln('[数据]') ..writeln('songs: $_songRows') ..writeln('metadata_cache: $_metadataRows') ..writeln('favorites: $_favoriteRows') ..writeln('artwork 文件: $_artworkCount 个 / ${_formatBytes(_artworkBytes)}') ..writeln('临时探测文件: $_tempCount 个 / ${_formatBytes(_tempBytes)}') ..writeln() ..writeln('[当前播放]') ..writeln('song: ${song?.title ?? '(无)'} - ${song?.artist ?? ''}') ..writeln('path: ${song?.id ?? ''}') ..writeln('position: ${AudioService().position}') ..writeln('duration: ${AudioService().duration}') ..writeln('lastError: ${AudioService().lastPlaybackError ?? '(无)'}'); await file.writeAsString(buffer.toString()); if (!mounted) return; final uri = await FileProviderUtils.getContentUri(file); if (uri != null) { try { final intent = AndroidIntent( action: 'android.intent.action.SEND', type: 'text/plain', arguments: { 'android.intent.extra.STREAM': uri.toString(), 'android.intent.extra.SUBJECT': '清听诊断信息', }, flags: const [Flag.FLAG_GRANT_READ_URI_PERMISSION], ); await intent.launch(); return; } catch (e) { debugPrint('⚠️ [Settings] 分享失败: $e'); } } if (mounted) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF1A1F1E), title: const Text('诊断信息已导出', style: TextStyle(color: Colors.white)), content: SelectableText( file.path, style: const TextStyle(color: Colors.grey, fontSize: 12), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('好'), ), ], ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('导出失败: $e'))); } } } Future _testConnection() async { try { await WebDAVService.instance.listDirectory(path: '/'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('连接正常')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('连接失败: $e')), ); } } } Future _logout() async { await WebDAVService.instance.clearCredentials(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('已退出登录,下次进入会要求重新配置')), ); } } String _formatBytes(int bytes) { if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB'; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0E1211), appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, title: const Text('设置'), ), body: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ _sectionTitle('缓存'), ListTile( leading: const Icon(Icons.cleaning_services_outlined, color: Color(0xFF7C9A9E)), title: const Text('元数据缓存'), subtitle: Text( '标签 $_songRows 首 · 封面 $_artworkCount 张 ' '(${_formatBytes(_artworkBytes)}) · 临时文件 ${_formatBytes(_tempBytes)}', style: const TextStyle(fontSize: 12), ), trailing: const Icon(Icons.chevron_right, color: Colors.grey), onTap: _clearMetadataCache, ), _sectionTitle('诊断'), ListTile( leading: const Icon(Icons.bug_report_outlined, color: Color(0xFF7C9A9E)), title: const Text('导出诊断信息'), subtitle: const Text( '版本、连接状态、缓存统计、当前播放与最近错误', style: TextStyle(fontSize: 12), ), trailing: const Icon(Icons.ios_share, color: Colors.grey), onTap: _exportDiagnostics, ), _sectionTitle('连接'), ListTile( leading: const Icon(Icons.cloud_outlined, color: Color(0xFF7C9A9E)), title: Text(WebDAVService.instance.baseUrl ?? '(未配置)'), subtitle: Text( '用户 ${WebDAVService.instance.username ?? '-'} · ' '${WebDAVService.instance.isConnected ? '已连接' : '未连接'}', style: const TextStyle(fontSize: 12), ), ), ListTile( leading: const Icon(Icons.wifi_tethering, color: Color(0xFF7C9A9E)), title: const Text('测试连接'), onTap: _testConnection, ), ListTile( leading: const Icon(Icons.logout, color: Colors.redAccent), title: const Text('退出登录'), subtitle: const Text( '清除本机保存的 WebDAV 账号', style: TextStyle(fontSize: 12), ), onTap: _logout, ), _sectionTitle('关于'), ListTile( leading: const Icon(Icons.info_outline, color: Color(0xFF7C9A9E)), title: const Text('清听'), subtitle: const Text( '版本 $kAppVersion ($kAppBuild)\n' '播放内核 media_kit (libmpv, LGPL/GPL) · UI Flutter (BSD)', style: TextStyle(fontSize: 12), ), ), const SizedBox(height: 24), ], ), ); } Widget _sectionTitle(String text) { return Padding( padding: const EdgeInsets.fromLTRB(4, 20, 4, 8), child: Text( text, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFB8D4D0), letterSpacing: 1.2, ), ), ); } }