import 'dart:async'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_ext/window_ext.dart'; import 'package:window_manager/window_manager.dart'; class WindowManager extends ConsumerStatefulWidget { final Widget child; const WindowManager({ super.key, required this.child, }); @override ConsumerState createState() => _WindowContainerState(); } class _WindowContainerState extends ConsumerState with WindowListener, WindowExtListener { @override Widget build(BuildContext context) { return widget.child; } @override void initState() { super.initState(); ref.listenManual( appSettingProvider.select((state) => state.autoLaunch), (prev, next) { if (prev != next) { debouncer.call( FunctionTag.autoLaunch, () { autoLaunch?.updateStatus(next); }, ); } }, ); windowExtManager.addListener(this); windowManager.addListener(this); } @override void onWindowClose() async { await globalState.appController.handleBackOrExit(); super.onWindowClose(); } @override void onWindowFocus() { super.onWindowFocus(); commonPrint.log('focus'); render?.resume(); } @override Future onShouldTerminate() async { await globalState.appController.handleExit(); super.onShouldTerminate(); } @override Future onWindowMoved() async { super.onWindowMoved(); final offset = await windowManager.getPosition(); ref.read(windowSettingProvider.notifier).updateState( (state) => state.copyWith( top: offset.dy, left: offset.dx, ), ); } @override Future onWindowResized() async { super.onWindowResized(); final size = await windowManager.getSize(); ref.read(windowSettingProvider.notifier).updateState( (state) => state.copyWith( width: size.width, height: size.height, ), ); } @override void onWindowMinimize() async { globalState.appController.savePreferencesDebounce(); commonPrint.log('minimize'); render?.pause(); super.onWindowMinimize(); } @override void onWindowRestore() { commonPrint.log('restore'); render?.resume(); super.onWindowRestore(); } @override Future dispose() async { windowManager.removeListener(this); windowExtManager.removeListener(this); super.dispose(); } } class WindowHeaderContainer extends StatelessWidget { final Widget child; const WindowHeaderContainer({ super.key, required this.child, }); @override Widget build(BuildContext context) { return Consumer( builder: (_, ref, child) { final isMobileView = ref.watch(isMobileViewProvider); final version = ref.watch(versionProvider); if ((version <= 10 || !isMobileView) && system.isMacOS) { return child!; } return Stack( children: [ Column( children: [ SizedBox( height: kHeaderHeight, ), Expanded( flex: 1, child: child!, ), ], ), const WindowHeader(), ], ); }, child: child, ); } } class WindowHeader extends StatefulWidget { const WindowHeader({super.key}); @override State createState() => _WindowHeaderState(); } class _WindowHeaderState extends State { final isMaximizedNotifier = ValueNotifier(false); final isPinNotifier = ValueNotifier(false); @override void initState() { super.initState(); _initNotifier(); } Future _initNotifier() async { isMaximizedNotifier.value = await windowManager.isMaximized(); isPinNotifier.value = await windowManager.isAlwaysOnTop(); } @override void dispose() { isMaximizedNotifier.dispose(); isPinNotifier.dispose(); super.dispose(); } Future _updateMaximized() async { final isMaximized = await windowManager.isMaximized(); switch (isMaximized) { case true: await windowManager.unmaximize(); break; case false: await windowManager.maximize(); break; } isMaximizedNotifier.value = await windowManager.isMaximized(); } Future _updatePin() async { final isAlwaysOnTop = await windowManager.isAlwaysOnTop(); await windowManager.setAlwaysOnTop(!isAlwaysOnTop); isPinNotifier.value = await windowManager.isAlwaysOnTop(); } Widget _buildActions() { return Row( children: [ IconButton( onPressed: () async { _updatePin(); }, icon: ValueListenableBuilder( valueListenable: isPinNotifier, builder: (_, value, ___) { return value ? const Icon( Icons.push_pin, ) : const Icon( Icons.push_pin_outlined, ); }, ), ), IconButton( onPressed: () { windowManager.minimize(); }, icon: const Icon(Icons.remove), ), IconButton( onPressed: () async { _updateMaximized(); }, icon: ValueListenableBuilder( valueListenable: isMaximizedNotifier, builder: (_, value, ___) { return value ? const Icon( Icons.filter_none, size: 20, ) : const Icon( Icons.crop_square, ); }, ), ), IconButton( onPressed: () { globalState.appController.handleBackOrExit(); }, icon: const Icon(Icons.close), ), // const SizedBox( // width: 8, // ), ], ); } @override Widget build(BuildContext context) { return Material( child: Stack( alignment: AlignmentDirectional.center, children: [ Positioned( child: GestureDetector( onPanStart: (_) { windowManager.startDragging(); }, onDoubleTap: () { _updateMaximized(); }, child: Container( color: context.colorScheme.secondary.opacity15, alignment: Alignment.centerLeft, height: kHeaderHeight, ), ), ), if (system.isMacOS) const Text( appName, ) else ...[ Positioned( right: 0, child: _buildActions(), ), ] ], ), ); } } class AppIcon extends StatelessWidget { const AppIcon({super.key}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: context.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(12), ), padding: EdgeInsets.all(6), child: SizedBox( width: 28, height: 28, child: CircleAvatar( foregroundImage: AssetImage('assets/images/icon.png'), backgroundColor: Colors.transparent, ), ), ); } }