647 lines
25 KiB
Dart
647 lines
25 KiB
Dart
|
|
import 'dart:async';
|
||
|
|
import 'dart:ui' show ImageFilter;
|
||
|
|
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
|
import 'package:flutter_riverpod/legacy.dart';
|
||
|
|
import 'package:go_router/go_router.dart';
|
||
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
|
|
||
|
|
import '../../providers/home_banner_provider.dart';
|
||
|
|
import '../../providers/di_providers.dart';
|
||
|
|
import '../../providers/library_overview_provider.dart';
|
||
|
|
import '../../providers/server_provider.dart';
|
||
|
|
import '../../providers/session_provider.dart';
|
||
|
|
import '../../providers/shell_backdrop_provider.dart';
|
||
|
|
import '../../features/settings/settings_page.dart';
|
||
|
|
import '../../providers/tmdb_settings_provider.dart';
|
||
|
|
import '../../providers/sm_account_provider.dart';
|
||
|
|
import '../../providers/trakt_settings_provider.dart';
|
||
|
|
import '../theme/app_theme.dart';
|
||
|
|
import 'server_action_menu.dart';
|
||
|
|
import 'server_menu_item.dart';
|
||
|
|
import 'top_drag_area.dart';
|
||
|
|
|
||
|
|
const _kSidebarCollapsedKey = 'smplayer.sidebar_collapsed';
|
||
|
|
|
||
|
|
const double _kSidebarBlurSigma = 18;
|
||
|
|
|
||
|
|
|
||
|
|
class _SidebarPalette {
|
||
|
|
final Color fg;
|
||
|
|
final Color fgMuted;
|
||
|
|
final Color fgFaint;
|
||
|
|
final Color glassTint;
|
||
|
|
final Color hairline;
|
||
|
|
|
||
|
|
const _SidebarPalette({
|
||
|
|
required this.fg,
|
||
|
|
required this.fgMuted,
|
||
|
|
required this.fgFaint,
|
||
|
|
required this.glassTint,
|
||
|
|
required this.hairline,
|
||
|
|
});
|
||
|
|
|
||
|
|
static const dark = _SidebarPalette(
|
||
|
|
fg: Colors.white,
|
||
|
|
fgMuted: Color(0x99FFFFFF),
|
||
|
|
fgFaint: Color(0x66FFFFFF),
|
||
|
|
glassTint: Color(0x42000000),
|
||
|
|
hairline: Color(0x14FFFFFF),
|
||
|
|
);
|
||
|
|
|
||
|
|
static const light = _SidebarPalette(
|
||
|
|
fg: Color(0xFF1A1A1A),
|
||
|
|
fgMuted: Color(0x99000000),
|
||
|
|
fgFaint: Color(0x59000000),
|
||
|
|
glassTint: Color(0x8CFFFFFF),
|
||
|
|
hairline: Color(0x0F000000),
|
||
|
|
);
|
||
|
|
|
||
|
|
static const darkSurface = _SidebarPalette(
|
||
|
|
fg: Colors.white,
|
||
|
|
fgMuted: Color(0x99FFFFFF),
|
||
|
|
fgFaint: Color(0x66FFFFFF),
|
||
|
|
glassTint: Color(0x14FFFFFF),
|
||
|
|
hairline: Color(0x1FFFFFFF),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
final sidebarCollapsedProvider = StateProvider<bool>((ref) => false);
|
||
|
|
|
||
|
|
Future<void> initSidebarState(WidgetRef ref) async {
|
||
|
|
final prefs = await SharedPreferences.getInstance();
|
||
|
|
final collapsed = prefs.getBool(_kSidebarCollapsedKey) ?? false;
|
||
|
|
ref.read(sidebarCollapsedProvider.notifier).state = collapsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _persistSidebarState(bool collapsed) async {
|
||
|
|
final prefs = await SharedPreferences.getInstance();
|
||
|
|
await prefs.setBool(_kSidebarCollapsedKey, collapsed);
|
||
|
|
}
|
||
|
|
|
||
|
|
class Sidebar extends ConsumerStatefulWidget {
|
||
|
|
|
||
|
|
|
||
|
|
final int selectedBranchIndex;
|
||
|
|
|
||
|
|
|
||
|
|
final void Function(int index, {bool initialLocation}) onNavigateBranch;
|
||
|
|
|
||
|
|
|
||
|
|
final bool immersiveRoute;
|
||
|
|
|
||
|
|
|
||
|
|
final String location;
|
||
|
|
|
||
|
|
const Sidebar({
|
||
|
|
super.key,
|
||
|
|
required this.selectedBranchIndex,
|
||
|
|
required this.onNavigateBranch,
|
||
|
|
required this.location,
|
||
|
|
this.immersiveRoute = false,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
ConsumerState<Sidebar> createState() => _SidebarState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _SidebarState extends ConsumerState<Sidebar> {
|
||
|
|
bool _suppressBlur = false;
|
||
|
|
Timer? _blurTimer;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_blurTimer?.cancel();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _onCollapseToggled() {
|
||
|
|
setState(() => _suppressBlur = true);
|
||
|
|
_blurTimer?.cancel();
|
||
|
|
_blurTimer = Timer(const Duration(milliseconds: 300), () {
|
||
|
|
if (mounted) setState(() => _suppressBlur = false);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _frostedGlass({required bool suppressBlur, required Widget child}) {
|
||
|
|
return BackdropFilter(
|
||
|
|
enabled: !suppressBlur,
|
||
|
|
filter: ImageFilter.blur(
|
||
|
|
sigmaX: _kSidebarBlurSigma,
|
||
|
|
sigmaY: _kSidebarBlurSigma,
|
||
|
|
),
|
||
|
|
child: child,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
ref.listen<bool>(sidebarCollapsedProvider, (prev, next) {
|
||
|
|
if (prev != next) _onCollapseToggled();
|
||
|
|
});
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
final hasBackdrop = ref.watch(
|
||
|
|
shellBackdropProvider.select((s) => s != null),
|
||
|
|
);
|
||
|
|
final immersive = widget.immersiveRoute && hasBackdrop;
|
||
|
|
final isDark = theme.brightness == Brightness.dark;
|
||
|
|
final palette = immersive
|
||
|
|
? _SidebarPalette.dark
|
||
|
|
: (isDark ? _SidebarPalette.darkSurface : _SidebarPalette.light);
|
||
|
|
final collapsed = ref.watch(sidebarCollapsedProvider);
|
||
|
|
final sessionState = ref.watch(sessionProvider);
|
||
|
|
final servers = ref.watch(serverListProvider).value ?? const [];
|
||
|
|
final serverById = {for (final s in servers) s.id: s};
|
||
|
|
final iconUrlByServer = {for (final s in servers) s.id: s.iconUrl};
|
||
|
|
final currentIndex = widget.selectedBranchIndex;
|
||
|
|
final locationSegments = Uri.parse(widget.location).pathSegments;
|
||
|
|
final activeModuleId =
|
||
|
|
locationSegments.length > 1 && locationSegments.first == 'modules'
|
||
|
|
? locationSegments[1]
|
||
|
|
: null;
|
||
|
|
final sessionByServerId = {
|
||
|
|
for (final session in sessionState.value?.sessions ?? const [])
|
||
|
|
session.serverId: session,
|
||
|
|
};
|
||
|
|
final sessionList = [
|
||
|
|
for (final server in servers)
|
||
|
|
if (!server.paused && sessionByServerId[server.id] != null)
|
||
|
|
sessionByServerId[server.id]!,
|
||
|
|
];
|
||
|
|
final tmdbConfigured =
|
||
|
|
ref.watch(tmdbSettingsProvider).value?.canRequest ?? false;
|
||
|
|
final traktConnected =
|
||
|
|
ref.watch(traktSettingsProvider).value?.isConnected ?? false;
|
||
|
|
final smAccountLoggedIn =
|
||
|
|
ref.watch(smAccountProvider).value?.loggedIn ?? false;
|
||
|
|
final enabledScriptWidgets =
|
||
|
|
(ref.watch(installedScriptWidgetsProvider).value ?? const [])
|
||
|
|
.where((installedWidget) => installedWidget.enabled)
|
||
|
|
.toList(growable: false);
|
||
|
|
final width = collapsed
|
||
|
|
? tokens.siderWidthCollapsed
|
||
|
|
: tokens.siderWidthExpanded;
|
||
|
|
|
||
|
|
return RepaintBoundary(
|
||
|
|
child: AnimatedContainer(
|
||
|
|
duration: tokens.siderTransition,
|
||
|
|
curve: Curves.easeOutCubic,
|
||
|
|
width: width,
|
||
|
|
child: ClipRect(
|
||
|
|
child: _frostedGlass(
|
||
|
|
suppressBlur: _suppressBlur,
|
||
|
|
child: AnimatedContainer(
|
||
|
|
duration: const Duration(milliseconds: 350),
|
||
|
|
curve: Curves.easeOut,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: palette.glassTint,
|
||
|
|
border: Border(
|
||
|
|
right: BorderSide(color: palette.hairline, width: 1),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: OverflowBox(
|
||
|
|
alignment: Alignment.topLeft,
|
||
|
|
minWidth: width,
|
||
|
|
maxWidth: width,
|
||
|
|
child: SizedBox(
|
||
|
|
width: width,
|
||
|
|
child: Padding(
|
||
|
|
padding: EdgeInsets.fromLTRB(
|
||
|
|
collapsed ? 8 : 16,
|
||
|
|
10,
|
||
|
|
collapsed ? 8 : 16,
|
||
|
|
10,
|
||
|
|
),
|
||
|
|
child: LayoutBuilder(
|
||
|
|
builder: (context, constraints) {
|
||
|
|
final effectiveCollapsed = constraints.maxWidth < 100;
|
||
|
|
const settingsNavReserveHeight = 48.0;
|
||
|
|
final reservedBottomHeight =
|
||
|
|
(sessionList.isNotEmpty ||
|
||
|
|
enabledScriptWidgets.isNotEmpty
|
||
|
|
? 16.0
|
||
|
|
: 0.0) +
|
||
|
|
4.0 +
|
||
|
|
settingsNavReserveHeight;
|
||
|
|
final availableHeaderHeight =
|
||
|
|
constraints.maxHeight - reservedBottomHeight;
|
||
|
|
final maxHeaderHeight = constraints.maxHeight.isFinite
|
||
|
|
? (availableHeaderHeight < 0
|
||
|
|
? 0.0
|
||
|
|
: availableHeaderHeight)
|
||
|
|
: double.infinity;
|
||
|
|
return Column(
|
||
|
|
crossAxisAlignment: effectiveCollapsed
|
||
|
|
? CrossAxisAlignment.center
|
||
|
|
: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
ConstrainedBox(
|
||
|
|
constraints: BoxConstraints(
|
||
|
|
maxHeight: maxHeaderHeight,
|
||
|
|
),
|
||
|
|
child: SingleChildScrollView(
|
||
|
|
child: AnimatedSize(
|
||
|
|
duration: const Duration(milliseconds: 200),
|
||
|
|
curve: Curves.easeOutCubic,
|
||
|
|
alignment: Alignment.topCenter,
|
||
|
|
child: Column(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
crossAxisAlignment: effectiveCollapsed
|
||
|
|
? CrossAxisAlignment.center
|
||
|
|
: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
_SidebarHeader(
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
onToggle: () {
|
||
|
|
final next = !collapsed;
|
||
|
|
ref
|
||
|
|
.read(
|
||
|
|
sidebarCollapsedProvider
|
||
|
|
.notifier,
|
||
|
|
)
|
||
|
|
.state =
|
||
|
|
next;
|
||
|
|
_persistSidebarState(next);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
_buildSearchEntry(
|
||
|
|
context,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
_navItem(
|
||
|
|
context,
|
||
|
|
icon: Icons.dns_outlined,
|
||
|
|
label: '服务器',
|
||
|
|
selected: currentIndex == 1,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
onTap: () => widget.onNavigateBranch(1),
|
||
|
|
),
|
||
|
|
_navItem(
|
||
|
|
context,
|
||
|
|
icon: Icons.dashboard_outlined,
|
||
|
|
label: '聚合视界',
|
||
|
|
selected: currentIndex == 2,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
onTap: () => widget.onNavigateBranch(2),
|
||
|
|
),
|
||
|
|
if (tmdbConfigured)
|
||
|
|
_navItem(
|
||
|
|
context,
|
||
|
|
icon: Icons.explore_outlined,
|
||
|
|
label: '发现',
|
||
|
|
selected: currentIndex == 3,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
onTap: () =>
|
||
|
|
widget.onNavigateBranch(3),
|
||
|
|
),
|
||
|
|
if (smAccountLoggedIn && traktConnected)
|
||
|
|
_navItem(
|
||
|
|
context,
|
||
|
|
icon: Icons.calendar_today_outlined,
|
||
|
|
label: '日历',
|
||
|
|
selected: currentIndex == 4,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
onTap: () =>
|
||
|
|
widget.onNavigateBranch(4),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (sessionList.isNotEmpty ||
|
||
|
|
enabledScriptWidgets.isNotEmpty) ...[
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
Expanded(
|
||
|
|
child: SingleChildScrollView(
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
...sessionList.map(
|
||
|
|
(session) => Padding(
|
||
|
|
padding: const EdgeInsets.only(
|
||
|
|
bottom: 4,
|
||
|
|
),
|
||
|
|
child: Builder(
|
||
|
|
builder: (context) {
|
||
|
|
final server =
|
||
|
|
serverById[session.serverId];
|
||
|
|
final actions =
|
||
|
|
buildServerActionCallbacks(
|
||
|
|
context: context,
|
||
|
|
ref: ref,
|
||
|
|
server: server,
|
||
|
|
session: session,
|
||
|
|
);
|
||
|
|
return ServerMenuItem(
|
||
|
|
serverName: session.serverName,
|
||
|
|
iconUrl:
|
||
|
|
iconUrlByServer[session
|
||
|
|
.serverId] ??
|
||
|
|
'',
|
||
|
|
isActive:
|
||
|
|
session.isActive &&
|
||
|
|
activeModuleId == null,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
foregroundActive: palette.fg,
|
||
|
|
foregroundInactive:
|
||
|
|
palette.fgMuted,
|
||
|
|
onTap: () async {
|
||
|
|
if (!session.isActive) {
|
||
|
|
await ref
|
||
|
|
.read(
|
||
|
|
sessionProvider
|
||
|
|
.notifier,
|
||
|
|
)
|
||
|
|
.switchSession(
|
||
|
|
session.serverId,
|
||
|
|
);
|
||
|
|
widget.onNavigateBranch(
|
||
|
|
0,
|
||
|
|
initialLocation: true,
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (widget.location == '/') {
|
||
|
|
await Future.wait([
|
||
|
|
ref
|
||
|
|
.read(
|
||
|
|
libraryOverviewProvider
|
||
|
|
.notifier,
|
||
|
|
)
|
||
|
|
.softRefresh(),
|
||
|
|
ref
|
||
|
|
.read(
|
||
|
|
homeBannerProvider
|
||
|
|
.notifier,
|
||
|
|
)
|
||
|
|
.softRefresh(),
|
||
|
|
]);
|
||
|
|
} else {
|
||
|
|
widget.onNavigateBranch(
|
||
|
|
0,
|
||
|
|
initialLocation: true,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onChangeIcon:
|
||
|
|
actions.onChangeIcon,
|
||
|
|
onEdit: actions.onEdit,
|
||
|
|
onChangePassword:
|
||
|
|
actions.onChangePassword,
|
||
|
|
onTogglePause:
|
||
|
|
actions.onTogglePause,
|
||
|
|
onDelete: actions.onDelete,
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
for (final installedWidget
|
||
|
|
in enabledScriptWidgets)
|
||
|
|
_navItem(
|
||
|
|
context,
|
||
|
|
icon: Icons.extension_rounded,
|
||
|
|
label: installedWidget.manifest.title,
|
||
|
|
selected:
|
||
|
|
activeModuleId ==
|
||
|
|
installedWidget.manifest.id,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
|
||
|
|
|
||
|
|
onTap: () => context.go(
|
||
|
|
'/modules/${Uri.encodeComponent(installedWidget.manifest.id)}',
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
] else
|
||
|
|
const Spacer(),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
_navItem(
|
||
|
|
context,
|
||
|
|
icon: Icons.settings_outlined,
|
||
|
|
label: '设置',
|
||
|
|
selected: false,
|
||
|
|
collapsed: effectiveCollapsed,
|
||
|
|
palette: palette,
|
||
|
|
onTap: () => showSettingsDialog(context),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildSearchEntry(
|
||
|
|
BuildContext context, {
|
||
|
|
required bool collapsed,
|
||
|
|
required _SidebarPalette palette,
|
||
|
|
}) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
|
||
|
|
if (collapsed) {
|
||
|
|
return Tooltip(
|
||
|
|
message: '搜索',
|
||
|
|
preferBelow: false,
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: () => context.push('/global-search'),
|
||
|
|
child: MouseRegion(
|
||
|
|
cursor: SystemMouseCursors.click,
|
||
|
|
child: SizedBox(
|
||
|
|
width: 44,
|
||
|
|
height: 38,
|
||
|
|
child: Center(
|
||
|
|
child: Icon(
|
||
|
|
Icons.search_outlined,
|
||
|
|
size: 20,
|
||
|
|
color: palette.fgFaint,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return GestureDetector(
|
||
|
|
onTap: () => context.push('/global-search'),
|
||
|
|
child: MouseRegion(
|
||
|
|
cursor: SystemMouseCursors.click,
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
Icon(Icons.search_outlined, size: 18, color: palette.fgMuted),
|
||
|
|
const SizedBox(width: 10),
|
||
|
|
Expanded(
|
||
|
|
child: Text(
|
||
|
|
'搜索',
|
||
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
||
|
|
color: palette.fgMuted,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
'⌘K',
|
||
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
||
|
|
color: palette.fgFaint,
|
||
|
|
fontSize: 10,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _navItem(
|
||
|
|
BuildContext context, {
|
||
|
|
required IconData icon,
|
||
|
|
required String label,
|
||
|
|
required bool selected,
|
||
|
|
required bool collapsed,
|
||
|
|
required _SidebarPalette palette,
|
||
|
|
required VoidCallback onTap,
|
||
|
|
}) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
final foreground = selected ? palette.fg : palette.fgMuted;
|
||
|
|
|
||
|
|
final child = GestureDetector(
|
||
|
|
onTap: onTap,
|
||
|
|
child: MouseRegion(
|
||
|
|
cursor: SystemMouseCursors.click,
|
||
|
|
child: Padding(
|
||
|
|
padding: EdgeInsets.symmetric(
|
||
|
|
horizontal: collapsed ? 0 : 4,
|
||
|
|
vertical: 8,
|
||
|
|
),
|
||
|
|
child: collapsed
|
||
|
|
? SizedBox(
|
||
|
|
width: 44,
|
||
|
|
child: Center(child: Icon(icon, size: 20, color: foreground)),
|
||
|
|
)
|
||
|
|
: Row(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
width: 2,
|
||
|
|
height: 16,
|
||
|
|
margin: const EdgeInsets.only(right: 10),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: selected ? palette.fg : Colors.transparent,
|
||
|
|
borderRadius: BorderRadius.circular(1),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Icon(icon, size: 18, color: foreground),
|
||
|
|
const SizedBox(width: 10),
|
||
|
|
Expanded(
|
||
|
|
child: Text(
|
||
|
|
label,
|
||
|
|
maxLines: 1,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||
|
|
color: foreground,
|
||
|
|
fontWeight: selected
|
||
|
|
? FontWeight.w600
|
||
|
|
: FontWeight.w400,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (collapsed) {
|
||
|
|
return Tooltip(
|
||
|
|
message: label,
|
||
|
|
preferBelow: false,
|
||
|
|
child: Padding(padding: const EdgeInsets.only(bottom: 2), child: child),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return Padding(padding: const EdgeInsets.only(bottom: 2), child: child);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class _SidebarHeader extends StatelessWidget {
|
||
|
|
final bool collapsed;
|
||
|
|
final _SidebarPalette palette;
|
||
|
|
final VoidCallback onToggle;
|
||
|
|
|
||
|
|
const _SidebarHeader({
|
||
|
|
required this.collapsed,
|
||
|
|
required this.palette,
|
||
|
|
required this.onToggle,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final tokens = context.appTokens;
|
||
|
|
|
||
|
|
if (collapsed) {
|
||
|
|
return Column(
|
||
|
|
children: [
|
||
|
|
TopDragArea(
|
||
|
|
child: SizedBox(
|
||
|
|
width: double.infinity,
|
||
|
|
height: tokens.titleBarHeight,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: onToggle,
|
||
|
|
child: MouseRegion(
|
||
|
|
cursor: SystemMouseCursors.click,
|
||
|
|
child: Icon(Icons.menu, size: 18, color: palette.fgFaint),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return TopDragArea(
|
||
|
|
child: SizedBox(
|
||
|
|
height: tokens.titleBarHeight,
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
const Spacer(),
|
||
|
|
GestureDetector(
|
||
|
|
onTap: onToggle,
|
||
|
|
child: MouseRegion(
|
||
|
|
cursor: SystemMouseCursors.click,
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(4),
|
||
|
|
child: Icon(Icons.menu, size: 18, color: palette.fgFaint),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|