forked from Foxyz/sync_replay_project
- Tests: the four known failures fixed (test-side causes), one duplicate file and four detail-pinning cases removed, every test that pins behaviour the restructure changes tagged with its step. - Every toast, sound and activity-log line goes through Notifier.notify (AppEvent), mapped one-to-one to today's behaviour. - Platform ports (shell, URLs, folders, link resolver, system notifications) with desktop implementations; window_manager, Process and file_picker now live only in platform/desktop/. - ROADMAP: protocol 16 planned; app id dev.foxyz.starsync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
3.7 KiB
Dart
151 lines
3.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../state/session_state.dart';
|
|
|
|
class WindowButtons extends StatefulWidget {
|
|
final FocusNode? focusNode;
|
|
final bool fullscreen;
|
|
final VoidCallback? onToggleFullscreen;
|
|
const WindowButtons({
|
|
super.key,
|
|
this.focusNode,
|
|
this.fullscreen = false,
|
|
this.onToggleFullscreen,
|
|
});
|
|
|
|
@override
|
|
State<WindowButtons> createState() => _WindowButtonsState();
|
|
}
|
|
|
|
class _WindowButtonsState extends State<WindowButtons> {
|
|
final _shell = SessionState.instance.ports.shell;
|
|
bool _maximized = false;
|
|
late final void Function() _stopWatch;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_stopWatch = _shell.watchMaximized((v) {
|
|
if (mounted) setState(() => _maximized = v);
|
|
});
|
|
_shell.isMaximized().then((v) {
|
|
if (mounted) setState(() => _maximized = v);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_stopWatch();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _toggleMaximize() async {
|
|
if (widget.onToggleFullscreen != null) {
|
|
widget.onToggleFullscreen!();
|
|
return;
|
|
}
|
|
if (await _shell.isMaximized()) {
|
|
await _shell.unmaximize();
|
|
} else {
|
|
await _shell.maximize();
|
|
}
|
|
}
|
|
|
|
Future<void> _close() async {
|
|
try {
|
|
await SessionState.instance.coordinator.leave();
|
|
await SessionState.instance.stopServer();
|
|
} catch (_) {}
|
|
await _shell.requestClose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final focus = widget.focusNode;
|
|
final restored = widget.fullscreen || _maximized;
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_WinBtn(
|
|
icon: Icons.remove,
|
|
tooltip: 'Minimize',
|
|
focusNode: focus,
|
|
onPressed: () => _shell.minimize(),
|
|
),
|
|
_WinBtn(
|
|
icon: restored ? Icons.filter_none : Icons.crop_square,
|
|
tooltip: restored ? 'Restore' : 'Maximize',
|
|
focusNode: focus,
|
|
onPressed: _toggleMaximize,
|
|
),
|
|
_WinBtn(
|
|
icon: Icons.close,
|
|
tooltip: 'Close',
|
|
focusNode: focus,
|
|
hoverColor: Colors.red,
|
|
onPressed: _close,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _WinBtn extends StatelessWidget {
|
|
final IconData icon;
|
|
final String tooltip;
|
|
final VoidCallback onPressed;
|
|
final FocusNode? focusNode;
|
|
final Color? hoverColor;
|
|
const _WinBtn({
|
|
required this.icon,
|
|
required this.tooltip,
|
|
required this.onPressed,
|
|
this.focusNode,
|
|
this.hoverColor,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: 40,
|
|
height: 32,
|
|
child: IconButton(
|
|
focusNode: focusNode,
|
|
tooltip: tooltip,
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints.tightFor(width: 40, height: 32),
|
|
iconSize: 15,
|
|
splashRadius: 12,
|
|
hoverColor: (hoverColor ?? Colors.white).withValues(alpha: 0.12),
|
|
highlightColor: Colors.transparent,
|
|
icon: Icon(icon, size: 15),
|
|
onPressed: onPressed,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The title area: drag moves the window, a double click maximizes or
|
|
/// restores it (window_manager's DragToMoveArea, through [ShellPort]).
|
|
class WindowDragArea extends StatelessWidget {
|
|
final Widget child;
|
|
const WindowDragArea({super.key, required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final shell = SessionState.instance.ports.shell;
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.translucent,
|
|
onPanStart: (_) => shell.startDragging(),
|
|
onDoubleTap: () async {
|
|
if (await shell.isMaximized()) {
|
|
await shell.unmaximize();
|
|
} else {
|
|
await shell.maximize();
|
|
}
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
}
|