use sheet-safe mobile sidebar content

This commit is contained in:
ngfchl
2026-07-19 13:03:24 +08:00
parent 80e00c7158
commit 4606af5995
+233 -94
View File
@@ -362,90 +362,41 @@ class _WorkspacePageState extends ConsumerState<WorkspacePage> {
context: context,
side: ShadSheetSide.left,
builder: (sheetContext) => ShadSheet(
// The desktop sidebars contain Expanded/ListView regions. A left
// ShadSheet only has a minimum height by default, so make the mobile
// viewport height explicit before those flex children are laid out.
constraints: BoxConstraints.tightFor(
width: width,
height: viewport.height,
),
padding: EdgeInsets.zero,
scrollable: false,
useSafeArea: false,
child: SizedBox.expand(
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: Row(
children: [
Expanded(
child: _SegmentButton(
icon: Icons.folder_rounded,
label: '光鸭云盘',
compact: false,
selected: _mode == WorkspaceMode.cloud,
onTap: () {
Navigator.of(sheetContext).pop();
_changeMode(WorkspaceMode.cloud);
},
),
),
const SizedBox(width: 6),
Expanded(
child: _SegmentButton(
icon: Icons.movie_rounded,
label: '光鸭影视',
compact: false,
selected: _mode == WorkspaceMode.media,
onTap: () {
Navigator.of(sheetContext).pop();
_changeMode(WorkspaceMode.media);
},
),
),
],
),
),
const ShadSeparator.horizontal(),
Expanded(
child: _mode == WorkspaceMode.cloud
? _CloudSidebar(
state: ref.read(fileProvider),
width: width,
onSection: (section) {
Navigator.of(sheetContext).pop();
ref.read(fileProvider.notifier).setSection(section);
},
onSettings: () {
Navigator.of(sheetContext).pop();
_showSettings(context);
},
onSignOut: () {
Navigator.of(sheetContext).pop();
ref.read(authProvider.notifier).signOut();
},
onTool: (tool) {
Navigator.of(sheetContext).pop();
_openTool(tool);
},
)
: _MediaSidebar(
width: width,
onCreate: () {
Navigator.of(sheetContext).pop();
MediaLibraryPage.showCreateDialog(context, ref);
},
onTool: (tool) {
Navigator.of(sheetContext).pop();
_openTool(tool);
},
),
),
],
),
),
constraints: BoxConstraints.tightFor(width: width),
padding: const EdgeInsets.fromLTRB(12, 12, 12, 20),
scrollable: true,
child: _MobileSidebarContent(
mode: _mode,
cloudState: ref.read(fileProvider),
mediaState: ref.read(mediaLibraryProvider),
onModeChanged: (mode) {
Navigator.of(sheetContext).pop();
_changeMode(mode);
},
onSection: (section) {
Navigator.of(sheetContext).pop();
ref.read(fileProvider.notifier).setSection(section);
},
onSettings: () {
Navigator.of(sheetContext).pop();
_showSettings(context);
},
onSignOut: () {
Navigator.of(sheetContext).pop();
ref.read(authProvider.notifier).signOut();
},
onCreateLibrary: () {
Navigator.of(sheetContext).pop();
MediaLibraryPage.showCreateDialog(context, ref);
},
onSelectLibrary: (libraryID) {
Navigator.of(sheetContext).pop();
ref.read(mediaLibraryProvider.notifier).selectLibrary(libraryID);
},
onTool: (tool) {
Navigator.of(sheetContext).pop();
_openTool(tool);
},
),
),
);
@@ -919,6 +870,201 @@ class _MobileDrawerSwipeAreaState extends State<_MobileDrawerSwipeArea> {
);
}
/// A sheet-safe sidebar without desktop flex regions. ShadDialog owns the
/// vertical scroll extent, so this widget deliberately contains no Expanded
/// or nested scrollable viewport.
class _MobileSidebarContent extends StatelessWidget {
final WorkspaceMode mode;
final FileState cloudState;
final MediaLibraryState mediaState;
final ValueChanged<WorkspaceMode> onModeChanged;
final ValueChanged<WorkspaceSection> onSection;
final VoidCallback onSettings;
final VoidCallback onSignOut;
final VoidCallback onCreateLibrary;
final ValueChanged<String> onSelectLibrary;
final ValueChanged<WorkspaceTool> onTool;
const _MobileSidebarContent({
required this.mode,
required this.cloudState,
required this.mediaState,
required this.onModeChanged,
required this.onSection,
required this.onSettings,
required this.onSignOut,
required this.onCreateLibrary,
required this.onSelectLibrary,
required this.onTool,
});
@override
Widget build(BuildContext context) {
final isCloud = mode == WorkspaceMode.cloud;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: _SegmentButton(
icon: Icons.folder_rounded,
label: '光鸭云盘',
compact: false,
selected: isCloud,
onTap: () => onModeChanged(WorkspaceMode.cloud),
),
),
const SizedBox(width: 6),
Expanded(
child: _SegmentButton(
icon: Icons.movie_rounded,
label: '光鸭影视',
compact: false,
selected: !isCloud,
onTap: () => onModeChanged(WorkspaceMode.media),
),
),
],
),
const SizedBox(height: 16),
_SidebarBrand(
icon: isCloud
? Icons.cloud_sync_rounded
: Icons.play_circle_fill_rounded,
title: isCloud ? '光鸭云盘' : '光鸭影视',
subtitle: isCloud ? 'Cloud Workspace' : 'Media Center',
imageAsset: isCloud ? 'assets/branding/guangya_icon.png' : null,
),
const SizedBox(height: 16),
if (isCloud) ...[
for (final section in WorkspaceSection.values.where(
(section) => section != WorkspaceSection.mediaLibrary,
))
_SidebarTile(
icon: _cloudSectionIcon(section),
label: section.label,
selected: cloudState.section == section,
onTap: () => onSection(section),
),
const ShadSeparator.horizontal(),
const SizedBox(height: 10),
_SidebarTile(
icon: Icons.manage_search_rounded,
label: '文件扫描与清理',
selected: false,
onTap: () => onTool(WorkspaceTool.scan),
),
_SidebarTile(
icon: Icons.text_fields_rounded,
label: '批量重命名',
selected: false,
onTap: () => onTool(WorkspaceTool.rename),
),
_SidebarTile(
icon: Icons.bolt_rounded,
label: '秒传工具',
selected: false,
onTap: () => onTool(WorkspaceTool.fastTransfer),
),
const SizedBox(height: 10),
const ShadSeparator.horizontal(),
const SizedBox(height: 10),
_SidebarTile(
icon: Icons.settings_rounded,
label: '工作区设置',
selected: false,
onTap: onSettings,
),
_SidebarTile(
icon: Icons.logout_rounded,
label: '退出登录',
selected: false,
onTap: onSignOut,
),
] else ...[
_SidebarTile(
icon: Icons.home_rounded,
label: '首页',
selected: true,
onTap: () => onTool(WorkspaceTool.tmdb),
),
_SidebarTile(
icon: Icons.movie_creation_rounded,
label: '电影',
count: mediaState.statistics.movies,
selected: false,
onTap: () => onTool(WorkspaceTool.tmdb),
),
_SidebarTile(
icon: Icons.live_tv_rounded,
label: '电视剧',
count: mediaState.statistics.series,
selected: false,
onTap: () => onTool(WorkspaceTool.tmdb),
),
_SidebarTile(
icon: Icons.help_outline_rounded,
label: '未识别',
count: mediaState.statistics.unmatched,
selected: false,
onTap: () => onTool(WorkspaceTool.tmdb),
),
const Padding(
padding: EdgeInsets.fromLTRB(8, 18, 8, 8),
child: Text(
'媒体库',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700),
),
),
for (final library in mediaState.libraries)
_SidebarTile(
icon: library.kind == MediaLibraryKind.series
? Icons.live_tv_rounded
: Icons.smart_display_rounded,
label: library.name,
selected: mediaState.selectedLibrary?.id == library.id,
onTap: () => onSelectLibrary(library.id),
),
_SidebarTile(
icon: Icons.add_rounded,
label: '新建媒体库',
selected: false,
onTap: onCreateLibrary,
),
_SidebarTile(
icon: Icons.auto_fix_high_rounded,
label: '媒体库管理',
selected: false,
onTap: () => onTool(WorkspaceTool.tmdb),
),
_SidebarTile(
icon: Icons.category_rounded,
label: '分类管理',
selected: false,
onTap: () => onTool(WorkspaceTool.categories),
),
],
],
);
}
IconData _cloudSectionIcon(WorkspaceSection section) => switch (section) {
WorkspaceSection.files => Icons.folder_rounded,
WorkspaceSection.recentViewed => Icons.access_time_rounded,
WorkspaceSection.recentRestored => Icons.history_rounded,
WorkspaceSection.photos => Icons.image_rounded,
WorkspaceSection.videos => Icons.smart_display_rounded,
WorkspaceSection.audio => Icons.music_note_rounded,
WorkspaceSection.documents => Icons.description_rounded,
WorkspaceSection.cloud => Icons.cloud_download_rounded,
WorkspaceSection.shares => Icons.ios_share_rounded,
WorkspaceSection.recycle => Icons.delete_outline_rounded,
WorkspaceSection.mediaLibrary => Icons.movie_filter_rounded,
};
}
// ignore: unused_element
class _MobileWorkspaceMenu extends StatelessWidget {
final WorkspaceMode mode;
@@ -1187,7 +1333,6 @@ class _MobileMenuRow extends StatelessWidget {
class _CloudSidebar extends StatelessWidget {
final FileState state;
final double width;
final ValueChanged<WorkspaceSection> onSection;
final VoidCallback onSettings;
final VoidCallback onSignOut;
@@ -1195,7 +1340,6 @@ class _CloudSidebar extends StatelessWidget {
const _CloudSidebar({
required this.state,
this.width = 250,
required this.onSection,
required this.onSettings,
required this.onSignOut,
@@ -1208,7 +1352,7 @@ class _CloudSidebar extends StatelessWidget {
.where((section) => section != WorkspaceSection.mediaLibrary)
.toList();
return SizedBox(
width: width,
width: 250,
child: OS26Glass(
radius: 24,
opacity: 0.56,
@@ -1302,22 +1446,17 @@ class _CloudSidebar extends StatelessWidget {
}
class _MediaSidebar extends ConsumerWidget {
final double width;
final VoidCallback onCreate;
final ValueChanged<WorkspaceTool> onTool;
const _MediaSidebar({
this.width = 250,
required this.onCreate,
required this.onTool,
});
const _MediaSidebar({required this.onCreate, required this.onTool});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(mediaLibraryProvider);
final notifier = ref.read(mediaLibraryProvider.notifier);
return SizedBox(
width: width,
width: 250,
child: OS26Glass(
radius: 24,
opacity: 0.56,