2025-03-12 17:15:31 +08:00
|
|
|
import 'dart:async';
|
2026-10-08 16:09:04 +08:00
|
|
|
import 'dart:io';
|
2024-07-19 02:14:58 +08:00
|
|
|
|
2024-07-08 17:34:14 +08:00
|
|
|
import 'package:fl_clash/common/common.dart';
|
2026-09-05 20:09:39 +08:00
|
|
|
import 'package:fl_clash/common/launch.dart';
|
2024-12-09 01:40:39 +08:00
|
|
|
import 'package:fl_clash/enum/enum.dart';
|
2026-09-25 03:56:56 +08:00
|
|
|
import 'package:fl_clash/icons/icons.dart';
|
2026-09-05 20:09:39 +08:00
|
|
|
import 'package:fl_clash/models/config.dart';
|
2025-06-07 01:48:34 +08:00
|
|
|
import 'package:fl_clash/providers/providers.dart';
|
2026-09-05 20:09:39 +08:00
|
|
|
import 'package:flutter/foundation.dart' show ValueListenable;
|
|
|
|
|
import 'package:material_ui/material_ui.dart';
|
2025-02-09 18:39:38 +08:00
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
2026-09-25 03:56:56 +08:00
|
|
|
import 'package:window/window.dart';
|
2024-04-30 23:38:49 +08:00
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
const _windowGeometryDelay = Duration(milliseconds: 120);
|
|
|
|
|
|
2025-02-09 18:39:38 +08:00
|
|
|
class WindowManager extends ConsumerStatefulWidget {
|
2024-04-30 23:38:49 +08:00
|
|
|
final Widget child;
|
|
|
|
|
|
2026-10-08 16:09:04 +08:00
|
|
|
/// A request to quit from outside the window, such as SIGTERM from `kill`
|
|
|
|
|
/// or the session ending; exits like the tray's quit.
|
|
|
|
|
final Stream<ProcessSignal>? terminateSignals;
|
|
|
|
|
|
|
|
|
|
const WindowManager({super.key, this.terminateSignals, required this.child});
|
2024-04-30 23:38:49 +08:00
|
|
|
|
|
|
|
|
@override
|
2025-02-09 18:39:38 +08:00
|
|
|
ConsumerState<WindowManager> createState() => _WindowContainerState();
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-09 18:39:38 +08:00
|
|
|
class _WindowContainerState extends ConsumerState<WindowManager>
|
2026-09-05 20:09:39 +08:00
|
|
|
with WindowListener {
|
|
|
|
|
Timer? _windowGeometryTimer;
|
2026-10-08 16:09:04 +08:00
|
|
|
StreamSubscription<ProcessSignal>? _terminateSubscription;
|
2026-09-05 20:09:39 +08:00
|
|
|
int _windowGeometryRevision = 0;
|
2026-09-25 03:56:56 +08:00
|
|
|
int _windowBlurRevision = 0;
|
2026-09-05 20:09:39 +08:00
|
|
|
|
2025-02-09 18:39:38 +08:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return widget.child;
|
|
|
|
|
}
|
2024-08-23 18:33:40 +08:00
|
|
|
|
2025-02-09 18:39:38 +08:00
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-07-31 17:09:18 +08:00
|
|
|
ref.listenManual(appSettingProvider.select((state) => state.autoLaunch), (
|
|
|
|
|
prev,
|
|
|
|
|
next,
|
|
|
|
|
) {
|
|
|
|
|
if (prev != next) {
|
|
|
|
|
debouncer.call(FunctionTag.autoLaunch, () {
|
|
|
|
|
autoLaunch?.updateStatus(next);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-09-25 03:56:56 +08:00
|
|
|
ref.listenManual(windowBlurRequestProvider, (prev, next) {
|
|
|
|
|
if (prev != next) {
|
|
|
|
|
unawaited(_applyWindowBlur(next));
|
|
|
|
|
}
|
|
|
|
|
}, fireImmediately: true);
|
|
|
|
|
desktopWindow.addListener(this);
|
2026-10-08 16:09:04 +08:00
|
|
|
_terminateSubscription = widget.terminateSignals?.listen(
|
|
|
|
|
(_) => unawaited(onWindowShouldTerminate()),
|
|
|
|
|
);
|
2026-09-25 03:56:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _applyWindowBlur(WindowBlurRequest request) async {
|
|
|
|
|
final port = windowPort;
|
|
|
|
|
if (port == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final revision = ++_windowBlurRevision;
|
|
|
|
|
bool active;
|
|
|
|
|
try {
|
|
|
|
|
active = await port.setBlur(
|
|
|
|
|
enabled: request.enabled,
|
|
|
|
|
brightness: request.brightness,
|
|
|
|
|
tint: request.tint,
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
commonPrint.log(
|
|
|
|
|
'Window blur failed: ${compactError(error)}',
|
|
|
|
|
logLevel: LogLevel.warning,
|
|
|
|
|
);
|
|
|
|
|
active = false;
|
|
|
|
|
}
|
|
|
|
|
if (!mounted || revision != _windowBlurRevision) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ref.read(windowBlurProvider.notifier).value = active;
|
2024-04-30 23:38:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowClose() async {
|
2026-05-27 15:16:51 +08:00
|
|
|
await ref.read(systemActionProvider.notifier).handleClose();
|
2024-04-30 23:38:49 +08:00
|
|
|
super.onWindowClose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 03:56:56 +08:00
|
|
|
/// Another launch, or a Dock reopen, asked for the window.
|
2026-09-05 20:09:39 +08:00
|
|
|
@override
|
|
|
|
|
void onWindowActivate() {
|
|
|
|
|
super.onWindowActivate();
|
|
|
|
|
unawaited(windowPort?.show());
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-13 19:08:17 +08:00
|
|
|
@override
|
2026-09-05 20:09:39 +08:00
|
|
|
Future<void> onWindowShouldTerminate() async {
|
2026-02-02 10:14:13 +08:00
|
|
|
await ref.read(systemActionProvider.notifier).handleExit();
|
2026-09-05 20:09:39 +08:00
|
|
|
super.onWindowShouldTerminate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _scheduleWindowGeometryCapture() {
|
|
|
|
|
final revision = ++_windowGeometryRevision;
|
|
|
|
|
_windowGeometryTimer?.cancel();
|
|
|
|
|
_windowGeometryTimer = Timer(_windowGeometryDelay, () {
|
|
|
|
|
_windowGeometryTimer = null;
|
|
|
|
|
unawaited(_captureWindowGeometry(revision));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _captureWindowGeometry(int revision) async {
|
|
|
|
|
final port = windowPort;
|
|
|
|
|
if (port == null || !mounted) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final current = ref.read(windowSettingProvider);
|
|
|
|
|
WindowProps? geometry;
|
|
|
|
|
try {
|
|
|
|
|
geometry = await port.captureNormalGeometry(current);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
commonPrint.log(
|
|
|
|
|
'Window geometry capture failed: ${compactError(error)}',
|
|
|
|
|
logLevel: LogLevel.warning,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!mounted || revision != _windowGeometryRevision || geometry == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ref.read(windowSettingProvider.notifier).value = geometry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _invalidateWindowGeometryCapture() {
|
|
|
|
|
_windowGeometryRevision++;
|
|
|
|
|
_windowGeometryTimer?.cancel();
|
|
|
|
|
_windowGeometryTimer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
2026-09-25 03:56:56 +08:00
|
|
|
void onWindowGeometryChanged() {
|
|
|
|
|
super.onWindowGeometryChanged();
|
2026-09-05 20:09:39 +08:00
|
|
|
_scheduleWindowGeometryCapture();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowMaximize() {
|
|
|
|
|
_invalidateWindowGeometryCapture();
|
|
|
|
|
super.onWindowMaximize();
|
|
|
|
|
if (system.isWindows) {
|
2026-09-25 03:56:56 +08:00
|
|
|
unawaited(desktopWindow.setRoundedCorners(false));
|
2026-09-05 20:09:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowUnmaximize() {
|
|
|
|
|
super.onWindowUnmaximize();
|
|
|
|
|
if (system.isWindows) {
|
2026-09-25 03:56:56 +08:00
|
|
|
unawaited(desktopWindow.setRoundedCorners(true));
|
2026-09-05 20:09:39 +08:00
|
|
|
}
|
|
|
|
|
_scheduleWindowGeometryCapture();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowEnterFullScreen() {
|
|
|
|
|
_invalidateWindowGeometryCapture();
|
|
|
|
|
super.onWindowEnterFullScreen();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowLeaveFullScreen() {
|
|
|
|
|
super.onWindowLeaveFullScreen();
|
|
|
|
|
_scheduleWindowGeometryCapture();
|
2024-07-15 22:06:09 +08:00
|
|
|
}
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
@override
|
|
|
|
|
void onWindowMinimize() async {
|
2026-09-05 20:09:39 +08:00
|
|
|
_invalidateWindowGeometryCapture();
|
2026-02-02 10:14:13 +08:00
|
|
|
ref.read(storeActionProvider.notifier).savePreferencesDebounce();
|
2024-04-30 23:38:49 +08:00
|
|
|
super.onWindowMinimize();
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-08 17:28:18 +08:00
|
|
|
@override
|
2025-05-02 02:24:12 +08:00
|
|
|
void onWindowRestore() {
|
|
|
|
|
super.onWindowRestore();
|
2026-09-05 20:09:39 +08:00
|
|
|
_scheduleWindowGeometryCapture();
|
2024-10-08 17:28:18 +08:00
|
|
|
}
|
|
|
|
|
|
2024-04-30 23:38:49 +08:00
|
|
|
@override
|
2026-09-05 20:09:39 +08:00
|
|
|
void dispose() {
|
|
|
|
|
_invalidateWindowGeometryCapture();
|
2026-10-08 16:09:04 +08:00
|
|
|
unawaited(_terminateSubscription?.cancel());
|
2026-09-25 03:56:56 +08:00
|
|
|
desktopWindow.removeListener(this);
|
2024-04-30 23:38:49 +08:00
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-19 02:14:58 +08:00
|
|
|
|
2024-08-15 16:18:00 +08:00
|
|
|
class WindowHeaderContainer extends StatelessWidget {
|
|
|
|
|
final Widget child;
|
|
|
|
|
|
2025-07-31 17:09:18 +08:00
|
|
|
const WindowHeaderContainer({super.key, required this.child});
|
2024-08-15 16:18:00 +08:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2025-02-09 18:39:38 +08:00
|
|
|
return Consumer(
|
|
|
|
|
builder: (_, ref, child) {
|
2025-06-07 01:48:34 +08:00
|
|
|
final isMobileView = ref.watch(isMobileViewProvider);
|
2025-02-09 18:39:38 +08:00
|
|
|
final version = ref.watch(versionProvider);
|
2026-09-05 20:09:39 +08:00
|
|
|
final showsHeader = showsWindowHeader(
|
|
|
|
|
isDesktop: system.isDesktop,
|
|
|
|
|
isMacOS: system.isMacOS,
|
|
|
|
|
version: version,
|
|
|
|
|
isMobileView: isMobileView,
|
|
|
|
|
);
|
|
|
|
|
if (!showsHeader) {
|
2024-09-08 21:21:21 +08:00
|
|
|
return child!;
|
|
|
|
|
}
|
2026-09-05 20:09:39 +08:00
|
|
|
return WindowHeaderLayout(
|
|
|
|
|
height: kHeaderHeight,
|
|
|
|
|
header: const WindowHeader(),
|
|
|
|
|
child: child!,
|
2024-09-08 21:21:21 +08:00
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
child: child,
|
2024-08-15 16:18:00 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
class WindowHeaderLayout extends StatelessWidget {
|
|
|
|
|
const WindowHeaderLayout({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.height,
|
|
|
|
|
required this.header,
|
|
|
|
|
required this.child,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final double height;
|
|
|
|
|
final Widget header;
|
|
|
|
|
final Widget child;
|
2024-07-19 02:14:58 +08:00
|
|
|
|
|
|
|
|
@override
|
2026-09-05 20:09:39 +08:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Overlay.wrap(
|
|
|
|
|
child: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
Column(
|
|
|
|
|
children: [
|
|
|
|
|
SizedBox(height: height),
|
|
|
|
|
Expanded(flex: 1, child: child),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
Positioned(top: 0, left: 0, right: 0, child: header),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-07-19 02:14:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
@immutable
|
|
|
|
|
class WindowCaptionState {
|
|
|
|
|
const WindowCaptionState({
|
|
|
|
|
this.isPinned = false,
|
|
|
|
|
this.isMaximized = false,
|
|
|
|
|
this.isFullScreen = false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final bool isPinned;
|
|
|
|
|
final bool isMaximized;
|
|
|
|
|
final bool isFullScreen;
|
|
|
|
|
|
|
|
|
|
WindowCaptionState copyWith({
|
|
|
|
|
bool? isPinned,
|
|
|
|
|
bool? isMaximized,
|
|
|
|
|
bool? isFullScreen,
|
|
|
|
|
}) {
|
|
|
|
|
return WindowCaptionState(
|
|
|
|
|
isPinned: isPinned ?? this.isPinned,
|
|
|
|
|
isMaximized: isMaximized ?? this.isMaximized,
|
|
|
|
|
isFullScreen: isFullScreen ?? this.isFullScreen,
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-07-19 02:14:58 +08:00
|
|
|
|
|
|
|
|
@override
|
2026-09-05 20:09:39 +08:00
|
|
|
bool operator ==(Object other) {
|
|
|
|
|
return other is WindowCaptionState &&
|
|
|
|
|
other.isPinned == isPinned &&
|
|
|
|
|
other.isMaximized == isMaximized &&
|
|
|
|
|
other.isFullScreen == isFullScreen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
int get hashCode => Object.hash(isPinned, isMaximized, isFullScreen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Maximize and fullscreen are written only from window events: the window
|
|
|
|
|
/// manager can change both on its own, and on Linux a maximize request is
|
|
|
|
|
/// applied later, so reading the state back right after the call is stale.
|
|
|
|
|
class WindowCaptionController extends ValueNotifier<WindowCaptionState>
|
|
|
|
|
with WindowListener {
|
|
|
|
|
WindowCaptionController() : super(const WindowCaptionState()) {
|
2026-09-25 03:56:56 +08:00
|
|
|
desktopWindow.addListener(this);
|
2026-09-05 20:09:39 +08:00
|
|
|
unawaited(_syncFromWindow());
|
2024-07-19 02:14:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
bool _disposed = false;
|
|
|
|
|
|
|
|
|
|
Future<void> _syncFromWindow() async {
|
|
|
|
|
final states = await Future.wait<bool>([
|
2026-09-25 03:56:56 +08:00
|
|
|
desktopWindow.isAlwaysOnTop(),
|
|
|
|
|
desktopWindow.isMaximized(),
|
|
|
|
|
desktopWindow.isFullScreen(),
|
2026-09-05 20:09:39 +08:00
|
|
|
]);
|
|
|
|
|
_set(
|
|
|
|
|
WindowCaptionState(
|
|
|
|
|
isPinned: states[0],
|
|
|
|
|
isMaximized: states[1],
|
|
|
|
|
isFullScreen: states[2],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _set(WindowCaptionState state) {
|
|
|
|
|
if (_disposed) return;
|
|
|
|
|
value = state;
|
2024-07-19 02:14:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
2026-09-05 20:09:39 +08:00
|
|
|
void onWindowMaximize() {
|
|
|
|
|
super.onWindowMaximize();
|
|
|
|
|
_set(value.copyWith(isMaximized: true));
|
2024-07-19 02:14:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
@override
|
|
|
|
|
void onWindowUnmaximize() {
|
|
|
|
|
super.onWindowUnmaximize();
|
|
|
|
|
_set(value.copyWith(isMaximized: false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowEnterFullScreen() {
|
|
|
|
|
super.onWindowEnterFullScreen();
|
|
|
|
|
_set(value.copyWith(isFullScreen: true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void onWindowLeaveFullScreen() {
|
|
|
|
|
super.onWindowLeaveFullScreen();
|
|
|
|
|
_set(value.copyWith(isFullScreen: false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> toggleMaximized() async {
|
2026-09-25 03:56:56 +08:00
|
|
|
if (await desktopWindow.isFullScreen()) {
|
|
|
|
|
await desktopWindow.setFullScreen(false);
|
|
|
|
|
} else if (await desktopWindow.isMaximized()) {
|
|
|
|
|
await desktopWindow.unmaximize();
|
2026-02-02 10:14:13 +08:00
|
|
|
} else {
|
2026-09-25 03:56:56 +08:00
|
|
|
await desktopWindow.maximize();
|
2026-05-27 15:16:51 +08:00
|
|
|
}
|
2024-07-19 02:14:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
Future<void> togglePin() async {
|
2026-09-25 03:56:56 +08:00
|
|
|
final isPinned = await desktopWindow.isAlwaysOnTop();
|
|
|
|
|
await desktopWindow.setAlwaysOnTop(!isPinned);
|
|
|
|
|
_set(value.copyWith(isPinned: await desktopWindow.isAlwaysOnTop()));
|
2026-09-05 20:09:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_disposed = true;
|
2026-09-25 03:56:56 +08:00
|
|
|
desktopWindow.removeListener(this);
|
2026-09-05 20:09:39 +08:00
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class WindowHeader extends ConsumerStatefulWidget {
|
|
|
|
|
const WindowHeader({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
ConsumerState<WindowHeader> createState() => _WindowHeaderState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _WindowHeaderState extends ConsumerState<WindowHeader> {
|
|
|
|
|
final caption = WindowCaptionController();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
caption.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-09-25 03:56:56 +08:00
|
|
|
final translucent =
|
|
|
|
|
ref.watch(windowBlurProvider) && !ref.watch(isMobileViewProvider);
|
2026-09-05 20:09:39 +08:00
|
|
|
return WindowHeaderBar(
|
|
|
|
|
height: kHeaderHeight,
|
2026-09-25 03:56:56 +08:00
|
|
|
translucent: translucent,
|
|
|
|
|
onDragStart: desktopWindow.startDragging,
|
2026-09-05 20:09:39 +08:00
|
|
|
onDoubleTap: caption.toggleMaximized,
|
|
|
|
|
title: system.isMacOS ? const Text(appName) : null,
|
|
|
|
|
actions: system.isMacOS
|
|
|
|
|
? null
|
|
|
|
|
: WindowHeaderActions(
|
|
|
|
|
state: caption,
|
|
|
|
|
onPin: caption.togglePin,
|
2026-09-25 03:56:56 +08:00
|
|
|
onMinimize: desktopWindow.minimize,
|
2026-09-05 20:09:39 +08:00
|
|
|
onMaximize: caption.toggleMaximized,
|
|
|
|
|
onClose: () {
|
|
|
|
|
ref.read(systemActionProvider.notifier).handleClose();
|
|
|
|
|
},
|
|
|
|
|
),
|
2024-07-19 02:14:58 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-09-05 20:09:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class WindowHeaderBar extends StatelessWidget {
|
|
|
|
|
const WindowHeaderBar({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.height,
|
|
|
|
|
required this.onDragStart,
|
|
|
|
|
required this.onDoubleTap,
|
2026-09-25 03:56:56 +08:00
|
|
|
this.translucent = false,
|
2026-09-05 20:09:39 +08:00
|
|
|
this.title,
|
|
|
|
|
this.actions,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final double height;
|
|
|
|
|
final VoidCallback onDragStart;
|
|
|
|
|
final VoidCallback onDoubleTap;
|
2026-09-25 03:56:56 +08:00
|
|
|
final bool translucent;
|
2026-09-05 20:09:39 +08:00
|
|
|
final Widget? title;
|
|
|
|
|
final Widget? actions;
|
2024-07-19 02:14:58 +08:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-09-25 03:56:56 +08:00
|
|
|
final colorScheme = context.colorScheme;
|
2024-07-19 02:14:58 +08:00
|
|
|
return Material(
|
2026-09-25 03:56:56 +08:00
|
|
|
color: translucent ? Colors.transparent : null,
|
|
|
|
|
shape: translucent
|
|
|
|
|
? null
|
|
|
|
|
: Border(bottom: BorderSide(color: colorScheme.outlineVariant)),
|
2026-09-05 20:09:39 +08:00
|
|
|
child: SizedBox(
|
|
|
|
|
height: height,
|
|
|
|
|
child: Stack(
|
|
|
|
|
alignment: AlignmentDirectional.center,
|
|
|
|
|
children: [
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
onPanStart: (_) {
|
|
|
|
|
onDragStart();
|
|
|
|
|
},
|
|
|
|
|
onDoubleTap: onDoubleTap,
|
|
|
|
|
child: ColoredBox(
|
2026-09-25 03:56:56 +08:00
|
|
|
color: translucent
|
|
|
|
|
? colorScheme.surfaceContainer.withValues(
|
|
|
|
|
alpha: kSidebarBlurOpacity,
|
|
|
|
|
)
|
|
|
|
|
: colorScheme.surfaceContainerHighest,
|
2026-09-05 20:09:39 +08:00
|
|
|
),
|
2024-07-19 02:14:58 +08:00
|
|
|
),
|
|
|
|
|
),
|
2026-09-05 20:09:39 +08:00
|
|
|
if (title != null)
|
|
|
|
|
Positioned.fill(
|
|
|
|
|
child: IgnorePointer(child: Center(child: title)),
|
|
|
|
|
),
|
|
|
|
|
if (actions != null)
|
|
|
|
|
Positioned(
|
|
|
|
|
top: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
child: IconButtonTheme(
|
|
|
|
|
data: IconButtonThemeData(
|
|
|
|
|
style: ButtonStyle(
|
|
|
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
|
|
|
minimumSize: WidgetStatePropertyAll(
|
|
|
|
|
getCaptionButtonSize(height),
|
|
|
|
|
),
|
|
|
|
|
maximumSize: WidgetStatePropertyAll(
|
|
|
|
|
getCaptionButtonSize(height),
|
|
|
|
|
),
|
|
|
|
|
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
|
|
|
|
shape: const WidgetStatePropertyAll(AppShape.none),
|
|
|
|
|
foregroundColor: WidgetStatePropertyAll(
|
|
|
|
|
context.colorScheme.onSurface,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: actions!,
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-07-31 17:09:18 +08:00
|
|
|
],
|
2026-09-05 20:09:39 +08:00
|
|
|
),
|
2024-07-19 02:14:58 +08:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 20:09:39 +08:00
|
|
|
class WindowHeaderActions extends StatelessWidget {
|
|
|
|
|
const WindowHeaderActions({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.state,
|
|
|
|
|
required this.onPin,
|
|
|
|
|
required this.onMinimize,
|
|
|
|
|
required this.onMaximize,
|
|
|
|
|
required this.onClose,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final ValueListenable<WindowCaptionState> state;
|
|
|
|
|
final VoidCallback onPin;
|
|
|
|
|
final VoidCallback onMinimize;
|
|
|
|
|
final VoidCallback onMaximize;
|
|
|
|
|
final VoidCallback onClose;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final appLocalizations = context.appLocalizations;
|
|
|
|
|
return ValueListenableBuilder(
|
|
|
|
|
valueListenable: state,
|
|
|
|
|
builder: (_, state, _) {
|
|
|
|
|
final (maximizeGlyph, maximizeTooltip) = switch (state) {
|
|
|
|
|
WindowCaptionState(isFullScreen: true) => (
|
|
|
|
|
CaptionGlyph.restore,
|
|
|
|
|
appLocalizations.exitFullScreen,
|
|
|
|
|
),
|
|
|
|
|
WindowCaptionState(isMaximized: true) => (
|
|
|
|
|
CaptionGlyph.restore,
|
|
|
|
|
appLocalizations.unmaximize,
|
|
|
|
|
),
|
|
|
|
|
_ => (CaptionGlyph.maximize, appLocalizations.maximize),
|
|
|
|
|
};
|
|
|
|
|
return Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
AspectRatio(
|
|
|
|
|
aspectRatio: 1,
|
|
|
|
|
child: IconButton(
|
|
|
|
|
tooltip: state.isPinned
|
|
|
|
|
? appLocalizations.unpinWindow
|
|
|
|
|
: appLocalizations.pinWindow,
|
|
|
|
|
style: const ButtonStyle(
|
|
|
|
|
shape: WidgetStatePropertyAll(CircleBorder()),
|
|
|
|
|
minimumSize: WidgetStatePropertyAll(Size.zero),
|
|
|
|
|
maximumSize: WidgetStatePropertyAll(Size.infinite),
|
|
|
|
|
iconSize: WidgetStatePropertyAll(pinIconSize),
|
|
|
|
|
),
|
|
|
|
|
onPressed: onPin,
|
2026-09-25 03:56:56 +08:00
|
|
|
icon: AnimatedGlyph(
|
|
|
|
|
glyph: AppGlyphs.pin,
|
|
|
|
|
filled: state.isPinned,
|
2026-09-05 20:09:39 +08:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
tooltip: appLocalizations.minimize,
|
|
|
|
|
onPressed: onMinimize,
|
|
|
|
|
icon: const CaptionIcon(CaptionGlyph.minimize),
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
tooltip: maximizeTooltip,
|
|
|
|
|
onPressed: onMaximize,
|
|
|
|
|
icon: CaptionIcon(maximizeGlyph),
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
tooltip: appLocalizations.close,
|
|
|
|
|
style: ButtonStyle(
|
|
|
|
|
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
|
|
|
|
final active =
|
|
|
|
|
states.contains(WidgetState.hovered) ||
|
|
|
|
|
states.contains(WidgetState.pressed);
|
|
|
|
|
return active ? context.colorScheme.error : null;
|
|
|
|
|
}),
|
|
|
|
|
foregroundColor: WidgetStateProperty.resolveWith((states) {
|
|
|
|
|
final active =
|
|
|
|
|
states.contains(WidgetState.hovered) ||
|
|
|
|
|
states.contains(WidgetState.pressed);
|
|
|
|
|
return active ? context.colorScheme.onError : null;
|
|
|
|
|
}),
|
|
|
|
|
overlayColor: WidgetStateProperty.resolveWith((states) {
|
|
|
|
|
return states.contains(WidgetState.pressed)
|
|
|
|
|
? context.colorScheme.onError.opacity12
|
|
|
|
|
: Colors.transparent;
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
onPressed: onClose,
|
|
|
|
|
icon: const CaptionIcon(CaptionGlyph.close),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|