diff --git a/lib/core/widgets/lazurite_app_bar.dart b/lib/core/widgets/lazurite_app_bar.dart index 141dd19..c677485 100644 --- a/lib/core/widgets/lazurite_app_bar.dart +++ b/lib/core/widgets/lazurite_app_bar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lazurite/core/logging/app_logger.dart'; import 'package:lazurite/core/router/app_shell.dart'; +import 'package:lazurite/core/theme/theme_extensions.dart'; import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; @@ -25,26 +26,23 @@ class LazuriteAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => Size.fromHeight(_toolbarHeight + (bottom?.preferredSize.height ?? 0)); @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return AppBar( - toolbarHeight: _toolbarHeight, - backgroundColor: theme.colorScheme.surfaceContainerLowest.withValues(alpha: 0.92), - surfaceTintColor: Colors.transparent, - elevation: 0, - scrolledUnderElevation: 0, - leading: const AppShellMenuButton(), - title: Text( - sectionLabel.toUpperCase(), - style: theme.textTheme.labelSmall?.copyWith(letterSpacing: 3, color: theme.colorScheme.onSurfaceVariant), - ), - centerTitle: false, - titleSpacing: 0, - actions: [...?actions, const _AppBarOfflineIndicator()], - bottom: bottom, - shape: Border(bottom: BorderSide(color: theme.colorScheme.outlineVariant)), - ); - } + Widget build(BuildContext context) => AppBar( + toolbarHeight: _toolbarHeight, + backgroundColor: context.colorScheme.surfaceContainerLowest.withValues(alpha: 0.92), + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + leading: const AppShellMenuButton(), + title: Text( + sectionLabel.toUpperCase(), + style: context.textTheme.labelSmall?.copyWith(letterSpacing: 3, color: context.colorScheme.onSurfaceVariant), + ), + centerTitle: false, + titleSpacing: 0, + actions: [...?actions, const _AppBarOfflineIndicator()], + bottom: bottom, + shape: Border(bottom: BorderSide(color: context.colorScheme.outlineVariant)), + ); } class _AppBarOfflineIndicator extends StatelessWidget { @@ -69,12 +67,7 @@ class _AppBarOfflineIndicator extends StatelessWidget { return const SizedBox.shrink(); } - final theme = Theme.of(context); - SettingsCubit? settingsCubit; - try { - settingsCubit = context.read(); - } catch (_) {} - final canDisableSimulatedOffline = state.isSimulatedOffline && settingsCubit != null; + final canDisableSimulatedOffline = state.isSimulatedOffline && _hasSettingsCubit(context); final tooltip = canDisableSimulatedOffline ? 'Disable simulated offline mode' : 'You\'re offline'; return Padding( @@ -83,12 +76,24 @@ class _AppBarOfflineIndicator extends StatelessWidget { message: tooltip, child: IconButton( tooltip: tooltip, - onPressed: canDisableSimulatedOffline ? () => settingsCubit?.setSimulateOffline(false) : null, - icon: Icon(Icons.cloud_off_outlined, color: theme.colorScheme.error), + onPressed: canDisableSimulatedOffline + ? () => context.read().setSimulateOffline(false) + : null, + icon: Icon(Icons.cloud_off_outlined, color: context.theme.colorScheme.error), ), ), ); }, ); } + + bool _hasSettingsCubit(BuildContext context) { + try { + context.read(); + return true; + } catch (error, stackTrace) { + log.d('showing offline indicator without simulated-offline controls', error: error, stackTrace: stackTrace); + return false; + } + } } diff --git a/lib/features/devtools/presentation/dev_tools_screen.dart b/lib/features/devtools/presentation/dev_tools_screen.dart index 24f2621..38119c7 100644 --- a/lib/features/devtools/presentation/dev_tools_screen.dart +++ b/lib/features/devtools/presentation/dev_tools_screen.dart @@ -44,18 +44,16 @@ class DevToolsScreen extends StatelessWidget { ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(message), behavior: SnackBarBehavior.floating)); }, - builder: (context, state) { - return Column( - children: [ - _SearchInput(state: state, initialQuery: initialQuery), - if (state.status == DevToolsStatus.repoLoaded || - state.status == DevToolsStatus.collectionLoaded || - state.status == DevToolsStatus.recordLoaded) - _BreadcrumbBar(state: state), - Expanded(child: _Content(state: state)), - ], - ); - }, + builder: (context, state) => Column( + children: [ + _SearchInput(state: state, initialQuery: initialQuery), + if (state.status == DevToolsStatus.repoLoaded || + state.status == DevToolsStatus.collectionLoaded || + state.status == DevToolsStatus.recordLoaded) + _BreadcrumbBar(state: state), + Expanded(child: _Content(state: state)), + ], + ), ), ); } @@ -192,41 +190,37 @@ class _TypeaheadResults extends StatelessWidget { final ValueChanged onSelected; @override - Widget build(BuildContext context) { - final listHeight = (actors.length * 56.0).clamp(56.0, 220.0); - - return Material( - color: context.colorScheme.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: context.theme.dividerColor), - ), - clipBehavior: Clip.antiAlias, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (isLoading) const LinearProgressIndicator(minHeight: 2), - if (actors.isNotEmpty) - SizedBox( - height: listHeight, - child: ListView.separated( - itemCount: actors.length, - separatorBuilder: (_, _) => Divider(height: 1, color: context.theme.dividerColor), - itemBuilder: (context, index) { - final actor = actors[index]; - return ListTile( - dense: true, - title: Text(actor.displayName ?? actor.handle), - subtitle: Text('@${actor.handle}'), - onTap: () => onSelected(actor), - ); - }, - ), + Widget build(BuildContext context) => Material( + color: context.colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: context.theme.dividerColor), + ), + clipBehavior: Clip.antiAlias, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isLoading) const LinearProgressIndicator(minHeight: 2), + if (actors.isNotEmpty) + SizedBox( + height: (actors.length * 56.0).clamp(56.0, 220.0), + child: ListView.separated( + itemCount: actors.length, + separatorBuilder: (_, _) => Divider(height: 1, color: context.theme.dividerColor), + itemBuilder: (context, index) { + final actor = actors[index]; + return ListTile( + dense: true, + title: Text(actor.displayName ?? actor.handle), + subtitle: Text('@${actor.handle}'), + onTap: () => onSelected(actor), + ); + }, ), - ], - ), - ); - } + ), + ], + ), + ); } class _BreadcrumbBar extends StatelessWidget { @@ -240,15 +234,13 @@ class _BreadcrumbBar extends StatelessWidget { } List _items(BuildContext context) { - final cubit = context.read(); - final l10n = context.l10n; - final repoLabel = state.repoHandle ?? state.handle ?? state.did ?? l10n.labelRepository; + final repoLabel = state.repoHandle ?? state.handle ?? state.did ?? context.l10n.labelRepository; final items = [ AppBreadcrumbItem( label: repoLabel, tooltip: state.did == null ? repoLabel : '$repoLabel\n${state.did}', key: const ValueKey('dev-tools-breadcrumb-repo'), - onTap: state.status == DevToolsStatus.repoLoaded ? null : cubit.goBackToRepo, + onTap: state.status == DevToolsStatus.repoLoaded ? null : () => context.read().goBackToRepo(), ), ]; @@ -257,7 +249,9 @@ class _BreadcrumbBar extends StatelessWidget { AppBreadcrumbItem( label: state.selectedCollection!, key: const ValueKey('dev-tools-breadcrumb-collection'), - onTap: state.status == DevToolsStatus.collectionLoaded ? null : cubit.goBackToCollection, + onTap: state.status == DevToolsStatus.collectionLoaded + ? null + : () => context.read().goBackToCollection(), ), ); } @@ -265,7 +259,7 @@ class _BreadcrumbBar extends StatelessWidget { if (state.selectedRecord != null) { items.add( AppBreadcrumbItem( - label: state.selectedRecord!.rkey.isEmpty ? l10n.labelRecordJson : state.selectedRecord!.rkey, + label: state.selectedRecord!.rkey.isEmpty ? context.l10n.labelRecordJson : state.selectedRecord!.rkey, tooltip: state.selectedRecord!.uri, key: const ValueKey('dev-tools-breadcrumb-record'), ), @@ -372,7 +366,6 @@ class _RepoOverview extends StatelessWidget { @override Widget build(BuildContext context) { - final totalRepoRecords = state.totalRepoRecords; return ListView( children: [ Container( @@ -417,11 +410,12 @@ class _RepoOverview extends StatelessWidget { style: context.textTheme.bodySmall!.copyWith(color: context.colorScheme.onSurface), ), Text( - totalRepoRecords == null + // FIXME: nested ternaries like this are bad + state.totalRepoRecords == null ? (state.isCollectionCountsLoading ? context.l10n.messageRecordCountsLoading : context.l10n.messageRecordCountsUnavailable) - : context.l10n.formatRecordsCount(totalRepoRecords), + : context.l10n.formatRecordsCount(state.totalRepoRecords!), style: context.textTheme.bodySmall!.copyWith(color: context.colorScheme.onSurfaceVariant), ), ], @@ -448,39 +442,37 @@ class _CollectionItem extends StatelessWidget { final CollectionSummary collection; @override - Widget build(BuildContext context) { - return ListTile( - leading: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: context.colorScheme.surfaceContainerHighest, - ), - child: Icon(_getCollectionIcon(collection.name), size: 16, color: context.colorScheme.primary), + Widget build(BuildContext context) => ListTile( + leading: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: context.colorScheme.surfaceContainerHighest, ), - title: Text(collection.name, style: context.codeTextStyle(fontSize: 13)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: context.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - collection.countLabel, - style: context.textTheme.labelSmall!.copyWith(color: context.colorScheme.onSurfaceVariant), - ), + child: Icon(_getCollectionIcon(collection.name), size: 16, color: context.colorScheme.primary), + ), + title: Text(collection.name, style: context.codeTextStyle(fontSize: 13)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(999), ), - const SizedBox(width: 4), - const Icon(Icons.chevron_right), - ], - ), - onTap: () => context.read().loadCollection(collection.name), - ); - } + child: Text( + collection.countLabel, + style: context.textTheme.labelSmall!.copyWith(color: context.colorScheme.onSurfaceVariant), + ), + ), + const SizedBox(width: 4), + const Icon(Icons.chevron_right), + ], + ), + onTap: () => context.read().loadCollection(collection.name), + ); /// NOTE: repost must come before post IconData _getCollectionIcon(String collection) { diff --git a/lib/features/feed/presentation/home_feed_screen.dart b/lib/features/feed/presentation/home_feed_screen.dart index 24698cd..2d4b1be 100644 --- a/lib/features/feed/presentation/home_feed_screen.dart +++ b/lib/features/feed/presentation/home_feed_screen.dart @@ -521,7 +521,7 @@ class _FeedListViewState extends State<_FeedListView> with AutomaticKeepAliveCli return const EmptyState(message: 'No posts yet', icon: Icons.article_outlined); } - final accountDid = context.read().state.tokens?.did ?? ''; + final accountDid = context.select((AuthBloc bloc) => bloc.state.tokens?.did ?? ''); Widget buildCard(int index, PostCardVariant variant) { final post = _posts[index]; diff --git a/lib/features/profile/presentation/widgets/profile_liked_posts_pane.dart b/lib/features/profile/presentation/widgets/profile_liked_posts_pane.dart index 15e5c42..6e6df8f 100644 --- a/lib/features/profile/presentation/widgets/profile_liked_posts_pane.dart +++ b/lib/features/profile/presentation/widgets/profile_liked_posts_pane.dart @@ -1,4 +1,3 @@ -import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -7,6 +6,7 @@ import 'package:lazurite/core/logging/app_logger.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_card_with_actions.dart'; import 'package:lazurite/features/profile/data/profile_repository.dart'; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; class ProfileLikedPostsPane extends StatefulWidget { const ProfileLikedPostsPane({super.key, required this.actor, required this.profileRepository}); @@ -132,7 +132,7 @@ class _ProfileLikedPostsPaneState extends State { ); } - final accountDid = context.read().state.tokens?.did ?? ''; + final accountDid = context.select((AuthBloc bloc) => bloc.state.tokens?.did ?? ''); return RefreshIndicator( onRefresh: _refresh, child: NotificationListener( @@ -179,19 +179,17 @@ class _UnavailableLikedPostCard extends StatelessWidget { final String reason; @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - leading: const Icon(Icons.hide_source_outlined), - title: Text(context.l10n.labelUnavailableLikedPost), - subtitle: Text(reason), - trailing: IconButton( - icon: const Icon(Icons.open_in_new), - onPressed: () => context.push('/post?uri=${Uri.encodeQueryComponent(subjectUri)}'), - tooltip: context.l10n.buttonOpen, - ), + Widget build(BuildContext context) => Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: ListTile( + leading: const Icon(Icons.hide_source_outlined), + title: Text(context.l10n.labelUnavailableLikedPost), + subtitle: Text(reason), + trailing: IconButton( + icon: const Icon(Icons.open_in_new), + onPressed: () => context.push('/post?uri=${Uri.encodeQueryComponent(subjectUri)}'), + tooltip: context.l10n.buttonOpen, ), - ); - } + ), + ); } diff --git a/lib/features/profile/presentation/widgets/profile_mentions_pane.dart b/lib/features/profile/presentation/widgets/profile_mentions_pane.dart index 5f81c96..d422967 100644 --- a/lib/features/profile/presentation/widgets/profile_mentions_pane.dart +++ b/lib/features/profile/presentation/widgets/profile_mentions_pane.dart @@ -140,7 +140,7 @@ class _ProfileMentionsPaneState extends State { ); } - final accountDid = context.read().state.tokens?.did ?? ''; + final accountDid = context.select((AuthBloc bloc) => bloc.state.tokens?.did ?? ''); return RefreshIndicator( onRefresh: _loadInitial, child: NotificationListener( diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index e0702b4..a13dde5 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -36,10 +36,12 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final authState = context.watch().state; + final authView = context.select( + (AuthBloc bloc) => (isAuthenticated: bloc.state.isAuthenticated, tokens: bloc.state.tokens), + ); final l10n = context.l10n; - final tokens = authState.tokens; - final showAccountSettings = authState.isAuthenticated && tokens != null; + final tokens = authView.tokens; + final showAccountSettings = authView.isAuthenticated && tokens != null; return Scaffold( appBar: AppBar( @@ -205,7 +207,6 @@ class SettingsScreen extends StatelessWidget { Widget _title(BuildContext context) => Text(context.l10n.labelSettings, style: context.textTheme.titleLarge); Widget _buildThemeSelector(BuildContext context) { - final settingsCubit = context.read(); return BlocBuilder( builder: (context, state) { return SettingsGroup( @@ -225,6 +226,7 @@ class SettingsScreen extends StatelessWidget { ], selected: {AppearanceMode.fromState(state)}, onSelectionChanged: (selected) { + final settingsCubit = context.read(); switch (selected.first) { case AppearanceMode.system: settingsCubit.setUseSystemTheme(true); @@ -254,7 +256,7 @@ class SettingsScreen extends StatelessWidget { ThemePaletteRow( palette: palette, isSelected: state.themePalette == palette, - onTap: () => settingsCubit.setThemePalette(palette), + onTap: () => context.read().setThemePalette(palette), ), const Divider(height: 1), SettingsDropdownTile( @@ -265,7 +267,7 @@ class SettingsScreen extends StatelessWidget { optionBuilder: _headingFontOption, onChanged: (value) { if (value != null) { - settingsCubit.setHeadingFontFamily(value); + context.read().setHeadingFontFamily(value); } }, ), @@ -278,7 +280,7 @@ class SettingsScreen extends StatelessWidget { optionBuilder: _contentFontOption, onChanged: (value) { if (value != null) { - settingsCubit.setContentFontFamily(value); + context.read().setContentFontFamily(value); } }, ), @@ -291,7 +293,7 @@ class SettingsScreen extends StatelessWidget { optionBuilder: _fontSizeOption, onChanged: (value) { if (value != null) { - settingsCubit.setContentFontSize(value); + context.read().setContentFontSize(value); } }, ), @@ -304,7 +306,7 @@ class SettingsScreen extends StatelessWidget { optionBuilder: _codeFontOption, onChanged: (value) { if (value != null) { - settingsCubit.setCodeFontFamily(value); + context.read().setCodeFontFamily(value); } }, ), @@ -349,60 +351,57 @@ class SettingsScreen extends StatelessWidget { style: AppTypography.code(fontFamily, color: context.colorScheme.onSurface), ); - Widget _buildLayoutSettings(BuildContext context) { - final settingsCubit = context.read(); - return BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Divider(height: 1, color: context.theme.dividerColor), - Material( - color: context.theme.cardColor, - child: Column( - children: [ - SettingsDropdownTile( - title: context.l10n.labelFeedLayout, - value: state.feedLayout, - options: FeedLayout.values, - labelBuilder: (layout) => switch (layout) { - FeedLayout.comfortable => context.l10n.messageFeedLayoutComfortable, - FeedLayout.compact => context.l10n.messageFeedLayoutCompact, - }, - onChanged: (value) { - if (value != null) { - settingsCubit.setFeedLayout(value); - } - }, - ), - const Divider(height: 1), - SettingsDropdownTile( - title: context.l10n.labelThreadAutoCollapse, - subtitle: context.l10n.messageThreadAutoCollapseSubtitle, - value: state.threadAutoCollapseDepth, - options: const [null, 1, 2, 3, 4, 5, 6], - labelBuilder: (depth) => depth == null ? context.l10n.commonOff : context.l10n.formatDepth(depth), - onChanged: settingsCubit.setThreadAutoCollapseDepth, - ), - const Divider(height: 1), - SettingsTile( - icon: Icons.motion_photos_off_outlined, - title: context.l10n.labelAnimations, - subtitle: context.l10n.messageTurnOffNonEssentialMotion, - trailing: Switch.adaptive( - value: state.animationsEnabled, - onChanged: settingsCubit.setAnimationsEnabled, - ), + Widget _buildLayoutSettings(BuildContext context) => BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Divider(height: 1, color: context.theme.dividerColor), + Material( + color: context.theme.cardColor, + child: Column( + children: [ + SettingsDropdownTile( + title: context.l10n.labelFeedLayout, + value: state.feedLayout, + options: FeedLayout.values, + labelBuilder: (layout) => switch (layout) { + FeedLayout.comfortable => context.l10n.messageFeedLayoutComfortable, + FeedLayout.compact => context.l10n.messageFeedLayoutCompact, + }, + onChanged: (value) { + if (value != null) { + context.read().setFeedLayout(value); + } + }, + ), + const Divider(height: 1), + SettingsDropdownTile( + title: context.l10n.labelThreadAutoCollapse, + subtitle: context.l10n.messageThreadAutoCollapseSubtitle, + value: state.threadAutoCollapseDepth, + options: const [null, 1, 2, 3, 4, 5, 6], + labelBuilder: (depth) => depth == null ? context.l10n.commonOff : context.l10n.formatDepth(depth), + onChanged: (depth) => context.read().setThreadAutoCollapseDepth(depth), + ), + const Divider(height: 1), + SettingsTile( + icon: Icons.motion_photos_off_outlined, + title: context.l10n.labelAnimations, + subtitle: context.l10n.messageTurnOffNonEssentialMotion, + trailing: Switch.adaptive( + value: state.animationsEnabled, + onChanged: (enabled) => context.read().setAnimationsEnabled(enabled), ), - ], - ), + ), + ], ), - Divider(height: 1, color: context.theme.dividerColor), - ], - ); - }, - ); - } + ), + Divider(height: 1, color: context.theme.dividerColor), + ], + ); + }, + ); Widget _buildSearchSettings(BuildContext context, {required bool showTypeaheadSettings}) => BlocBuilder( @@ -459,7 +458,6 @@ class SettingsScreen extends StatelessWidget { ); Widget _buildDeveloperSettings(BuildContext context) { - final settingsCubit = context.read(); final crashReportingService = _readCrashReportingServiceOrNull(context); return BlocBuilder( builder: (context, state) => Column( @@ -474,7 +472,10 @@ class SettingsScreen extends StatelessWidget { icon: Icons.cloud_off_outlined, title: context.l10n.labelGoOffline, subtitle: context.l10n.messageDeveloperGoOfflineSubtitle, - trailing: Switch.adaptive(value: state.simulateOffline, onChanged: settingsCubit.setSimulateOffline), + trailing: Switch.adaptive( + value: state.simulateOffline, + onChanged: (enabled) => context.read().setSimulateOffline(enabled), + ), ), const Divider(height: 1), SettingsTile( @@ -526,146 +527,143 @@ class SettingsScreen extends StatelessWidget { } } - Widget _buildAdvancedSettings(BuildContext context) { - final settingsCubit = context.read(); - return BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Divider(height: 1, color: context.colorScheme.outline), - Material( - color: context.colorScheme.surface, - child: Column( - children: [ - SettingsTile( - icon: Icons.description_outlined, - title: context.l10n.labelLogs, - subtitle: context.l10n.messageLogsSubtitle, - onTap: () => context.push('/settings/logs'), - ), - const Divider(height: 1), - ConstellationUrlTile(currentUrl: state.constellationUrl), - const Divider(height: 1), - SettingsTile( - icon: Icons.route_outlined, - title: context.l10n.labelAppViewProvider, - subtitle: _appViewSubtitle(context, state.appViewProvider), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - child: Align( - alignment: Alignment.centerLeft, - child: SegmentedButton( - key: const Key('appview-provider-segmented'), - segments: [ - ButtonSegment( - value: AppViewProviders.blueskyKey, - label: Text(context.l10n.labelBluesky), - ), - ButtonSegment( - value: AppViewProviders.blackskyKey, - label: Text(context.l10n.labelBlacksky), - ), - ], - selected: {state.appViewProvider}, - onSelectionChanged: (selection) async { - final selectedProvider = selection.first; - if (selectedProvider == state.appViewProvider) { - return; - } - await _confirmAndApplyProviderChange(context, selectedProvider); - }, - ), - ), - ), - const Divider(height: 1), - SettingsTile( - icon: Icons.compare_arrows_outlined, - title: context.l10n.labelCrossProviderFallback, - subtitle: context.l10n.messageCrossProviderFallbackSubtitle, - trailing: Switch.adaptive( - value: state.crossProviderFallbackEnabled, - onChanged: settingsCubit.setCrossProviderFallbackEnabled, - ), - ), - const Divider(height: 1), - SettingsTile( - icon: Icons.alt_route_outlined, - title: context.l10n.labelSlingshotIdentityFallback, - subtitle: context.l10n.messageSlingshotIdentityFallbackSubtitle, - trailing: Switch.adaptive( - value: state.slingshotIdentityFallbackEnabled, - onChanged: settingsCubit.setSlingshotIdentityFallbackEnabled, - ), - ), - const Divider(height: 1), - SettingsTile( - icon: Icons.bug_report_outlined, - title: context.l10n.labelCrashReporting, - subtitle: state.crashReportingEnabled - ? context.l10n.messageCrashReportingEnabled - : context.l10n.messageCrashReportingDisabled, - trailing: Switch.adaptive( - value: state.crashReportingEnabled, - onChanged: (enabled) => unawaited(_handleCrashReportingToggle(context, enabled)), + Widget _buildAdvancedSettings(BuildContext context) => BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Divider(height: 1, color: context.colorScheme.outline), + Material( + color: context.colorScheme.surface, + child: Column( + children: [ + SettingsTile( + icon: Icons.description_outlined, + title: context.l10n.labelLogs, + subtitle: context.l10n.messageLogsSubtitle, + onTap: () => context.push('/settings/logs'), + ), + const Divider(height: 1), + ConstellationUrlTile(currentUrl: state.constellationUrl), + const Divider(height: 1), + SettingsTile( + icon: Icons.route_outlined, + title: context.l10n.labelAppViewProvider, + subtitle: _appViewSubtitle(context, state.appViewProvider), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Align( + alignment: Alignment.centerLeft, + child: SegmentedButton( + key: const Key('appview-provider-segmented'), + segments: [ + ButtonSegment( + value: AppViewProviders.blueskyKey, + label: Text(context.l10n.labelBluesky), + ), + ButtonSegment( + value: AppViewProviders.blackskyKey, + label: Text(context.l10n.labelBlacksky), + ), + ], + selected: {state.appViewProvider}, + onSelectionChanged: (selection) async { + final selectedProvider = selection.first; + if (selectedProvider == state.appViewProvider) { + return; + } + await _confirmAndApplyProviderChange(context, selectedProvider); + }, ), ), - const Divider(height: 1), - SettingsTile( - icon: Icons.monitor_heart_outlined, - title: context.l10n.labelProviderDiagnostics, - subtitle: context.l10n.messageProviderDiagnosticsSubtitle, - ), - ConnectionDetailRow( - label: context.l10n.labelActiveProvider, - value: _appViewProviderLabel(context, state.appViewProvider), - ), - const Divider(height: 1), - ConnectionDetailRow( - label: context.l10n.labelHealth, - value: state.appViewHealthSummary ?? context.l10n.commonNotCheckedYet, - ), - const Divider(height: 1), - ConnectionDetailRow( - label: context.l10n.labelLastHealthCheck, - value: state.appViewHealthCheckedAt == null - ? context.l10n.commonNever - : formatTimestamp(state.appViewHealthCheckedAt!.toLocal()), - ), - const Divider(height: 1), - ConnectionDetailRow( - label: context.l10n.labelLastFallback, - value: state.appViewLastFallback ?? context.l10n.commonNone, + ), + const Divider(height: 1), + SettingsTile( + icon: Icons.compare_arrows_outlined, + title: context.l10n.labelCrossProviderFallback, + subtitle: context.l10n.messageCrossProviderFallbackSubtitle, + trailing: Switch.adaptive( + value: state.crossProviderFallbackEnabled, + onChanged: (enabled) => context.read().setCrossProviderFallbackEnabled(enabled), ), - const Divider(height: 1), - ConnectionDetailRow( - label: context.l10n.labelLastError, - value: state.appViewLastError ?? context.l10n.commonNone, + ), + const Divider(height: 1), + SettingsTile( + icon: Icons.alt_route_outlined, + title: context.l10n.labelSlingshotIdentityFallback, + subtitle: context.l10n.messageSlingshotIdentityFallbackSubtitle, + trailing: Switch.adaptive( + value: state.slingshotIdentityFallbackEnabled, + onChanged: (enabled) => context.read().setSlingshotIdentityFallbackEnabled(enabled), ), - const Divider(height: 1), - SettingsTile( - icon: Icons.medical_information_outlined, - title: context.l10n.labelRefreshProviderHealth, - subtitle: context.l10n.messageRefreshProviderHealthSubtitle, - trailing: state.appViewHealthRefreshing - ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2)) - : const Icon(Icons.refresh_outlined), - onTap: state.appViewHealthRefreshing - ? null - : () { - unawaited(settingsCubit.refreshAppViewHealth()); - }, + ), + const Divider(height: 1), + SettingsTile( + icon: Icons.bug_report_outlined, + title: context.l10n.labelCrashReporting, + subtitle: state.crashReportingEnabled + ? context.l10n.messageCrashReportingEnabled + : context.l10n.messageCrashReportingDisabled, + trailing: Switch.adaptive( + value: state.crashReportingEnabled, + onChanged: (enabled) => unawaited(_handleCrashReportingToggle(context, enabled)), ), - ], - ), + ), + const Divider(height: 1), + SettingsTile( + icon: Icons.monitor_heart_outlined, + title: context.l10n.labelProviderDiagnostics, + subtitle: context.l10n.messageProviderDiagnosticsSubtitle, + ), + ConnectionDetailRow( + label: context.l10n.labelActiveProvider, + value: _appViewProviderLabel(context, state.appViewProvider), + ), + const Divider(height: 1), + ConnectionDetailRow( + label: context.l10n.labelHealth, + value: state.appViewHealthSummary ?? context.l10n.commonNotCheckedYet, + ), + const Divider(height: 1), + ConnectionDetailRow( + label: context.l10n.labelLastHealthCheck, + value: state.appViewHealthCheckedAt == null + ? context.l10n.commonNever + : formatTimestamp(state.appViewHealthCheckedAt!.toLocal()), + ), + const Divider(height: 1), + ConnectionDetailRow( + label: context.l10n.labelLastFallback, + value: state.appViewLastFallback ?? context.l10n.commonNone, + ), + const Divider(height: 1), + ConnectionDetailRow( + label: context.l10n.labelLastError, + value: state.appViewLastError ?? context.l10n.commonNone, + ), + const Divider(height: 1), + SettingsTile( + icon: Icons.medical_information_outlined, + title: context.l10n.labelRefreshProviderHealth, + subtitle: context.l10n.messageRefreshProviderHealthSubtitle, + trailing: state.appViewHealthRefreshing + ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2)) + : const Icon(Icons.refresh_outlined), + onTap: state.appViewHealthRefreshing + ? null + : () { + unawaited(context.read().refreshAppViewHealth()); + }, + ), + ], ), - Divider(height: 1, color: context.theme.dividerColor), - ], - ); - }, - ); - } + ), + Divider(height: 1, color: context.theme.dividerColor), + ], + ); + }, + ); String _appViewSubtitle(BuildContext context, String providerKey) { final provider = _appViewProviderLabel(context, providerKey); diff --git a/lib/features/settings/presentation/widgets/account_feed_display_preferences.dart b/lib/features/settings/presentation/widgets/account_feed_display_preferences.dart index f90552e..95d5b1c 100644 --- a/lib/features/settings/presentation/widgets/account_feed_display_preferences.dart +++ b/lib/features/settings/presentation/widgets/account_feed_display_preferences.dart @@ -18,7 +18,6 @@ class AccountFeedDisplayPreferences extends StatelessWidget { @override Widget build(BuildContext context) => BlocBuilder( builder: (context, state) { - final cubit = context.read(); final preference = state.feedViewPref; final hideReplies = preference?.hideReplies ?? false; final hideRepliesByUnfollowed = preference?.hideRepliesByUnfollowed ?? true; @@ -46,7 +45,12 @@ class AccountFeedDisplayPreferences extends StatelessWidget { icon: Icons.reply_outlined, title: 'Hide replies', subtitle: 'Only show top-level posts in this feed.', - trailing: Switch.adaptive(value: hideReplies, onChanged: state.isBusy ? null : cubit.setHideReplies), + trailing: Switch.adaptive( + value: hideReplies, + onChanged: state.isBusy + ? null + : (value) => context.read().setHideReplies(value), + ), ), const Divider(height: 1), SettingsTile( @@ -55,21 +59,28 @@ class AccountFeedDisplayPreferences extends StatelessWidget { subtitle: 'Keep replies from people you follow or yourself.', trailing: Switch.adaptive( value: hideRepliesByUnfollowed, - onChanged: state.isBusy ? null : cubit.setHideRepliesByUnfollowed, + onChanged: state.isBusy + ? null + : (value) => context.read().setHideRepliesByUnfollowed(value), ), ), const Divider(height: 1), _ReplyLikeThresholdTile( value: likeThreshold, enabled: !state.isBusy, - onChanged: cubit.setHideRepliesByLikeCount, + onChanged: (value) => context.read().setHideRepliesByLikeCount(value), ), const Divider(height: 1), SettingsTile( icon: Icons.repeat_outlined, title: 'Hide reposts', subtitle: 'Hide posts shown because someone reposted them.', - trailing: Switch.adaptive(value: hideReposts, onChanged: state.isBusy ? null : cubit.setHideReposts), + trailing: Switch.adaptive( + value: hideReposts, + onChanged: state.isBusy + ? null + : (value) => context.read().setHideReposts(value), + ), ), const Divider(height: 1), SettingsTile( @@ -78,7 +89,9 @@ class AccountFeedDisplayPreferences extends StatelessWidget { subtitle: 'Hide posts that quote another post.', trailing: Switch.adaptive( value: hideQuotePosts, - onChanged: state.isBusy ? null : cubit.setHideQuotePosts, + onChanged: state.isBusy + ? null + : (value) => context.read().setHideQuotePosts(value), ), ), ],