mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: remove dead settings

+87 -373
+5 -4
lib/core/database/app_database.dart
··· 25 25 static const activeAccountDidSettingKey = 'active_account_did'; 26 26 27 27 @override 28 - int get schemaVersion => 12; 28 + int get schemaVersion => 13; 29 29 30 30 @override 31 31 MigrationStrategy get migration => MigrationStrategy( ··· 62 62 await migrator.createTable(labelerCache); 63 63 } 64 64 if (from < 10) { 65 - await customStatement( 66 - "INSERT OR IGNORE INTO settings (key, value) VALUES ('ui_density', 'standard'), ('feed_architecture', 'grid')", 67 - ); 65 + await customStatement("INSERT OR IGNORE INTO settings (key, value) VALUES ('feed_architecture', 'grid')"); 68 66 } 69 67 if (from < 11) { 70 68 /* ··· 74 72 } 75 73 if (from < 12) { 76 74 await migrator.createTable(cachedFeedPages); 75 + } 76 + if (from < 13) { 77 + await customStatement("DELETE FROM settings WHERE key = 'ui_density'"); 77 78 } 78 79 }, 79 80 );
-37
lib/core/theme/density_spacing.dart
··· 1 - import 'package:flutter/material.dart'; 2 - import 'package:lazurite/core/theme/ui_density.dart'; 3 - 4 - /// ThemeExtension providing density-scaled spacing values. 5 - /// 6 - /// Obtain via `Theme.of(context).extension<DensitySpacing>()`. 7 - class DensitySpacing extends ThemeExtension<DensitySpacing> { 8 - const DensitySpacing({required this.scale}); 9 - 10 - factory DensitySpacing.fromDensity(UiDensity density) => DensitySpacing(scale: density.scaleFactor); 11 - 12 - /// The multiplier applied to all spacing values (0.75 / 1.0 / 1.25). 13 - final double scale; 14 - 15 - double get xs => 4.0 * scale; 16 - double get sm => 8.0 * scale; 17 - double get md => 16.0 * scale; 18 - double get lg => 24.0 * scale; 19 - double get xl => 32.0 * scale; 20 - double get xxl => 48.0 * scale; 21 - 22 - @override 23 - DensitySpacing copyWith({double? scale}) => DensitySpacing(scale: scale ?? this.scale); 24 - 25 - @override 26 - DensitySpacing lerp(ThemeExtension<DensitySpacing>? other, double t) { 27 - if (other is! DensitySpacing) return this; 28 - return DensitySpacing(scale: scale + (other.scale - scale) * t); 29 - } 30 - 31 - @override 32 - bool operator ==(Object other) => 33 - identical(this, other) || other is DensitySpacing && runtimeType == other.runtimeType && scale == other.scale; 34 - 35 - @override 36 - int get hashCode => scale.hashCode; 37 - }
-27
lib/core/theme/ui_density.dart
··· 1 - enum UiDensity { 2 - compact, 3 - standard, 4 - relaxed; 5 - 6 - double get scaleFactor { 7 - switch (this) { 8 - case UiDensity.compact: 9 - return 0.75; 10 - case UiDensity.standard: 11 - return 1.0; 12 - case UiDensity.relaxed: 13 - return 1.25; 14 - } 15 - } 16 - 17 - static UiDensity fromString(String? value) { 18 - switch (value) { 19 - case 'compact': 20 - return UiDensity.compact; 21 - case 'relaxed': 22 - return UiDensity.relaxed; 23 - default: 24 - return UiDensity.standard; 25 - } 26 - } 27 - }
+1 -1
lib/features/auth/data/auth_repository.dart
··· 564 564 } 565 565 566 566 String _buildCallbackPageHtml(Uri reopenUri) { 567 - final escapedReopenUrl = HtmlEscape(HtmlEscapeMode.element).convert(reopenUri.toString()); 567 + final escapedReopenUrl = const HtmlEscape(HtmlEscapeMode.element).convert(reopenUri.toString()); 568 568 return ''' 569 569 <!DOCTYPE html> 570 570 <html lang="en">
-11
lib/features/settings/bloc/settings_cubit.dart
··· 2 2 import 'package:lazurite/core/database/app_database.dart'; 3 3 import 'package:lazurite/core/theme/app_theme.dart'; 4 4 import 'package:lazurite/core/theme/feed_architecture.dart'; 5 - import 'package:lazurite/core/theme/ui_density.dart'; 6 5 import 'package:lazurite/features/settings/bloc/settings_state.dart'; 7 6 8 7 class SettingsCubit extends Cubit<SettingsState> { ··· 11 10 AppThemePalette? initialPalette, 12 11 AppThemeVariant? initialVariant, 13 12 bool? initialUseSystemTheme, 14 - UiDensity? initialUiDensity, 15 13 FeedArchitecture? initialFeedArchitecture, 16 14 bool? initialSimulateOffline, 17 15 int? initialThreadAutoCollapseDepth, ··· 20 18 themePalette: initialPalette ?? AppThemePalette.oxocarbon, 21 19 themeVariant: initialVariant ?? AppThemeVariant.dark, 22 20 useSystemTheme: initialUseSystemTheme ?? false, 23 - uiDensity: initialUiDensity ?? UiDensity.standard, 24 21 feedArchitecture: initialFeedArchitecture ?? FeedArchitecture.grid, 25 22 simulateOffline: initialSimulateOffline ?? false, 26 23 threadAutoCollapseDepth: initialThreadAutoCollapseDepth, ··· 32 29 static const String _keyThemePalette = 'theme_palette'; 33 30 static const String _keyThemeVariant = 'theme_variant'; 34 31 static const String _keyUseSystemTheme = 'use_system_theme'; 35 - static const String _keyUiDensity = 'ui_density'; 36 32 static const String _keyFeedArchitecture = 'feed_architecture'; 37 33 static const String _keySimulateOffline = 'simulate_offline'; 38 34 static const String _keyThreadAutoCollapseDepth = 'thread_auto_collapse_depth'; ··· 41 37 final paletteStr = await database.getSetting(_keyThemePalette); 42 38 final variantStr = await database.getSetting(_keyThemeVariant); 43 39 final useSystemStr = await database.getSetting(_keyUseSystemTheme); 44 - final uiDensityStr = await database.getSetting(_keyUiDensity); 45 40 final feedArchStr = await database.getSetting(_keyFeedArchitecture); 46 41 final simulateOfflineStr = await database.getSetting(_keySimulateOffline); 47 42 final threadAutoCollapseDepthStr = await database.getSetting(_keyThreadAutoCollapseDepth); ··· 51 46 themePalette: AppTheme.parsePalette(paletteStr), 52 47 themeVariant: AppTheme.parseVariant(variantStr), 53 48 useSystemTheme: useSystemStr == 'true', 54 - uiDensity: UiDensity.fromString(uiDensityStr), 55 49 feedArchitecture: FeedArchitecture.fromString(feedArchStr), 56 50 simulateOffline: simulateOfflineStr == 'true', 57 51 threadAutoCollapseDepth: int.tryParse(threadAutoCollapseDepthStr ?? ''), ··· 78 72 Future<void> setUseSystemTheme(bool value) async { 79 73 await database.setSetting(_keyUseSystemTheme, value.toString()); 80 74 emit(state.copyWith(useSystemTheme: value)); 81 - } 82 - 83 - Future<void> setUiDensity(UiDensity density) async { 84 - await database.setSetting(_keyUiDensity, density.name); 85 - emit(state.copyWith(uiDensity: density)); 86 75 } 87 76 88 77 Future<void> setFeedArchitecture(FeedArchitecture architecture) async {
-13
lib/features/settings/bloc/settings_state.dart
··· 1 1 import 'package:equatable/equatable.dart'; 2 - import 'package:flutter/material.dart'; 3 2 import 'package:lazurite/core/theme/app_theme.dart'; 4 - import 'package:lazurite/core/theme/density_spacing.dart'; 5 3 import 'package:lazurite/core/theme/feed_architecture.dart'; 6 - import 'package:lazurite/core/theme/ui_density.dart'; 7 4 8 5 const Object _threadAutoCollapseDepthUnset = Object(); 9 6 ··· 12 9 required this.themePalette, 13 10 required this.themeVariant, 14 11 required this.useSystemTheme, 15 - this.uiDensity = UiDensity.standard, 16 12 this.feedArchitecture = FeedArchitecture.grid, 17 13 this.simulateOffline = false, 18 14 this.threadAutoCollapseDepth, ··· 21 17 final AppThemePalette themePalette; 22 18 final AppThemeVariant themeVariant; 23 19 final bool useSystemTheme; 24 - final UiDensity uiDensity; 25 20 final FeedArchitecture feedArchitecture; 26 21 final bool simulateOffline; 27 22 final int? threadAutoCollapseDepth; 28 23 29 - ThemeData get themeData { 30 - final base = AppTheme.getTheme(themePalette, themeVariant); 31 - return base.copyWith(extensions: [DensitySpacing.fromDensity(uiDensity)]); 32 - } 33 - 34 24 SettingsState copyWith({ 35 25 AppThemePalette? themePalette, 36 26 AppThemeVariant? themeVariant, 37 27 bool? useSystemTheme, 38 - UiDensity? uiDensity, 39 28 FeedArchitecture? feedArchitecture, 40 29 bool? simulateOffline, 41 30 Object? threadAutoCollapseDepth = _threadAutoCollapseDepthUnset, ··· 44 33 themePalette: themePalette ?? this.themePalette, 45 34 themeVariant: themeVariant ?? this.themeVariant, 46 35 useSystemTheme: useSystemTheme ?? this.useSystemTheme, 47 - uiDensity: uiDensity ?? this.uiDensity, 48 36 feedArchitecture: feedArchitecture ?? this.feedArchitecture, 49 37 simulateOffline: simulateOffline ?? this.simulateOffline, 50 38 threadAutoCollapseDepth: identical(threadAutoCollapseDepth, _threadAutoCollapseDepthUnset) ··· 58 46 themePalette, 59 47 themeVariant, 60 48 useSystemTheme, 61 - uiDensity, 62 49 feedArchitecture, 63 50 simulateOffline, 64 51 threadAutoCollapseDepth,
-32
lib/features/settings/presentation/settings_screen.dart
··· 5 5 import 'package:lazurite/core/router/app_shell.dart'; 6 6 import 'package:lazurite/core/theme/app_theme.dart'; 7 7 import 'package:lazurite/core/theme/feed_architecture.dart'; 8 - import 'package:lazurite/core/theme/ui_density.dart'; 9 8 import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; 10 9 import 'package:lazurite/features/account/presentation/account_switcher_sheet.dart'; 11 10 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; ··· 84 83 subtitle: 'View your saved posts', 85 84 onTap: () => context.push('/saved'), 86 85 ), 87 - _SettingsTile(icon: Icons.person_outline, title: 'Edit Profile', subtitle: 'Name, bio, avatar', onTap: () {}), 88 - _SettingsTile(icon: Icons.lock_outline, title: 'Privacy', subtitle: 'Visibility settings', onTap: () {}), 89 - const SizedBox(height: 24), 90 - _buildSectionHeader(context, 'Notifications'), 91 - _SettingsTile( 92 - icon: Icons.notifications_outlined, 93 - title: 'Push Notifications', 94 - trailing: Switch(value: true, onChanged: (_) {}), 95 - ), 96 - _SettingsTile( 97 - icon: Icons.email_outlined, 98 - title: 'Email Notifications', 99 - trailing: Switch(value: false, onChanged: (_) {}), 100 - ), 101 86 const SizedBox(height: 24), 102 87 if (!kReleaseMode) ...[ 103 88 _buildSectionHeader(context, 'Developer'), ··· 117 102 subtitle: 'View app log files', 118 103 onTap: () => context.push('/settings/logs'), 119 104 ), 120 - _SettingsTile(icon: Icons.help_outline, title: 'Help & Support', onTap: () {}), 121 105 _SettingsTile( 122 106 icon: Icons.info_outline, 123 107 title: 'About', ··· 241 225 ), 242 226 child: Column( 243 227 children: [ 244 - _SettingsDropdownTile<UiDensity>( 245 - title: 'UI Density', 246 - value: state.uiDensity, 247 - options: UiDensity.values, 248 - labelBuilder: (density) => switch (density) { 249 - UiDensity.compact => 'Compact', 250 - UiDensity.standard => 'Standard', 251 - UiDensity.relaxed => 'Relaxed', 252 - }, 253 - onChanged: (value) { 254 - if (value != null) { 255 - settingsCubit.setUiDensity(value); 256 - } 257 - }, 258 - ), 259 - const Divider(height: 1), 260 228 _SettingsDropdownTile<FeedArchitecture>( 261 229 title: 'Feed Architecture', 262 230 value: state.feedArchitecture,
-122
test/core/theme/density_spacing_test.dart
··· 1 - import 'package:flutter_test/flutter_test.dart'; 2 - import 'package:lazurite/core/theme/density_spacing.dart'; 3 - import 'package:lazurite/core/theme/ui_density.dart'; 4 - 5 - void main() { 6 - group('DensitySpacing', () { 7 - group('fromDensity', () { 8 - test('compact uses 0.75 scale', () { 9 - final spacing = DensitySpacing.fromDensity(UiDensity.compact); 10 - expect(spacing.scale, 0.75); 11 - }); 12 - 13 - test('standard uses 1.0 scale', () { 14 - final spacing = DensitySpacing.fromDensity(UiDensity.standard); 15 - expect(spacing.scale, 1.0); 16 - }); 17 - 18 - test('relaxed uses 1.25 scale', () { 19 - final spacing = DensitySpacing.fromDensity(UiDensity.relaxed); 20 - expect(spacing.scale, 1.25); 21 - }); 22 - }); 23 - 24 - group('spacing values at standard (1.0) scale', () { 25 - late DensitySpacing spacing; 26 - 27 - setUp(() { 28 - spacing = DensitySpacing.fromDensity(UiDensity.standard); 29 - }); 30 - 31 - test('xs is 4.0', () => expect(spacing.xs, 4.0)); 32 - test('sm is 8.0', () => expect(spacing.sm, 8.0)); 33 - test('md is 16.0', () => expect(spacing.md, 16.0)); 34 - test('lg is 24.0', () => expect(spacing.lg, 24.0)); 35 - test('xl is 32.0', () => expect(spacing.xl, 32.0)); 36 - test('xxl is 48.0', () => expect(spacing.xxl, 48.0)); 37 - }); 38 - 39 - group('spacing values scale correctly', () { 40 - test('compact halves relative to relaxed', () { 41 - final compact = DensitySpacing.fromDensity(UiDensity.compact); 42 - final relaxed = DensitySpacing.fromDensity(UiDensity.relaxed); 43 - expect(compact.md, lessThan(relaxed.md)); 44 - }); 45 - 46 - test('all spacing values are proportional to scale', () { 47 - const scale = 2.0; 48 - const spacing = DensitySpacing(scale: scale); 49 - expect(spacing.xs, 4.0 * scale); 50 - expect(spacing.sm, 8.0 * scale); 51 - expect(spacing.md, 16.0 * scale); 52 - expect(spacing.lg, 24.0 * scale); 53 - expect(spacing.xl, 32.0 * scale); 54 - expect(spacing.xxl, 48.0 * scale); 55 - }); 56 - }); 57 - 58 - group('copyWith', () { 59 - test('returns new instance with updated scale', () { 60 - const original = DensitySpacing(scale: 1.0); 61 - final copy = original.copyWith(scale: 0.5); 62 - expect(copy.scale, 0.5); 63 - expect(original.scale, 1.0); 64 - }); 65 - 66 - test('preserves scale when not provided', () { 67 - const original = DensitySpacing(scale: 1.25); 68 - final copy = original.copyWith(); 69 - expect(copy.scale, 1.25); 70 - }); 71 - }); 72 - 73 - group('lerp', () { 74 - test('lerps scale between two instances', () { 75 - const a = DensitySpacing(scale: 0.75); 76 - const b = DensitySpacing(scale: 1.25); 77 - final mid = a.lerp(b, 0.5); 78 - expect(mid.scale, closeTo(1.0, 0.001)); 79 - }); 80 - 81 - test('lerp at t=0 returns self values', () { 82 - const a = DensitySpacing(scale: 0.75); 83 - const b = DensitySpacing(scale: 1.25); 84 - final result = a.lerp(b, 0.0); 85 - expect(result.scale, 0.75); 86 - }); 87 - 88 - test('lerp at t=1 returns other values', () { 89 - const a = DensitySpacing(scale: 0.75); 90 - const b = DensitySpacing(scale: 1.25); 91 - final result = a.lerp(b, 1.0); 92 - expect(result.scale, 1.25); 93 - }); 94 - 95 - test('lerp with null returns self', () { 96 - const a = DensitySpacing(scale: 1.0); 97 - final result = a.lerp(null, 0.5); 98 - expect(result, a); 99 - }); 100 - }); 101 - 102 - group('equality', () { 103 - test('equal when scale is the same', () { 104 - const a = DensitySpacing(scale: 1.0); 105 - const b = DensitySpacing(scale: 1.0); 106 - expect(a, equals(b)); 107 - }); 108 - 109 - test('not equal when scale differs', () { 110 - const a = DensitySpacing(scale: 1.0); 111 - const b = DensitySpacing(scale: 1.25); 112 - expect(a, isNot(equals(b))); 113 - }); 114 - 115 - test('hashCode is equal for same scale', () { 116 - const a = DensitySpacing(scale: 1.0); 117 - const b = DensitySpacing(scale: 1.0); 118 - expect(a.hashCode, b.hashCode); 119 - }); 120 - }); 121 - }); 122 - }
-56
test/core/theme/ui_density_test.dart
··· 1 - import 'package:flutter_test/flutter_test.dart'; 2 - import 'package:lazurite/core/theme/ui_density.dart'; 3 - 4 - void main() { 5 - group('UiDensity', () { 6 - group('scaleFactor', () { 7 - test('compact has scale 0.75', () { 8 - expect(UiDensity.compact.scaleFactor, 0.75); 9 - }); 10 - 11 - test('standard has scale 1.0', () { 12 - expect(UiDensity.standard.scaleFactor, 1.0); 13 - }); 14 - 15 - test('relaxed has scale 1.25', () { 16 - expect(UiDensity.relaxed.scaleFactor, 1.25); 17 - }); 18 - 19 - test('compact scale is less than standard', () { 20 - expect(UiDensity.compact.scaleFactor, lessThan(UiDensity.standard.scaleFactor)); 21 - }); 22 - 23 - test('relaxed scale is greater than standard', () { 24 - expect(UiDensity.relaxed.scaleFactor, greaterThan(UiDensity.standard.scaleFactor)); 25 - }); 26 - }); 27 - 28 - group('fromString', () { 29 - test('parses compact', () { 30 - expect(UiDensity.fromString('compact'), UiDensity.compact); 31 - }); 32 - 33 - test('parses standard', () { 34 - expect(UiDensity.fromString('standard'), UiDensity.standard); 35 - }); 36 - 37 - test('parses relaxed', () { 38 - expect(UiDensity.fromString('relaxed'), UiDensity.relaxed); 39 - }); 40 - 41 - test('null returns standard', () { 42 - expect(UiDensity.fromString(null), UiDensity.standard); 43 - }); 44 - 45 - test('unknown value returns standard', () { 46 - expect(UiDensity.fromString('unknown'), UiDensity.standard); 47 - }); 48 - 49 - test('round-trips all values via name', () { 50 - for (final density in UiDensity.values) { 51 - expect(UiDensity.fromString(density.name), density, reason: 'density: $density'); 52 - } 53 - }); 54 - }); 55 - }); 56 - }
-2
test/features/feed/presentation/home_feed_screen_test.dart
··· 7 7 import 'package:flutter_test/flutter_test.dart'; 8 8 import 'package:lazurite/core/theme/app_theme.dart'; 9 9 import 'package:lazurite/core/theme/feed_architecture.dart'; 10 - import 'package:lazurite/core/theme/ui_density.dart'; 11 10 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 12 11 import 'package:lazurite/features/auth/data/models/auth_models.dart'; 13 12 import 'package:lazurite/features/connectivity/cubit/connectivity_cubit.dart'; ··· 33 32 themePalette: AppThemePalette.oxocarbon, 34 33 themeVariant: AppThemeVariant.dark, 35 34 useSystemTheme: false, 36 - uiDensity: UiDensity.standard, 37 35 feedArchitecture: architecture, 38 36 ); 39 37
-3
test/features/profile/presentation/profile_screen_test.dart
··· 11 11 import 'package:go_router/go_router.dart'; 12 12 import 'package:lazurite/core/theme/app_theme.dart'; 13 13 import 'package:lazurite/core/theme/feed_architecture.dart'; 14 - import 'package:lazurite/core/theme/ui_density.dart'; 15 14 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 16 15 import 'package:lazurite/features/auth/data/models/auth_models.dart'; 17 16 import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; ··· 79 78 themePalette: AppThemePalette.oxocarbon, 80 79 themeVariant: AppThemeVariant.dark, 81 80 useSystemTheme: false, 82 - uiDensity: UiDensity.standard, 83 81 feedArchitecture: FeedArchitecture.grid, 84 82 ); 85 83 ··· 87 85 themePalette: AppThemePalette.oxocarbon, 88 86 themeVariant: AppThemeVariant.dark, 89 87 useSystemTheme: false, 90 - uiDensity: UiDensity.standard, 91 88 feedArchitecture: architecture, 92 89 ); 93 90
+1 -32
test/features/settings/bloc/settings_cubit_test.dart
··· 4 4 import 'package:lazurite/core/database/app_database.dart'; 5 5 import 'package:lazurite/core/theme/app_theme.dart'; 6 6 import 'package:lazurite/core/theme/feed_architecture.dart'; 7 - import 'package:lazurite/core/theme/ui_density.dart'; 8 7 import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; 9 8 import 'package:lazurite/features/settings/bloc/settings_state.dart'; 10 9 ··· 25 24 expect(cubit.state.themePalette, AppThemePalette.oxocarbon); 26 25 expect(cubit.state.themeVariant, AppThemeVariant.dark); 27 26 expect(cubit.state.useSystemTheme, false); 28 - expect(cubit.state.uiDensity, UiDensity.standard); 29 27 expect(cubit.state.feedArchitecture, FeedArchitecture.grid); 30 28 expect(cubit.state.simulateOffline, false); 31 29 expect(cubit.state.threadAutoCollapseDepth, isNull); ··· 37 35 initialPalette: AppThemePalette.catppuccin, 38 36 initialVariant: AppThemeVariant.light, 39 37 initialUseSystemTheme: true, 40 - initialUiDensity: UiDensity.compact, 41 38 initialFeedArchitecture: FeedArchitecture.linear, 42 39 initialSimulateOffline: true, 43 40 initialThreadAutoCollapseDepth: 3, ··· 45 42 expect(cubit.state.themePalette, AppThemePalette.catppuccin); 46 43 expect(cubit.state.themeVariant, AppThemeVariant.light); 47 44 expect(cubit.state.useSystemTheme, true); 48 - expect(cubit.state.uiDensity, UiDensity.compact); 49 45 expect(cubit.state.feedArchitecture, FeedArchitecture.linear); 50 46 expect(cubit.state.simulateOffline, true); 51 47 expect(cubit.state.threadAutoCollapseDepth, 3); ··· 58 54 await database.setSetting('theme_palette', 'nord'); 59 55 await database.setSetting('theme_variant', 'light'); 60 56 await database.setSetting('use_system_theme', 'true'); 61 - await database.setSetting('ui_density', 'compact'); 62 57 await database.setSetting('feed_architecture', 'linear'); 63 58 await database.setSetting('simulate_offline', 'true'); 64 59 await database.setSetting('thread_auto_collapse_depth', '4'); ··· 69 64 .having((s) => s.themePalette, 'themePalette', AppThemePalette.nord) 70 65 .having((s) => s.themeVariant, 'themeVariant', AppThemeVariant.light) 71 66 .having((s) => s.useSystemTheme, 'useSystemTheme', true) 72 - .having((s) => s.uiDensity, 'uiDensity', UiDensity.compact) 73 67 .having((s) => s.feedArchitecture, 'feedArchitecture', FeedArchitecture.linear) 74 68 .having((s) => s.simulateOffline, 'simulateOffline', true) 75 69 .having((s) => s.threadAutoCollapseDepth, 'threadAutoCollapseDepth', 4), ··· 85 79 .having((s) => s.themePalette, 'themePalette', AppThemePalette.oxocarbon) 86 80 .having((s) => s.themeVariant, 'themeVariant', AppThemeVariant.dark) 87 81 .having((s) => s.useSystemTheme, 'useSystemTheme', false) 88 - .having((s) => s.uiDensity, 'uiDensity', UiDensity.standard) 89 82 .having((s) => s.feedArchitecture, 'feedArchitecture', FeedArchitecture.grid) 90 83 .having((s) => s.simulateOffline, 'simulateOffline', false) 91 84 .having((s) => s.threadAutoCollapseDepth, 'threadAutoCollapseDepth', isNull), ··· 141 134 ); 142 135 143 136 blocTest<SettingsCubit, SettingsState>( 144 - 'setUiDensity updates state and persists to database', 145 - build: () => SettingsCubit(database: database), 146 - act: (cubit) => cubit.setUiDensity(UiDensity.compact), 147 - expect: () => [isA<SettingsState>().having((s) => s.uiDensity, 'uiDensity', UiDensity.compact)], 148 - verify: (cubit) async { 149 - final value = await database.getSetting('ui_density'); 150 - expect(value, 'compact'); 151 - }, 152 - ); 153 - 154 - blocTest<SettingsCubit, SettingsState>( 155 - 'setUiDensity relaxed updates state and persists to database', 156 - build: () => SettingsCubit(database: database), 157 - act: (cubit) => cubit.setUiDensity(UiDensity.relaxed), 158 - expect: () => [isA<SettingsState>().having((s) => s.uiDensity, 'uiDensity', UiDensity.relaxed)], 159 - verify: (cubit) async { 160 - final value = await database.getSetting('ui_density'); 161 - expect(value, 'relaxed'); 162 - }, 163 - ); 164 - 165 - blocTest<SettingsCubit, SettingsState>( 166 137 'setFeedArchitecture updates state and persists to database', 167 138 build: () => SettingsCubit(database: database), 168 139 act: (cubit) => cubit.setFeedArchitecture(FeedArchitecture.linear), ··· 223 194 ); 224 195 225 196 blocTest<SettingsCubit, SettingsState>( 226 - 'loadSettings round-trips ui_density, feed_architecture, and thread auto-collapse depth', 197 + 'loadSettings round-trips feed_architecture and thread auto-collapse depth', 227 198 build: () => SettingsCubit(database: database), 228 199 setUp: () async { 229 - await database.setSetting('ui_density', 'relaxed'); 230 200 await database.setSetting('feed_architecture', 'linear'); 231 201 await database.setSetting('thread_auto_collapse_depth', '6'); 232 202 }, 233 203 act: (cubit) => cubit.loadSettings(), 234 204 expect: () => [ 235 205 isA<SettingsState>() 236 - .having((s) => s.uiDensity, 'uiDensity', UiDensity.relaxed) 237 206 .having((s) => s.feedArchitecture, 'feedArchitecture', FeedArchitecture.linear) 238 207 .having((s) => s.threadAutoCollapseDepth, 'threadAutoCollapseDepth', 6), 239 208 ],
-33
test/features/settings/bloc/settings_state_test.dart
··· 1 1 import 'package:flutter_test/flutter_test.dart'; 2 2 import 'package:lazurite/core/theme/app_theme.dart'; 3 3 import 'package:lazurite/core/theme/feed_architecture.dart'; 4 - import 'package:lazurite/core/theme/ui_density.dart'; 5 4 import 'package:lazurite/features/settings/bloc/settings_state.dart'; 6 5 7 6 void main() { ··· 66 65 expect(state1, isNot(equals(state2))); 67 66 }); 68 67 69 - test('inequality when uiDensity differs', () { 70 - const state1 = SettingsState( 71 - themePalette: AppThemePalette.oxocarbon, 72 - themeVariant: AppThemeVariant.dark, 73 - useSystemTheme: false, 74 - uiDensity: UiDensity.standard, 75 - ); 76 - const state2 = SettingsState( 77 - themePalette: AppThemePalette.oxocarbon, 78 - themeVariant: AppThemeVariant.dark, 79 - useSystemTheme: false, 80 - uiDensity: UiDensity.compact, 81 - ); 82 - 83 - expect(state1, isNot(equals(state2))); 84 - }); 85 - 86 68 test('inequality when feedArchitecture differs', () { 87 69 const state1 = SettingsState( 88 70 themePalette: AppThemePalette.oxocarbon, ··· 145 127 themePalette: AppThemePalette.nord, 146 128 themeVariant: AppThemeVariant.light, 147 129 useSystemTheme: true, 148 - uiDensity: UiDensity.compact, 149 130 feedArchitecture: FeedArchitecture.linear, 150 131 simulateOffline: true, 151 132 threadAutoCollapseDepth: 3, ··· 154 135 expect(updated.themePalette, AppThemePalette.nord); 155 136 expect(updated.themeVariant, AppThemeVariant.light); 156 137 expect(updated.useSystemTheme, true); 157 - expect(updated.uiDensity, UiDensity.compact); 158 138 expect(updated.feedArchitecture, FeedArchitecture.linear); 159 139 expect(updated.simulateOffline, true); 160 140 expect(updated.threadAutoCollapseDepth, 3); ··· 166 146 themePalette: AppThemePalette.catppuccin, 167 147 themeVariant: AppThemeVariant.light, 168 148 useSystemTheme: true, 169 - uiDensity: UiDensity.relaxed, 170 149 feedArchitecture: FeedArchitecture.linear, 171 150 simulateOffline: true, 172 151 threadAutoCollapseDepth: 4, ··· 177 156 expect(updated.themePalette, AppThemePalette.catppuccin); 178 157 expect(updated.themeVariant, AppThemeVariant.light); 179 158 expect(updated.useSystemTheme, true); 180 - expect(updated.uiDensity, UiDensity.relaxed); 181 159 expect(updated.feedArchitecture, FeedArchitecture.linear); 182 160 expect(updated.simulateOffline, true); 183 161 expect(updated.threadAutoCollapseDepth, 4); ··· 201 179 themePalette: AppThemePalette.rosePine, 202 180 themeVariant: AppThemeVariant.light, 203 181 useSystemTheme: true, 204 - uiDensity: UiDensity.compact, 205 182 feedArchitecture: FeedArchitecture.linear, 206 183 simulateOffline: true, 207 184 threadAutoCollapseDepth: 6, ··· 210 187 expect(state.props, contains(AppThemePalette.rosePine)); 211 188 expect(state.props, contains(AppThemeVariant.light)); 212 189 expect(state.props, contains(true)); 213 - expect(state.props, contains(UiDensity.compact)); 214 190 expect(state.props, contains(FeedArchitecture.linear)); 215 191 expect(state.props, contains(true)); 216 192 expect(state.props, contains(6)); 217 - }); 218 - 219 - test('defaults uiDensity to standard', () { 220 - const state = SettingsState( 221 - themePalette: AppThemePalette.oxocarbon, 222 - themeVariant: AppThemeVariant.dark, 223 - useSystemTheme: false, 224 - ); 225 - expect(state.uiDensity, UiDensity.standard); 226 193 }); 227 194 228 195 test('defaults feedArchitecture to grid', () {
+80
test/features/settings/presentation/settings_screen_test.dart
··· 1 + import 'package:bloc_test/bloc_test.dart'; 2 + import 'package:flutter/material.dart'; 3 + import 'package:flutter_bloc/flutter_bloc.dart'; 4 + import 'package:flutter_test/flutter_test.dart'; 5 + import 'package:lazurite/core/theme/app_theme.dart'; 6 + import 'package:lazurite/core/theme/feed_architecture.dart'; 7 + import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 8 + import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; 9 + import 'package:lazurite/features/settings/bloc/settings_state.dart'; 10 + import 'package:lazurite/features/settings/presentation/settings_screen.dart'; 11 + import 'package:mocktail/mocktail.dart'; 12 + 13 + class MockAuthBloc extends MockBloc<AuthEvent, AuthState> implements AuthBloc {} 14 + 15 + class MockSettingsCubit extends MockCubit<SettingsState> implements SettingsCubit {} 16 + 17 + void main() { 18 + late MockAuthBloc authBloc; 19 + late MockSettingsCubit settingsCubit; 20 + 21 + setUp(() { 22 + authBloc = MockAuthBloc(); 23 + settingsCubit = MockSettingsCubit(); 24 + 25 + when(() => authBloc.state).thenReturn(const AuthState.unauthenticated()); 26 + whenListen(authBloc, const Stream<AuthState>.empty(), initialState: const AuthState.unauthenticated()); 27 + 28 + when(() => settingsCubit.state).thenReturn( 29 + const SettingsState( 30 + themePalette: AppThemePalette.oxocarbon, 31 + themeVariant: AppThemeVariant.dark, 32 + useSystemTheme: false, 33 + feedArchitecture: FeedArchitecture.grid, 34 + ), 35 + ); 36 + whenListen( 37 + settingsCubit, 38 + const Stream<SettingsState>.empty(), 39 + initialState: const SettingsState( 40 + themePalette: AppThemePalette.oxocarbon, 41 + themeVariant: AppThemeVariant.dark, 42 + useSystemTheme: false, 43 + feedArchitecture: FeedArchitecture.grid, 44 + ), 45 + ); 46 + }); 47 + 48 + Widget buildSubject() { 49 + return MultiBlocProvider( 50 + providers: [ 51 + BlocProvider<AuthBloc>.value(value: authBloc), 52 + BlocProvider<SettingsCubit>.value(value: settingsCubit), 53 + ], 54 + child: const MaterialApp(home: SettingsScreen()), 55 + ); 56 + } 57 + 58 + testWidgets('shows active settings controls that are wired up', (tester) async { 59 + await tester.pumpWidget(buildSubject()); 60 + await tester.pumpAndSettle(); 61 + 62 + expect(find.text('APPEARANCE'), findsOneWidget); 63 + expect(find.text('System'), findsOneWidget); 64 + expect(find.text('LAYOUT'), findsOneWidget); 65 + expect(find.text('Feed Architecture'), findsOneWidget); 66 + expect(find.text('Thread Auto-Collapse'), findsOneWidget); 67 + }); 68 + 69 + testWidgets('does not render removed placeholder settings', (tester) async { 70 + await tester.pumpWidget(buildSubject()); 71 + await tester.pumpAndSettle(); 72 + 73 + expect(find.text('UI Density'), findsNothing); 74 + expect(find.text('Edit Profile'), findsNothing); 75 + expect(find.text('Privacy'), findsNothing); 76 + expect(find.text('Push Notifications'), findsNothing); 77 + expect(find.text('Email Notifications'), findsNothing); 78 + expect(find.text('Help & Support'), findsNothing); 79 + }); 80 + }