Skip to content

Commit e1709c2

Browse files
committed
feat: add custom window controls for Linux and Windows
Add minimize, maximize, and close buttons to the app bar on non-macOS platforms. On macOS, native traffic lights are preserved. Bump version to 1.1.0.
1 parent 6c96331 commit e1709c2

6 files changed

Lines changed: 141 additions & 2 deletions

File tree

lib/screens/analytics_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter_heatmap_calendar/flutter_heatmap_calendar.dart';
88
import 'package:window_manager/window_manager.dart';
99
import '../providers/providers.dart';
1010
import '../models/models.dart';
11+
import '../widgets/window_controls.dart';
1112

1213
class AnalyticsScreen extends ConsumerWidget {
1314
const AnalyticsScreen({super.key});
@@ -34,6 +35,7 @@ class AnalyticsScreen extends ConsumerWidget {
3435
backgroundColor: Colors.transparent,
3536
elevation: 0,
3637
title: Text('Analytics', style: TextStyle(color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold, fontSize: 14)),
38+
actions: const [WindowControls()],
3739
),
3840
),
3941
),

lib/screens/journal_screen.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:window_manager/window_manager.dart';
88
import '../providers/providers.dart';
99
import '../models/models.dart';
1010
import '../database/db_helper.dart';
11+
import '../widgets/window_controls.dart';
1112

1213
class JournalScreen extends ConsumerStatefulWidget {
1314
const JournalScreen({super.key});
@@ -201,7 +202,8 @@ class _JournalScreenState extends ConsumerState<JournalScreen> {
201202
}
202203
},
203204
),
204-
const SizedBox(width: 12),
205+
const SizedBox(width: 4),
206+
const WindowControls(),
205207
],
206208
),
207209
),

lib/screens/ocd_tracker_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:line_icons/line_icons.dart';
66
import 'package:window_manager/window_manager.dart';
77
import '../models/models.dart';
88
import '../providers/providers.dart';
9+
import '../widgets/window_controls.dart';
910

1011
class OcdTrackerScreen extends ConsumerStatefulWidget {
1112
const OcdTrackerScreen({super.key});
@@ -74,6 +75,7 @@ class _OcdTrackerScreenState extends ConsumerState<OcdTrackerScreen> {
7475
),
7576
),
7677
),
78+
const WindowControls(),
7779
],
7880
),
7981
),

lib/screens/settings_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:line_icons/line_icons.dart';
77
import 'package:window_manager/window_manager.dart';
88
import '../database/db_helper.dart';
99
import '../providers/providers.dart';
10+
import '../widgets/window_controls.dart';
1011
import '../main.dart';
1112

1213
class SettingsScreen extends ConsumerWidget {
@@ -132,6 +133,7 @@ class SettingsScreen extends ConsumerWidget {
132133
backgroundColor: Colors.transparent,
133134
elevation: 0,
134135
title: Text('Settings', style: TextStyle(color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold, fontSize: 14)),
136+
actions: const [WindowControls()],
135137
),
136138
),
137139
),

lib/widgets/window_controls.dart

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import 'dart:io';
2+
import 'package:flutter/material.dart';
3+
import 'package:window_manager/window_manager.dart';
4+
5+
/// Custom window control buttons (minimize, maximize, close) for Linux and Windows.
6+
/// On macOS, returns an empty widget since native traffic lights are preserved.
7+
class WindowControls extends StatefulWidget {
8+
const WindowControls({super.key});
9+
10+
@override
11+
State<WindowControls> createState() => _WindowControlsState();
12+
}
13+
14+
class _WindowControlsState extends State<WindowControls> {
15+
bool _isMaximized = false;
16+
17+
@override
18+
void initState() {
19+
super.initState();
20+
_checkMaximized();
21+
}
22+
23+
Future<void> _checkMaximized() async {
24+
final maximized = await windowManager.isMaximized();
25+
if (mounted) setState(() => _isMaximized = maximized);
26+
}
27+
28+
@override
29+
Widget build(BuildContext context) {
30+
if (Platform.isMacOS) return const SizedBox.shrink();
31+
32+
final theme = Theme.of(context);
33+
final color = theme.colorScheme.onSurface.withOpacity(0.6);
34+
const size = 14.0;
35+
36+
return Row(
37+
mainAxisSize: MainAxisSize.min,
38+
children: [
39+
_ControlButton(
40+
icon: Icons.remove_rounded,
41+
iconSize: size,
42+
color: color,
43+
hoverColor: theme.colorScheme.onSurface.withOpacity(0.08),
44+
onTap: () => windowManager.minimize(),
45+
tooltip: 'Minimize',
46+
),
47+
_ControlButton(
48+
icon: _isMaximized ? Icons.filter_none_rounded : Icons.crop_square_rounded,
49+
iconSize: _isMaximized ? size - 2 : size,
50+
color: color,
51+
hoverColor: theme.colorScheme.onSurface.withOpacity(0.08),
52+
onTap: () async {
53+
if (_isMaximized) {
54+
await windowManager.unmaximize();
55+
} else {
56+
await windowManager.maximize();
57+
}
58+
_checkMaximized();
59+
},
60+
tooltip: _isMaximized ? 'Restore' : 'Maximize',
61+
),
62+
_ControlButton(
63+
icon: Icons.close_rounded,
64+
iconSize: size,
65+
color: color,
66+
hoverColor: Colors.redAccent.withOpacity(0.15),
67+
hoverIconColor: Colors.redAccent,
68+
onTap: () => windowManager.close(),
69+
tooltip: 'Close',
70+
),
71+
const SizedBox(width: 8),
72+
],
73+
);
74+
}
75+
}
76+
77+
class _ControlButton extends StatefulWidget {
78+
final IconData icon;
79+
final double iconSize;
80+
final Color color;
81+
final Color hoverColor;
82+
final Color? hoverIconColor;
83+
final VoidCallback onTap;
84+
final String tooltip;
85+
86+
const _ControlButton({
87+
required this.icon,
88+
required this.iconSize,
89+
required this.color,
90+
required this.hoverColor,
91+
required this.onTap,
92+
required this.tooltip,
93+
this.hoverIconColor,
94+
});
95+
96+
@override
97+
State<_ControlButton> createState() => _ControlButtonState();
98+
}
99+
100+
class _ControlButtonState extends State<_ControlButton> {
101+
bool _isHovered = false;
102+
103+
@override
104+
Widget build(BuildContext context) {
105+
return Tooltip(
106+
message: widget.tooltip,
107+
child: MouseRegion(
108+
cursor: SystemMouseCursors.click,
109+
onEnter: (_) => setState(() => _isHovered = true),
110+
onExit: (_) => setState(() => _isHovered = false),
111+
child: GestureDetector(
112+
onTap: widget.onTap,
113+
child: AnimatedContainer(
114+
duration: const Duration(milliseconds: 150),
115+
width: 32,
116+
height: 32,
117+
decoration: BoxDecoration(
118+
color: _isHovered ? widget.hoverColor : Colors.transparent,
119+
borderRadius: BorderRadius.circular(6),
120+
),
121+
child: Icon(
122+
widget.icon,
123+
size: widget.iconSize,
124+
color: _isHovered ? (widget.hoverIconColor ?? widget.color) : widget.color,
125+
),
126+
),
127+
),
128+
),
129+
);
130+
}
131+
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
1616
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
1717
# In Windows, build-name is used as the major, minor, and patch parts
1818
# of the product and file versions while build-number is used as the build suffix.
19-
version: 1.0.0+1
19+
version: 1.1.0+2
2020

2121
environment:
2222
sdk: ^3.11.1

0 commit comments

Comments
 (0)