mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: frameless window for mac
This commit is contained in:
parent
ef0d59ff30
commit
c4db17f73c
67
frontend/app_flowy/lib/core/frameless_window.dart
Normal file
67
frontend/app_flowy/lib/core/frameless_window.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
class CocoaWindowChannel {
|
||||
CocoaWindowChannel._();
|
||||
|
||||
final MethodChannel _channel = const MethodChannel("flutter/cocoaWindow");
|
||||
|
||||
static final CocoaWindowChannel instance = CocoaWindowChannel._();
|
||||
|
||||
Future<void> setWindowPosition(Offset offset) async {
|
||||
await _channel.invokeMethod("setWindowPosition", [offset.dx, offset.dy]);
|
||||
}
|
||||
|
||||
Future<List<double>> getWindowPosition() async {
|
||||
final raw = await _channel.invokeMethod("getWindowPosition");
|
||||
final arr = raw as List<dynamic>;
|
||||
final List<double> result = arr.map((s) => s as double).toList();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> zoom() async {
|
||||
await _channel.invokeMethod("zoom");
|
||||
}
|
||||
}
|
||||
|
||||
class MoveWindowDetector extends StatefulWidget {
|
||||
const MoveWindowDetector({Key? key, this.child}) : super(key: key);
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
_MoveWindowDetectorState createState() => _MoveWindowDetectorState();
|
||||
}
|
||||
|
||||
class _MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
double winX = 0;
|
||||
double winY = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Platform.isMacOS) {
|
||||
return widget.child ?? Container();
|
||||
}
|
||||
return GestureDetector(
|
||||
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onDoubleTap: () async {
|
||||
await CocoaWindowChannel.instance.zoom();
|
||||
},
|
||||
onPanStart: (DragStartDetails details) {
|
||||
winX = details.globalPosition.dx;
|
||||
winY = details.globalPosition.dy;
|
||||
},
|
||||
onPanUpdate: (DragUpdateDetails details) async {
|
||||
final windowPos = await CocoaWindowChannel.instance.getWindowPosition();
|
||||
final double dx = windowPos[0];
|
||||
final double dy = windowPos[1];
|
||||
final deltaX = details.globalPosition.dx - winX;
|
||||
final deltaY = details.globalPosition.dy - winY;
|
||||
await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY));
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
@ -49,6 +49,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
unauthorized: (_Unauthorized value) {
|
||||
emit(state.copyWith(unauthorized: true));
|
||||
},
|
||||
collapseMenu: (e) {
|
||||
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -77,6 +80,7 @@ class HomeEvent with _$HomeEvent {
|
||||
const factory HomeEvent.dismissEditPannel() = _DismissEditPannel;
|
||||
const factory HomeEvent.didReceiveWorkspaceSetting(CurrentWorkspaceSetting setting) = _DidReceiveWorkspaceSetting;
|
||||
const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
|
||||
const factory HomeEvent.collapseMenu() = _CollapseMenu;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -87,6 +91,7 @@ class HomeState with _$HomeState {
|
||||
required Option<EditPannelContext> pannelContext,
|
||||
required CurrentWorkspaceSetting workspaceSetting,
|
||||
required bool unauthorized,
|
||||
required bool isMenuCollapsed,
|
||||
}) = _HomeState;
|
||||
|
||||
factory HomeState.initial(CurrentWorkspaceSetting workspaceSetting) => HomeState(
|
||||
@ -95,5 +100,6 @@ class HomeState with _$HomeState {
|
||||
pannelContext: none(),
|
||||
workspaceSetting: workspaceSetting,
|
||||
unauthorized: false,
|
||||
isMenuCollapsed: false,
|
||||
);
|
||||
}
|
||||
|
@ -25,10 +25,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
|
||||
listener.start(addAppCallback: _handleAppsOrFail);
|
||||
await _fetchApps(emit);
|
||||
},
|
||||
collapse: (e) async {
|
||||
final isCollapse = state.isCollapse;
|
||||
emit(state.copyWith(isCollapse: !isCollapse));
|
||||
},
|
||||
openPage: (e) async {
|
||||
emit(state.copyWith(plugin: e.plugin));
|
||||
},
|
||||
@ -94,7 +90,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
|
||||
@freezed
|
||||
class MenuEvent with _$MenuEvent {
|
||||
const factory MenuEvent.initial() = _Initial;
|
||||
const factory MenuEvent.collapse() = _Collapse;
|
||||
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
|
||||
const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp;
|
||||
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
|
||||
@ -104,14 +99,12 @@ class MenuEvent with _$MenuEvent {
|
||||
@freezed
|
||||
class MenuState with _$MenuState {
|
||||
const factory MenuState({
|
||||
required bool isCollapse,
|
||||
required List<App> apps,
|
||||
required Either<Unit, FlowyError> successOrFailure,
|
||||
required Plugin plugin,
|
||||
}) = _MenuState;
|
||||
|
||||
factory MenuState.initial() => MenuState(
|
||||
isCollapse: false,
|
||||
apps: [],
|
||||
successOrFailure: left(unit),
|
||||
plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),
|
||||
|
@ -1,8 +1,12 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:time/time.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
@ -11,6 +15,7 @@ import 'package:app_flowy/plugin/plugin.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/navigation.dart';
|
||||
import 'package:app_flowy/core/frameless_window.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
@ -152,7 +157,7 @@ class HomeStackManager {
|
||||
child: Selector<HomeStackNotifier, Widget>(
|
||||
selector: (context, notifier) => notifier.titleWidget,
|
||||
builder: (context, widget, child) {
|
||||
return const HomeTopBar();
|
||||
return const MoveWindowDetector(child: HomeTopBar());
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -191,6 +196,14 @@ class HomeTopBar extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
buildWhen: ((previous, current) => previous.isMenuCollapsed != current.isMenuCollapsed),
|
||||
builder: (context, state) {
|
||||
if (state.isMenuCollapsed && Platform.isMacOS) {
|
||||
return const HSpace(80);
|
||||
}
|
||||
return const HSpace(0);
|
||||
}),
|
||||
const FlowyNavigation(),
|
||||
const HSpace(16),
|
||||
ChangeNotifierProvider.value(
|
||||
|
@ -1,6 +1,8 @@
|
||||
export './app/header/header.dart';
|
||||
export './app/menu_app.dart';
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
@ -18,7 +20,9 @@ import 'package:expandable/expandable.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/core/frameless_window.dart';
|
||||
// import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
|
||||
@ -59,10 +63,10 @@ class HomeMenu extends StatelessWidget {
|
||||
getIt<HomeStackManager>().setPlugin(state.plugin);
|
||||
},
|
||||
),
|
||||
BlocListener<MenuBloc, MenuState>(
|
||||
listenWhen: (p, c) => p.isCollapse != c.isCollapse,
|
||||
BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
|
||||
listener: (context, state) {
|
||||
_collapsedNotifier.value = state.isCollapse;
|
||||
_collapsedNotifier.value = state.isMenuCollapsed;
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -179,6 +183,17 @@ class MenuSharedState {
|
||||
|
||||
class MenuTopBar extends StatelessWidget {
|
||||
const MenuTopBar({Key? key}) : super(key: key);
|
||||
|
||||
Widget renderIcon(BuildContext context) {
|
||||
if (Platform.isMacOS) {
|
||||
return Container();
|
||||
}
|
||||
final theme = context.watch<AppTheme>();
|
||||
return (theme.isDark
|
||||
? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
|
||||
: svgWithSize("flowy_logo_with_text", const Size(92, 17)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
@ -186,20 +201,19 @@ class MenuTopBar extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
height: HomeSizes.topBarHeight,
|
||||
child: Row(
|
||||
child: MoveWindowDetector(
|
||||
child: Row(
|
||||
children: [
|
||||
(theme.isDark
|
||||
? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
|
||||
: svgWithSize("flowy_logo_with_text", const Size(92, 17))),
|
||||
renderIcon(context),
|
||||
const Spacer(),
|
||||
FlowyIconButton(
|
||||
width: 28,
|
||||
onPressed: () => context.read<MenuBloc>().add(const MenuEvent.collapse()),
|
||||
onPressed: () => context.read<HomeBloc>().add(const HomeEvent.collapseMenu()),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: svgWidget("home/hide_menu", color: theme.iconColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
@ -95,6 +96,7 @@ class FlowyNavigation extends StatelessWidget {
|
||||
width: 24,
|
||||
onPressed: () {
|
||||
notifier.value = false;
|
||||
ctx.read<HomeBloc>().add(const HomeEvent.collapseMenu());
|
||||
},
|
||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
||||
icon: svgWidget("home/hide_menu", color: theme.iconColor),
|
||||
|
@ -1,12 +1,82 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
private let kTrafficLightOffetTop = 22
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
func registerMethodChannel(flutterViewController: FlutterViewController) {
|
||||
let cocoaWindowChannel = FlutterMethodChannel(name: "flutter/cocoaWindow", binaryMessenger: flutterViewController.engine.binaryMessenger)
|
||||
cocoaWindowChannel.setMethodCallHandler({
|
||||
(call: FlutterMethodCall, result: FlutterResult) -> Void in
|
||||
if call.method == "setWindowPosition" {
|
||||
guard let position = call.arguments as? NSArray else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
let nX = position[0] as! NSNumber
|
||||
let nY = position[1] as! NSNumber
|
||||
let x = nX.doubleValue
|
||||
let y = nY.doubleValue
|
||||
|
||||
self.setFrameOrigin(NSPoint(x: x, y: y))
|
||||
result(nil)
|
||||
return
|
||||
} else if call.method == "getWindowPosition" {
|
||||
let frame = self.frame
|
||||
result([frame.origin.x, frame.origin.y])
|
||||
return
|
||||
} else if call.method == "zoom" {
|
||||
self.zoom(self)
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
|
||||
result(FlutterMethodNotImplemented)
|
||||
})
|
||||
}
|
||||
|
||||
func layoutTrafficLightButton(titlebarView: NSView, button: NSButton, offsetTop: CGFloat, offsetLeft: CGFloat) {
|
||||
button.translatesAutoresizingMaskIntoConstraints = false;
|
||||
titlebarView.addConstraint(NSLayoutConstraint.init(
|
||||
item: button,
|
||||
attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titlebarView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: offsetTop))
|
||||
titlebarView.addConstraint(NSLayoutConstraint.init(
|
||||
item: button,
|
||||
attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titlebarView, attribute: NSLayoutConstraint.Attribute.left, multiplier: 1, constant: offsetLeft))
|
||||
}
|
||||
|
||||
func layoutTrafficLights() {
|
||||
let closeButton = self.standardWindowButton(ButtonType.closeButton)!
|
||||
let minButton = self.standardWindowButton(ButtonType.miniaturizeButton)!
|
||||
let zoomButton = self.standardWindowButton(ButtonType.zoomButton)!
|
||||
let titlebarView = closeButton.superview!
|
||||
|
||||
self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20)
|
||||
self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38)
|
||||
self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56)
|
||||
|
||||
let customToolbar = NSTitlebarAccessoryViewController()
|
||||
let newView = NSView()
|
||||
newView.frame = NSRect(origin: CGPoint(), size: CGSize(width: 0, height: 40)) // only the height is cared
|
||||
customToolbar.view = newView
|
||||
self.addTitlebarAccessoryViewController(customToolbar)
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController.init()
|
||||
let windowFrame = self.frame
|
||||
self.contentViewController = flutterViewController
|
||||
|
||||
self.registerMethodChannel(flutterViewController: flutterViewController)
|
||||
|
||||
self.setFrame(windowFrame, display: true)
|
||||
self.titlebarAppearsTransparent = true
|
||||
self.titleVisibility = .hidden
|
||||
self.styleMask.insert(StyleMask.fullSizeContentView)
|
||||
self.isMovableByWindowBackground = true
|
||||
self.isMovable = false
|
||||
|
||||
self.layoutTrafficLights()
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user