chore: fix window resizing

This commit is contained in:
Kingkor Roy Tirtho 2025-01-12 21:32:33 +06:00
parent 3649b67869
commit 5930c342b5
11 changed files with 132 additions and 354 deletions

View File

@ -20,17 +20,6 @@ class $AssetsBackgroundsGen {
List<AssetGenImage> get values => [xmasEffect]; List<AssetGenImage> get values => [xmasEffect];
} }
class $AssetsIllustrationsGen {
const $AssetsIllustrationsGen();
/// File path: assets/illustrations/fixing_bugs.png
AssetGenImage get fixingBugs =>
const AssetGenImage('assets/illustrations/fixing_bugs.png');
/// List of all assets
List<AssetGenImage> get values => [fixingBugs];
}
class $AssetsLogosGen { class $AssetsLogosGen {
const $AssetsLogosGen(); const $AssetsLogosGen();
@ -151,8 +140,6 @@ class Assets {
AssetGenImage('assets/bengali-patterns-bg.jpg'); AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage branding = AssetGenImage('assets/branding.png');
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png');
static const $AssetsIllustrationsGen illustrations =
$AssetsIllustrationsGen();
static const AssetGenImage invidious = AssetGenImage('assets/invidious.jpg'); static const AssetGenImage invidious = AssetGenImage('assets/invidious.jpg');
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png'); static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
static const AssetGenImage likedTracks = static const AssetGenImage likedTracks =

View File

@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
typedef MouseStateBuilderCB = Widget Function(
BuildContext context, MouseState mouseState);
class MouseState {
bool isMouseOver = false;
bool isMouseDown = false;
MouseState();
@override
String toString() {
return "isMouseDown: $isMouseDown - isMouseOver: $isMouseOver";
}
}
T? _ambiguate<T>(T? value) => value;
class MouseStateBuilder extends StatefulWidget {
final MouseStateBuilderCB builder;
final VoidCallback? onPressed;
const MouseStateBuilder({super.key, required this.builder, this.onPressed});
@override
// ignore: library_private_types_in_public_api
_MouseStateBuilderState createState() => _MouseStateBuilderState();
}
class _MouseStateBuilderState extends State<MouseStateBuilder> {
late MouseState _mouseState;
_MouseStateBuilderState() {
_mouseState = MouseState();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setState(() {
_mouseState.isMouseOver = true;
});
},
onExit: (event) {
setState(() {
_mouseState.isMouseOver = false;
});
},
child: GestureDetector(
onTapDown: (_) {
setState(() {
_mouseState.isMouseDown = true;
});
},
onTapCancel: () {
setState(() {
_mouseState.isMouseDown = false;
});
},
onTap: () {
setState(() {
_mouseState.isMouseDown = false;
_mouseState.isMouseOver = false;
});
_ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
if (widget.onPressed != null) {
widget.onPressed!();
}
});
},
onTapUp: (_) {},
child: widget.builder(context, _mouseState),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/components/button/back_button.dart'; import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/titlebar/titlebar_buttons.dart'; import 'package:spotube/components/titlebar/titlebar_buttons.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -49,7 +50,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
this.height, this.height,
this.surfaceBlur, this.surfaceBlur,
this.surfaceOpacity, this.surfaceOpacity,
this.useSafeArea = true, this.useSafeArea = false,
}); });
void onDrag(WidgetRef ref) { void onDrag(WidgetRef ref) {
@ -66,7 +67,7 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
final lastClicked = useRef<int>(DateTime.now().millisecondsSinceEpoch); final lastClicked = useRef<int>(DateTime.now().millisecondsSinceEpoch);
return SizedBox( return SizedBox(
height: height ?? 56, height: height ?? (48 * context.theme.scaling),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final hasFullscreen = final hasFullscreen =
@ -102,18 +103,22 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
: leading, : leading,
trailing: [ trailing: [
...trailing, ...trailing,
WindowTitleBarButtons(foregroundColor: foregroundColor), Align(
alignment: Alignment.topRight,
child:
WindowTitleBarButtons(foregroundColor: foregroundColor),
),
], ],
title: title, title: title,
header: header, header: header,
subtitle: subtitle, subtitle: subtitle,
trailingExpanded: trailingExpanded, trailingExpanded: trailingExpanded,
alignment: alignment, alignment: alignment,
padding: padding, padding: padding ?? EdgeInsets.zero,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
leadingGap: leadingGap, leadingGap: leadingGap,
trailingGap: trailingGap, trailingGap: trailingGap,
height: height, height: height ?? (48 * context.theme.scaling),
surfaceBlur: surfaceBlur, surfaceBlur: surfaceBlur,
surfaceOpacity: surfaceOpacity, surfaceOpacity: surfaceOpacity,
useSafeArea: useSafeArea, useSafeArea: useSafeArea,
@ -127,5 +132,5 @@ class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
} }
@override @override
Size get preferredSize => Size.fromHeight(height ?? 56.0); Size get preferredSize => Size.fromHeight(height ?? 48);
} }

View File

@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/components/hover_builder.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/titlebar/titlebar_icon_buttons.dart'; import 'package:spotube/components/titlebar/titlebar_icon_buttons.dart';
import 'package:spotube/components/titlebar/window_button.dart';
import 'package:spotube/hooks/configurators/use_window_listener.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:titlebar_buttons/titlebar_buttons.dart'; import 'package:titlebar_buttons/titlebar_buttons.dart';
@ -22,12 +24,20 @@ class WindowTitleBarButtons extends HookConsumerWidget {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final isMaximized = useState<bool?>(null); final isMaximized = useState<bool?>(null);
const type = ThemeType.auto; const type = ThemeType.auto;
final scale = context.theme.scaling;
Future<void> onClose() async { Future<void> onClose() async {
await windowManager.close(); await windowManager.close();
} }
useWindowListener(
onWindowMaximize: () {
isMaximized.value = true;
},
onWindowUnmaximize: () {
isMaximized.value = false;
},
);
useEffect(() { useEffect(() {
if (kIsDesktop) { if (kIsDesktop) {
windowManager.isMaximized().then((value) { windowManager.isMaximized().then((value) {
@ -42,86 +52,68 @@ class WindowTitleBarButtons extends HookConsumerWidget {
} }
if (kIsWindows) { if (kIsWindows) {
final theme = Theme.of(context); return Row(
final colors = WindowButtonColors( crossAxisAlignment: CrossAxisAlignment.start,
normal: Colors.transparent, children: [
iconNormal: foregroundColor ?? theme.colorScheme.onSurface, ShadcnWindowButton(
mouseOver: theme.colorScheme.onSurface.withAlpha(25), icon: MinimizeIcon(color: context.theme.colorScheme.foreground),
mouseDown: theme.colorScheme.onSurface.withAlpha(51), onPressed: windowManager.minimize,
iconMouseOver: theme.colorScheme.onSurface, ),
iconMouseDown: theme.colorScheme.onSurface, if (isMaximized.value != true)
); ShadcnWindowButton(
icon: MaximizeIcon(color: context.theme.colorScheme.foreground),
final closeColors = WindowButtonColors( onPressed: () {
normal: Colors.transparent, windowManager.maximize();
iconNormal: foregroundColor ?? theme.colorScheme.onSurface, isMaximized.value = true;
mouseOver: Colors.red, },
mouseDown: Colors.red[800]!, )
iconMouseOver: Colors.white, else
iconMouseDown: Colors.black, ShadcnWindowButton(
); icon: RestoreIcon(color: context.theme.colorScheme.foreground),
onPressed: () {
return Transform( windowManager.unmaximize();
transform: Matrix4.translationValues(18, -12, 0) * scale, isMaximized.value = false;
child: Row( },
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MinimizeWindowButton(
onPressed: windowManager.minimize,
colors: colors,
), ),
if (isMaximized.value != true) HoverBuilder(builder: (context, isHovered) {
MaximizeWindowButton( return ShadcnWindowButton(
colors: colors, icon: CloseIcon(
onPressed: () { color: isHovered
windowManager.maximize(); ? Colors.white
isMaximized.value = true; : context.theme.colorScheme.foreground,
},
)
else
RestoreWindowButton(
colors: colors,
onPressed: () {
windowManager.unmaximize();
isMaximized.value = false;
},
), ),
CloseWindowButton(
colors: closeColors,
onPressed: onClose, onPressed: onClose,
), hoverBackgroundColor: const Color(0xFFD32F2F),
], );
), }),
],
); );
} }
return Transform( return Row(
transform: Matrix4.translationValues(18, -12, 0) * scale, crossAxisAlignment: CrossAxisAlignment.start,
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.start, DecoratedMinimizeButton(
children: [ type: type,
DecoratedMinimizeButton( onPressed: windowManager.minimize,
type: type, ),
onPressed: windowManager.minimize, DecoratedMaximizeButton(
), type: type,
DecoratedMaximizeButton( onPressed: () async {
type: type, if (await windowManager.isMaximized()) {
onPressed: () async { await windowManager.unmaximize();
if (await windowManager.isMaximized()) { isMaximized.value = false;
await windowManager.unmaximize(); } else {
isMaximized.value = false; await windowManager.maximize();
} else { isMaximized.value = true;
await windowManager.maximize(); }
isMaximized.value = true; },
} ),
}, DecoratedCloseButton(
), type: type,
DecoratedCloseButton( onPressed: onClose,
type: type, ),
onPressed: onClose, ],
),
],
),
); );
} }
} }

View File

@ -1,56 +1,50 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/titlebar/window_button.dart'; import 'package:spotube/extensions/button_variance.dart';
class MinimizeWindowButton extends WindowButton { class ShadcnWindowButton extends StatelessWidget {
MinimizeWindowButton( final Widget icon;
{super.key, super.colors, super.onPressed, bool? animate}) final VoidCallback onPressed;
: super( final Color? hoverBackgroundColor;
animate: animate ?? false,
iconBuilder: (buttonContext) => const ShadcnWindowButton({
MinimizeIcon(color: buttonContext.iconColor), super.key,
); required this.icon,
required this.onPressed,
this.hoverBackgroundColor,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 45,
height: 32,
child: IconButton(
variance: ButtonVariance.ghost.copyWith(
decoration: (context, states) {
final decoration = ButtonVariance.ghost.decoration(context, states)
as BoxDecoration;
if (hoverBackgroundColor != null &&
states.contains(WidgetState.hovered)) {
return decoration.copyWith(
borderRadius: BorderRadius.zero,
color: hoverBackgroundColor,
);
}
return decoration.copyWith(
borderRadius: BorderRadius.zero,
);
},
),
icon: icon,
onPressed: onPressed,
),
);
}
} }
class MaximizeWindowButton extends WindowButton {
MaximizeWindowButton(
{super.key, super.colors, super.onPressed, bool? animate})
: super(
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor),
);
}
class RestoreWindowButton extends WindowButton {
RestoreWindowButton({super.key, super.colors, super.onPressed, bool? animate})
: super(
animate: animate ?? false,
iconBuilder: (buttonContext) =>
RestoreIcon(color: buttonContext.iconColor),
);
}
final _defaultCloseButtonColors = WindowButtonColors(
mouseOver: const Color(0xFFD32F2F),
mouseDown: const Color(0xFFB71C1C),
iconNormal: const Color(0xFF805306),
iconMouseOver: const Color(0xFFFFFFFF));
class CloseWindowButton extends WindowButton {
CloseWindowButton(
{super.key, WindowButtonColors? colors, super.onPressed, bool? animate})
: super(
colors: colors ?? _defaultCloseButtonColors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor),
);
}
// Switched to CustomPaint icons by https://github.com/esDotDev
/// Close /// Close
class CloseIcon extends StatelessWidget { class CloseIcon extends StatelessWidget {
final Color color; final Color color;
@ -149,8 +143,9 @@ class _AlignedPaint extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( return Align(
alignment: Alignment.center, alignment: Alignment.center,
child: CustomPaint(size: const Size(10, 10), painter: painter)); child: CustomPaint(size: const Size(10, 10), painter: painter),
);
} }
} }

View File

@ -1,125 +0,0 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/titlebar/mouse_state.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
typedef WindowButtonIconBuilder = Widget Function(
WindowButtonContext buttonContext);
typedef WindowButtonBuilder = Widget Function(
WindowButtonContext buttonContext, Widget icon);
class WindowButtonContext {
BuildContext context;
MouseState mouseState;
Color? backgroundColor;
Color iconColor;
WindowButtonContext(
{required this.context,
required this.mouseState,
this.backgroundColor,
required this.iconColor});
}
class WindowButtonColors {
late Color normal;
late Color mouseOver;
late Color mouseDown;
late Color iconNormal;
late Color iconMouseOver;
late Color iconMouseDown;
WindowButtonColors(
{Color? normal,
Color? mouseOver,
Color? mouseDown,
Color? iconNormal,
Color? iconMouseOver,
Color? iconMouseDown}) {
this.normal = normal ?? _defaultButtonColors.normal;
this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
}
}
final _defaultButtonColors = WindowButtonColors(
normal: Colors.transparent,
iconNormal: const Color(0xFF805306),
mouseOver: const Color(0xFF404040),
mouseDown: const Color(0xFF202020),
iconMouseOver: const Color(0xFFFFFFFF),
iconMouseDown: const Color(0xFFF0F0F0),
);
class WindowButton extends StatelessWidget {
final WindowButtonBuilder? builder;
final WindowButtonIconBuilder? iconBuilder;
late final WindowButtonColors colors;
final bool animate;
final EdgeInsets? padding;
final VoidCallback? onPressed;
WindowButton(
{super.key,
WindowButtonColors? colors,
this.builder,
@required this.iconBuilder,
this.padding,
this.onPressed,
this.animate = false}) {
this.colors = colors ?? _defaultButtonColors;
}
Color getBackgroundColor(MouseState mouseState) {
if (mouseState.isMouseDown) return colors.mouseDown;
if (mouseState.isMouseOver) return colors.mouseOver;
return colors.normal;
}
Color getIconColor(MouseState mouseState) {
if (mouseState.isMouseDown) return colors.iconMouseDown;
if (mouseState.isMouseOver) return colors.iconMouseOver;
return colors.iconNormal;
}
@override
Widget build(BuildContext context) {
if (!kTitlebarVisible) return const SizedBox.shrink();
return MouseStateBuilder(
builder: (context, mouseState) {
WindowButtonContext buttonContext = WindowButtonContext(
mouseState: mouseState,
context: context,
backgroundColor: getBackgroundColor(mouseState),
iconColor: getIconColor(mouseState));
var icon = (iconBuilder != null)
? iconBuilder!(buttonContext)
: const SizedBox();
var fadeOutColor =
getBackgroundColor(MouseState()..isMouseOver = true).withAlpha(0);
var padding = this.padding ?? const EdgeInsets.all(10);
var animationMs =
mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0);
Widget iconWithPadding = Padding(padding: padding, child: icon);
iconWithPadding = AnimatedContainer(
curve: Curves.easeOut,
duration: Duration(milliseconds: animationMs),
color: buttonContext.backgroundColor ?? fadeOutColor,
child: iconWithPadding);
var button =
(builder != null) ? builder!(buttonContext, icon) : iconWithPadding;
return SizedBox(
width: 45,
height: 32,
child: button,
);
},
onPressed: () {
if (onPressed != null) onPressed!();
},
);
}
}

View File

@ -207,7 +207,9 @@ class Spotube extends HookConsumerWidget {
child: child!, child: child!,
); );
if (kIsDesktop && !kIsMacOS) child = DragToResizeArea(child: child); if (kIsLinux) {
child = DragToResizeArea(child: child);
}
return child; return child;
}, },

View File

@ -36,7 +36,7 @@ class HomePage extends HookConsumerWidget {
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
headers: [ headers: [
if (kTitlebarVisible) const TitleBar(), if (kTitlebarVisible) const TitleBar(height: 30),
], ],
child: CustomScrollView( child: CustomScrollView(
controller: controller, controller: controller,

View File

@ -2,8 +2,6 @@ import 'package:flutter/material.dart' show Badge;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/modules/library/user_local_tracks.dart'; import 'package:spotube/modules/library/user_local_tracks.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/library/user_albums.dart'; import 'package:spotube/modules/library/user_albums.dart';
@ -19,7 +17,6 @@ class LibraryPage extends HookConsumerWidget {
const LibraryPage({super.key}); const LibraryPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scale = context.theme.scaling;
final downloadingCount = ref.watch(downloadManagerProvider).$downloadCount; final downloadingCount = ref.watch(downloadManagerProvider).$downloadCount;
final index = useState(0); final index = useState(0);
@ -40,11 +37,6 @@ class LibraryPage extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
headers: [ headers: [
TitleBar( TitleBar(
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 12,
).copyWith(left: 0) *
scale,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: TabList( child: TabList(

View File

@ -151,6 +151,8 @@ class LyricsPage extends HookConsumerWidget {
? TitleBar( ? TitleBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
title: tabbar, title: tabbar,
height: 58 * context.theme.scaling,
surfaceBlur: 0,
) )
: tabbar : tabbar
], ],

View File

@ -70,7 +70,8 @@ class SearchPage extends HookConsumerWidget {
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
headers: [ headers: [
if (kTitlebarVisible) const TitleBar(automaticallyImplyLeading: true) if (kTitlebarVisible)
const TitleBar(automaticallyImplyLeading: true, height: 30)
], ],
child: auth.asData?.value == null child: auth.asData?.value == null
? const AnonymousFallback() ? const AnonymousFallback()