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": "探索按興趣組織的各類社群。從同好小組到創意團體,找到屬於你的那群人。", 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, ), ], ), diff --git a/lib/misc/settings.dart b/lib/misc/settings.dart index eca9094a1..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,15 +2405,18 @@ 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: [ Material( - color: colorScheme.surfaceContainerHighest.withOpacity(0.55), + color: topBarColor, child: DefaultTabController( key: tabBarKey, length: visibleCategories.length, @@ -2429,16 +2437,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, + ), + ), + ), + ), ), ], ); @@ -2451,6 +2471,16 @@ 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), + // 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( @@ -2486,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) { 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,