mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 19:17:43 +03:00
feat: custom windows title bar (#5311)
This commit is contained in:
parent
a0ed043cb8
commit
cdcb393efd
@ -1,6 +1,37 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class WindowsButtonListener extends WindowListener {
|
||||
WindowsButtonListener();
|
||||
|
||||
final ValueNotifier<bool> isMaximized = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
isMaximized.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUnmaximize() {
|
||||
isMaximized.value = false;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
isMaximized.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class CocoaWindowChannel {
|
||||
CocoaWindowChannel._();
|
||||
@ -26,29 +57,117 @@ class CocoaWindowChannel {
|
||||
}
|
||||
|
||||
class MoveWindowDetector extends StatefulWidget {
|
||||
const MoveWindowDetector({super.key, this.child});
|
||||
const MoveWindowDetector({
|
||||
super.key,
|
||||
this.child,
|
||||
this.showTitleBar = false,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
final bool showTitleBar;
|
||||
|
||||
@override
|
||||
MoveWindowDetectorState createState() => MoveWindowDetectorState();
|
||||
}
|
||||
|
||||
class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
late final WindowsButtonListener? windowsButtonListener;
|
||||
|
||||
double winX = 0;
|
||||
double winY = 0;
|
||||
|
||||
bool isMaximized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (PlatformExtension.isWindows) {
|
||||
windowsButtonListener = WindowsButtonListener();
|
||||
windowManager.addListener(windowsButtonListener!);
|
||||
windowsButtonListener!.isMaximized.addListener(() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => isMaximized = windowsButtonListener!.isMaximized.value,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
windowsButtonListener = null;
|
||||
}
|
||||
|
||||
windowManager.isMaximized().then(
|
||||
(v) => mounted ? setState(() => isMaximized = v) : null,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (windowsButtonListener != null) {
|
||||
windowManager.removeListener(windowsButtonListener!);
|
||||
windowsButtonListener?.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Platform.isMacOS) {
|
||||
if (!Platform.isMacOS && !Platform.isWindows) {
|
||||
return widget.child ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.showTitleBar) ...[
|
||||
Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
child: DragToMoveArea(
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(4),
|
||||
_buildToggleMenuButton(context),
|
||||
const Spacer(),
|
||||
WindowCaptionButton.minimize(
|
||||
brightness: brightness,
|
||||
onPressed: () => windowManager.minimize(),
|
||||
),
|
||||
if (isMaximized) ...[
|
||||
WindowCaptionButton.unmaximize(
|
||||
brightness: brightness,
|
||||
onPressed: () => windowManager.unmaximize(),
|
||||
),
|
||||
] else ...[
|
||||
WindowCaptionButton.maximize(
|
||||
brightness: brightness,
|
||||
onPressed: () => windowManager.maximize(),
|
||||
),
|
||||
],
|
||||
WindowCaptionButton.close(
|
||||
brightness: brightness,
|
||||
onPressed: () => windowManager.close(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
widget.child ?? const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onDoubleTap: () async {
|
||||
await CocoaWindowChannel.instance.zoom();
|
||||
},
|
||||
onDoubleTap: () async => CocoaWindowChannel.instance.zoom(),
|
||||
onPanStart: (DragStartDetails details) {
|
||||
winX = details.globalPosition.dx;
|
||||
winY = details.globalPosition.dy;
|
||||
@ -65,4 +184,29 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleMenuButton(BuildContext context) {
|
||||
if (!context.read<HomeSettingBloc>().state.isMenuCollapsed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return FlowyTooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n'),
|
||||
const TextSpan(text: 'Ctrl+\\'),
|
||||
],
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context
|
||||
.read<HomeSettingBloc>()
|
||||
.add(const HomeSettingEvent.collapseMenu()),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
|
||||
? const FlowySvg(FlowySvgs.show_menu_s)
|
||||
: const FlowySvg(FlowySvgs.hide_menu_m),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ import 'package:flutter/material.dart';
|
||||
class AppFlowyApplication implements EntryPoint {
|
||||
@override
|
||||
Widget create(LaunchConfiguration config) {
|
||||
return SplashScreen(
|
||||
isAnon: config.isAnon,
|
||||
);
|
||||
return SplashScreen(isAnon: config.isAnon);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'dart:ui';
|
||||
import 'package:appflowy/core/helpers/helpers.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scaled_app/scaled_app.dart';
|
||||
@ -46,6 +47,11 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
|
||||
if (PlatformExtension.isWindows) {
|
||||
// Hide title bar on Windows, we implement a custom solution elsewhere
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
}
|
||||
|
||||
final position = await windowsManager.getPosition();
|
||||
if (position != null) {
|
||||
await windowManager.setPosition(position);
|
||||
@ -54,8 +60,7 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
|
||||
unawaited(
|
||||
windowsManager.getScaleFactor().then(
|
||||
(value) =>
|
||||
ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => value,
|
||||
(v) => ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => v,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -16,10 +16,7 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
class SplashScreen extends StatelessWidget {
|
||||
/// Root Page of the app.
|
||||
const SplashScreen({
|
||||
super.key,
|
||||
required this.isAnon,
|
||||
});
|
||||
const SplashScreen({super.key, required this.isAnon});
|
||||
|
||||
final bool isAnon;
|
||||
|
||||
|
@ -220,9 +220,10 @@ class PageManager {
|
||||
],
|
||||
child: Selector<PageNotifier, Widget>(
|
||||
selector: (context, notifier) => notifier.titleWidget,
|
||||
builder: (context, widget, child) {
|
||||
return MoveWindowDetector(child: HomeTopBar(layout: layout));
|
||||
},
|
||||
builder: (_, __, child) => MoveWindowDetector(
|
||||
showTitleBar: true,
|
||||
child: HomeTopBar(layout: layout),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
@ -25,20 +26,18 @@ class SidebarTopMenu extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
height: HomeSizes.topBarHeight,
|
||||
child: MoveWindowDetector(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildLogoIcon(context),
|
||||
const Spacer(),
|
||||
_buildCollapseMenuButton(context),
|
||||
],
|
||||
),
|
||||
builder: (context, _) => SizedBox(
|
||||
height: !PlatformExtension.isWindows ? HomeSizes.topBarHeight : 45,
|
||||
child: MoveWindowDetector(
|
||||
child: Row(
|
||||
children: [
|
||||
_buildLogoIcon(context),
|
||||
const Spacer(),
|
||||
_buildCollapseMenuButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -72,15 +71,13 @@ class SidebarTopMenu extends StatelessWidget {
|
||||
return FlowyTooltip(
|
||||
richMessage: textSpan,
|
||||
child: FlowyIconButton(
|
||||
width: 28,
|
||||
width: PlatformExtension.isWindows ? 30 : 28,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context
|
||||
.read<HomeSettingBloc>()
|
||||
.add(const HomeSettingEvent.collapseMenu()),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.hide_menu_m,
|
||||
),
|
||||
icon: const FlowySvg(FlowySvgs.hide_menu_m),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -63,7 +64,7 @@ class FlowyNavigation extends StatelessWidget {
|
||||
return BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||
buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
|
||||
builder: (context, state) {
|
||||
if (state.isMenuCollapsed) {
|
||||
if (!PlatformExtension.isWindows && state.isMenuCollapsed) {
|
||||
return RotationTransition(
|
||||
turns: const AlwaysStoppedAnimation(180 / 360),
|
||||
child: FlowyTooltip(
|
||||
|
Loading…
Reference in New Issue
Block a user