Enhance operations configuration and UI; add new functions for schedule upload and duty filtering

This commit is contained in:
ImBenji
2026-03-29 15:10:07 +01:00
parent 427bcadc77
commit 7049e58049
8 changed files with 1435 additions and 314 deletions
+111 -105
View File
@@ -26,71 +26,71 @@ class HomeLeftSidebar extends StatelessWidget {
final displayName = user?.email ?? "Signed in user";
final initials = _buildInitials(displayName);
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Row(
children: [
SizedBox(
width: 70,
child: Column(
children: [
Expanded(
child: _ServerRail(
organizations: organizations,
selectedOrganizationId: collab.selectedOrganizationId,
double topPadding = MediaQuery.of(context).padding.top;
double bottomPadding = MediaQuery.of(context).padding.bottom;
return Row(
children: [
SizedBox(
width: 70,
child: Column(
children: [
Expanded(
child: _ServerRail(
organizations: organizations,
selectedOrganizationId: collab.selectedOrganizationId,
),
),
Container(
padding: const EdgeInsets.all(8.0),
width: 100,
child: AspectRatio(
aspectRatio: 1,
child: IconButton.outline(
onPressed: () {
unawaited(showCreateOrganizationDialog(context));
},
shape: ButtonShape.circle,
size: ButtonSize.normal,
icon: const Icon(LucideIcons.plus),
),
),
Container(
padding: const EdgeInsets.all(8.0),
width: 100,
child: AspectRatio(
aspectRatio: 1,
child: IconButton.outline(
onPressed: () {
unawaited(showCreateOrganizationDialog(context));
},
shape: ButtonShape.circle,
size: ButtonSize.normal,
icon: const Icon(LucideIcons.plus),
),
),
const Divider(),
RotatedBox(
quarterTurns: 3,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"ROADBOUND",
style: const TextStyle(height: 1),
).extraBold.x3Large,
Text(
"by IMBENJI.NET LTD",
style: const TextStyle(height: 1),
).small.muted,
],
),
),
const Divider(),
RotatedBox(
quarterTurns: 3,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"ROADBOUND",
style: const TextStyle(height: 1),
).extraBold.x3Large,
Text(
"by IMBENJI.NET LTD",
style: const TextStyle(height: 1),
).small.muted,
],
),
),
),
],
),
),
Gap(bottomPadding)
],
),
const VerticalDivider(),
Container(
),
const VerticalDivider(),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.background,
width: 300,
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width - 50,
),
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Gap(topPadding),
Padding(
padding: const EdgeInsets.all(8),
child: SizedBox(
@@ -178,8 +178,8 @@ class HomeLeftSidebar extends StatelessWidget {
),
),
),
],
),
),
],
);
}
}
@@ -301,62 +301,68 @@ class _ServerRail extends StatelessWidget {
return const SizedBox.shrink();
}
double topPadding = MediaQuery.of(context).padding.top;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: organizations.map((organization) {
final isSelected = selectedOrganizationId == organization.id;
final fallbackInitial = _fallbackInitial(organization.name);
final buttonStyle = _organizationButtonStyle(isSelected);
children: [
Gap(topPadding),
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: HoverCard(
hoverBuilder: (context) {
return SurfaceCard(
child: Basic(title: Text(organization.name).medium),
);
},
anchorAlignment: Alignment.centerLeft,
popoverAlignment: Alignment.centerRight,
popoverOffset: const Offset(-4, 0),
child: ContextMenu(
items: [
MenuLabel(child: Text(organization.name)),
const MenuDivider(),
MenuButton(
leading: const Icon(LucideIcons.settings2).iconSmall,
onPressed: (_) {
_openOrganizationSettings(context, organization.id);
},
child: const Text("Server Settings"),
),
],
child: Stack(
children: [
_buildOrganizationAvatar(
organization: organization,
fallbackInitial: fallbackInitial,
),
SizedBox(
width: 54,
height: 54,
child: Button(
style: buttonStyle,
onPressed: () {
unawaited(
_openOrganization(context, organization.id),
);
},
child: Container(),
),
...organizations.map((organization) {
final isSelected = selectedOrganizationId == organization.id;
final fallbackInitial = _fallbackInitial(organization.name);
final buttonStyle = _organizationButtonStyle(isSelected);
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: HoverCard(
hoverBuilder: (context) {
return SurfaceCard(
child: Basic(title: Text(organization.name).medium),
);
},
anchorAlignment: Alignment.centerLeft,
popoverAlignment: Alignment.centerRight,
popoverOffset: const Offset(-4, 0),
child: ContextMenu(
items: [
MenuLabel(child: Text(organization.name)),
const MenuDivider(),
MenuButton(
leading: const Icon(LucideIcons.settings2).iconSmall,
onPressed: (_) {
_openOrganizationSettings(context, organization.id);
},
child: const Text("Server Settings"),
),
],
child: Stack(
children: [
_buildOrganizationAvatar(
organization: organization,
fallbackInitial: fallbackInitial,
),
SizedBox(
width: 54,
height: 54,
child: Button(
style: buttonStyle,
onPressed: () {
unawaited(
_openOrganization(context, organization.id),
);
},
child: Container(),
),
),
],
),
),
),
),
);
}).toList(),
);
})
],
),
);
}
+43 -100
View File
@@ -1,126 +1,69 @@
import "dart:math" as math;
import "package:flutter/material.dart";
import 'package:shadcn_flutter/shadcn_flutter.dart';
class SidebarSwiper extends StatefulWidget {
const SidebarSwiper({
required this.sidebar,
required this.child,
this.maxSidebarWidth = 360,
this.sidebarWidthFactor = 0.92,
this.edgeDragWidth = 44,
this.animationDuration = const Duration(milliseconds: 220),
super.key,
});
final Widget sidebar;
final Widget child;
final double maxSidebarWidth;
final double sidebarWidthFactor;
final double edgeDragWidth;
final Duration animationDuration;
@override
State<SidebarSwiper> createState() => _SidebarSwiperState();
}
class _SidebarSwiperState extends State<SidebarSwiper> {
static const double _closedExtraOffset = 12;
final _controller = PageController(initialPage: 1);
double _progress = 0; // 0 = closed, 1 = fully open
bool _isDragging = false;
bool _canDrag = false;
double _dragStartGlobalX = 0;
double _dragStartProgress = 0;
void _setOpen(bool value) {
setState(() {
_isDragging = false;
_progress = value ? 1 : 0;
});
}
void _handleDragStart(DragStartDetails details, double sidebarWidth) {
final isOpen = _progress > 0.001;
final fromEdge = details.globalPosition.dx <= widget.edgeDragWidth;
final fromSidebarZone = details.globalPosition.dx <= sidebarWidth;
_canDrag = fromEdge || (isOpen && fromSidebarZone);
if (!_canDrag) return;
setState(() {
_isDragging = true;
_dragStartGlobalX = details.globalPosition.dx;
_dragStartProgress = _progress;
});
}
void _handleDragUpdate(DragUpdateDetails details, double sidebarWidth) {
if (!_canDrag || !_isDragging) return;
if (sidebarWidth <= 0) return;
final movedX = details.globalPosition.dx - _dragStartGlobalX;
setState(() {
_progress = (_dragStartProgress + (movedX / sidebarWidth)).clamp(0, 1);
});
}
void _handleDragEnd(DragEndDetails details) {
if (!_canDrag) return;
_canDrag = false;
final velocity = details.primaryVelocity ?? 0;
final shouldOpen = velocity > 250
? true
: (velocity < -250 ? false : _progress >= 0.35);
_setOpen(shouldOpen);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final sidebarWidth = math.min(
widget.maxSidebarWidth,
screenWidth * widget.sidebarWidthFactor,
);
final leftOffset =
-(sidebarWidth + _closedExtraOffset) +
((sidebarWidth + _closedExtraOffset) * _progress);
final showScrim = _progress > 0;
return Scaffold(
child: PageView(
controller: _controller,
physics: const ClampingScrollPhysics(),
children: [
// page 0 - sidebar
Row(
children: [
Expanded(child: _KeepAlive(child: Scaffold(child: widget.sidebar))),
const VerticalDivider(width: 1),
],
),
return Stack(
children: [
widget.child,
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onHorizontalDragStart: (details) =>
_handleDragStart(details, sidebarWidth),
onHorizontalDragUpdate: (details) =>
_handleDragUpdate(details, sidebarWidth),
onHorizontalDragEnd: _handleDragEnd,
child: const SizedBox.expand(),
),
),
if (showScrim)
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _setOpen(false),
child: Container(
color: Colors.black.withValues(alpha: 0.45 * _progress),
),
),
),
AnimatedPositioned(
duration: _isDragging ? Duration.zero : widget.animationDuration,
curve: Curves.easeOutCubic,
left: leftOffset,
top: 0,
bottom: 0,
width: sidebarWidth,
child: Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: widget.sidebar,
),
),
],
// page 1 - main content
_KeepAlive(child: widget.child),
],
),
);
}
}
class _KeepAlive extends StatefulWidget {
const _KeepAlive({required this.child});
final Widget child;
@override
State<_KeepAlive> createState() => _KeepAliveState();
}
class _KeepAliveState extends State<_KeepAlive>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return SizedBox.expand(child: widget.child);
}
}