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.

feat: change currently logged in user to use /profile/me route

+100 -60
+4
CHANGELOG.md
··· 78 78 79 79 - Configurable autocomplete/typeahead for login & profile/actor search 80 80 - Starter Pack search (not implemented upstream) screen 81 + 82 + #### 2026-04-30 83 + 84 + - Added shades of purple/blacksky inspired theme
+17 -26
lib/core/router/app_router.dart
··· 473 473 navigatorKey: _profileNavigatorKey, 474 474 routes: [ 475 475 GoRoute( 476 - path: '/profile', 477 - pageBuilder: (context, state) => _page(context, state, const ProfileScreen()), 478 - routes: [ 479 - GoRoute( 480 - path: ':actor', 481 - pageBuilder: (context, state) => _page( 482 - context, 483 - state, 484 - ProfileScreen( 485 - actor: Uri.decodeComponent(state.pathParameters['actor'] ?? ''), 486 - showBackButton: true, 487 - ), 488 - ), 489 - ), 490 - GoRoute( 491 - path: 'view', 492 - redirect: (_, state) { 493 - final rawActor = state.uri.queryParameters['actor']?.trim() ?? ''; 494 - if (rawActor.isEmpty) { 495 - return '/profile'; 496 - } 497 - final normalizedActor = rawActor.startsWith('@') ? rawActor.substring(1) : rawActor; 498 - return '/profile/${Uri.encodeComponent(normalizedActor)}'; 499 - }, 500 - ), 501 - ], 476 + path: '/profile/me', 477 + pageBuilder: (context, state) => _page(context, state, const ProfileScreen(actor: 'me')), 478 + ), 479 + GoRoute( 480 + path: '/profile/:actor', 481 + redirect: (_, state) { 482 + final actor = (state.pathParameters['actor'] ?? '').trim().toLowerCase(); 483 + if (actor == 'me') { 484 + return '/profile/me'; 485 + } 486 + return null; 487 + }, 488 + pageBuilder: (context, state) => _page( 489 + context, 490 + state, 491 + ProfileScreen(actor: Uri.decodeComponent(state.pathParameters['actor'] ?? ''), showBackButton: true), 492 + ), 502 493 ), 503 494 ], 504 495 ),
+2 -2
lib/core/router/app_shell.dart
··· 7 7 import 'package:go_router/go_router.dart'; 8 8 import 'package:lazurite/core/theme/animation_tokens.dart'; 9 9 import 'package:lazurite/core/theme/animation_utils.dart'; 10 + import 'package:lazurite/core/theme/theme_extensions.dart'; 10 11 import 'package:lazurite/features/account/presentation/account_switcher_sheet.dart'; 11 12 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 12 13 import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; ··· 14 15 import 'package:lazurite/features/notifications/cubit/unread_count_cubit.dart'; 15 16 import 'package:lazurite/features/profile/data/profile_repository.dart'; 16 17 import 'package:provider/provider.dart'; 17 - import 'package:lazurite/core/theme/theme_extensions.dart'; 18 18 19 19 class AppShellScope extends InheritedWidget { 20 20 const AppShellScope({super.key, required super.child, required this.openMenu}); ··· 248 248 final isHomeRoute = currentPath == '/'; 249 249 final isSearchRoute = currentPath == '/search'; 250 250 final isFeedsRoute = currentPath == '/feeds'; 251 - final isProfileRoute = currentPath == '/profile'; 251 + final isProfileRoute = currentPath.startsWith('/profile/'); 252 252 final isSettingsRoute = currentPath == '/settings' || currentPath.startsWith('/settings/'); 253 253 final isDevToolsRoute = currentPath == '/settings/devtools'; 254 254 final isCleanFollowsRoute = currentPath == '/settings/clean-follows';
+66 -26
lib/core/theme/purple_theme.dart
··· 3 3 4 4 /// Shades of Purple — dark theme by Alexander Keliris (Rigellute). 5 5 /// https://github.com/Rigellute/shades-of-purple.vim 6 + /// 7 + /// Light mode specific colors are L0-L6 6 8 class PurpleTheme { 7 9 PurpleTheme._(); 8 10 9 - // ── Dark palette ────────────────────────────────────────────────────────── 10 - static const Color sop0 = Color(0xFF1E1E3F); // darkest bg (ColorColumn, WildMenu) 11 - static const Color sop1 = Color(0xFF28284E); // panel bg (LineNr bg, VertSplit bg) 12 - static const Color sop2 = Color(0xFF2D2B55); // main bg (Normal bg) 13 - static const Color sop3 = Color(0xFFA599E9); // muted lavender (LineNr fg, NonText) 14 - static const Color sop4 = Color(0xFFE1EFFF); // main fg (Normal fg) 15 - static const Color sop5 = Color(0xFF9EFFFF); // cyan (Special, Title) 16 - static const Color sop6 = Color(0xFFFAD000); // yellow (Cursor, WarningMsg) 17 - static const Color sop7 = Color(0xFFFF9D00); // orange (Function, Identifier) 18 - static const Color sop8 = Color(0xFFB362FF); // vivid purple (Comment) 19 - static const Color sop9 = Color(0xFFFF628C); // pink-rose (Constant, SpellBad) 20 - static const Color sop10 = Color(0xFFA5FF90); // green (String) 21 - static const Color sop11 = Color(0xFFEC3A37); // red (Error, DiffDelete) 22 - static const Color sop12 = Color(0xFF80FFBB); // teal (Type) 23 - static const Color sop13 = Color(0xFFFB94FF); // light magenta (jsThis, jsFunction) 24 - static const Color sop14 = Color(0xFF6943FF); // blue (terminal blue) 11 + /// darkest bg (ColorColumn, WildMenu) 12 + static const Color sop0 = Color(0xFF1E1E3F); 13 + 14 + /// panel bg (LineNr bg, VertSplit bg) 15 + static const Color sop1 = Color(0xFF28284E); 16 + 17 + /// main bg (Normal bg) 18 + static const Color sop2 = Color(0xFF2D2B55); 19 + 20 + /// muted lavender (LineNr fg, NonText) 21 + static const Color sop3 = Color(0xFFA599E9); 22 + 23 + /// main fg (Normal fg) 24 + static const Color sop4 = Color(0xFFE1EFFF); 25 + 26 + /// cyan (Special, Title) 27 + static const Color sop5 = Color(0xFF9EFFFF); 28 + 29 + /// yellow (Cursor, WarningMsg) 30 + static const Color sop6 = Color(0xFFFAD000); 31 + 32 + /// orange (Function, Identifier) 33 + static const Color sop7 = Color(0xFFFF9D00); 34 + 35 + /// vivid purple (Comment) 36 + static const Color sop8 = Color(0xFFB362FF); 37 + 38 + /// pink-rose (Constant, SpellBad) 39 + static const Color sop9 = Color(0xFFFF628C); 40 + 41 + /// green (String) 42 + static const Color sop10 = Color(0xFFA5FF90); 43 + 44 + /// red (Error, DiffDelete) 45 + static const Color sop11 = Color(0xFFEC3A37); 46 + 47 + /// teal (Type) 48 + static const Color sop12 = Color(0xFF80FFBB); 49 + 50 + /// light magenta (jsThis, jsFunction) 51 + static const Color sop13 = Color(0xFFFB94FF); 52 + 53 + /// blue (terminal blue) 54 + static const Color sop14 = Color(0xFF6943FF); 25 55 26 56 // Semi-transparent overlay for subtle dark-mode borders/dividers 27 57 static const Color darkOutlineVariant = Color(0x26A599E9); // lavender ~15% opacity 28 58 29 - // ── Light palette ───────────────────────────────────────────────────────── 30 - static const Color sopL0 = Color(0xFFF8F6FF); // lightest bg (scaffold) 31 - static const Color sopL1 = Color(0xFFEDE9FF); // panel bg (cards, surfaces) 32 - static const Color sopL2 = Color(0xFFD6CEFF); // border / divider 33 - static const Color sopL3 = Color(0xFF8B7FD4); // muted purple (secondary text) 34 - static const Color sopL4 = Color(0xFF2D2B55); // main fg (body text = sop2) 35 - static const Color sopL5 = Color(0xFF6943FF); // primary accent (sop14) 36 - static const Color sopL6 = Color(0xFF7B6EC0); // secondary accent 59 + /// lightest bg (scaffold) 60 + static const Color sopL0 = Color(0xFFF8F6FF); 61 + 62 + /// panel bg (cards, surfaces) 63 + static const Color sopL1 = Color(0xFFEDE9FF); 64 + 65 + /// border / divider 66 + static const Color sopL2 = Color(0xFFD6CEFF); 37 67 38 - // ── Dark theme ──────────────────────────────────────────────────────────── 68 + /// muted purple (secondary text) 69 + static const Color sopL3 = Color(0xFF8B7FD4); 70 + 71 + /// main fg (body text = sop2) 72 + static const Color sopL4 = Color(0xFF2D2B55); 73 + 74 + /// primary accent (sop14) 75 + static const Color sopL5 = Color(0xFF6943FF); 76 + 77 + /// secondary accent 78 + static const Color sopL6 = Color(0xFF7B6EC0); 79 + 39 80 static ThemeData dark() { 40 81 return ThemeData( 41 82 useMaterial3: true, ··· 109 150 ); 110 151 } 111 152 112 - // ── Light theme ─────────────────────────────────────────────────────────── 113 153 static ThemeData light() { 114 154 return ThemeData( 115 155 useMaterial3: true,
+1 -1
lib/features/auth/presentation/home_screen.dart
··· 13 13 appBar: AppBar( 14 14 title: const Text('Lazurite'), 15 15 actions: [ 16 - IconButton(icon: const Icon(Icons.person_outline), onPressed: () => context.push('/profile')), 16 + IconButton(icon: const Icon(Icons.person_outline), onPressed: () => context.push('/profile/me')), 17 17 IconButton(icon: const Icon(Icons.settings_outlined), onPressed: () => context.push('/settings')), 18 18 IconButton( 19 19 icon: const Icon(Icons.logout),
+1 -1
lib/features/lists/presentation/list_detail_screen.dart
··· 172 172 if (context.canPop()) { 173 173 context.pop(); 174 174 } else { 175 - context.go('/profile'); 175 + context.go('/profile/me'); 176 176 } 177 177 }, 178 178 child: BlocBuilder<ListBloc, ListState>(
+8 -2
lib/features/profile/presentation/profile_screen.dart
··· 83 83 String? _lastScheduledProfileActorLoad; 84 84 String? _lastScheduledFeedLoadKey; 85 85 86 + bool get _isCurrentRoute => ModalRoute.of(context)?.isCurrent ?? true; 87 + 86 88 @override 87 89 void initState() { 88 90 super.initState(); ··· 118 120 String? get _resolvedActor { 119 121 final authState = context.read<AuthBloc>().state; 120 122 if (!authState.isAuthenticated) return null; 123 + final authDid = authState.tokens?.did; 121 124 final rawActor = widget.actor ?? authState.tokens?.did; 122 125 if (rawActor == null) { 123 126 return null; 124 127 } 125 128 126 129 final normalizedActor = _normalizeActor(rawActor); 130 + if (normalizedActor.toLowerCase() == 'me') { 131 + return authDid; 132 + } 127 133 return normalizedActor.isEmpty ? null : normalizedActor; 128 134 } 129 135 ··· 336 342 final profileMatchesExpectedActor = expectedActor == null 337 343 ? true 338 344 : _profileMatchesExpectedActor(profile, expectedActor); 339 - if (expectedActor != null) { 345 + if (expectedActor != null && _isCurrentRoute) { 340 346 _scheduleProfileLoadIfNeeded(expectedActor, profileState); 341 347 _scheduleFeedLoadIfNeeded(expectedActor, _currentFilter, feedState, profile); 342 348 } ··· 379 385 leading: widget.showBackButton 380 386 ? IconButton( 381 387 icon: const Icon(Icons.arrow_back), 382 - onPressed: () => context.canPop() ? context.pop() : context.go('/profile'), 388 + onPressed: () => context.canPop() ? context.pop() : context.go('/profile/me'), 383 389 ) 384 390 : const AppShellMenuButton(), 385 391 actions: [
+1 -1
lib/features/starter_packs/presentation/starter_pack_detail_screen.dart
··· 42 42 if (context.canPop()) { 43 43 context.pop(); 44 44 } else { 45 - context.go('/profile'); 45 + context.go('/profile/me'); 46 46 } 47 47 return; 48 48 }
-1
lib/shared/presentation/helpers/navigation_helpers.dart
··· 62 62 path.startsWith('/search/') || 63 63 path == '/alerts' || 64 64 path.startsWith('/alerts/') || 65 - path == '/profile' || 66 65 path.startsWith('/profile/'); 67 66 }