mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-17 14:07:22 +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/services.dart';
|
||||||
import 'package:flutter/material.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 {
|
class CocoaWindowChannel {
|
||||||
CocoaWindowChannel._();
|
CocoaWindowChannel._();
|
||||||
@ -26,29 +57,117 @@ class CocoaWindowChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MoveWindowDetector extends StatefulWidget {
|
class MoveWindowDetector extends StatefulWidget {
|
||||||
const MoveWindowDetector({super.key, this.child});
|
const MoveWindowDetector({
|
||||||
|
super.key,
|
||||||
|
this.child,
|
||||||
|
this.showTitleBar = false,
|
||||||
|
});
|
||||||
|
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
final bool showTitleBar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MoveWindowDetectorState createState() => MoveWindowDetectorState();
|
MoveWindowDetectorState createState() => MoveWindowDetectorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||||
|
late final WindowsButtonListener? windowsButtonListener;
|
||||||
|
|
||||||
double winX = 0;
|
double winX = 0;
|
||||||
double winY = 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!Platform.isMacOS) {
|
if (!Platform.isMacOS && !Platform.isWindows) {
|
||||||
return widget.child ?? const SizedBox.shrink();
|
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(
|
return GestureDetector(
|
||||||
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
|
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onDoubleTap: () async {
|
onDoubleTap: () async => CocoaWindowChannel.instance.zoom(),
|
||||||
await CocoaWindowChannel.instance.zoom();
|
|
||||||
},
|
|
||||||
onPanStart: (DragStartDetails details) {
|
onPanStart: (DragStartDetails details) {
|
||||||
winX = details.globalPosition.dx;
|
winX = details.globalPosition.dx;
|
||||||
winY = details.globalPosition.dy;
|
winY = details.globalPosition.dy;
|
||||||
@ -65,4 +184,29 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
|||||||
child: widget.child,
|
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 {
|
class AppFlowyApplication implements EntryPoint {
|
||||||
@override
|
@override
|
||||||
Widget create(LaunchConfiguration config) {
|
Widget create(LaunchConfiguration config) {
|
||||||
return SplashScreen(
|
return SplashScreen(isAnon: config.isAnon);
|
||||||
isAnon: config.isAnon,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'dart:ui';
|
|||||||
import 'package:appflowy/core/helpers/helpers.dart';
|
import 'package:appflowy/core/helpers/helpers.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/startup/tasks/app_window_size_manager.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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:scaled_app/scaled_app.dart';
|
import 'package:scaled_app/scaled_app.dart';
|
||||||
@ -46,6 +47,11 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
|||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
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();
|
final position = await windowsManager.getPosition();
|
||||||
if (position != null) {
|
if (position != null) {
|
||||||
await windowManager.setPosition(position);
|
await windowManager.setPosition(position);
|
||||||
@ -54,8 +60,7 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
|||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
windowsManager.getScaleFactor().then(
|
windowsManager.getScaleFactor().then(
|
||||||
(value) =>
|
(v) => ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => v,
|
||||||
ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => value,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
class SplashScreen extends StatelessWidget {
|
class SplashScreen extends StatelessWidget {
|
||||||
/// Root Page of the app.
|
/// Root Page of the app.
|
||||||
const SplashScreen({
|
const SplashScreen({super.key, required this.isAnon});
|
||||||
super.key,
|
|
||||||
required this.isAnon,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool isAnon;
|
final bool isAnon;
|
||||||
|
|
||||||
|
@ -220,9 +220,10 @@ class PageManager {
|
|||||||
],
|
],
|
||||||
child: Selector<PageNotifier, Widget>(
|
child: Selector<PageNotifier, Widget>(
|
||||||
selector: (context, notifier) => notifier.titleWidget,
|
selector: (context, notifier) => notifier.titleWidget,
|
||||||
builder: (context, widget, child) {
|
builder: (_, __, child) => MoveWindowDetector(
|
||||||
return MoveWindowDetector(child: HomeTopBar(layout: layout));
|
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/home/home_setting_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_sizes.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.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/flowy_tooltip.dart';
|
||||||
@ -25,20 +26,18 @@ class SidebarTopMenu extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
||||||
builder: (context, state) {
|
builder: (context, _) => SizedBox(
|
||||||
return SizedBox(
|
height: !PlatformExtension.isWindows ? HomeSizes.topBarHeight : 45,
|
||||||
height: HomeSizes.topBarHeight,
|
child: MoveWindowDetector(
|
||||||
child: MoveWindowDetector(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
_buildLogoIcon(context),
|
||||||
_buildLogoIcon(context),
|
const Spacer(),
|
||||||
const Spacer(),
|
_buildCollapseMenuButton(context),
|
||||||
_buildCollapseMenuButton(context),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,15 +71,13 @@ class SidebarTopMenu extends StatelessWidget {
|
|||||||
return FlowyTooltip(
|
return FlowyTooltip(
|
||||||
richMessage: textSpan,
|
richMessage: textSpan,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
width: 28,
|
width: PlatformExtension.isWindows ? 30 : 28,
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
onPressed: () => context
|
onPressed: () => context
|
||||||
.read<HomeSettingBloc>()
|
.read<HomeSettingBloc>()
|
||||||
.add(const HomeSettingEvent.collapseMenu()),
|
.add(const HomeSettingEvent.collapseMenu()),
|
||||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||||
icon: const FlowySvg(
|
icon: const FlowySvg(FlowySvgs.hide_menu_m),
|
||||||
FlowySvgs.hide_menu_m,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
@ -63,7 +64,7 @@ class FlowyNavigation extends StatelessWidget {
|
|||||||
return BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
return BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||||
buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
|
buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isMenuCollapsed) {
|
if (!PlatformExtension.isWindows && state.isMenuCollapsed) {
|
||||||
return RotationTransition(
|
return RotationTransition(
|
||||||
turns: const AlwaysStoppedAnimation(180 / 360),
|
turns: const AlwaysStoppedAnimation(180 / 360),
|
||||||
child: FlowyTooltip(
|
child: FlowyTooltip(
|
||||||
|
Loading…
Reference in New Issue
Block a user