Skip to content

Commit 089c6a2

Browse files
committed
feat: custom titlebar for desktops
1 parent 8bc6dd7 commit 089c6a2

File tree

2 files changed

+219
-2
lines changed

2 files changed

+219
-2
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:window_manager/window_manager.dart';
3+
4+
class AnymexTitleBar {
5+
static final ValueNotifier<bool> isFullScreen = ValueNotifier(false);
6+
7+
static Future<void> initialize() async {
8+
WidgetsFlutterBinding.ensureInitialized();
9+
await windowManager.ensureInitialized();
10+
11+
const windowOptions = WindowOptions(
12+
backgroundColor: Colors.transparent,
13+
skipTaskbar: false,
14+
titleBarStyle: TitleBarStyle.hidden,
15+
);
16+
17+
await windowManager.waitUntilReadyToShow(windowOptions, () async {
18+
await windowManager.show();
19+
await windowManager.focus();
20+
});
21+
}
22+
23+
static Widget titleBar() => ValueListenableBuilder<bool>(
24+
valueListenable: isFullScreen,
25+
builder: (_, fullscreen, __) {
26+
return fullscreen ? const SizedBox.shrink() : _TitleBarWidget();
27+
},
28+
);
29+
30+
static Future<void> setFullScreen(bool enable) async {
31+
windowManager.setFullScreen(enable);
32+
isFullScreen.value = enable;
33+
}
34+
}
35+
36+
class _TitleBarWidget extends StatelessWidget {
37+
@override
38+
Widget build(BuildContext context) {
39+
final defaultColor = Theme.of(context).colorScheme.onSurface;
40+
41+
return Material(
42+
color: Colors.transparent,
43+
child: ClipRect(
44+
child: Container(
45+
height: 40,
46+
decoration: BoxDecoration(
47+
color: Colors.black.withOpacity(0.2),
48+
border: Border(
49+
bottom: BorderSide(
50+
color: defaultColor.withOpacity(0.1),
51+
width: 0.5,
52+
),
53+
),
54+
),
55+
child: Row(
56+
children: [
57+
const SizedBox(width: 16),
58+
Container(
59+
padding: const EdgeInsets.all(5),
60+
decoration: BoxDecoration(
61+
color: defaultColor.withOpacity(0.1),
62+
borderRadius: BorderRadius.circular(8),
63+
),
64+
child: ClipRRect(
65+
borderRadius: BorderRadius.circular(8),
66+
child: Image.asset(
67+
'assets/images/logo.png',
68+
width: 18,
69+
height: 18,
70+
),
71+
),
72+
),
73+
const SizedBox(width: 10),
74+
Text(
75+
'AnymeX',
76+
style: TextStyle(
77+
fontSize: 13,
78+
fontWeight: FontWeight.w600,
79+
color: defaultColor,
80+
letterSpacing: 0.5,
81+
),
82+
),
83+
Expanded(
84+
child: GestureDetector(
85+
behavior: HitTestBehavior.translucent,
86+
onPanStart: (details) {
87+
windowManager.startDragging();
88+
},
89+
onDoubleTap: () async {
90+
bool isMaximized = await windowManager.isMaximized();
91+
if (isMaximized) {
92+
windowManager.unmaximize();
93+
} else {
94+
windowManager.maximize();
95+
}
96+
},
97+
),
98+
),
99+
_WindowButton(
100+
icon: Icons.remove,
101+
onPressed: () => windowManager.minimize(),
102+
buttonColor: defaultColor,
103+
),
104+
_WindowButton(
105+
icon: Icons.crop_square_rounded,
106+
onPressed: () async {
107+
bool isMaximized = await windowManager.isMaximized();
108+
if (isMaximized) {
109+
windowManager.unmaximize();
110+
} else {
111+
windowManager.maximize();
112+
}
113+
},
114+
buttonColor: defaultColor,
115+
),
116+
_WindowButton(
117+
icon: Icons.close_rounded,
118+
onPressed: () => windowManager.close(),
119+
isClose: true,
120+
buttonColor: defaultColor,
121+
),
122+
],
123+
),
124+
),
125+
),
126+
);
127+
}
128+
}
129+
130+
class _WindowButton extends StatefulWidget {
131+
final IconData icon;
132+
final VoidCallback onPressed;
133+
final bool isClose;
134+
final Color buttonColor;
135+
136+
const _WindowButton({
137+
required this.icon,
138+
required this.onPressed,
139+
this.isClose = false,
140+
required this.buttonColor,
141+
});
142+
143+
@override
144+
State<_WindowButton> createState() => _WindowButtonState();
145+
}
146+
147+
class _WindowButtonState extends State<_WindowButton>
148+
with SingleTickerProviderStateMixin {
149+
bool isHovered = false;
150+
late AnimationController _controller;
151+
late Animation<double> _scaleAnimation;
152+
153+
@override
154+
void initState() {
155+
super.initState();
156+
_controller = AnimationController(
157+
duration: const Duration(milliseconds: 150),
158+
vsync: this,
159+
);
160+
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
161+
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
162+
);
163+
}
164+
165+
@override
166+
void dispose() {
167+
_controller.dispose();
168+
super.dispose();
169+
}
170+
171+
@override
172+
Widget build(BuildContext context) {
173+
final isDark = Theme.of(context).brightness == Brightness.dark;
174+
175+
return MouseRegion(
176+
onEnter: (_) => setState(() => isHovered = true),
177+
onExit: (_) => setState(() => isHovered = false),
178+
child: GestureDetector(
179+
onTapDown: (_) => _controller.forward(),
180+
onTapUp: (_) {
181+
_controller.reverse();
182+
widget.onPressed();
183+
},
184+
onTapCancel: () => _controller.reverse(),
185+
child: ScaleTransition(
186+
scale: _scaleAnimation,
187+
child: AnimatedContainer(
188+
duration: const Duration(milliseconds: 200),
189+
curve: Curves.easeInOut,
190+
width: 46,
191+
height: 40,
192+
decoration: BoxDecoration(
193+
color: isHovered
194+
? (widget.isClose
195+
? Colors.red.withOpacity(0.9)
196+
: widget.buttonColor.withOpacity(isDark ? 0.15 : 0.1))
197+
: Colors.transparent,
198+
),
199+
child: Center(
200+
child: AnimatedContainer(
201+
duration: const Duration(milliseconds: 200),
202+
child: Icon(
203+
widget.icon,
204+
size: widget.isClose ? 18 : 16,
205+
color: isHovered && widget.isClose
206+
? Colors.white
207+
: widget.buttonColor.withOpacity(0.9),
208+
),
209+
),
210+
),
211+
),
212+
),
213+
),
214+
);
215+
}
216+
}

lib/widgets/header.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,9 @@ class Header extends StatelessWidget {
195195
? ClipRRect(
196196
borderRadius: BorderRadius.circular(50),
197197
child: CachedNetworkImage(
198-
width: 50,
199-
height: 50,
198+
width: 45,
199+
height: 45,
200+
fit: BoxFit.cover,
200201
errorWidget: (context, url, error) =>
201202
const Icon(IconlyBold.profile),
202203
imageUrl: profileData.profileData.value.avatar ?? '',

0 commit comments

Comments
 (0)