From 6263b90d290c5a7b62251cd38329ee1604e20f8d Mon Sep 17 00:00:00 2001 From: BluewhaleYF Date: Wed, 7 Oct 2026 09:29:21 +0800 Subject: [PATCH 1/6] :lipstick: Animate settings category tab switching The narrow settings layout keyed DefaultTabController on the selected category, so every tap rebuilt the controller and the tab indicator jumped straight to its final position instead of sliding. Key it on the visible category set only so the controller survives selection changes, and cross-fade the body with AnimatedSwitcher for a real transition. --- lib/misc/settings.dart | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/misc/settings.dart b/lib/misc/settings.dart index eca9094a1..30faa0fc5 100644 --- a/lib/misc/settings.dart +++ b/lib/misc/settings.dart @@ -2402,8 +2402,12 @@ class SettingsScreen extends HookConsumerWidget { // Narrow layout with category tabs final colorScheme = Theme.of(context).colorScheme; final selectedIndex = visibleCategories.indexOf(selectedCategory); + // Key only on the set of visible categories, not on the current selection. + // Tapping a tab changes `selectedCategory`, so including it here used to + // recreate the DefaultTabController on every tap, resetting its animation + // to the end value and making the tab indicator jump instead of slide. final tabBarKey = ValueKey( - '${selectedCategory.title}:${visibleCategories.map((category) => category.title).join('|')}', + visibleCategories.map((category) => category.title).join('|'), ); return Column( children: [ @@ -2429,16 +2433,28 @@ class SettingsScreen extends HookConsumerWidget { ), ), Expanded( - child: selectedCategory.wideContent != null - ? selectedCategory.wideContent!(context) - : SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 24), - child: _SettingsSection( - title: selectedCategory.title, - localizedTitleKey: selectedCategory.localizedTitleKey, - children: selectedCategory.children, - ), - ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeIn, + layoutBuilder: (currentChild, previousChildren) => Stack( + fit: StackFit.expand, + children: [...previousChildren, ?currentChild], + ), + child: KeyedSubtree( + key: ValueKey(selectedCategory.title), + child: selectedCategory.wideContent != null + ? selectedCategory.wideContent!(context) + : SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 24), + child: _SettingsSection( + title: selectedCategory.title, + localizedTitleKey: selectedCategory.localizedTitleKey, + children: selectedCategory.children, + ), + ), + ), + ), ), ], ); From a81d20524a4d5fb78c5d6c23b80661a83127034e Mon Sep 17 00:00:00 2001 From: BluewhaleYF Date: Wed, 7 Oct 2026 09:35:12 +0800 Subject: [PATCH 2/6] :lipstick: Flatten the settings search bar The Material 3 SearchBar defaults to a 6dp elevation with a shadow, which looked out of place floating above the flat app bar. Drop the elevation and force a transparent shadow/surface tint so the field sits flush with the bar. --- lib/misc/settings.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/misc/settings.dart b/lib/misc/settings.dart index 30faa0fc5..4c7546c0c 100644 --- a/lib/misc/settings.dart +++ b/lib/misc/settings.dart @@ -2467,6 +2467,11 @@ class SettingsScreen extends HookConsumerWidget { controller: searchController, focusNode: searchFocusNode, constraints: const BoxConstraints(maxWidth: 420, minHeight: 36), + // Flat, shadowless search field so it blends into the app bar instead + // of floating above it with the default M3 elevation (6dp + shadow). + elevation: const WidgetStatePropertyAll(0), + shadowColor: const WidgetStatePropertyAll(Colors.transparent), + surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent), hintText: 'searchSettings'.tr(), hintStyle: WidgetStatePropertyAll( TextStyle( From 0d8fd66060480f2d558fade45edd0ca14e228a6e Mon Sep 17 00:00:00 2001 From: BluewhaleYF Date: Wed, 7 Oct 2026 09:43:50 +0800 Subject: [PATCH 3/6] :lipstick: Unify settings top bar and raise search field contrast The category tab strip used a gray surface while the app bar stayed on the default (near-white) surface, so the top looked split. Use one shared tone for both, disable the app bar's surface tint and scrolled-under elevation, and set the search field one container tone deeper so it stays readable. --- lib/misc/settings.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/misc/settings.dart b/lib/misc/settings.dart index 4c7546c0c..5a6b51ed5 100644 --- a/lib/misc/settings.dart +++ b/lib/misc/settings.dart @@ -81,6 +81,11 @@ class SettingsScreen extends HookConsumerWidget { !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); final isIos = !kIsWeb && Platform.isIOS; final isWide = isWideScreen(context); + // Shared tone for the whole top area (app bar + category tab strip) so the + // gray tab strip doesn't sit under a lighter/white app bar. + final topBarColor = Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withOpacity(0.55); final pools = ref.watch(poolsProvider); final user = ref.watch(userInfoProvider); final docBasepath = useState(null); @@ -2400,7 +2405,6 @@ class SettingsScreen extends HookConsumerWidget { } // Narrow layout with category tabs - final colorScheme = Theme.of(context).colorScheme; final selectedIndex = visibleCategories.indexOf(selectedCategory); // Key only on the set of visible categories, not on the current selection. // Tapping a tab changes `selectedCategory`, so including it here used to @@ -2412,7 +2416,7 @@ class SettingsScreen extends HookConsumerWidget { return Column( children: [ Material( - color: colorScheme.surfaceContainerHighest.withOpacity(0.55), + color: topBarColor, child: DefaultTabController( key: tabBarKey, length: visibleCategories.length, @@ -2472,6 +2476,11 @@ class SettingsScreen extends HookConsumerWidget { elevation: const WidgetStatePropertyAll(0), shadowColor: const WidgetStatePropertyAll(Colors.transparent), surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent), + // One tone deeper than the surrounding top bar so the field still + // reads clearly against the unified gray background. + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.surfaceContainerHighest, + ), hintText: 'searchSettings'.tr(), hintStyle: WidgetStatePropertyAll( TextStyle( @@ -2507,6 +2516,10 @@ class SettingsScreen extends HookConsumerWidget { ), leading: const AutoLeadingButton(), elevation: 0, + // Match the category tab strip so the entire top reads as one surface. + backgroundColor: topBarColor, + surfaceTintColor: Colors.transparent, + scrolledUnderElevation: 0, ), body: LayoutBuilder( builder: (context, constraints) { From 9be77ade534bf3c76400c2d443b187a1481ebd51 Mon Sep 17 00:00:00 2001 From: BluewhaleYF Date: Wed, 7 Oct 2026 12:08:09 +0800 Subject: [PATCH 4/6] :lipstick: Apply theme color to the chat list top bar Paint the chat list app bar with colorScheme.primary and an onPrimary foreground when the transparent app bar setting is off, falling back to the theme's transparent treatment when it's on, matching the explore page. --- lib/chat/widgets/chat_list_screen.dart | 45 +++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/chat/widgets/chat_list_screen.dart b/lib/chat/widgets/chat_list_screen.dart index 7ad17746a..c12a68e89 100644 --- a/lib/chat/widgets/chat_list_screen.dart +++ b/lib/chat/widgets/chat_list_screen.dart @@ -1289,12 +1289,14 @@ class _ChatListAppBar extends HookConsumerWidget { final List chatGroups; final Future Function() onChatGroupsChanged; final String? accountId; + final Color foreground; const _ChatListAppBar({ required this.tabController, required this.chatGroups, required this.onChatGroupsChanged, required this.accountId, + required this.foreground, }); @override @@ -1314,6 +1316,7 @@ class _ChatListAppBar extends HookConsumerWidget { Widget buildInviteButton() { return IconButton( tooltip: 'Chat Invites', + color: foreground, onPressed: openInvites, icon: Badge( label: Text( @@ -1354,6 +1357,7 @@ class _ChatListAppBar extends HookConsumerWidget { Symbols.inbox, fill: tabController.index == 0 ? 1 : 0, ), + color: foreground, onPressed: () => tabController.animateTo(0), tooltip: 'chatTabAll'.tr(), ), @@ -1362,6 +1366,7 @@ class _ChatListAppBar extends HookConsumerWidget { Symbols.person, fill: tabController.index == 1 ? 1 : 0, ), + color: foreground, onPressed: () => tabController.animateTo(1), tooltip: 'chatTabDirect'.tr(), ), @@ -1370,6 +1375,7 @@ class _ChatListAppBar extends HookConsumerWidget { Symbols.group, fill: tabController.index == 2 ? 1 : 0, ), + color: foreground, onPressed: () => tabController.animateTo(2), tooltip: 'chatTabGroup'.tr(), ), @@ -1387,13 +1393,11 @@ class _ChatListAppBar extends HookConsumerWidget { size: 24, speed: 7, fontSize: 12, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant.withOpacity(0.65), + color: foreground.withOpacity(0.65), ), ), ), - const _MarkAllReadButton(), + _MarkAllReadButton(color: foreground), buildInviteButton(), ], ), @@ -1403,7 +1407,9 @@ class _ChatListAppBar extends HookConsumerWidget { } class _MarkAllReadButton extends ConsumerWidget { - const _MarkAllReadButton(); + final Color? color; + + const _MarkAllReadButton({this.color}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -1415,17 +1421,14 @@ class _MarkAllReadButton extends ConsumerWidget { } return IconButton( + color: color, icon: readSyncState.isLoading ? SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), + child: CircularProgressIndicator(strokeWidth: 2, color: color), ) - : Icon( - Symbols.done_all, - ), + : Icon(Symbols.done_all), tooltip: 'Mark all as read', onPressed: readSyncState.isLoading ? null @@ -2125,6 +2128,20 @@ class ChatListWidget extends HookConsumerWidget { ); } + final colorScheme = Theme.of(context).colorScheme; + final appBarTransparent = ref.watch( + appSettingsProvider.select((s) => s.appBarTransparent), + ); + // With the transparent app bar setting off the header is a solid primary + // band, matching the explore page; with it on it defers to the theme's + // transparent treatment. The foreground has to be threaded into every + // child because `flexibleSpace` is not covered by the AppBar's own + // foreground `IconTheme`. + final appBarForeground = appBarTransparent + ? (Theme.of(context).appBarTheme.foregroundColor ?? + colorScheme.onSurface) + : colorScheme.onPrimary; + return AppScaffold( extendBody: false, floatingActionButton: const ChatFabWidget().padding( @@ -2133,6 +2150,11 @@ class ChatListWidget extends HookConsumerWidget { appBar: AppBar( leading: null, automaticallyImplyLeading: false, + // Null keeps the theme background (transparent when the setting is on). + backgroundColor: appBarTransparent ? null : colorScheme.primary, + foregroundColor: appBarForeground, + surfaceTintColor: Colors.transparent, + scrolledUnderElevation: 0, flexibleSpace: Stack( children: [ _ChatListAppBar( @@ -2140,6 +2162,7 @@ class ChatListWidget extends HookConsumerWidget { chatGroups: chatGroups, onChatGroupsChanged: refreshChatGroups, accountId: accountId, + foreground: appBarForeground, ), ], ), From 9bb8119b244e71b60e9c8d0910289baf9e4a84c7 Mon Sep 17 00:00:00 2001 From: BluewhaleYF Date: Wed, 7 Oct 2026 12:20:53 +0800 Subject: [PATCH 5/6] :globe_with_meridians: Localize the onboarding anonymous-continue button The onboardingContinueAnonymous key only existed in en-US, so the "Continue as anonymous" label fell back to English. Add zh-CN and zh-TW translations. --- assets/i18n/zh-CN.json | 1 + assets/i18n/zh-TW.json | 1 + 2 files changed, 2 insertions(+) diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index c3e8ff935..8186a3475 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -2517,6 +2517,7 @@ "ok": "确定", "okay": "了解", "onboardingContinue": "继续", + "onboardingContinueAnonymous": "以匿名身份继续", "onboardingCreateAccount": "创建账号", "onboardingLogIn": "登录", "onboardingRealmsDesc": "探索按兴趣组织的各类社群。从同好小组到创意团体,找到属于你的那群人。", diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index 643aa0709..fba253238 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -2412,6 +2412,7 @@ "ok": "確定", "okay": "瞭解", "onboardingContinue": "繼續", + "onboardingContinueAnonymous": "以匿名身分繼續", "onboardingCreateAccount": "建立帳號", "onboardingLogIn": "登入", "onboardingRealmsDesc": "探索按興趣組織的各類社群。從同好小組到創意團體,找到屬於你的那群人。", From 26c8b50065a3e9f7dd5c7b7ed701135ac3e8604b Mon Sep 17 00:00:00 2001 From: BluewhaleYF Date: Wed, 7 Oct 2026 12:43:22 +0800 Subject: [PATCH 6/6] :lipstick: Tighten the image gap in the post attachment carousel The featured-post style started marking attachments as contained, routing them through the non-full-bleed CarouselView branch. CarouselView applies its padding to every item, so the 8px body gutter doubled into a 16px gap between two images. Pass half the gutter for the horizontal carousel so the images sit 8px apart again, matching the full-bleed strip. --- lib/posts/widgets/compose/post_shared.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/posts/widgets/compose/post_shared.dart b/lib/posts/widgets/compose/post_shared.dart index ee6e448c5..9804a30ff 100644 --- a/lib/posts/widgets/compose/post_shared.dart +++ b/lib/posts/widgets/compose/post_shared.dart @@ -2074,6 +2074,15 @@ class PostBody extends ConsumerWidget { fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize! * (textScale ?? 1), ); + // The attachment carousel is a `CarouselView`, whose padding is applied to + // every item — so passing the body gutter verbatim also made it the gap + // between two images (an 8px gutter became a 16px gap). Halve it for the + // horizontal carousel so images sit 8px apart while the edge still roughly + // matches the body text; the column layout keeps the full gutter because + // its spacing comes from the column itself. + final containedAttachmentInset = isInteractive + ? renderingPadding.horizontal / 2 + : renderingPadding.horizontal; if (item.debugRank != null && kDebugMode) { metadataChildren.add( @@ -2398,9 +2407,9 @@ class PostBody extends ConsumerWidget { isColumn: !isInteractive, isFullBleed: !useContainedAttachments, padding: EdgeInsets.fromLTRB( - useContainedAttachments ? renderingPadding.horizontal : 0, + useContainedAttachments ? containedAttachmentInset : 0, 4, - useContainedAttachments ? renderingPadding.horizontal : 0, + useContainedAttachments ? containedAttachmentInset : 0, 4, ), borderRadius: useContainedAttachments ? 12 : 8,