mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00

* chore: fix analyzer issues * fix(updater): dead link (#1408) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * Update use_update_checker.dart --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com> * fix(linux): tray icon not showing #541 upgrade old packages * fix(search): load more button not working #1417 * fix: spotify friends and user profile icon (mobile) showing when not authenticated #1410 * chore: add docker and m1 based linux arm build * cd: fix sed failing us * cd: use docker cask * fix: windows SSL Certificate error breaking login #905 (#1474) * fix: certificate error by using custom ssl certificate * Cd/docker linux ar (#1468) * cd: use docker buildx * cd: use linux host for linux arm instead of macos m1 m1 doesn't support nested virtualization. (Apple truly sucks) * cd: don't specify arch in Dockerfile * cd: use custom Dockerfile from ubuntu instead of flutter image * cd: add setup java for android * cd: add flutter distributor pre-built docker image for arm * cd: save me from this cursed arm build * cd: ?? * cd: ?? * cd: use docker build * fix: windows SSL Exception for Signing in * refactor: extract update checker as a basic function instead of a hook * cd: fix windows build error due to nightly version format * cd: fix github versioning scheme * chore: remove assets/ca entry in pubspec.yaml * fix(macos): Logs directory not created by default #1353 * refactor: Dart based Github Workflow CLI (#1490) * feat: add build dart script for windows * feat: add android build support * feat: add linux build support * feat: add macos build support * feat: add ios build support * feat: add deps install command and workflow file * cd: what? * cd: what? * cd: what? * cd: update workflow inputs * cd: replace release binary * cd: run flutter pub get * cd: use dpkg zstd instead of xz, windows disable innoInstall, fix channel enum.name and reset pubspec after changing build no for nightly * cd: fix tar copy path * cd: fix copy linux command * cd: fix windows inno depend and fix android aab path * cd: idk * cd: linux why??? * cd: windows choco copy failed * cd: use dart tar archive for creating tar file * cd: fix linux file copy error * cd: use tar command directly * feat: add linux_arm platform * cd: add linux_arm platform * cd: don't know what? * feat: notification about nightly channel update * chore: fix some errors parsing nightly version info * refactor: move dart scripts as commands under CLI * chore: add translated message command to command list * feat(translations): add Basque translation (#1493) * added Basque translation * chore: fix country codes and language native name --------- Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com> * feat(translations): add georgian language (#1450) * feat: add georgian language * feat: translate more georgian words * feat(translations): add Finnish translations (#1449) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * added finnish translation * chore: fix arb syntax errors and language in l10n entries --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com> Co-authored-by: Onni Nevala <nevalaonni@gmail.com> * feat(translations): add Indonesian translation (#1426) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * Add Indonesia translation --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com> * feat(translations): Improve tr locales (#1419) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * Improve tr locales --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com> * feat(player): add volume slider floating label showing percentage (#1445) * docs: broken link in README.md (fixes #1310) (#1311) * docs: remove appimage link in readme #1082 (#1171) * Updating Readme according to #1082 Updating Readme according to #1082 * Added explanation The explanation is now given and the expression is more formal and explanatory, instead of just linking the issue. * add volume level tooltip in volume_slider --------- Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Kingkor Roy Tirtho <krtirtho@gmail.com> * fix: fallback to LRCLIB when lyrics line less than 6 lines #1461 * feat: Local music library (#1479) * feat: add one additional library folder This folder just doesn't get downloaded to. I think I'm going to rework it so that it can be multiple folders, but I'm going to commit my progress so far anyway. Signed-off-by: Blake Leonard <me@blakes.dev> * chore: update dependencies so that it builds I'm not sure if this breaks CI or something, but I couldn't build it locally to test my changes, so I made these changes and it builds again. Signed-off-by: Blake Leonard <me@blakes.dev> * feat: index multiple folders of local music If you used a previous commit from this branch, this is a breaking change, because it changes the type of a configuration field. but since this is still in development, it should be fine. Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: manage local library in local tracks tab This also refactors the list to use slivers instead. That's the easiest way to have multiple scrolling lists here... The console keeps getting spammed with some intermediate layout error but I can't hold it long enough to figure out what's causing it. Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: use folder add/remove icons in library Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: remove redundant settings page Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: rename "Local Tracks" to just "Local" Not sure if this would be the recommended way to do it... Signed-off-by: Blake Leonard <me@blakes.dev> * fix: console spam about useless Expanded Signed-off-by: Blake Leonard <me@blakes.dev> * chore: remove completed TODO Signed-off-by: Blake Leonard <me@blakes.dev> * chore: use new Platform constants; regenerate plugins Signed-off-by: Blake Leonard <me@blakes.dev> * refactor: put local libraries on separate pages Signed-off-by: Blake Leonard <me@blakes.dev> --------- Signed-off-by: Blake Leonard <me@blakes.dev> * fix: local track not showing up in queue * feat: local library folder cards * feat: personalized stats based on local music history (#1522) * feat: add playback history provider * feat: implement recently played section * refactor: use route names * feat: add stats summary and top tracks/artists/albums * feat: add top date based filtering * feat: add stream money calculation * refactor: place search in mobile navbar and settings in home appbar * feat: add individual minutes and streams page * feat(stats): add individual minutes and streams page * chore: default period to 1 month * feat: add text to explain user how hypothetical fees are calculated * chore: ensure usage of route names instead of direct paths * cd: add cache key * cd: remove media_kit_event_loop from git * fix: some text are garbled in different parts of the app #1463 #1505 * refactor: use replace http with dio and use it as the default * cd: use dio in cli as well * chore: fix home feed not showing up * chore: downloaded tracks folder not opening * feat: play initially available tracks of playlist/album immediately and fetch rest in background #670 * feat: upgrade to Flutter 3.22.0 * refactor: migrate deprecated warnings * fix(playback): skipping tracks with unplayable sources instead of falling back #1492 * chore: migrate android gradle to declarative config syntax * chore: disable impeller for now * fix(windows): installer tries to install in current directory * chore: upgrade deps and appbar bg fix * chore: podspec update * chore: bump version and generate changelogs --------- Signed-off-by: Blake Leonard <me@blakes.dev> Co-authored-by: Kshamendra <github@ghoulcloud.slmail.me> Co-authored-by: MerkomassDev <70111455+MerkomassDev@users.noreply.github.com> Co-authored-by: Karim <37943746+ksaadDE@users.noreply.github.com> Co-authored-by: Josu Igoa <josuigoa@ni.eus> Co-authored-by: Omari Sopromadze <omari.sopromadze@gmail.com> Co-authored-by: ctih <78687256+ctih1@users.noreply.github.com> Co-authored-by: Onni Nevala <nevalaonni@gmail.com> Co-authored-by: Yusril Rapsanjani <yusriltakeuchi@gmail.com> Co-authored-by: W͏ I͏ N͏ Z͏ O͏ R͏ T͏ <75412448+mikropsoft@users.noreply.github.com> Co-authored-by: Akash Pattnaik <akashjio66666@gmail.com> Co-authored-by: Blake Leonard <blake@1024256.xyz>
648 lines
19 KiB
Dart
648 lines
19 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
|
import 'package:spotube/utils/platform.dart';
|
|
import 'package:titlebar_buttons/titlebar_buttons.dart';
|
|
import 'dart:math';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
class PageWindowTitleBar extends StatefulHookConsumerWidget
|
|
implements PreferredSizeWidget {
|
|
final Widget? leading;
|
|
final bool automaticallyImplyLeading;
|
|
final List<Widget>? actions;
|
|
final Color? backgroundColor;
|
|
final Color? foregroundColor;
|
|
final IconThemeData? actionsIconTheme;
|
|
final bool? centerTitle;
|
|
final double? titleSpacing;
|
|
final double toolbarOpacity;
|
|
final double? leadingWidth;
|
|
final TextStyle? toolbarTextStyle;
|
|
final TextStyle? titleTextStyle;
|
|
final double? titleWidth;
|
|
final Widget? title;
|
|
|
|
final bool _sliver;
|
|
|
|
const PageWindowTitleBar({
|
|
super.key,
|
|
this.actions,
|
|
this.title,
|
|
this.toolbarOpacity = 1,
|
|
this.backgroundColor,
|
|
this.actionsIconTheme,
|
|
this.automaticallyImplyLeading = false,
|
|
this.centerTitle,
|
|
this.foregroundColor,
|
|
this.leading,
|
|
this.leadingWidth,
|
|
this.titleSpacing,
|
|
this.titleTextStyle,
|
|
this.titleWidth,
|
|
this.toolbarTextStyle,
|
|
}) : _sliver = false,
|
|
pinned = false,
|
|
floating = false,
|
|
snap = false,
|
|
stretch = false;
|
|
|
|
final bool pinned;
|
|
final bool floating;
|
|
final bool snap;
|
|
final bool stretch;
|
|
|
|
const PageWindowTitleBar.sliver({
|
|
super.key,
|
|
this.actions,
|
|
this.title,
|
|
this.backgroundColor,
|
|
this.actionsIconTheme,
|
|
this.automaticallyImplyLeading = false,
|
|
this.centerTitle,
|
|
this.foregroundColor,
|
|
this.leading,
|
|
this.leadingWidth,
|
|
this.titleSpacing,
|
|
this.titleTextStyle,
|
|
this.titleWidth,
|
|
this.toolbarTextStyle,
|
|
this.pinned = false,
|
|
this.floating = false,
|
|
this.snap = false,
|
|
this.stretch = false,
|
|
}) : _sliver = true,
|
|
toolbarOpacity = 1;
|
|
|
|
@override
|
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
|
|
@override
|
|
ConsumerState<PageWindowTitleBar> createState() => _PageWindowTitleBarState();
|
|
}
|
|
|
|
class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
|
|
void onDrag(details) {
|
|
final systemTitleBar =
|
|
ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));
|
|
if (kIsDesktop && !systemTitleBar) {
|
|
windowManager.startDragging();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final mediaQuery = MediaQuery.of(context);
|
|
|
|
if (widget._sliver) {
|
|
return SliverLayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final hasFullscreen =
|
|
mediaQuery.size.width == constraints.crossAxisExtent;
|
|
final hasLeadingOrCanPop =
|
|
widget.leading != null || Navigator.canPop(context);
|
|
|
|
return SliverPadding(
|
|
padding: EdgeInsets.only(
|
|
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
|
|
),
|
|
sliver: SliverAppBar(
|
|
leading: widget.leading,
|
|
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
|
actions: [
|
|
...?widget.actions,
|
|
WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
|
|
],
|
|
backgroundColor: widget.backgroundColor,
|
|
foregroundColor: widget.foregroundColor,
|
|
actionsIconTheme: widget.actionsIconTheme,
|
|
centerTitle: widget.centerTitle,
|
|
titleSpacing: widget.titleSpacing,
|
|
leadingWidth: widget.leadingWidth,
|
|
toolbarTextStyle: widget.toolbarTextStyle,
|
|
titleTextStyle: widget.titleTextStyle,
|
|
title: widget.title,
|
|
pinned: widget.pinned,
|
|
floating: widget.floating,
|
|
snap: widget.snap,
|
|
stretch: widget.stretch,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
return LayoutBuilder(builder: (context, constrains) {
|
|
final hasFullscreen = mediaQuery.size.width == constrains.maxWidth;
|
|
final hasLeadingOrCanPop =
|
|
widget.leading != null || Navigator.canPop(context);
|
|
|
|
return GestureDetector(
|
|
onHorizontalDragStart: onDrag,
|
|
onVerticalDragStart: onDrag,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(
|
|
left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0,
|
|
),
|
|
child: AppBar(
|
|
leading: widget.leading,
|
|
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
|
actions: [
|
|
...?widget.actions,
|
|
WindowTitleBarButtons(foregroundColor: widget.foregroundColor),
|
|
],
|
|
backgroundColor: widget.backgroundColor,
|
|
foregroundColor: widget.foregroundColor,
|
|
actionsIconTheme: widget.actionsIconTheme,
|
|
centerTitle: widget.centerTitle,
|
|
titleSpacing: widget.titleSpacing,
|
|
toolbarOpacity: widget.toolbarOpacity,
|
|
leadingWidth: widget.leadingWidth,
|
|
toolbarTextStyle: widget.toolbarTextStyle,
|
|
titleTextStyle: widget.titleTextStyle,
|
|
title: widget.title,
|
|
scrolledUnderElevation: 0,
|
|
shadowColor: Colors.transparent,
|
|
forceMaterialTransparency: true,
|
|
elevation: 0,
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class WindowTitleBarButtons extends HookConsumerWidget {
|
|
final Color? foregroundColor;
|
|
const WindowTitleBarButtons({
|
|
super.key,
|
|
this.foregroundColor,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, ref) {
|
|
final preferences = ref.watch(userPreferencesProvider);
|
|
final isMaximized = useState<bool?>(null);
|
|
const type = ThemeType.auto;
|
|
|
|
Future<void> onClose() async {
|
|
await windowManager.close();
|
|
}
|
|
|
|
useEffect(() {
|
|
if (kIsDesktop) {
|
|
windowManager.isMaximized().then((value) {
|
|
isMaximized.value = value;
|
|
});
|
|
}
|
|
return null;
|
|
}, []);
|
|
|
|
if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
if (kIsWindows) {
|
|
final theme = Theme.of(context);
|
|
final colors = WindowButtonColors(
|
|
normal: Colors.transparent,
|
|
iconNormal: foregroundColor ?? theme.colorScheme.onSurface,
|
|
mouseOver: theme.colorScheme.onSurface.withOpacity(0.1),
|
|
mouseDown: theme.colorScheme.onSurface.withOpacity(0.2),
|
|
iconMouseOver: theme.colorScheme.onSurface,
|
|
iconMouseDown: theme.colorScheme.onSurface,
|
|
);
|
|
|
|
final closeColors = WindowButtonColors(
|
|
normal: Colors.transparent,
|
|
iconNormal: foregroundColor ?? theme.colorScheme.onSurface,
|
|
mouseOver: Colors.red,
|
|
mouseDown: Colors.red[800]!,
|
|
iconMouseOver: Colors.white,
|
|
iconMouseDown: Colors.black,
|
|
);
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 25),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MinimizeWindowButton(
|
|
onPressed: windowManager.minimize,
|
|
colors: colors,
|
|
),
|
|
if (isMaximized.value != true)
|
|
MaximizeWindowButton(
|
|
colors: colors,
|
|
onPressed: () {
|
|
windowManager.maximize();
|
|
isMaximized.value = true;
|
|
},
|
|
)
|
|
else
|
|
RestoreWindowButton(
|
|
colors: colors,
|
|
onPressed: () {
|
|
windowManager.unmaximize();
|
|
isMaximized.value = false;
|
|
},
|
|
),
|
|
CloseWindowButton(
|
|
colors: closeColors,
|
|
onPressed: onClose,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 20, left: 10),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
DecoratedMinimizeButton(
|
|
type: type,
|
|
onPressed: windowManager.minimize,
|
|
),
|
|
DecoratedMaximizeButton(
|
|
type: type,
|
|
onPressed: () async {
|
|
if (await windowManager.isMaximized()) {
|
|
await windowManager.unmaximize();
|
|
isMaximized.value = false;
|
|
} else {
|
|
await windowManager.maximize();
|
|
isMaximized.value = true;
|
|
}
|
|
},
|
|
),
|
|
DecoratedCloseButton(
|
|
type: type,
|
|
onPressed: onClose,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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 (kIsWeb) {
|
|
return Container();
|
|
} else {
|
|
// Don't show button on macOS
|
|
if (Platform.isMacOS) {
|
|
return Container();
|
|
}
|
|
}
|
|
|
|
return MouseStateBuilder(
|
|
builder: (context, mouseState) {
|
|
WindowButtonContext buttonContext = WindowButtonContext(
|
|
mouseState: mouseState,
|
|
context: context,
|
|
backgroundColor: getBackgroundColor(mouseState),
|
|
iconColor: getIconColor(mouseState));
|
|
|
|
var icon =
|
|
(iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
|
|
|
|
var fadeOutColor =
|
|
getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(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!();
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class MinimizeWindowButton extends WindowButton {
|
|
MinimizeWindowButton(
|
|
{super.key, super.colors, super.onPressed, bool? animate})
|
|
: super(
|
|
animate: animate ?? false,
|
|
iconBuilder: (buttonContext) =>
|
|
MinimizeIcon(color: buttonContext.iconColor),
|
|
);
|
|
}
|
|
|
|
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
|
|
class CloseIcon extends StatelessWidget {
|
|
final Color color;
|
|
const CloseIcon({super.key, required this.color});
|
|
@override
|
|
Widget build(BuildContext context) => Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Stack(children: [
|
|
// Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
|
|
Transform.rotate(
|
|
angle: pi * .25,
|
|
child:
|
|
Center(child: Container(width: 14, height: 1, color: color))),
|
|
Transform.rotate(
|
|
angle: pi * -.25,
|
|
child:
|
|
Center(child: Container(width: 14, height: 1, color: color))),
|
|
]),
|
|
);
|
|
}
|
|
|
|
/// Maximize
|
|
class MaximizeIcon extends StatelessWidget {
|
|
final Color color;
|
|
const MaximizeIcon({super.key, required this.color});
|
|
@override
|
|
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
|
|
}
|
|
|
|
class _MaximizePainter extends _IconPainter {
|
|
_MaximizePainter(super.color);
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
Paint p = getPaint(color);
|
|
canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
|
|
}
|
|
}
|
|
|
|
/// Restore
|
|
class RestoreIcon extends StatelessWidget {
|
|
final Color color;
|
|
const RestoreIcon({
|
|
super.key,
|
|
required this.color,
|
|
});
|
|
@override
|
|
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
|
|
}
|
|
|
|
class _RestorePainter extends _IconPainter {
|
|
_RestorePainter(super.color);
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
Paint p = getPaint(color);
|
|
canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
|
|
canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
|
|
canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
|
|
canvas.drawLine(
|
|
Offset(size.width, 0), Offset(size.width, size.height - 2), p);
|
|
canvas.drawLine(Offset(size.width, size.height - 2),
|
|
Offset(size.width - 2, size.height - 2), p);
|
|
}
|
|
}
|
|
|
|
/// Minimize
|
|
class MinimizeIcon extends StatelessWidget {
|
|
final Color color;
|
|
const MinimizeIcon({super.key, required this.color});
|
|
@override
|
|
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
|
|
}
|
|
|
|
class _MinimizePainter extends _IconPainter {
|
|
_MinimizePainter(super.color);
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
Paint p = getPaint(color);
|
|
canvas.drawLine(
|
|
Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
|
|
}
|
|
}
|
|
|
|
/// Helpers
|
|
abstract class _IconPainter extends CustomPainter {
|
|
_IconPainter(this.color);
|
|
final Color color;
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|
|
|
|
class _AlignedPaint extends StatelessWidget {
|
|
const _AlignedPaint(this.painter);
|
|
final CustomPainter painter;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Align(
|
|
alignment: Alignment.center,
|
|
child: CustomPaint(size: const Size(10, 10), painter: painter));
|
|
}
|
|
}
|
|
|
|
Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
|
|
..color = color
|
|
..style = PaintingStyle.stroke
|
|
..isAntiAlias = isAntiAlias
|
|
..strokeWidth = 1;
|
|
|
|
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)));
|
|
}
|
|
}
|