Files
MookNote/lib/pages/main_content_page.dart
DelLevin-Home 9fd80f83c8 代码优化
2026-06-30 15:12:11 +08:00

458 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/app_provider.dart';
import '../utils/user_prefs.dart';
import '../utils/sync/webdav_service.dart';
import 'movies/movie_tab_page.dart';
import 'book/book_tab_page.dart';
import 'note/note_tab_page.dart';
import 'note_plus/note_plus_tab_page.dart';
import 'online_search/search_page.dart';
import 'online_search/online_search_page.dart';
import 'sync/webdav_sync_page.dart';
/// 主内容页 - 观影/阅读/笔记标签页PageView 滑动切换)
class MainContentPage extends StatefulWidget {
const MainContentPage({super.key});
@override
State<MainContentPage> createState() => _MainContentPageState();
}
class _MainContentPageState extends State<MainContentPage> {
final UserPrefs _userPrefs = UserPrefs();
bool _showMovieTab = true;
bool _showBookTab = true;
bool _showNoteTab = true;
bool _showNotePlusTab = false;
late PageController _pageController;
bool _isTabTap = false;
bool _syncScheduled = false;
@override
void initState() {
super.initState();
_loadTabSettings();
_pageController = PageController(initialPage: 0);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
void _loadTabSettings() {
setState(() {
_showMovieTab = _userPrefs.showMovieTab;
_showBookTab = _userPrefs.showBookTab;
_showNoteTab = _userPrefs.showNoteTab;
_showNotePlusTab = _userPrefs.showNotePlusTab;
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadTabSettings();
}
List<_TabItem> get _enabledTabs {
final tabs = <_TabItem>[];
if (_showMovieTab) tabs.add(_TabItem('影视', 0));
if (_showBookTab) tabs.add(_TabItem('阅读', 1));
if (_showNoteTab) tabs.add(_TabItem('笔记', 2));
if (_showNotePlusTab) tabs.add(_TabItem('Plus', 3));
return tabs;
}
int _mapToEnabledTabIndex(int originalIndex) {
final tabs = _enabledTabs;
for (int i = 0; i < tabs.length; i++) {
if (tabs[i].originalIndex == originalIndex) return i;
}
return 0;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildAppBar(context),
_buildTabBar(context),
Expanded(child: _buildTabContent()),
],
);
}
// ─── AppBar ──────────────────────────────────────────
Widget _buildAppBar(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
return AppBar(
titleSpacing: 8,
leadingWidth: 44,
title: Text(_getAppBarTitle(provider)),
actionsPadding: const EdgeInsets.only(right: 4),
actions: [
_buildCloudSyncButton(context),
IconButton(
icon: const Icon(Icons.search),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SearchPage())),
),
if (UserPrefs().enhancedSearchEnabled)
IconButton(
icon: Stack(
clipBehavior: Clip.none,
children: [
const Icon(Icons.search, size: 22),
Positioned(
right: -3,
top: -3,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: colors.surface,
shape: BoxShape.circle,
),
child: Icon(Icons.add, size: 10, color: colors.onSurface),
),
),
],
),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const OnlineSearchPage())),
),
],
);
},
);
}
String _getAppBarTitle(AppProvider provider) {
switch (provider.mainTabIndex) {
case 0: return '影视';
case 1: return '阅读';
case 2: return '笔记';
case 3: return 'Note Plus';
default: return 'MookNote';
}
}
// ─── 云备份 ──────────────────────────────────────────
Widget _buildCloudSyncButton(BuildContext context) {
return IconButton(
icon: const Icon(Icons.cloud_sync_outlined),
tooltip: '云备份',
onPressed: () => _showCloudSheet(context),
);
}
void _showCloudSheet(BuildContext context) async {
final colors = Theme.of(context).colorScheme;
final hasConfig = (await WebDAVService.instance.getConfig()) != null;
if (!mounted) return;
showModalBottomSheet(
context: context,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
builder: (ctx) {
final bc = Theme.of(ctx).colorScheme;
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 16),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Center(child: Container(
width: 36, height: 4, margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(color: bc.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2)),
)),
Text('云备份', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: bc.onSurface)),
const SizedBox(height: 14),
_cloudCard(icon: Icons.cloud_upload_outlined, title: '上传数据', desc: hasConfig ? '将本地数据同步到云端' : '请先配置 WebDAV 服务器', enabled: hasConfig, onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.upload); } : null, colors: bc),
const SizedBox(height: 8),
_cloudCard(icon: Icons.cloud_download_outlined, title: '下载数据', desc: hasConfig ? '从云端恢复数据到本地' : '请先配置 WebDAV 服务器', enabled: hasConfig, onTap: hasConfig ? () { Navigator.pop(ctx); _performSync(context, SyncDirection.download); } : null, colors: bc),
const SizedBox(height: 8),
_cloudCard(icon: Icons.settings_outlined, title: 'WebDAV 设置', desc: '配置服务器地址与认证信息', enabled: true, onTap: () { Navigator.pop(ctx); Navigator.push(context, MaterialPageRoute(builder: (_) => const WebDAVSyncPage())); }, colors: bc),
]),
),
);
},
);
}
Widget _cloudCard({required IconData icon, required String title, required String desc, required bool enabled, required VoidCallback? onTap, required ColorScheme colors}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: enabled ? colors.primary.withValues(alpha: 0.04) : colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: enabled ? colors.primary.withValues(alpha: 0.1) : colors.outlineVariant, width: 0.5),
),
child: Row(children: [
Container(width: 36, height: 36, decoration: BoxDecoration(color: enabled ? colors.primary.withValues(alpha: 0.08) : colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 20, color: enabled ? colors.primary : colors.onSurface.withValues(alpha: 0.18))),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: enabled ? colors.onSurface : colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 1),
Text(desc, style: TextStyle(fontSize: 11, color: enabled ? colors.onSurface.withValues(alpha: 0.4) : colors.onSurface.withValues(alpha: 0.2))),
])),
Icon(Icons.chevron_right, size: 20, color: enabled ? colors.onSurface.withValues(alpha: 0.15) : colors.onSurface.withValues(alpha: 0.08)),
]),
),
);
}
Future<void> _performSync(BuildContext context, SyncDirection direction) async {
final colors = Theme.of(context).colorScheme;
final config = await WebDAVService.instance.getConfig();
if (config == null) {
if (context.mounted) _showResultDialog(context, title: '同步失败', message: '请先配置 WebDAV 服务器', isSuccess: false);
return;
}
if (context.mounted) {
showDialog(context: context, barrierDismissible: false, builder: (_) => Center(child: CircularProgressIndicator(color: colors.primary)));
}
final result = await WebDAVService.instance.syncData(direction: direction);
if (context.mounted) Navigator.pop(context);
if (result.success && result.needReload && context.mounted) {
final provider = context.read<AppProvider>();
await provider.loadMovies();
await provider.loadBooks();
await provider.loadNotes();
}
if (context.mounted) {
_showResultDialog(context, title: result.success ? '同步成功' : '同步失败', message: result.message.isNotEmpty ? result.message : (result.success ? '同步成功' : '同步失败'), isSuccess: result.success, details: {'uploaded': result.uploadedFiles + result.uploadedImages, 'downloaded': result.downloadedFiles + result.downloadedImages});
}
}
void _showResultDialog(BuildContext context, {required String title, required String message, required bool isSuccess, Map<String, dynamic>? details}) {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: colors.surface, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Row(children: [
Container(width: 40, height: 40, decoration: BoxDecoration(color: isSuccess ? const Color(0xFFE8F5E9) : const Color(0xFFFFEBEE), borderRadius: BorderRadius.circular(10)), child: Icon(isSuccess ? Icons.check_circle : Icons.error, color: isSuccess ? const Color(0xFF4CAF50) : const Color(0xFFE57373), size: 24)),
const SizedBox(width: 12),
Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(message, style: TextStyle(fontSize: 14, color: colors.onSurface.withValues(alpha: 0.6), height: 1.5)),
if (details != null) ...[const SizedBox(height: 16), Container(padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: colors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
if (details['uploaded'] != null) _detailRow('上传文件', '${details['uploaded']}', colors),
if (details['downloaded'] != null) _detailRow('下载文件', '${details['downloaded']}', colors),
]))],
]),
actions: [ElevatedButton(onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom(backgroundColor: colors.primary, foregroundColor: colors.onPrimary, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), child: const Text('确定'))],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
Widget _detailRow(String label, String value, ColorScheme colors) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(label, style: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.4))),
Text(value, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
]),
);
// ─── Tab 栏 + 指示条 ─────────────────────────────────
Widget _buildTabBar(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final colors = Theme.of(context).colorScheme;
final tabs = _enabledTabs;
final safeIndex = _mapToEnabledTabIndex(provider.mainTabIndex).clamp(0, tabs.length - 1);
return Container(
color: colors.surface,
padding: const EdgeInsets.only(top: 4),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: tabs.asMap().entries.map((entry) {
final idx = entry.key;
final tab = entry.value;
final selected = idx == safeIndex;
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_isTabTap = true;
_pageController.animateToPage(idx, duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
provider.setMainTabIndex(tab.originalIndex);
Future.delayed(const Duration(milliseconds: 400), () => _isTabTap = false);
},
onLongPress: tab.label == '影视'
? () => _showSortMenu(context, '影视排序', UserPrefs().movieSortMode, [
(0, '按更新时间排序', Icons.update),
(1, '按创建时间排序', Icons.calendar_today_outlined),
(2, '按评分排序', Icons.star_outline),
], (v) { UserPrefs().setMovieSortMode(v); context.read<AppProvider>().loadMovies(); })
: tab.label == '阅读'
? () => _showSortMenu(context, '书籍排序', UserPrefs().bookSortMode, [
(0, '按更新时间排序', Icons.update),
(1, '按创建时间排序', Icons.calendar_today_outlined),
(2, '按评分排序', Icons.star_outline),
], (v) { UserPrefs().setBookSortMode(v); context.read<AppProvider>().loadBooks(); })
: tab.label == '笔记'
? () => _showSortMenu(context, '笔记排序', UserPrefs().noteSortMode, [
(0, '按更新时间排序', Icons.update),
(1, '按创建时间排序', Icons.calendar_today_outlined),
], (v) { UserPrefs().setNoteSortMode(v); context.read<AppProvider>().loadNotes(); })
: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [
Icon(_tabIcon(tab.label), size: 18, color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3)),
const SizedBox(width: 5),
Text(tab.label, textAlign: TextAlign.center, style: TextStyle(fontSize: 15, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.3))),
]),
),
),
);
}).toList(),
),
),
AnimatedBuilder(
animation: _pageController,
builder: (context, _) {
final page = _pageController.hasClients ? _pageController.page ?? 0.0 : safeIndex.toDouble();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: LayoutBuilder(
builder: (context, constraints) {
final tabWidth = tabs.isNotEmpty ? constraints.maxWidth / tabs.length : 0.0;
return SizedBox(height: 2.5, child: Stack(children: [
Positioned(
left: page * tabWidth,
top: 0,
width: tabWidth,
child: Container(height: 2.5, decoration: BoxDecoration(color: colors.primary, borderRadius: BorderRadius.circular(2))),
),
]));
},
),
);
},
),
]),
);
},
);
}
void _showSortMenu(BuildContext context, String title, int current, List<(int, String, IconData)> options, ValueChanged<int> onSelected) {
final colors = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: colors.surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => SafeArea(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Container(width: 36, height: 4, margin: const EdgeInsets.only(top: 12, bottom: 16),
decoration: BoxDecoration(color: colors.onSurface.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(2))),
Align(alignment: Alignment.centerLeft, child: Padding(padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface)))),
const SizedBox(height: 8),
for (int i = 0; i < options.length; i++) ...[
if (i > 0) Divider(height: 0.5, indent: 20, endIndent: 20, color: colors.outlineVariant),
_sortOption(ctx, options[i].$1, options[i].$2, options[i].$3, current, colors, onSelected),
],
const SizedBox(height: 12),
]),
),
);
}
Widget _sortOption(BuildContext ctx, int value, String label, IconData icon, int current, ColorScheme colors, ValueChanged<int> onSelected) {
final selected = current == value;
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: Container(width: 36, height: 36, decoration: BoxDecoration(color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(10)),
child: Icon(icon, size: 20, color: selected ? colors.primary : colors.onSurface.withValues(alpha: 0.6))),
title: Text(label, style: TextStyle(fontSize: 14, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, color: colors.onSurface)),
trailing: selected ? Icon(Icons.check, size: 20, color: colors.primary) : null,
onTap: () {
Navigator.pop(ctx);
onSelected(value);
},
);
}
// ─── PageView 内容区 ───
Widget _buildTabContent() {
return Consumer<AppProvider>(
builder: (context, provider, child) {
final tabs = _enabledTabs;
final safeIndex = _mapToEnabledTabIndex(provider.mainTabIndex).clamp(0, tabs.length - 1);
// 从其他页面返回时,修正 PageView 页面与 tab 的一致性
if (!_syncScheduled) {
_syncScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncScheduled = false;
if (!mounted || !_pageController.hasClients) return;
final currentPage = _pageController.page?.round() ?? 0;
if (currentPage != safeIndex) {
_pageController.jumpToPage(safeIndex);
}
});
}
if (_isTabTap && _pageController.hasClients) {
_isTabTap = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_pageController.hasClients) return;
_pageController.animateToPage(safeIndex, duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
return PageView(
controller: _pageController,
onPageChanged: (index) {
if (!_isTabTap && index < tabs.length) {
provider.setMainTabIndex(tabs[index].originalIndex);
}
},
children: [
if (_showMovieTab) const MovieTabPage(),
if (_showBookTab) const BookTabPage(),
if (_showNoteTab) const NoteTabPage(),
if (_showNotePlusTab) const NotePlusTabPage(),
],
);
},
);
}
IconData _tabIcon(String label) {
switch (label) {
case '影视': return Icons.movie_outlined;
case '阅读': return Icons.menu_book_outlined;
case '笔记': return Icons.note_outlined;
case 'Plus': return Icons.edit_note;
default: return Icons.circle;
}
}
}
class _TabItem {
final String label;
final int originalIndex;
_TabItem(this.label, this.originalIndex);
}