@@ -10,7 +10,7 @@ import 'note/note_tab_page.dart';
import ' search_page.dart ' ;
import ' sync/webdav_sync_page.dart ' ;
/// 主内容页 - 观影/阅读/笔记标签页
/// 主内容页 - 观影/阅读/笔记标签页( PageView 滑动切换)
class MainContentPage extends StatefulWidget {
const MainContentPage ( { super . key } ) ;
@@ -25,10 +25,20 @@ class _MainContentPageState extends State<MainContentPage> {
bool _showBookTab = true ;
bool _showNoteTab = true ;
late PageController _pageController ;
bool _isTabTap = false ; // 防止点击 Tab 和滑动互斥
@ override
void initState ( ) {
super . initState ( ) ;
_loadTabSettings ( ) ;
_pageController = PageController ( initialPage: 0 ) ;
}
@ override
void dispose ( ) {
_pageController . dispose ( ) ;
super . dispose ( ) ;
}
void _loadTabSettings ( ) {
@@ -118,32 +128,11 @@ class _MainContentPageState extends State<MainContentPage> {
) ) ,
Text ( ' 云备份 ' , style: TextStyle ( fontSize: 18 , fontWeight: FontWeight . w600 , color: bc . onSurface ) ) ,
const SizedBox ( height: 20 ) ,
_cloudCard (
icon: Icons . cloud_upload_outlined ,
title: ' 上传数据 ' ,
desc: hasConfig ? ' 将本地数据同步到云端 ' : ' 请先配置 WebDAV 服务器 ' ,
onTap: hasConfig ? ( ) { Navigator . pop ( ctx ) ; _performSync ( context , SyncDirection . upload ) ; } : null ,
enabled: hasConfig ,
colors: bc ,
) ,
_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: 12 ) ,
_cloudCard (
icon: Icons . cloud_download_outlined ,
title: ' 下载数据 ' ,
desc: hasConfig ? ' 从云端恢复数据到本地 ' : ' 请先配置 WebDAV 服务器 ' ,
onTap: hasConfig ? ( ) { Navigator . pop ( ctx ) ; _performSync ( context , SyncDirection . download ) ; } : null ,
enabled: hasConfig ,
colors: bc ,
) ,
_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: 12 ) ,
_cloudCard (
icon: Icons . settings_outlined ,
title: ' WebDAV 设置 ' ,
desc: ' 配置服务器地址与认证信息 ' ,
onTap: ( ) { Navigator . pop ( ctx ) ; Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > const WebDAVSyncPage ( ) ) ) ; } ,
enabled: true ,
colors: bc ,
) ,
_cloudCard ( icon: Icons . settings_outlined , title: ' WebDAV 设置 ' , desc: ' 配置服务器地址与认证信息 ' , enabled: true , onTap: ( ) { Navigator . pop ( ctx ) ; Navigator . push ( context , MaterialPageRoute ( builder: ( _ ) = > const WebDAVSyncPage ( ) ) ) ; } , colors: bc ) ,
] ) ,
) ,
) ;
@@ -162,14 +151,7 @@ class _MainContentPageState extends State<MainContentPage> {
border: Border . all ( color: enabled ? colors . primary . withValues ( alpha: 0.1 ) : colors . outlineVariant , width: 0.5 ) ,
) ,
child: Row ( children: [
Container (
width: 48 , height: 48 ,
decoration: BoxDecoration (
color: enabled ? colors . primary . withValues ( alpha: 0.08 ) : colors . surfaceContainerHighest ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Icon ( icon , size: 24 , color: enabled ? colors . primary : colors . onSurface . withValues ( alpha: 0.18 ) ) ,
) ,
Container ( width: 48 , height: 48 , decoration: BoxDecoration ( color: enabled ? colors . primary . withValues ( alpha: 0.08 ) : colors . surfaceContainerHighest , borderRadius: BorderRadius . circular ( 12 ) ) , child: Icon ( icon , size: 24 , color: enabled ? colors . primary : colors . onSurface . withValues ( alpha: 0.18 ) ) ) ,
const SizedBox ( width: 16 ) ,
Expanded ( child: Column ( crossAxisAlignment: CrossAxisAlignment . start , children: [
Text ( title , style: TextStyle ( fontSize: 15 , fontWeight: FontWeight . w600 , color: enabled ? colors . onSurface : colors . onSurface . withValues ( alpha: 0.25 ) ) ) ,
@@ -201,12 +183,7 @@ class _MainContentPageState extends State<MainContentPage> {
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 } ,
) ;
_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 } ) ;
}
}
@@ -218,31 +195,18 @@ class _MainContentPageState extends State<MainContentPage> {
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 ) ,
) ,
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 ) ,
] ) ) ,
] ,
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 ( ' 确定 ' ) ,
) ,
] ,
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 ) ,
) ,
) ;
@@ -265,6 +229,10 @@ class _MainContentPageState extends State<MainContentPage> {
}
}
// ═══════════════════════════════════════════════════════════════════
// Tab 栏 + 指示条(跟随 PageView 滑动)
// ═══════════════════════════════════════════════════════════════════
Widget _buildTabBar ( BuildContext context ) {
return Consumer < AppProvider > (
builder: ( context , provider , child ) {
@@ -286,47 +254,47 @@ class _MainContentPageState extends State<MainContentPage> {
return Expanded (
child: GestureDetector (
behavior: HitTestBehavior . opaque ,
onTap: ( ) = > provider . setMainTabIndex ( tab . originalIndex ) ,
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 ) ,
) ) ,
] ,
) ,
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 ( ) ,
) ,
) ,
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 24 ) ,
child: LayoutBuild er(
builder: ( context , constraints ) {
final tabWidth = tabs . isNotEmpty ? c ons traints . maxWidth / tabs . length : 0.0 ;
return SizedBox ( height: 2.5 , child: Stack ( children: [
AnimatedPositioned (
duration: const Duration ( milliseconds: 300 ) , curve: Curves . easeInOut ,
left: safeIndex * tabWidth , top: 0 ,
width: tabWidth ,
child: Container ( height: 2.5 , decoration: BoxDecoration ( color: colors . primary , borderRadius: BorderRadius . circular ( 2 ) ) ) ,
) ,
] ) ) ;
} ,
) ,
// 指示条 — 跟随 PageView 滑动
AnimatedBuilder (
animation: _pageControll er,
builder: ( context , _ ) {
final page = _pageController . hasClients ? _pageC ontroller . 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 ) ) ) ,
) ,
] ) ) ;
} ,
) ,
) ;
} ,
) ,
] ) ,
) ;
@@ -334,23 +302,34 @@ class _MainContentPageState extends State<MainContentPage> {
) ;
}
// ═══════════════════════════════════════════════════════════════════
// PageView 内容区
// ═══════════════════════════════════════════════════════════════════
Widget _buildTabContent ( ) {
return Consumer < AppProvider > (
builder: ( context , provider , child ) {
final tabs = _enabledTabs ;
_TabItem ? currentTab ;
for ( final tab in tabs ) { if ( tab . originalIndex = = provider . mainTabIndex ) { currentTab = tab ; break ; } }
if ( currentTab = = null & & tabs . isNotEmpty ) {
currentTab = tabs . first ;
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) = > provider . setMainTabIndex ( currentTab ! . originalIndex ) ) ;
}
if ( currentTab = = null ) return const Center ( child: Text ( ' 请至少启用一个标签页 ' ) ) ;
switch ( currentTab . originalIndex ) {
case 0 : return const MovieTabPage ( ) ;
case 1 : return const BookTabPage ( ) ;
case 2 : return const NoteTabPage ( ) ;
default : return const MovieTabPage ( ) ;
final safeIndex = _mapToEnabledTabIndex ( provider . mainTabIndex ) . clamp ( 0 , tabs . length - 1 ) ;
// 同步 provider → PageView( 点击 Tab 触发)
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 ( ) ,
] ,
) ;
} ,
) ;
}
@@ -363,23 +342,6 @@ class _MainContentPageState extends State<MainContentPage> {
default : return Icons . circle ;
}
}
void _showAddDialog ( BuildContext context , AppProvider provider ) {
final colors = Theme . of ( context ) . colorScheme ;
showModalBottomSheet (
context: context , backgroundColor: colors . surface ,
shape: const RoundedRectangleBorder ( borderRadius: BorderRadius . zero ) ,
builder: ( _ ) = > SafeArea (
child: Wrap ( children: [
ListTile ( leading: Icon ( Icons . movie , color: colors . onSurface ) , title: const Text ( ' 添加观影 ' ) , onTap: ( ) { Navigator . pop ( context ) ; Navigator . pushNamed ( context , ' /movie-form ' , arguments: { ' initialStatus ' : [ ' watched ' , ' watching ' , ' want_to_watch ' ] [ provider . movieStatusIndex ] } ) ; } ) ,
Divider ( height: 0.5 , indent: 56 , color: colors . outlineVariant ) ,
ListTile ( leading: Icon ( Icons . menu_book , color: colors . onSurface ) , title: const Text ( ' 添加阅读 ' ) , onTap: ( ) { Navigator . pop ( context ) ; Navigator . pushNamed ( context , ' /book-form ' , arguments: { ' initialStatus ' : [ ' read ' , ' reading ' , ' want_to_read ' ] [ provider . bookStatusIndex ] } ) ; } ) ,
Divider ( height: 0.5 , indent: 56 , color: colors . outlineVariant ) ,
ListTile ( leading: Icon ( Icons . note , color: colors . onSurface ) , title: const Text ( ' 添加笔记 ' ) , onTap: ( ) { Navigator . pop ( context ) ; Navigator . pushNamed ( context , ' /note-form ' ) ; } ) ,
] ) ,
) ,
) ;
}
}
class _TabItem {