From 68755432424c4b684c6cd92c217f07a222349043 Mon Sep 17 00:00:00 2001 From: CherretGit Date: Sat, 20 Jun 2026 22:06:00 +0700 Subject: [PATCH] floating appbar --- assets/l10n/intl_en.arb | 3 +- assets/l10n/intl_ru.arb | 3 +- lib/config/setting_keys.dart | 1 + lib/pages/chat/chat_floating_app_bar.dart | 140 ++++++++++++++++++ lib/pages/chat/chat_legacy_app_bar.dart | 105 +++++++++++++ lib/pages/chat/chat_view.dart | 105 ++++--------- .../settings_features_view.dart | 5 + 7 files changed, 287 insertions(+), 75 deletions(-) create mode 100644 lib/pages/chat/chat_floating_app_bar.dart create mode 100644 lib/pages/chat/chat_legacy_app_bar.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 470cbb3f4..99caf6b41 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3842,5 +3842,6 @@ "chatFont": "Chat font", "chatFontFallback": "Fallback chat fonts", "openMessageSource": "Remote message", - "showProfileSource": "Profile data" + "showProfileSource": "Profile data", + "enableFloatingAppBar": "Floating chat app bar" } diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index c7b9eb8a1..ed9d5555f 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -3913,5 +3913,6 @@ "monospaceFontFallback": "Дополнительные моноширинные шрифты", "chatFont": "Шрифт чата", "chatFontFallback": "Дополнительные шрифты чата", - "openMessageSource": "Исходное сообщение" + "openMessageSource": "Исходное сообщение", + "enableFloatingAppBar": "Плавающий заголовок чата" } diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 82e2953cd..5f0fa0821 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -116,6 +116,7 @@ enum AppSettings { audioRecordingSamplingRate('audioRecordingSamplingRate', 44100), enableVideoNotes('xyz.extera.next.enableVideoNotes', false), enableChatFrostedGlass('xyz.extera.next.enableChatFrostedGlass', false), + enableFloatingAppBar('xyz.extera.enableFloatingAppBar', true), showSeconds('xyz.extera.showSeconds', false), enableAppBarCenterTitle( 'xyz.extera.next.enableAppBarCenterTitle', diff --git a/lib/pages/chat/chat_floating_app_bar.dart b/lib/pages/chat/chat_floating_app_bar.dart new file mode 100644 index 000000000..19fcf6686 --- /dev/null +++ b/lib/pages/chat/chat_floating_app_bar.dart @@ -0,0 +1,140 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:extera_next/config/setting_keys.dart'; +import 'package:extera_next/config/themes.dart'; +import 'package:extera_next/generated/l10n/l10n.dart'; +import 'package:extera_next/pages/chat/chat.dart'; +import 'package:extera_next/pages/chat/chat_app_bar_title.dart'; +import 'package:extera_next/pages/chat/pinned_events.dart'; +import 'package:extera_next/utils/stream_extension.dart'; +import 'package:extera_next/widgets/matrix.dart'; +import 'package:extera_next/widgets/unread_rooms_badge.dart'; + +class FloatingChatAppbar extends StatelessWidget implements PreferredSizeWidget { + final ChatController controller; + final double appbarBottomHeight; + final List actions; + + const FloatingChatAppbar({ + super.key, + required this.controller, + required this.appbarBottomHeight, + required this.actions, + }); + + @override + Size get preferredSize => + Size.fromHeight(kToolbarHeight + appbarBottomHeight + 8); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4), + child: (() { + final appBar = AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + actionsIconTheme: IconThemeData( + color: controller.selectedEvents.isEmpty + ? null + : theme.colorScheme.tertiary, + ), + automaticallyImplyLeading: false, + centerTitle: AppSettings.enableAppBarCenterTitle.value, + leading: controller.selectMode + ? IconButton( + icon: const Icon(Icons.close), + onPressed: controller.clearSelectedEvents, + tooltip: L10n.of(context).close, + color: theme.colorScheme.tertiary, + ) + : FluffyThemes.isColumnMode(context) + ? null + : StreamBuilder( + stream: Matrix.of(context).client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) => UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId, + badgePosition: .topEnd(top: 4, end: 8), + child: const Center(child: BackButton()), + ), + ), + titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0, + title: ChatAppBarTitle(controller), + actions: actions, + bottom: PreferredSize( + preferredSize: Size.fromHeight(appbarBottomHeight), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [PinnedEvents(controller)], + ), + ), + ); + return AppSettings.enableChatFrostedGlass.value + ? _FloatingAppBar(child: appBar) + : Material( + clipBehavior: Clip.hardEdge, + color: theme.colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + child: appBar, + ); + })(), + ), + ); + } +} + +class _FloatingAppBar extends StatelessWidget { + final Widget child; + + const _FloatingAppBar({ + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha( + theme.brightness == Brightness.dark ? 60 : 20, + ), + blurRadius: 24, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface.withAlpha( + theme.brightness == Brightness.dark ? 180 : 200, + ), + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outlineVariant.withAlpha(80), + width: 0.5, + ), + ), + ), + child: child, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/chat/chat_legacy_app_bar.dart b/lib/pages/chat/chat_legacy_app_bar.dart new file mode 100644 index 000000000..d1e95385b --- /dev/null +++ b/lib/pages/chat/chat_legacy_app_bar.dart @@ -0,0 +1,105 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:extera_next/config/setting_keys.dart'; +import 'package:extera_next/config/themes.dart'; +import 'package:extera_next/generated/l10n/l10n.dart'; +import 'package:extera_next/pages/chat/chat.dart'; +import 'package:extera_next/pages/chat/chat_app_bar_title.dart'; +import 'package:extera_next/pages/chat/pinned_events.dart'; +import 'package:extera_next/utils/stream_extension.dart'; +import 'package:extera_next/widgets/matrix.dart'; +import 'package:extera_next/widgets/unread_rooms_badge.dart'; + +class ChatLegacyAppBar extends StatelessWidget implements PreferredSizeWidget { + final ChatController controller; + final double appbarBottomHeight; + final List actions; + + const ChatLegacyAppBar({ + super.key, + required this.controller, + required this.appbarBottomHeight, + required this.actions + }); + + @override + Size get preferredSize => + Size.fromHeight(kToolbarHeight + appbarBottomHeight); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return AppBar( + backgroundColor: AppSettings.enableChatFrostedGlass.value + ? Colors.transparent + : null, + elevation: AppSettings.enableChatFrostedGlass.value ? 0 : null, + shape: FluffyThemes.isColumnMode(context) + ? Border( + bottom: BorderSide(color: theme.dividerColor, width: 1), + ) + : null, + scrolledUnderElevation: AppSettings.enableChatFrostedGlass.value + ? 0 + : null, + flexibleSpace: AppSettings.enableChatFrostedGlass.value + ? ClipRect( + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface.withAlpha( + theme.brightness == Brightness.dark ? 180 : 200, + ), + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outlineVariant + .withAlpha(80), + width: 0.5, + ), + ), + ), + ), + ), + ) + : null, + actionsIconTheme: IconThemeData( + color: controller.selectedEvents.isEmpty + ? null + : theme.colorScheme.tertiary, + ), + automaticallyImplyLeading: false, + centerTitle: AppSettings.enableAppBarCenterTitle.value, + leading: controller.selectMode + ? IconButton( + icon: const Icon(Icons.close), + onPressed: controller.clearSelectedEvents, + tooltip: L10n.of(context).close, + color: theme.colorScheme.tertiary, + ) + : FluffyThemes.isColumnMode(context) + ? null + : StreamBuilder( + stream: Matrix.of(context).client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) => UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId, + badgePosition: .topEnd(top: 4, end: 8), + child: const Center(child: BackButton()), + ), + ), + titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0, + title: ChatAppBarTitle(controller), + actions: actions, + bottom: PreferredSize( + preferredSize: Size.fromHeight(appbarBottomHeight), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [PinnedEvents(controller)], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 97db52d09..32de0e5e9 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'dart:ui' as ui; +import 'package:extera_next/pages/chat/chat_floating_app_bar.dart'; +import 'package:extera_next/pages/chat/chat_legacy_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -13,20 +15,16 @@ import 'package:extera_next/config/themes.dart'; import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:extera_next/pages/chat/chat.dart'; import 'package:extera_next/pages/chat/chat_app_bar_list_tile.dart'; -import 'package:extera_next/pages/chat/chat_app_bar_title.dart'; import 'package:extera_next/pages/chat/chat_event_list.dart'; import 'package:extera_next/pages/chat/encryption_button.dart'; import 'package:extera_next/pages/chat/jitsi_popup_button.dart'; -import 'package:extera_next/pages/chat/pinned_events.dart'; import 'package:extera_next/pages/chat/reply_display.dart'; import 'package:extera_next/pages/dialer/back_to_call_button.dart'; -import 'package:extera_next/utils/stream_extension.dart'; import 'package:extera_next/utils/url_launcher.dart'; import 'package:extera_next/widgets/avatar.dart'; import 'package:extera_next/widgets/chat_settings_popup_menu.dart'; import 'package:extera_next/widgets/matrix.dart'; import 'package:extera_next/widgets/mini_audio_player.dart'; -import 'package:extera_next/widgets/unread_rooms_badge.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; @@ -390,77 +388,17 @@ class _ChatViewState extends State { return Scaffold( extendBodyBehindAppBar: true, - appBar: AppBar( - backgroundColor: AppSettings.enableChatFrostedGlass.value - ? Colors.transparent - : null, - elevation: AppSettings.enableChatFrostedGlass.value ? 0 : null, - shape: FluffyThemes.isColumnMode(context) - ? Border( - bottom: BorderSide(color: theme.dividerColor, width: 1), - ) - : null, - scrolledUnderElevation: AppSettings.enableChatFrostedGlass.value - ? 0 - : null, - flexibleSpace: AppSettings.enableChatFrostedGlass.value - ? ClipRect( - child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface.withAlpha( - theme.brightness == Brightness.dark ? 180 : 200, - ), - border: Border( - bottom: BorderSide( - color: theme.colorScheme.outlineVariant - .withAlpha(80), - width: 0.5, - ), - ), - ), - ), - ), - ) - : null, - actionsIconTheme: IconThemeData( - color: controller.selectedEvents.isEmpty - ? null - : theme.colorScheme.tertiary, - ), - automaticallyImplyLeading: false, - centerTitle: AppSettings.enableAppBarCenterTitle.value, - leading: controller.selectMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: controller.clearSelectedEvents, - tooltip: L10n.of(context).close, - color: theme.colorScheme.tertiary, + appBar: AppSettings.enableFloatingAppBar.value + ? FloatingChatAppbar( + controller: controller, + appbarBottomHeight: appbarBottomHeight, + actions: _appBarActions(context) ) - : FluffyThemes.isColumnMode(context) - ? null - : StreamBuilder( - stream: Matrix.of(context).client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, _) => UnreadRoomsBadge( - filter: (r) => r.id != controller.roomId, - badgePosition: .topEnd(top: 4, end: 8), - child: const Center(child: BackButton()), - ), + : ChatLegacyAppBar( + controller: controller, + appbarBottomHeight: appbarBottomHeight, + actions: _appBarActions(context), ), - titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0, - title: ChatAppBarTitle(controller), - actions: _appBarActions(context), - bottom: PreferredSize( - preferredSize: Size.fromHeight(appbarBottomHeight), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [PinnedEvents(controller)], - ), - ), - ), floatingActionButton: ValueListenableBuilder( valueListenable: controller.scrolledUpNotifier, builder: (context, scrolledUp, _) { @@ -523,6 +461,27 @@ class _ChatViewState extends State { ), ), ), + if (AppSettings.enableFloatingAppBar.value) + Positioned( + top: 0, + left: 0, + right: 0, + child: IgnorePointer( + child: Container( + height: MediaQuery.of(context).padding.top + kToolbarHeight + appbarBottomHeight, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.colorScheme.surface, + theme.colorScheme.surface.withValues(alpha: 0), + ], + ), + ), + ), + ), + ), if (controller.room.canSendDefaultMessages && controller.room.membership == Membership.join) diff --git a/lib/pages/settings_features/settings_features_view.dart b/lib/pages/settings_features/settings_features_view.dart index c981c84b7..ac951eb23 100644 --- a/lib/pages/settings_features/settings_features_view.dart +++ b/lib/pages/settings_features/settings_features_view.dart @@ -81,6 +81,11 @@ class SettingsFeaturesView extends StatelessWidget { title: L10n.of(context).enablePeopleTab, setting: AppSettings.enablePeopleTab, ), + const ListDivider(), + SettingsSwitchListTile.adaptive( + title: L10n.of(context).enableFloatingAppBar, + setting: AppSettings.enableFloatingAppBar, + ), if (PlatformInfos.isMobile) ...[ const ListDivider(), SettingsSwitchListTile.adaptive(