Files
MookNote/lib/pages/main_content_page.dart
DelLevin-Home 22797603f5 结构重构2
2026-06-25 23:57:10 +08:00

392 lines
17 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 '../utils/toast_util.dart';
import 'movies/movie_tab_page.dart';
import 'book/book_tab_page.dart';
import 'note/note_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;
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;
});
}
@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));
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 '笔记';
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);
},
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))),
),
]));
},
),
);
},
),
]),
);
},
);
}
// ─── 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) {
_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(),
],
);
},
);
}
IconData _tabIcon(String label) {
switch (label) {
case '影视': return Icons.movie_outlined;
case '阅读': return Icons.menu_book_outlined;
case '笔记': return Icons.note_outlined;
default: return Icons.circle;
}
}
}
class _TabItem {
final String label;
final int originalIndex;
_TabItem(this.label, this.originalIndex);
}