diff --git a/lib/models/settings/arguments_model.dart b/lib/models/settings/arguments_model.dart index 73708db43..db836e8b5 100644 --- a/lib/models/settings/arguments_model.dart +++ b/lib/models/settings/arguments_model.dart @@ -11,6 +11,7 @@ abstract class ArgumentsModel with _$ArgumentsModel { factory ArgumentsModel({ @Default(false) bool htpcMode, + @Default(false) bool noTitleBar, @Default(false) bool leanBackMode, @Default(false) bool newWindow, }) = _ArgumentsModel; @@ -21,6 +22,7 @@ abstract class ArgumentsModel with _$ArgumentsModel { final parsedWindowArgs = windowArguments.split(','); return ArgumentsModel( htpcMode: arguments.contains('--htpc') || leanBackEnabled, + noTitleBar: arguments.contains('--no-titlebar'), leanBackMode: leanBackEnabled, newWindow: parsedWindowArgs.contains('--newWindow'), ); diff --git a/lib/models/settings/arguments_model.freezed.dart b/lib/models/settings/arguments_model.freezed.dart index 73d6f9f48..3e992f5ee 100644 --- a/lib/models/settings/arguments_model.freezed.dart +++ b/lib/models/settings/arguments_model.freezed.dart @@ -15,12 +15,13 @@ T _$identity(T value) => value; /// @nodoc mixin _$ArgumentsModel { bool get htpcMode; + bool get noTitleBar; bool get leanBackMode; bool get newWindow; @override String toString() { - return 'ArgumentsModel(htpcMode: $htpcMode, leanBackMode: $leanBackMode, newWindow: $newWindow)'; + return 'ArgumentsModel(htpcMode: $htpcMode, noTitleBar: $noTitleBar, leanBackMode: $leanBackMode, newWindow: $newWindow)'; } } @@ -117,14 +118,16 @@ extension ArgumentsModelPatterns on ArgumentsModel { @optionalTypeArgs TResult maybeWhen( - TResult Function(bool htpcMode, bool leanBackMode, bool newWindow)? + TResult Function( + bool htpcMode, bool noTitleBar, bool leanBackMode, bool newWindow)? $default, { required TResult orElse(), }) { final _that = this; switch (_that) { case _ArgumentsModel() when $default != null: - return $default(_that.htpcMode, _that.leanBackMode, _that.newWindow); + return $default(_that.htpcMode, _that.noTitleBar, _that.leanBackMode, + _that.newWindow); case _: return orElse(); } @@ -145,12 +148,15 @@ extension ArgumentsModelPatterns on ArgumentsModel { @optionalTypeArgs TResult when( - TResult Function(bool htpcMode, bool leanBackMode, bool newWindow) $default, + TResult Function( + bool htpcMode, bool noTitleBar, bool leanBackMode, bool newWindow) + $default, ) { final _that = this; switch (_that) { case _ArgumentsModel(): - return $default(_that.htpcMode, _that.leanBackMode, _that.newWindow); + return $default(_that.htpcMode, _that.noTitleBar, _that.leanBackMode, + _that.newWindow); case _: throw StateError('Unexpected subclass'); } @@ -170,13 +176,15 @@ extension ArgumentsModelPatterns on ArgumentsModel { @optionalTypeArgs TResult? whenOrNull( - TResult? Function(bool htpcMode, bool leanBackMode, bool newWindow)? + TResult? Function( + bool htpcMode, bool noTitleBar, bool leanBackMode, bool newWindow)? $default, ) { final _that = this; switch (_that) { case _ArgumentsModel() when $default != null: - return $default(_that.htpcMode, _that.leanBackMode, _that.newWindow); + return $default(_that.htpcMode, _that.noTitleBar, _that.leanBackMode, + _that.newWindow); case _: return null; } @@ -188,6 +196,7 @@ extension ArgumentsModelPatterns on ArgumentsModel { class _ArgumentsModel extends ArgumentsModel { _ArgumentsModel( {this.htpcMode = false, + this.noTitleBar = false, this.leanBackMode = false, this.newWindow = false}) : super._(); @@ -197,6 +206,9 @@ class _ArgumentsModel extends ArgumentsModel { final bool htpcMode; @override @JsonKey() + final bool noTitleBar; + @override + @JsonKey() final bool leanBackMode; @override @JsonKey() @@ -204,7 +216,7 @@ class _ArgumentsModel extends ArgumentsModel { @override String toString() { - return 'ArgumentsModel(htpcMode: $htpcMode, leanBackMode: $leanBackMode, newWindow: $newWindow)'; + return 'ArgumentsModel(htpcMode: $htpcMode, noTitleBar: $noTitleBar, leanBackMode: $leanBackMode, newWindow: $newWindow)'; } } diff --git a/lib/screens/shared/default_title_bar.dart b/lib/screens/shared/default_title_bar.dart index 456821ae6..05b8cb052 100644 --- a/lib/screens/shared/default_title_bar.dart +++ b/lib/screens/shared/default_title_bar.dart @@ -14,13 +14,20 @@ class DefaultTitleBar extends ConsumerStatefulWidget { final String? label; final double? height; final Brightness? brightness; - const DefaultTitleBar({this.height = defaultTitleBarHeight, this.label, this.brightness, super.key}); + const DefaultTitleBar({ + this.height = defaultTitleBarHeight, + this.label, + this.brightness, + super.key, + }); @override - ConsumerState createState() => _DefaultTitleBarState(); + ConsumerState createState() => + _DefaultTitleBarState(); } -class _DefaultTitleBarState extends ConsumerState with WindowListener { +class _DefaultTitleBarState extends ConsumerState + with WindowListener { bool hovering = false; @override @@ -37,12 +44,21 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi @override Widget build(BuildContext context) { - if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink(); + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) + return const SizedBox.shrink(); final theme = Theme.of(context); final brightness = widget.brightness ?? theme.brightness; final iconColor = theme.colorScheme.onSurface.withValues(alpha: 0.65); - final isOffline = ref.watch(connectivityStatusProvider.select((value) => value == ConnectionState.offline)); + final isOffline = ref.watch( + connectivityStatusProvider.select( + (value) => value == ConnectionState.offline, + ), + ); final surfaceColor = theme.colorScheme.surface; + final noTitleBar = + (AdaptiveLayout.of(context).platform == TargetPlatform.linux || + AdaptiveLayout.of(context).platform == TargetPlatform.windows) && + ref.read(argumentsStateProvider).noTitleBar; return ExcludeFocus( child: MouseRegion( @@ -51,161 +67,221 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi child: AnimatedContainer( duration: const Duration(milliseconds: 250), decoration: BoxDecoration( - gradient: LinearGradient( - colors: isOffline - ? [ - theme.colorScheme.errorContainer.withValues(alpha: 0.8), - theme.colorScheme.errorContainer.withValues(alpha: 0.25), - ] - : [ - surfaceColor.withValues(alpha: hovering ? 0.7 : 0), - surfaceColor.withValues(alpha: 0), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - )), + gradient: LinearGradient( + colors: isOffline + ? [ + theme.colorScheme.errorContainer.withValues(alpha: 0.8), + theme.colorScheme.errorContainer.withValues(alpha: 0.25), + ] + : [ + surfaceColor.withValues(alpha: hovering ? 0.7 : 0), + surfaceColor.withValues(alpha: 0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), height: widget.height, child: kIsWeb ? const SizedBox.shrink() : Stack( fit: StackFit.expand, children: [ - switch (AdaptiveLayout.of(context).platform) { - TargetPlatform.android || - TargetPlatform.iOS => - SizedBox(height: MediaQuery.paddingOf(context).top), - TargetPlatform.windows || TargetPlatform.linux => Container( - child: Row( - children: [ - Expanded( - child: Container( - color: Colors.black.withValues(alpha: 0), - child: DragToMoveArea( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - padding: const EdgeInsets.only(left: 16), - child: DefaultTextStyle( - style: TextStyle( - color: iconColor, - fontSize: 14, - ), - child: Text(widget.label ?? ""), - ), - ), - ], - ), - ), - ), + noTitleBar + ? const SizedBox.expand() + : switch (AdaptiveLayout.of(context).platform) { + TargetPlatform.android || + TargetPlatform.iOS => + SizedBox( + height: MediaQuery.paddingOf(context).top, ), + TargetPlatform.windows || + TargetPlatform.linux => Container( - decoration: BoxDecoration(boxShadow: [ - BoxShadow( - color: surfaceColor.withValues(alpha: isOffline ? 0 : 0.5), - blurRadius: 32, - spreadRadius: 10, - offset: const Offset(8, -6), - ), - ]), child: Row( children: [ - FutureBuilder>(future: Future.microtask(() async { - final isMinimized = await windowManager.isMinimized(); - return [isMinimized]; - }), builder: (context, snapshot) { - final isMinimized = snapshot.data?.firstOrNull ?? false; - return IconButton( - style: IconButton.styleFrom( - hoverColor: brightness == Brightness.light - ? Colors.black.withValues(alpha: 0.1) - : Colors.white.withValues(alpha: 0.2), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2))), - onPressed: () async { - fullScreenHelper.closeFullScreen(ref); - if (isMinimized) { - windowManager.restore(); - } else { - windowManager.minimize(); - } - }, - icon: Transform.translate( - offset: const Offset(0, -2), - child: Icon( - Icons.minimize_rounded, - color: iconColor, - size: 20, + Expanded( + child: Container( + color: + Colors.black.withValues(alpha: 0), + child: DragToMoveArea( + child: Row( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + padding: const EdgeInsets.only( + left: 16, + ), + child: DefaultTextStyle( + style: TextStyle( + color: iconColor, + fontSize: 14, + ), + child: + Text(widget.label ?? ""), + ), + ), + ], ), ), - ); - }), - FutureBuilder>( - future: Future.microtask(() async { - final isMaximized = await windowManager.isMaximized(); - return [isMaximized]; - }), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - final maximized = snapshot.data?.firstOrNull ?? false; - return IconButton( - style: IconButton.styleFrom( - hoverColor: brightness == Brightness.light - ? Colors.black.withValues(alpha: 0.1) - : Colors.white.withValues(alpha: 0.2), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), - ), - onPressed: () async { - fullScreenHelper.closeFullScreen(ref); - if (maximized) { - await windowManager.unmaximize(); - return; - } - if (!maximized) { - await windowManager.maximize(); - } else { - await windowManager.unmaximize(); - } - }, - icon: Transform.translate( - offset: const Offset(0, 0), - child: Icon( - maximized ? Icons.maximize_rounded : Icons.crop_square_rounded, - color: iconColor, - size: 19, + ), + ), + Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: surfaceColor.withValues( + alpha: isOffline ? 0 : 0.5, ), + blurRadius: 32, + spreadRadius: 10, + offset: const Offset(8, -6), ), - ); - }, - ), - IconButton( - style: IconButton.styleFrom( - hoverColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(2), - ), + ], ), - onPressed: () async { - windowManager.close(); - }, - icon: Transform.translate( - offset: const Offset(0, -2), - child: Icon( - Icons.close_rounded, - color: iconColor, - size: 23, - ), + child: Row( + children: [ + FutureBuilder>( + future: Future.microtask(() async { + final isMinimized = + await windowManager + .isMinimized(); + return [isMinimized]; + }), + builder: (context, snapshot) { + final isMinimized = + snapshot.data?.firstOrNull ?? + false; + return IconButton( + style: IconButton.styleFrom( + hoverColor: brightness == + Brightness.light + ? Colors.black.withValues( + alpha: 0.1, + ) + : Colors.white.withValues( + alpha: 0.2, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 2), + ), + ), + onPressed: () async { + fullScreenHelper + .closeFullScreen(ref); + if (isMinimized) { + windowManager.restore(); + } else { + windowManager.minimize(); + } + }, + icon: Transform.translate( + offset: const Offset(0, -2), + child: Icon( + Icons.minimize_rounded, + color: iconColor, + size: 20, + ), + ), + ); + }, + ), + FutureBuilder>( + future: Future.microtask(() async { + final isMaximized = + await windowManager + .isMaximized(); + return [isMaximized]; + }), + builder: ( + BuildContext context, + AsyncSnapshot> + snapshot, + ) { + final maximized = + snapshot.data?.firstOrNull ?? + false; + return IconButton( + style: IconButton.styleFrom( + hoverColor: brightness == + Brightness.light + ? Colors.black.withValues( + alpha: 0.1, + ) + : Colors.white.withValues( + alpha: 0.2, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 2, + ), + ), + ), + onPressed: () async { + fullScreenHelper + .closeFullScreen(ref); + if (maximized) { + await windowManager + .unmaximize(); + return; + } + if (!maximized) { + await windowManager + .maximize(); + } else { + await windowManager + .unmaximize(); + } + }, + icon: Transform.translate( + offset: const Offset(0, 0), + child: Icon( + maximized + ? Icons.maximize_rounded + : Icons + .crop_square_rounded, + color: iconColor, + size: 19, + ), + ), + ); + }, + ), + IconButton( + style: IconButton.styleFrom( + hoverColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(2), + ), + ), + onPressed: () async { + windowManager.close(); + }, + icon: Transform.translate( + offset: const Offset(0, -2), + child: Icon( + Icons.close_rounded, + color: iconColor, + size: 23, + ), + ), + ), + ], ), ), ], ), ), - ], - ), - ), - TargetPlatform.macOS => const SizedBox.expand(), - _ => Text(widget.label ?? "Fladder"), - }, - const OfflineBanner() + TargetPlatform.macOS => const SizedBox.expand(), + _ => Text(widget.label ?? "Fladder"), + }, + const OfflineBanner(), ], ), ),