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: context extension for theming constants

+531 -491
+5 -7
docs/tasks/testing.md
··· 28 28 29 29 ## M3 - Theme & Spacing Constants 30 30 31 - - [ ] Create `lib/core/theme/theme_extensions.dart` - `BuildContext` extension for `colorScheme` access 32 - - [ ] Create `lib/core/theme/spacing.dart` with padding/margin constants 33 - - [ ] Create `lib/core/theme/color_filters.dart` - extract greyscale matrix from: 34 - - `lib/features/feed/presentation/widgets/grid_post_card.dart` 35 - - `lib/features/profile/presentation/profile_screen.dart` 36 - - [ ] Refactor files to use new constants/extensions (27 files use `Theme.of(context).colorScheme`) 37 - - [ ] Tests for theme extension 31 + - [x] Create `lib/core/theme/theme_extensions.dart` - `BuildContext` extension for `colorScheme` access 32 + - [x] Create `lib/core/theme/spacing.dart` with padding/margin constants 33 + - [x] Create `lib/core/theme/color_filters.dart` - extract greyscale matrix 34 + - [x] Refactor files to use new constants/extensions 35 + - [x] Tests for theme extension 38 36 39 37 ## M4 - Widget Extraction 40 38
+2 -1
lib/core/router/app_shell.dart
··· 8 8 import 'package:lazurite/features/notifications/cubit/unread_count_cubit.dart'; 9 9 import 'package:lazurite/features/profile/data/profile_repository.dart'; 10 10 import 'package:provider/provider.dart'; 11 + import 'package:lazurite/core/theme/theme_extensions.dart'; 11 12 12 13 class AppShellScope extends InheritedWidget { 13 14 const AppShellScope({super.key, required super.child, required this.openMenu}); ··· 446 447 final avatarUrl = snapshot.data; 447 448 return CircleAvatar( 448 449 radius: 24, 449 - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, 450 + backgroundColor: context.colorScheme.surfaceContainerHighest, 450 451 backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null, 451 452 child: avatarUrl == null ? Text(widget.initials) : null, 452 453 );
+28
lib/core/theme/color_filters.dart
··· 1 + import 'package:flutter/widgets.dart'; 2 + 3 + abstract final class AppColorFilters { 4 + static const List<double> greyscaleMatrix = <double>[ 5 + 0.2126, 6 + 0.7152, 7 + 0.0722, 8 + 0, 9 + 0, 10 + 0.2126, 11 + 0.7152, 12 + 0.0722, 13 + 0, 14 + 0, 15 + 0.2126, 16 + 0.7152, 17 + 0.0722, 18 + 0, 19 + 0, 20 + 0, 21 + 0, 22 + 0, 23 + 1, 24 + 0, 25 + ]; 26 + 27 + static const ColorFilter greyscale = ColorFilter.matrix(greyscaleMatrix); 28 + }
+27
lib/core/theme/spacing.dart
··· 1 + import 'package:flutter/widgets.dart'; 2 + 3 + abstract final class AppSpacing { 4 + static const double xxs = 4; 5 + static const double xs = 8; 6 + static const double sm = 12; 7 + static const double md = 16; 8 + static const double lg = 24; 9 + static const double xl = 32; 10 + } 11 + 12 + abstract final class AppInsets { 13 + static const EdgeInsets allXs = EdgeInsets.all(AppSpacing.xs); 14 + static const EdgeInsets allSm = EdgeInsets.all(AppSpacing.sm); 15 + static const EdgeInsets allMd = EdgeInsets.all(AppSpacing.md); 16 + static const EdgeInsets allLg = EdgeInsets.all(AppSpacing.lg); 17 + 18 + static const EdgeInsets horizontalXs = EdgeInsets.symmetric(horizontal: AppSpacing.xs); 19 + static const EdgeInsets horizontalSm = EdgeInsets.symmetric(horizontal: AppSpacing.sm); 20 + static const EdgeInsets horizontalMd = EdgeInsets.symmetric(horizontal: AppSpacing.md); 21 + 22 + static const EdgeInsets verticalXs = EdgeInsets.symmetric(vertical: AppSpacing.xs); 23 + static const EdgeInsets verticalSm = EdgeInsets.symmetric(vertical: AppSpacing.sm); 24 + static const EdgeInsets verticalMd = EdgeInsets.symmetric(vertical: AppSpacing.md); 25 + 26 + static const EdgeInsets screenPadding = EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm); 27 + }
+9
lib/core/theme/theme_extensions.dart
··· 1 + import 'package:flutter/material.dart'; 2 + 3 + extension ThemeX on BuildContext { 4 + ThemeData get theme => Theme.of(this); 5 + 6 + ColorScheme get colorScheme => theme.colorScheme; 7 + 8 + TextTheme get textTheme => theme.textTheme; 9 + }
+2 -1
lib/core/widgets/sliver_tab_bar_delegate.dart
··· 1 1 import 'dart:ui'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:flutter/material.dart'; 4 5 ··· 17 18 18 19 @override 19 20 Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { 20 - final colorScheme = Theme.of(context).colorScheme; 21 + final colorScheme = context.colorScheme; 21 22 return ClipRect( 22 23 child: BackdropFilter( 23 24 filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
+2 -1
lib/features/auth/presentation/home_screen.dart
··· 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:go_router/go_router.dart'; 4 4 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 5 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 6 6 7 class HomeScreen extends StatelessWidget { 7 8 const HomeScreen({super.key}); ··· 37 38 children: [ 38 39 Text( 39 40 tokens.displayName ?? 'Welcome', 40 - style: Theme.of(context).textTheme.headlineMedium, 41 + style: context.textTheme.headlineMedium, 41 42 textAlign: TextAlign.center, 42 43 ), 43 44 const SizedBox(height: 12),
+2 -1
lib/features/compose/presentation/compose_screen.dart
··· 1 1 import 'dart:convert'; 2 2 import 'dart:io'; 3 3 import 'dart:ui' as ui; 4 + import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 5 6 import 'package:bluesky_text/bluesky_text.dart'; 6 7 import 'package:flutter/material.dart'; ··· 932 933 final entities = BlueskyText(text).entities.where((e) => e.type != EntityType.markdownLink).toList(); 933 934 if (entities.isEmpty) return TextSpan(style: style, text: text); 934 935 935 - final colorScheme = Theme.of(context).colorScheme; 936 + final colorScheme = context.colorScheme; 936 937 final textBytes = utf8.encode(text); 937 938 final spans = <InlineSpan>[]; 938 939 int lastCharEnd = 0;
+24 -36
lib/features/devtools/presentation/dev_tools_screen.dart
··· 6 6 import 'package:flutter/material.dart'; 7 7 import 'package:flutter/services.dart'; 8 8 import 'package:flutter_bloc/flutter_bloc.dart'; 9 + import 'package:lazurite/core/theme/theme_extensions.dart'; 9 10 import 'package:lazurite/core/widgets/app_breadcrumbs.dart'; 10 11 import 'package:lazurite/features/devtools/cubit/dev_tools_cubit.dart'; 11 12 import 'package:url_launcher/url_launcher.dart'; ··· 293 294 child: Column( 294 295 mainAxisSize: MainAxisSize.min, 295 296 children: [ 296 - Icon(Icons.error_outline, size: 48, color: Theme.of(context).colorScheme.error), 297 + Icon(Icons.error_outline, size: 48, color: context.colorScheme.error), 297 298 const SizedBox(height: 16), 298 - Text('Error', style: Theme.of(context).textTheme.titleMedium), 299 + Text('Error', style: context.textTheme.titleMedium), 299 300 const SizedBox(height: 8), 300 301 Text( 301 302 state.errorMessage ?? 'Unknown error', 302 303 textAlign: TextAlign.center, 303 - style: Theme.of(context).textTheme.bodyMedium, 304 + style: context.textTheme.bodyMedium, 304 305 ), 305 306 ], 306 307 ), ··· 339 340 child: Column( 340 341 mainAxisSize: MainAxisSize.min, 341 342 children: [ 342 - Icon(Icons.explore_outlined, size: 64, color: Theme.of(context).colorScheme.outline), 343 + Icon(Icons.explore_outlined, size: 64, color: context.colorScheme.outline), 343 344 const SizedBox(height: 16), 344 - Text('PDS Explorer', style: Theme.of(context).textTheme.titleMedium), 345 + Text('PDS Explorer', style: context.textTheme.titleMedium), 345 346 const SizedBox(height: 8), 346 347 Text( 347 348 'Enter a handle, DID, or AT-URI to explore\n' 348 349 'a user\'s repository.', 349 350 textAlign: TextAlign.center, 350 - style: Theme.of( 351 - context, 352 - ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.outline), 351 + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.outline), 353 352 ), 354 353 const SizedBox(height: 16), 355 354 TextButton.icon( ··· 381 380 Container( 382 381 padding: const EdgeInsets.all(16), 383 382 decoration: BoxDecoration( 384 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 383 + color: context.colorScheme.surfaceContainerHighest, 385 384 border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), 386 385 ), 387 386 child: Column( ··· 395 394 child: Column( 396 395 crossAxisAlignment: CrossAxisAlignment.start, 397 396 children: [ 398 - Text( 399 - state.repoHandle ?? state.handle ?? 'Unknown', 400 - style: Theme.of(context).textTheme.titleMedium, 401 - ), 397 + Text(state.repoHandle ?? state.handle ?? 'Unknown', style: context.textTheme.titleMedium), 402 398 const SizedBox(height: 2), 403 399 Text( 404 400 state.did ?? '', 405 - style: Theme.of( 406 - context, 407 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 401 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 408 402 overflow: TextOverflow.ellipsis, 409 403 ), 410 404 ], ··· 425 419 totalRepoRecords == null 426 420 ? (state.isCollectionCountsLoading ? 'Counting records...' : 'Record counts unavailable') 427 421 : '$totalRepoRecords records', 428 - style: Theme.of( 429 - context, 430 - ).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 422 + style: context.textTheme.bodySmall!.copyWith(color: context.colorScheme.onSurfaceVariant), 431 423 ), 432 424 ], 433 425 ), ··· 438 430 padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), 439 431 child: Text( 440 432 'COLLECTIONS', 441 - style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 433 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 442 434 ), 443 435 ), 444 436 ...state.collections.map((collection) => _CollectionItem(collection: collection)), ··· 460 452 height: 32, 461 453 decoration: BoxDecoration( 462 454 borderRadius: BorderRadius.circular(6), 463 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 455 + color: context.colorScheme.surfaceContainerHighest, 464 456 ), 465 - child: Icon(_getCollectionIcon(collection.name), size: 16, color: Theme.of(context).colorScheme.primary), 457 + child: Icon(_getCollectionIcon(collection.name), size: 16, color: context.colorScheme.primary), 466 458 ), 467 459 title: Text(collection.name, style: const TextStyle(fontFamily: 'JetBrains Mono', fontSize: 13)), 468 460 trailing: Row( ··· 471 463 Container( 472 464 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 473 465 decoration: BoxDecoration( 474 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 466 + color: context.colorScheme.surfaceContainerHighest, 475 467 borderRadius: BorderRadius.circular(999), 476 468 ), 477 469 child: Text( 478 470 collection.countLabel, 479 - style: Theme.of( 480 - context, 481 - ).textTheme.labelSmall!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 471 + style: context.textTheme.labelSmall!.copyWith(color: context.colorScheme.onSurfaceVariant), 482 472 ), 483 473 ), 484 474 const SizedBox(width: 4), ··· 546 536 Container( 547 537 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 548 538 decoration: BoxDecoration( 549 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 539 + color: context.colorScheme.surfaceContainerHighest, 550 540 border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), 551 541 ), 552 542 child: Row( ··· 554 544 Expanded( 555 545 child: Text( 556 546 widget.state.selectedCollection ?? '', 557 - style: Theme.of(context).textTheme.titleSmall?.copyWith( 547 + style: context.textTheme.titleSmall?.copyWith( 558 548 fontFamily: 'JetBrains Mono', 559 - color: Theme.of(context).colorScheme.primary, 549 + color: context.colorScheme.primary, 560 550 ), 561 551 ), 562 552 ), ··· 564 554 selectedCollection?.recordCount == null 565 555 ? '${records.length} loaded' 566 556 : '${records.length} of ${selectedCollection!.recordCount}', 567 - style: Theme.of(context).textTheme.bodySmall, 557 + style: context.textTheme.bodySmall, 568 558 ), 569 559 ], 570 560 ), ··· 641 631 width: double.infinity, 642 632 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 643 633 decoration: BoxDecoration( 644 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 634 + color: context.colorScheme.surfaceContainerHighest, 645 635 border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), 646 636 ), 647 637 child: Column( ··· 649 639 children: [ 650 640 Text( 651 641 record.rkey, 652 - style: Theme.of(context).textTheme.titleSmall?.copyWith(color: Theme.of(context).colorScheme.primary), 642 + style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), 653 643 overflow: TextOverflow.ellipsis, 654 644 ), 655 645 const SizedBox(height: 4), 656 646 Text( 657 647 record.uri, 658 - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.tertiary), 648 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.tertiary), 659 649 overflow: TextOverflow.ellipsis, 660 650 ), 661 651 if (record.cid != null) ...[ 662 652 const SizedBox(height: 2), 663 653 Text( 664 654 'CID: ${record.cid!}', 665 - style: Theme.of( 666 - context, 667 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.secondary), 655 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.secondary), 668 656 ), 669 657 ], 670 658 const SizedBox(height: 8),
+8 -11
lib/features/feed/presentation/feed_management_screen.dart
··· 8 8 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 9 9 import 'package:lazurite/shared/presentation/widgets/empty_state.dart'; 10 10 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 11 + import 'package:lazurite/core/theme/theme_extensions.dart'; 11 12 12 13 class FeedManagementScreen extends StatefulWidget { 13 14 const FeedManagementScreen({super.key}); ··· 138 139 children: [ 139 140 Text( 140 141 title.toUpperCase(), 141 - style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 142 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 142 143 ), 143 144 const Spacer(), 144 145 if (showReorder) TextButton(onPressed: onAction, child: Text(isReordering ? 'Done' : 'Reorder')), ··· 160 161 subtitle: Text(state.subtitleFor(feed)), 161 162 trailing: IconButton( 162 163 icon: const Icon(Icons.check_circle), 163 - color: Theme.of(context).colorScheme.primary, 164 + color: context.colorScheme.primary, 164 165 onPressed: () => context.read<FeedPreferencesCubit>().unpinFeed(feed.id), 165 166 ), 166 167 ); ··· 183 184 onPressed: () => context.read<FeedPreferencesCubit>().pinFeed(feed.id), 184 185 ), 185 186 IconButton( 186 - icon: Icon(Icons.close, color: Theme.of(context).colorScheme.error), 187 + icon: Icon(Icons.close, color: context.colorScheme.error), 187 188 onPressed: () => _confirmRemoveFeed(context, feed.id), 188 189 ), 189 190 ], ··· 225 226 children: [ 226 227 Text( 227 228 _feedDisplayName(feed), 228 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), 229 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), 229 230 ), 230 231 Text( 231 232 'by ${feed.creator.displayName ?? feed.creator.handle}', 232 - style: Theme.of( 233 - context, 234 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 233 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 235 234 ), 236 235 if (feed.description != null) ...[ 237 236 const SizedBox(height: 4), ··· 239 238 feed.description!, 240 239 maxLines: 2, 241 240 overflow: TextOverflow.ellipsis, 242 - style: Theme.of(context).textTheme.bodySmall, 241 + style: context.textTheme.bodySmall, 243 242 ), 244 243 ], 245 244 if (feed.likeCount != null) ...[ 246 245 const SizedBox(height: 4), 247 246 Text( 248 247 '${feed.likeCount} likes', 249 - style: Theme.of( 250 - context, 251 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 248 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 252 249 ), 253 250 ], 254 251 ],
+17 -21
lib/features/feed/presentation/post_thread_screen.dart
··· 1 1 import 'dart:async'; 2 2 import 'dart:convert'; 3 + import 'package:lazurite/core/theme/theme_extensions.dart'; 3 4 4 5 import 'package:bluesky/app_bsky_feed_defs.dart'; 5 6 import 'package:bluesky/app_bsky_feed_post.dart'; ··· 190 191 padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), 191 192 child: Text( 192 193 'Replies', 193 - style: Theme.of(context).textTheme.labelSmall?.copyWith( 194 - color: Theme.of(context).colorScheme.onSurfaceVariant, 194 + style: context.textTheme.labelSmall?.copyWith( 195 + color: context.colorScheme.onSurfaceVariant, 195 196 fontWeight: FontWeight.w600, 196 197 letterSpacing: 0.5, 197 198 ), ··· 379 380 @override 380 381 Widget build(BuildContext context) { 381 382 final post = thread.post; 382 - final colorScheme = Theme.of(context).colorScheme; 383 + final colorScheme = context.colorScheme; 383 384 384 385 return GestureDetector( 385 386 behavior: HitTestBehavior.opaque, ··· 400 401 Text( 401 402 _hiddenReplyLabel(hiddenReplyCount).toUpperCase(), 402 403 key: ValueKey('collapsed-indicator-${post.uri}'), 403 - style: Theme.of( 404 - context, 405 - ).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, letterSpacing: 1.1), 404 + style: context.textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, letterSpacing: 1.1), 406 405 ), 407 406 ], 408 407 ), ··· 419 418 420 419 @override 421 420 Widget build(BuildContext context) { 422 - final colorScheme = Theme.of(context).colorScheme; 421 + final colorScheme = context.colorScheme; 423 422 final timestamp = _parsePostRecord(post.record)?.createdAt ?? post.indexedAt; 424 423 final moderationService = maybeModerationService(context); 425 424 final avatarUi = ··· 447 446 Expanded( 448 447 child: Text( 449 448 post.author.displayName ?? post.author.handle, 450 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 449 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 451 450 maxLines: 1, 452 451 overflow: TextOverflow.ellipsis, 453 452 ), ··· 455 454 const SizedBox(width: 8), 456 455 Text( 457 456 DateFormat('MMM d').format(timestamp.toLocal()).toUpperCase(), 458 - style: Theme.of( 459 - context, 460 - ).textTheme.labelSmall?.copyWith(color: colorScheme.onSurfaceVariant, letterSpacing: 0.8), 457 + style: context.textTheme.labelSmall?.copyWith( 458 + color: colorScheme.onSurfaceVariant, 459 + letterSpacing: 0.8, 460 + ), 461 461 ), 462 462 ], 463 463 ), 464 464 const SizedBox(height: 2), 465 465 Text( 466 466 '@${post.author.handle}'.toUpperCase(), 467 - style: Theme.of(context).textTheme.labelSmall?.copyWith( 467 + style: context.textTheme.labelSmall?.copyWith( 468 468 color: colorScheme.onSurfaceVariant, 469 469 fontWeight: FontWeight.w700, 470 470 letterSpacing: 1.5, ··· 577 577 String _hiddenReplyLabel(int count) => count == 1 ? '1 reply hidden' : '$count replies hidden'; 578 578 579 579 List<Color> _threadLineColors(BuildContext context) { 580 - final colorScheme = Theme.of(context).colorScheme; 580 + final colorScheme = context.colorScheme; 581 581 final surface = colorScheme.surface; 582 582 583 583 Color blend(Color color, double amount) => Color.lerp(color, surface, amount)!; ··· 672 672 const SizedBox(height: 10), 673 673 Text( 674 674 _formatTimestamp(timestamp), 675 - style: Theme.of( 676 - context, 677 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 675 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 678 676 ), 679 677 const SizedBox(height: 10), 680 678 const Divider(height: 1), ··· 742 740 children: [ 743 741 TextSpan( 744 742 text: '$count', 745 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), 743 + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), 746 744 ), 747 745 TextSpan( 748 746 text: ' $label', 749 - style: Theme.of( 750 - context, 751 - ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 747 + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), 752 748 ), 753 749 ], 754 750 ), ··· 859 855 OptionsSheetItem(leading: const Icon(Icons.edit_outlined), title: 'Edit Post', onTap: () => _onEdit(context)), 860 856 if (post.author.did == accountDid) 861 857 OptionsSheetItem( 862 - leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), 858 + leading: Icon(Icons.delete_outline, color: context.colorScheme.error), 863 859 title: 'Delete Post', 864 860 isDestructive: true, 865 861 onTap: () => _confirmDelete(context),
+6 -5
lib/features/feed/presentation/saved_posts_screen.dart
··· 1 1 import 'dart:convert'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:bluesky/app_bsky_feed_defs.dart'; 4 5 import 'package:flutter/material.dart'; ··· 98 99 context.read<SavedPostsCubit>().clearAllSaved(); 99 100 }, 100 101 style: FilledButton.styleFrom( 101 - backgroundColor: Theme.of(context).colorScheme.error, 102 - foregroundColor: Theme.of(context).colorScheme.onError, 102 + backgroundColor: context.colorScheme.error, 103 + foregroundColor: context.colorScheme.onError, 103 104 ), 104 105 child: const Text('Clear All'), 105 106 ), ··· 181 182 background: Container( 182 183 alignment: Alignment.centerRight, 183 184 padding: const EdgeInsets.only(right: 16), 184 - color: Theme.of(context).colorScheme.error, 185 - child: Icon(Icons.bookmark_remove, color: Theme.of(context).colorScheme.onError), 185 + color: context.colorScheme.error, 186 + child: Icon(Icons.bookmark_remove, color: context.colorScheme.onError), 186 187 ), 187 188 onDismissed: (_) => onUnsave(), 188 189 child: feedViewPost != null ··· 197 198 child: ListTile( 198 199 leading: const Icon(Icons.bookmark), 199 200 title: const Text('Saved Post'), 200 - subtitle: Text('Saved on ${_formatDate(savedPost.savedAt)}', style: Theme.of(context).textTheme.bodySmall), 201 + subtitle: Text('Saved on ${_formatDate(savedPost.savedAt)}', style: context.textTheme.bodySmall), 201 202 trailing: Row( 202 203 mainAxisSize: MainAxisSize.min, 203 204 children: [
+2 -1
lib/features/feed/presentation/widgets/facet_text.dart
··· 1 1 import 'dart:convert'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:bluesky/app_bsky_richtext_facet.dart'; 4 5 import 'package:bluesky_text/bluesky_text.dart'; ··· 144 145 145 146 TextStyle _linkStyle(BuildContext context) { 146 147 return TextStyle( 147 - color: Theme.of(context).colorScheme.primary, 148 + color: context.colorScheme.primary, 148 149 decoration: TextDecoration.underline, 149 150 fontWeight: FontWeight.w600, 150 151 );
+13 -32
lib/features/feed/presentation/widgets/grid_post_card.dart
··· 5 5 import 'package:bluesky/moderation.dart' as bsky_moderation; 6 6 import 'package:flutter/material.dart'; 7 7 import 'package:go_router/go_router.dart'; 8 + import 'package:lazurite/core/theme/color_filters.dart'; 9 + import 'package:lazurite/core/theme/spacing.dart'; 10 + import 'package:lazurite/core/theme/theme_extensions.dart'; 8 11 import 'package:lazurite/features/feed/presentation/widgets/facet_text.dart'; 9 12 import 'package:lazurite/features/feed/presentation/widgets/post_card_footer.dart'; 10 13 import 'package:lazurite/features/feed/presentation/widgets/post_embed_view.dart'; ··· 15 18 import 'package:lazurite/features/moderation/presentation/widgets/moderation_badge_row.dart'; 16 19 import 'package:lazurite/shared/utils/format_utils.dart'; 17 20 18 - const _greyscale = ColorFilter.matrix(<double>[ 19 - 0.2126, 20 - 0.7152, 21 - 0.0722, 22 - 0, 23 - 0, 24 - 0.2126, 25 - 0.7152, 26 - 0.0722, 27 - 0, 28 - 0, 29 - 0.2126, 30 - 0.7152, 31 - 0.0722, 32 - 0, 33 - 0, 34 - 0, 35 - 0, 36 - 0, 37 - 1, 38 - 0, 39 - ]); 40 21 const double _gridEmbedPreviewMaxHeight = 240; 41 22 42 23 /// Grid layout post card. ··· 68 49 final record = _tryParseRecord(post.record); 69 50 final primaryImageUrl = _extractPrimaryImageUrl(post.embed); 70 51 final bodyText = record?.text ?? ''; 71 - final colorScheme = Theme.of(context).colorScheme; 52 + final colorScheme = context.colorScheme; 72 53 final isCompactGrid = MediaQuery.of(context).size.width >= 600; 73 54 final moderationService = maybeModerationService(context); 74 55 final postUi = moderationService?.postUi(post, moderationContext) ?? const bsky_moderation.ModerationUI(); ··· 101 82 child: AspectRatio( 102 83 aspectRatio: 1.0, 103 84 child: ColorFiltered( 104 - colorFilter: _greyscale, 85 + colorFilter: AppColorFilters.greyscale, 105 86 child: Image.network( 106 87 primaryImageUrl, 107 88 fit: BoxFit.cover, ··· 113 94 ), 114 95 ), 115 96 Padding( 116 - padding: const EdgeInsets.all(16), 97 + padding: AppInsets.allMd, 117 98 child: Column( 118 99 crossAxisAlignment: CrossAxisAlignment.start, 119 100 children: [ 120 101 _buildAuthorRow(context, post.author), 121 102 if (postUi.alert || postUi.inform) ...[const SizedBox(height: 10), ModerationBadgeRow(ui: postUi)], 122 103 if (bodyText.isNotEmpty) ...[ 123 - const SizedBox(height: 8), 104 + const SizedBox(height: AppSpacing.xs), 124 105 if (primaryImageUrl == null && contentEmbed == null) 125 106 FacetText( 126 107 text: bodyText, ··· 141 122 ), 142 123 ], 143 124 if (contentEmbed != null) ...[ 144 - const SizedBox(height: 8), 125 + const SizedBox(height: AppSpacing.xs), 145 126 _buildEmbedPreview(contentEmbed, compact: isCompactGrid), 146 127 ], 147 128 ], ··· 156 137 } 157 138 158 139 Widget _buildAuthorRow(BuildContext context, ProfileViewBasic author) { 159 - final colorScheme = Theme.of(context).colorScheme; 140 + final colorScheme = context.colorScheme; 160 141 final moderationService = maybeModerationService(context); 161 142 final avatarUi = 162 143 moderationService?.profileBasicUi(author, bsky_moderation.ModerationBehaviorContext.avatar) ?? ··· 173 154 initials: formatInitials(author.displayName ?? author.handle), 174 155 shape: BoxShape.rectangle, 175 156 border: Border.all(color: colorScheme.outlineVariant), 176 - placeholderTextStyle: Theme.of(context).textTheme.labelMedium, 157 + placeholderTextStyle: context.textTheme.labelMedium, 177 158 ), 178 159 ), 179 - const SizedBox(width: 8), 160 + const SizedBox(width: AppSpacing.xs), 180 161 Expanded( 181 162 child: Column( 182 163 crossAxisAlignment: CrossAxisAlignment.start, ··· 184 165 if (author.displayName != null && author.displayName!.isNotEmpty) 185 166 Text( 186 167 author.displayName!, 187 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), 168 + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), 188 169 maxLines: 1, 189 170 overflow: TextOverflow.ellipsis, 190 171 ), 191 172 Text( 192 173 '@${author.handle}'.toUpperCase(), 193 - style: Theme.of(context).textTheme.labelSmall?.copyWith( 174 + style: context.textTheme.labelSmall?.copyWith( 194 175 fontWeight: FontWeight.w700, 195 176 letterSpacing: 1.5, 196 177 color: colorScheme.onSurfaceVariant,
+9 -10
lib/features/feed/presentation/widgets/post_action_bar.dart
··· 5 5 import 'package:lazurite/shared/presentation/widgets/options_sheet.dart'; 6 6 import 'package:lazurite/shared/utils/format_utils.dart'; 7 7 import 'package:share_plus/share_plus.dart'; 8 + import 'package:lazurite/core/theme/theme_extensions.dart'; 8 9 9 10 class PostActionBar extends StatelessWidget { 10 11 const PostActionBar({ ··· 69 70 count: replyCount, 70 71 onTap: isOffline ? null : onReply, 71 72 tooltip: isOffline ? offlineActionMessage('reply to this post') : null, 72 - color: Theme.of(context).colorScheme.onSurfaceVariant, 73 + color: context.colorScheme.onSurfaceVariant, 73 74 ), 74 75 _ActionButton( 75 76 icon: Icons.repeat, ··· 99 100 isActive: isSaved, 100 101 onTap: onSave != null ? () => _showSaveOptions(context) : null, 101 102 onLongPress: onLongPressSave, 102 - color: Theme.of(context).colorScheme.onSurfaceVariant, 103 - activeColor: (saveType == 'cloud' || saveType == 'both') 104 - ? Theme.of(context).colorScheme.primary 105 - : Colors.amber, 103 + color: context.colorScheme.onSurfaceVariant, 104 + activeColor: (saveType == 'cloud' || saveType == 'both') ? context.colorScheme.primary : Colors.amber, 106 105 ), 107 106 _ActionButton( 108 107 icon: Icons.share_outlined, 109 108 activeIcon: Icons.share, 110 109 count: 0, 111 110 onTap: onShare ?? () => _defaultShare(context), 112 - color: Theme.of(context).colorScheme.onSurfaceVariant, 111 + color: context.colorScheme.onSurfaceVariant, 113 112 ), 114 113 if (onMore != null) 115 114 _ActionButton( ··· 117 116 activeIcon: Icons.more_vert, 118 117 count: 0, 119 118 onTap: onMore, 120 - color: Theme.of(context).colorScheme.onSurfaceVariant, 119 + color: context.colorScheme.onSurfaceVariant, 121 120 ), 122 121 ], 123 122 ); ··· 163 162 OptionsSheetItem( 164 163 leading: Icon( 165 164 isCloudSaved ? Icons.cloud_off_outlined : Icons.cloud_outlined, 166 - color: Theme.of(context).colorScheme.primary, 165 + color: context.colorScheme.primary, 167 166 ), 168 167 title: isCloudSaved ? 'Remove from Bluesky' : 'Save to Bluesky', 169 168 onTap: isCloudSaved ? onCloudUnsave : onCloudSave, ··· 220 219 221 220 @override 222 221 Widget build(BuildContext context) { 223 - final defaultColor = color ?? Theme.of(context).colorScheme.onSurfaceVariant; 222 + final defaultColor = color ?? context.colorScheme.onSurfaceVariant; 224 223 final iconColor = isActive ? (activeColor ?? defaultColor) : defaultColor; 225 224 226 225 Widget button = InkWell( ··· 238 237 Icon(isActive ? activeIcon : icon, size: 18, color: iconColor), 239 238 if (count > 0) ...[ 240 239 const SizedBox(width: 4), 241 - Text(formatCount(count), style: Theme.of(context).textTheme.bodySmall?.copyWith(color: iconColor)), 240 + Text(formatCount(count), style: context.textTheme.bodySmall?.copyWith(color: iconColor)), 242 241 ], 243 242 ], 244 243 ),
+7 -8
lib/features/feed/presentation/widgets/post_card.dart
··· 13 13 import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; 14 14 import 'package:lazurite/features/moderation/presentation/widgets/moderation_badge_row.dart'; 15 15 import 'package:lazurite/shared/utils/format_utils.dart'; 16 + import 'package:lazurite/core/theme/theme_extensions.dart'; 16 17 17 18 class PostCard extends StatelessWidget { 18 19 const PostCard({ ··· 39 40 Widget build(BuildContext context) { 40 41 final post = feedViewPost.post; 41 42 final record = _tryParseRecord(post.record); 42 - final colorScheme = Theme.of(context).colorScheme; 43 + final colorScheme = context.colorScheme; 43 44 final moderationService = maybeModerationService(context); 44 45 final postUi = moderationService?.postUi(post, moderationContext) ?? const bsky_moderation.ModerationUI(); 45 46 ··· 87 88 } 88 89 89 90 Widget _buildHeader(BuildContext context, ProfileViewBasic author) { 90 - final colorScheme = Theme.of(context).colorScheme; 91 + final colorScheme = context.colorScheme; 91 92 final moderationService = maybeModerationService(context); 92 93 final avatarUi = 93 94 moderationService?.profileBasicUi(author, bsky_moderation.ModerationBehaviorContext.avatar) ?? ··· 114 115 children: [ 115 116 Text( 116 117 author.displayName ?? author.handle, 117 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 118 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 118 119 maxLines: 1, 119 120 overflow: TextOverflow.ellipsis, 120 121 ), 121 122 const SizedBox(height: 2), 122 123 Text( 123 124 '@${author.handle}'.toUpperCase(), 124 - style: Theme.of(context).textTheme.labelSmall?.copyWith( 125 + style: context.textTheme.labelSmall?.copyWith( 125 126 color: colorScheme.onSurfaceVariant, 126 127 fontWeight: FontWeight.w700, 127 128 letterSpacing: 1.5, ··· 139 140 Widget _buildReplyLabel(BuildContext context) { 140 141 return Row( 141 142 children: [ 142 - Icon(Icons.reply, size: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), 143 + Icon(Icons.reply, size: 14, color: context.colorScheme.onSurfaceVariant), 143 144 const SizedBox(width: 6), 144 145 Flexible( 145 146 child: Text( 146 147 'Reply in a thread', 147 148 maxLines: 1, 148 149 overflow: TextOverflow.ellipsis, 149 - style: Theme.of( 150 - context, 151 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 150 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 152 151 ), 153 152 ), 154 153 ],
+10 -7
lib/features/feed/presentation/widgets/post_card_footer.dart
··· 3 3 import 'package:lazurite/features/connectivity/connectivity_helpers.dart'; 4 4 import 'package:lazurite/shared/presentation/widgets/options_sheet.dart'; 5 5 import 'package:lazurite/shared/utils/format_utils.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 7 8 /// Formats a post timestamp as a short, uppercase string. 8 9 String formatPostTime(DateTime time) { ··· 59 60 60 61 @override 61 62 Widget build(BuildContext context) { 62 - final colorScheme = Theme.of(context).colorScheme; 63 + final colorScheme = context.colorScheme; 63 64 final saveActiveColor = (saveType == 'cloud' || saveType == 'both') ? colorScheme.primary : Colors.amber; 64 65 const horizontalPadding = 12.0; 65 66 const iconSize = 18.0; ··· 167 168 maxLines: 1, 168 169 overflow: TextOverflow.ellipsis, 169 170 softWrap: false, 170 - style: Theme.of( 171 - context, 172 - ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, fontSize: 10, letterSpacing: 1.0), 171 + style: context.textTheme.bodySmall?.copyWith( 172 + color: colorScheme.onSurfaceVariant, 173 + fontSize: 10, 174 + letterSpacing: 1.0, 175 + ), 173 176 ); 174 177 } 175 178 ··· 192 195 OptionsSheetItem( 193 196 leading: Icon( 194 197 isCloudSaved ? Icons.cloud_off_outlined : Icons.cloud_outlined, 195 - color: Theme.of(context).colorScheme.primary, 198 + color: context.colorScheme.primary, 196 199 ), 197 200 title: isCloudSaved ? 'Remove from Bluesky' : 'Save to Bluesky', 198 201 onTap: isCloudSaved ? onCloudUnsave : onCloudSave, ··· 235 238 236 239 @override 237 240 Widget build(BuildContext context) { 238 - final defaultColor = color ?? Theme.of(context).colorScheme.onSurfaceVariant; 241 + final defaultColor = color ?? context.colorScheme.onSurfaceVariant; 239 242 final iconColor = isActive ? (activeColor ?? defaultColor) : defaultColor; 240 243 241 244 Widget button = InkWell( ··· 257 260 Icon(isActive ? activeIcon : icon, size: iconSize, color: iconColor), 258 261 if (showCount && count > 0) ...[ 259 262 const SizedBox(width: 4), 260 - Text(formatCount(count), style: Theme.of(context).textTheme.bodySmall?.copyWith(color: iconColor)), 263 + Text(formatCount(count), style: context.textTheme.bodySmall?.copyWith(color: iconColor)), 261 264 ], 262 265 ], 263 266 ),
+9 -14
lib/features/feed/presentation/widgets/post_embed_view.dart
··· 18 18 import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; 19 19 import 'package:lazurite/shared/utils/format_utils.dart'; 20 20 import 'package:url_launcher/url_launcher.dart'; 21 + import 'package:lazurite/core/theme/theme_extensions.dart'; 21 22 22 23 /// Renders the appropriate embed widget for a post embed. 23 24 /// ··· 116 117 image.thumb, 117 118 fit: BoxFit.cover, 118 119 errorBuilder: (_, _, _) => ColoredBox( 119 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 120 + color: context.colorScheme.surfaceContainerHighest, 120 121 child: const Center(child: Icon(Icons.image_not_supported_outlined)), 121 122 ), 122 123 ), ··· 204 205 AspectRatio( 205 206 aspectRatio: video.aspectRatio == null ? 16 / 9 : video.aspectRatio!.width / video.aspectRatio!.height, 206 207 child: video.thumbnail == null 207 - ? ColoredBox( 208 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 209 - child: const SizedBox.expand(), 210 - ) 208 + ? ColoredBox(color: context.colorScheme.surfaceContainerHighest, child: const SizedBox.expand()) 211 209 : Image.network( 212 210 video.thumbnail!, 213 211 fit: BoxFit.cover, 214 212 errorBuilder: (_, _, _) => ColoredBox( 215 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 213 + color: context.colorScheme.surfaceContainerHighest, 216 214 child: const SizedBox.expand(), 217 215 ), 218 216 ), ··· 232 230 video.alt!, 233 231 maxLines: 2, 234 232 overflow: TextOverflow.ellipsis, 235 - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white), 233 + style: context.textTheme.bodySmall?.copyWith(color: Colors.white), 236 234 ), 237 235 ), 238 236 ], ··· 273 271 width: 28, 274 272 height: 28, 275 273 decoration: BoxDecoration( 276 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 277 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 274 + color: context.colorScheme.surfaceContainerHighest, 275 + border: Border.all(color: context.colorScheme.outlineVariant), 278 276 ), 279 277 child: quoted.author.avatar != null 280 278 ? Image.network(quoted.author.avatar!, fit: BoxFit.cover) ··· 330 328 width: double.infinity, 331 329 padding: const EdgeInsets.all(12), 332 330 decoration: BoxDecoration( 333 - color: Theme.of(context).colorScheme.surfaceContainerLow, 331 + color: context.colorScheme.surfaceContainerLow, 334 332 borderRadius: BorderRadius.circular(12), 335 333 ), 336 - child: Text( 337 - label, 338 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 339 - ), 334 + child: Text(label, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant)), 340 335 ); 341 336 } 342 337
+2 -1
lib/features/feed/presentation/widgets/post_interactions_sheet.dart
··· 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:go_router/go_router.dart'; 5 5 import 'package:lazurite/features/feed/data/post_action_repository.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 7 8 enum InteractionTab { likes, reposts } 8 9 ··· 97 98 98 99 @override 99 100 Widget build(BuildContext context) { 100 - final colorScheme = Theme.of(context).colorScheme; 101 + final colorScheme = context.colorScheme; 101 102 final hasBothTabs = widget.likeCount > 0 && widget.repostCount > 0; 102 103 103 104 return DraggableScrollableSheet(
+7 -9
lib/features/lists/presentation/list_detail_screen.dart
··· 13 13 import 'package:lazurite/features/lists/presentation/widgets/create_edit_list_dialog.dart'; 14 14 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 15 15 import 'package:lazurite/shared/presentation/widgets/options_sheet.dart'; 16 + import 'package:lazurite/core/theme/theme_extensions.dart'; 16 17 17 18 class ListDetailScreen extends StatelessWidget { 18 19 const ListDetailScreen({super.key, required this.listUri}); ··· 142 143 ), 143 144 if (isOwn) 144 145 OptionsSheetItem( 145 - leading: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), 146 + leading: Icon(Icons.delete_outline, color: context.colorScheme.error), 146 147 title: 'Delete list', 147 148 isDestructive: true, 148 149 onTap: () => _confirmDelete(context), ··· 252 253 } 253 254 254 255 Widget _buildHeader(BuildContext context, bsky_graph.ListView list) { 255 - final colorScheme = Theme.of(context).colorScheme; 256 - final textTheme = Theme.of(context).textTheme; 256 + final colorScheme = context.colorScheme; 257 + final textTheme = context.textTheme; 257 258 258 259 return Padding( 259 260 padding: const EdgeInsets.all(16), ··· 386 387 key: ValueKey(item.uri), 387 388 leading: CircleAvatar( 388 389 backgroundImage: subject.avatar != null ? NetworkImage(subject.avatar!) : null, 389 - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, 390 + backgroundColor: context.colorScheme.surfaceContainerHighest, 390 391 child: subject.avatar == null 391 392 ? Text( 392 393 (subject.displayName?.isNotEmpty == true ? subject.displayName! : subject.handle) ··· 396 397 : null, 397 398 ), 398 399 title: Text(subject.displayName ?? subject.handle, maxLines: 1, overflow: TextOverflow.ellipsis), 399 - subtitle: Text( 400 - '@${subject.handle}', 401 - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), 402 - ), 400 + subtitle: Text('@${subject.handle}', style: TextStyle(color: context.colorScheme.onSurfaceVariant)), 403 401 onTap: () => context.push('/profile/view?actor=${subject.did}'), 404 402 trailing: isOwn 405 403 ? IconButton( 406 - icon: Icon(Icons.remove_circle_outline, color: Theme.of(context).colorScheme.error), 404 + icon: Icon(Icons.remove_circle_outline, color: context.colorScheme.error), 407 405 onPressed: () => context.read<ListBloc>().add(ListItemRemoved(listItemUri: item.uri)), 408 406 ) 409 407 : null,
+3 -2
lib/features/lists/presentation/list_members_screen.dart
··· 5 5 import 'package:go_router/go_router.dart'; 6 6 import 'package:lazurite/features/lists/bloc/list_bloc.dart'; 7 7 import 'package:lazurite/features/lists/data/list_repository.dart'; 8 + import 'package:lazurite/core/theme/theme_extensions.dart'; 8 9 9 10 /// Screen for adding and removing members from a list. 10 11 /// ··· 104 105 105 106 Widget _buildSearchResults(BuildContext context, ListState state) { 106 107 final currentDids = state.items.map((item) => item.subject.did).toSet(); 107 - final colorScheme = Theme.of(context).colorScheme; 108 + final colorScheme = context.colorScheme; 108 109 109 110 return Expanded( 110 111 child: ListView.builder( ··· 152 153 return const Center(child: Text('No members yet. Search above to add people.')); 153 154 } 154 155 155 - final colorScheme = Theme.of(context).colorScheme; 156 + final colorScheme = context.colorScheme; 156 157 157 158 return Column( 158 159 crossAxisAlignment: CrossAxisAlignment.start,
+3 -2
lib/features/lists/presentation/widgets/create_edit_list_dialog.dart
··· 1 1 import 'dart:io'; 2 2 import 'dart:typed_data'; 3 + import 'package:lazurite/core/theme/theme_extensions.dart'; 3 4 4 5 import 'package:bluesky/app_bsky_graph_defs.dart' show KnownListPurpose; 5 6 import 'package:flutter/material.dart'; ··· 125 126 126 127 @override 127 128 Widget build(BuildContext context) { 128 - final colorScheme = Theme.of(context).colorScheme; 129 + final colorScheme = context.colorScheme; 129 130 final hasAvatar = _avatarBytes != null || widget.initialAvatarUrl != null; 130 131 131 132 return AlertDialog( ··· 189 190 ), 190 191 if (!_isEditing) ...[ 191 192 const SizedBox(height: 12), 192 - Text('Type', style: Theme.of(context).textTheme.labelLarge), 193 + Text('Type', style: context.textTheme.labelLarge), 193 194 const SizedBox(height: 8), 194 195 SegmentedButton<KnownListPurpose>( 195 196 segments: const [
+2 -1
lib/features/lists/presentation/widgets/list_row_tile.dart
··· 1 1 import 'package:bluesky/app_bsky_graph_defs.dart' as bsky_graph; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:lazurite/core/theme/theme_extensions.dart'; 3 4 4 5 /// A reusable tile for a single [bsky_graph.ListView] entry. 5 6 class ListRowTile extends StatelessWidget { ··· 11 12 12 13 @override 13 14 Widget build(BuildContext context) { 14 - final colorScheme = Theme.of(context).colorScheme; 15 + final colorScheme = context.colorScheme; 15 16 final isMod = list.purpose.knownValue == bsky_graph.KnownListPurpose.appBskyGraphDefsModlist; 16 17 final purposeLabel = isMod ? 'MOD' : 'FEED'; 17 18 final purposeColor = isMod ? colorScheme.error : colorScheme.primary;
+15 -22
lib/features/logs/presentation/logs_screen.dart
··· 7 7 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 8 8 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 9 9 import 'package:share_plus/share_plus.dart'; 10 + import 'package:lazurite/core/theme/theme_extensions.dart'; 10 11 11 12 class LogsScreen extends StatelessWidget { 12 13 const LogsScreen({super.key}); ··· 89 90 onPressed: () => _shareLogs(context), 90 91 ), 91 92 IconButton( 92 - icon: Icon(Icons.delete_outline, color: Theme.of(context).colorScheme.error), 93 + icon: Icon(Icons.delete_outline, color: context.colorScheme.error), 93 94 tooltip: 'Clear all logs', 94 95 onPressed: () => _confirmClearLogs(context), 95 96 ), ··· 135 136 TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), 136 137 TextButton( 137 138 onPressed: () => Navigator.pop(context, true), 138 - child: Text('Clear', style: TextStyle(color: Theme.of(context).colorScheme.error)), 139 + child: Text('Clear', style: TextStyle(color: context.colorScheme.error)), 139 140 ), 140 141 ], 141 142 ), ··· 155 156 156 157 @override 157 158 Widget build(BuildContext context) { 158 - final colorScheme = Theme.of(context).colorScheme; 159 + final colorScheme = context.colorScheme; 159 160 160 161 return Material( 161 162 color: colorScheme.surface, ··· 201 202 contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), 202 203 isDense: true, 203 204 ), 204 - style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 13, color: Theme.of(context).colorScheme.onSurface), 205 + style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 13, color: context.colorScheme.onSurface), 205 206 onChanged: (query) => context.read<LogViewerCubit>().setSearchQuery(query), 206 207 ), 207 208 ); ··· 245 246 labelStyle: TextStyle( 246 247 fontSize: 12, 247 248 fontWeight: FontWeight.w600, 248 - color: isEnabled ? Theme.of(context).colorScheme.onPrimary : null, 249 + color: isEnabled ? context.colorScheme.onPrimary : null, 249 250 ), 250 - selectedColor: Theme.of(context).colorScheme.primary, 251 - backgroundColor: Theme.of(context).colorScheme.surface, 252 - side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant), 251 + selectedColor: context.colorScheme.primary, 252 + backgroundColor: context.colorScheme.surface, 253 + side: BorderSide(color: context.colorScheme.outlineVariant), 253 254 showCheckmark: false, 254 255 ); 255 256 }, ··· 292 293 return ListView.separated( 293 294 controller: controller, 294 295 itemCount: state.filteredEntries.length, 295 - separatorBuilder: (context, index) => Divider(height: 1, color: Theme.of(context).colorScheme.outlineVariant), 296 + separatorBuilder: (context, index) => Divider(height: 1, color: context.colorScheme.outlineVariant), 296 297 itemBuilder: (context, index) { 297 298 return _LogEntryTile(entry: state.filteredEntries[index]); 298 299 }, ··· 328 329 children: [ 329 330 Text( 330 331 widget.entry.formatTimestamp(), 331 - style: TextStyle( 332 - fontFamily: 'JetBrains Mono', 333 - fontSize: 11, 334 - color: Theme.of(context).colorScheme.outline, 335 - ), 332 + style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: context.colorScheme.outline), 336 333 ), 337 334 const SizedBox(width: 8), 338 335 Container( ··· 355 352 children: [ 356 353 Text( 357 354 widget.entry.message, 358 - style: TextStyle( 359 - fontFamily: 'JetBrains Mono', 360 - fontSize: 12, 361 - color: Theme.of(context).colorScheme.onSurface, 362 - ), 355 + style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12, color: context.colorScheme.onSurface), 363 356 maxLines: _expanded ? null : 2, 364 357 overflow: _expanded ? null : TextOverflow.ellipsis, 365 358 ), ··· 371 364 style: TextStyle( 372 365 fontFamily: 'JetBrains Mono', 373 366 fontSize: 11, 374 - color: Theme.of(context).colorScheme.outline, 367 + color: context.colorScheme.outline, 375 368 ), 376 369 ), 377 370 ), ··· 385 378 } 386 379 387 380 Color _getLevelColor(BuildContext context, Level level) { 388 - final colorScheme = Theme.of(context).colorScheme; 381 + final colorScheme = context.colorScheme; 389 382 switch (level) { 390 383 case Level.fatal: 391 384 case Level.error: ··· 400 393 } 401 394 402 395 Color _getBadgeColor(BuildContext context, Level level) { 403 - final colorScheme = Theme.of(context).colorScheme; 396 + final colorScheme = context.colorScheme; 404 397 switch (level) { 405 398 case Level.fatal: 406 399 return colorScheme.error;
+2 -1
lib/features/messages/presentation/convo_list_screen.dart
··· 3 3 import 'package:lazurite/core/router/app_shell.dart'; 4 4 import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; 5 5 import 'package:lazurite/features/messages/presentation/widgets/convo_list_pane.dart'; 6 + import 'package:lazurite/core/theme/theme_extensions.dart'; 6 7 7 8 class ConvoListScreen extends StatefulWidget { 8 9 const ConvoListScreen({super.key}); ··· 40 41 return Scaffold( 41 42 appBar: AppBar( 42 43 leading: const AppShellMenuButton(), 43 - title: Text('Messages', style: Theme.of(context).textTheme.titleMedium), 44 + title: Text('Messages', style: context.textTheme.titleMedium), 44 45 bottom: TabBar( 45 46 controller: _tabController, 46 47 tabs: const [
+4 -7
lib/features/messages/presentation/widgets/convo_list_pane.dart
··· 10 10 import 'package:lazurite/shared/presentation/widgets/empty_state.dart'; 11 11 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 12 12 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 13 + import 'package:lazurite/core/theme/theme_extensions.dart'; 13 14 14 15 class ConvoListPane extends StatefulWidget { 15 16 const ConvoListPane({super.key, required this.tab}); ··· 169 170 child: Column( 170 171 mainAxisSize: MainAxisSize.min, 171 172 children: [ 172 - Icon(Icons.cloud_off_outlined, size: 48, color: Theme.of(context).colorScheme.outline), 173 + Icon(Icons.cloud_off_outlined, size: 48, color: context.colorScheme.outline), 173 174 const SizedBox(height: 12), 174 - Text('No connection', style: Theme.of(context).textTheme.titleMedium), 175 + Text('No connection', style: context.textTheme.titleMedium), 175 176 const SizedBox(height: 8), 176 - Text( 177 - 'Reconnect to load messages.', 178 - textAlign: TextAlign.center, 179 - style: Theme.of(context).textTheme.bodyMedium, 180 - ), 177 + Text('Reconnect to load messages.', textAlign: TextAlign.center, style: context.textTheme.bodyMedium), 181 178 ], 182 179 ), 183 180 ),
+20 -27
lib/features/moderation/presentation/screens/labeler_detail_screen.dart
··· 2 2 import 'package:bluesky/app_bsky_labeler_defs.dart'; 3 3 import 'package:flutter/material.dart'; 4 4 import 'package:flutter_bloc/flutter_bloc.dart'; 5 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 6 import 'package:lazurite/features/moderation/data/moderation_service.dart'; 6 7 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 7 8 import 'package:lazurite/features/moderation/presentation/widgets/moderated_avatar.dart'; ··· 100 101 child: Column( 101 102 mainAxisSize: MainAxisSize.min, 102 103 children: [ 103 - Text('Unable to load labeler', style: Theme.of(context).textTheme.titleMedium), 104 + Text('Unable to load labeler', style: context.textTheme.titleMedium), 104 105 const SizedBox(height: 8), 105 106 Text('${snapshot.error}', textAlign: TextAlign.center), 106 107 const SizedBox(height: 16), ··· 124 125 Container( 125 126 padding: const EdgeInsets.all(20), 126 127 decoration: BoxDecoration( 127 - color: Theme.of(context).colorScheme.surfaceContainerLow, 128 + color: context.colorScheme.surfaceContainerLow, 128 129 borderRadius: BorderRadius.circular(24), 129 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 130 + border: Border.all(color: context.colorScheme.outlineVariant), 130 131 ), 131 132 child: Column( 132 133 crossAxisAlignment: CrossAxisAlignment.start, ··· 147 148 children: [ 148 149 Text( 149 150 creator.displayName ?? creator.handle, 150 - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700), 151 + style: context.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700), 151 152 ), 152 153 const SizedBox(height: 4), 153 154 Text( 154 155 '@${creator.handle}', 155 - style: Theme.of( 156 - context, 157 - ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 156 + style: context.textTheme.bodyMedium?.copyWith( 157 + color: context.colorScheme.onSurfaceVariant, 158 + ), 158 159 ), 159 160 ], 160 161 ), ··· 163 164 ), 164 165 if (creator.description?.isNotEmpty ?? false) ...[ 165 166 const SizedBox(height: 16), 166 - Text(creator.description!, style: Theme.of(context).textTheme.bodyMedium), 167 + Text(creator.description!, style: context.textTheme.bodyMedium), 167 168 ], 168 169 const SizedBox(height: 16), 169 170 Wrap( ··· 193 194 const SizedBox(height: 24), 194 195 Text( 195 196 'Published policies'.toUpperCase(), 196 - style: Theme.of( 197 - context, 198 - ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 197 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 199 198 ), 200 199 const SizedBox(height: 8), 201 200 Container( 202 201 padding: const EdgeInsets.all(16), 203 202 decoration: BoxDecoration( 204 - color: Theme.of(context).colorScheme.surfaceContainerLowest, 203 + color: context.colorScheme.surfaceContainerLowest, 205 204 borderRadius: BorderRadius.circular(20), 206 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 205 + border: Border.all(color: context.colorScheme.outlineVariant), 207 206 ), 208 207 child: Wrap( 209 208 spacing: 8, ··· 214 213 const SizedBox(height: 24), 215 214 Text( 216 215 'Label preferences'.toUpperCase(), 217 - style: Theme.of( 218 - context, 219 - ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 216 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 220 217 ), 221 218 const SizedBox(height: 8), 222 219 if (definitions.isEmpty) ··· 236 233 children: [ 237 234 Text( 238 235 formatLocalizedLabelName(definition.locales, locale, fallback: definition.identifier), 239 - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 236 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 240 237 ), 241 238 const SizedBox(height: 6), 242 239 Text( ··· 245 242 locale, 246 243 fallback: 'No description available for this label.', 247 244 ), 248 - style: Theme.of( 249 - context, 250 - ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 245 + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), 251 246 ), 252 247 const SizedBox(height: 12), 253 248 Wrap( ··· 287 282 const SizedBox(height: 10), 288 283 Text( 289 284 'Enable adult content to change this 18+ label.', 290 - style: Theme.of( 291 - context, 292 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 285 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 293 286 ), 294 287 ], 295 288 ], ··· 330 323 return Container( 331 324 padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), 332 325 decoration: BoxDecoration( 333 - color: Theme.of(context).colorScheme.surfaceContainerHigh, 326 + color: context.colorScheme.surfaceContainerHigh, 334 327 borderRadius: BorderRadius.circular(999), 335 328 ), 336 - child: Text(label, style: Theme.of(context).textTheme.labelSmall), 329 + child: Text(label, style: context.textTheme.labelSmall), 337 330 ); 338 331 } 339 332 } ··· 347 340 Widget build(BuildContext context) { 348 341 return Container( 349 342 decoration: BoxDecoration( 350 - color: Theme.of(context).colorScheme.surfaceContainerLowest, 343 + color: context.colorScheme.surfaceContainerLowest, 351 344 borderRadius: BorderRadius.circular(20), 352 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 345 + border: Border.all(color: context.colorScheme.outlineVariant), 353 346 ), 354 347 child: child, 355 348 );
+18 -21
lib/features/moderation/presentation/screens/moderation_settings_screen.dart
··· 9 9 import 'package:lazurite/shared/presentation/helpers/snackbar_helper.dart'; 10 10 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 11 11 import 'package:lazurite/shared/utils/format_utils.dart'; 12 + import 'package:lazurite/core/theme/theme_extensions.dart'; 12 13 13 14 class ModerationSettingsScreen extends StatefulWidget { 14 15 const ModerationSettingsScreen({super.key}); ··· 152 153 const SizedBox(height: 12), 153 154 Text( 154 155 'Paste a labeler DID to review and subscribe to its labels.', 155 - style: Theme.of(context).textTheme.bodySmall, 156 + style: context.textTheme.bodySmall, 156 157 ), 157 158 ], 158 159 ), ··· 189 190 child: Column( 190 191 mainAxisSize: MainAxisSize.min, 191 192 children: [ 192 - Text('Failed to load moderation settings', style: Theme.of(context).textTheme.titleMedium), 193 + Text('Failed to load moderation settings', style: context.textTheme.titleMedium), 193 194 const SizedBox(height: 8), 194 195 Text('${snapshot.error}', textAlign: TextAlign.center), 195 196 const SizedBox(height: 16), ··· 228 229 title: 'Built-in labeler', 229 230 trailing: Text( 230 231 'Always on', 231 - style: Theme.of( 232 - context, 233 - ).textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.primary), 232 + style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.primary), 234 233 ), 235 234 ), 236 235 const SizedBox(height: 8), ··· 308 307 309 308 @override 310 309 Widget build(BuildContext context) { 311 - final colorScheme = Theme.of(context).colorScheme; 310 + final colorScheme = context.colorScheme; 312 311 313 312 return Container( 314 313 padding: const EdgeInsets.all(20), ··· 322 321 children: [ 323 322 Text( 324 323 title.toUpperCase(), 325 - style: Theme.of(context).textTheme.labelLarge?.copyWith(letterSpacing: 1.1, fontWeight: FontWeight.w700), 324 + style: context.textTheme.labelLarge?.copyWith(letterSpacing: 1.1, fontWeight: FontWeight.w700), 326 325 ), 327 326 const SizedBox(height: 10), 328 - Text(title, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700)), 327 + Text(title, style: context.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700)), 329 328 const SizedBox(height: 8), 330 - Text(subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), 329 + Text(subtitle, style: context.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)), 331 330 ], 332 331 ), 333 332 ); ··· 347 346 Expanded( 348 347 child: Text( 349 348 title.toUpperCase(), 350 - style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 349 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 0.8), 351 350 ), 352 351 ), 353 352 ?trailing, ··· 365 364 Widget build(BuildContext context) { 366 365 return Container( 367 366 decoration: BoxDecoration( 368 - color: Theme.of(context).colorScheme.surfaceContainerLowest, 367 + color: context.colorScheme.surfaceContainerLowest, 369 368 borderRadius: BorderRadius.circular(20), 370 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 369 + border: Border.all(color: context.colorScheme.outlineVariant), 371 370 ), 372 371 child: child, 373 372 ); ··· 421 420 creator.displayName ?? creator.handle, 422 421 maxLines: 1, 423 422 overflow: TextOverflow.ellipsis, 424 - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 423 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 425 424 ), 426 425 ), 427 426 if (isOfficial) 428 427 Container( 429 428 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 430 429 decoration: BoxDecoration( 431 - color: Theme.of(context).colorScheme.primaryContainer, 430 + color: context.colorScheme.primaryContainer, 432 431 borderRadius: BorderRadius.circular(999), 433 432 ), 434 433 child: Text( 435 434 'Built-in', 436 - style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700), 435 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700), 437 436 ), 438 437 ), 439 438 ], ··· 441 440 const SizedBox(height: 2), 442 441 Text( 443 442 '@${creator.handle}', 444 - style: Theme.of( 445 - context, 446 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 443 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 447 444 ), 448 445 if (creator.description?.isNotEmpty ?? false) ...[ 449 446 const SizedBox(height: 8), ··· 451 448 creator.description!, 452 449 maxLines: 2, 453 450 overflow: TextOverflow.ellipsis, 454 - style: Theme.of(context).textTheme.bodyMedium, 451 + style: context.textTheme.bodyMedium, 455 452 ), 456 453 ], 457 454 const SizedBox(height: 10), ··· 492 489 return Container( 493 490 padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), 494 491 decoration: BoxDecoration( 495 - color: Theme.of(context).colorScheme.surfaceContainerHigh, 492 + color: context.colorScheme.surfaceContainerHigh, 496 493 borderRadius: BorderRadius.circular(999), 497 494 ), 498 - child: Text(label, style: Theme.of(context).textTheme.labelSmall), 495 + child: Text(label, style: context.textTheme.labelSmall), 499 496 ); 500 497 } 501 498 }
+2 -1
lib/features/moderation/presentation/widgets/moderated_avatar.dart
··· 1 1 import 'package:bluesky/moderation.dart' as bsky_moderation; 2 2 import 'package:flutter/material.dart'; 3 + import 'package:lazurite/core/theme/theme_extensions.dart'; 3 4 4 5 class ModeratedAvatar extends StatelessWidget { 5 6 const ModeratedAvatar({ ··· 25 26 26 27 @override 27 28 Widget build(BuildContext context) { 28 - final colorScheme = Theme.of(context).colorScheme; 29 + final colorScheme = context.colorScheme; 29 30 final shouldMask = ui?.blur ?? false; 30 31 31 32 return Container(
+4 -5
lib/features/moderation/presentation/widgets/moderated_blur_overlay.dart
··· 1 1 import 'dart:ui'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:bluesky/moderation.dart' as bsky_moderation; 4 5 import 'package:flutter/material.dart'; ··· 33 34 return widget.child; 34 35 } 35 36 36 - final colorScheme = Theme.of(context).colorScheme; 37 + final colorScheme = context.colorScheme; 37 38 final canReveal = !widget.ui.noOverride; 38 39 39 40 Widget content = Stack( ··· 65 66 Text( 66 67 moderationOverlayTitle(widget.ui, fallback: widget.fallbackLabel), 67 68 textAlign: TextAlign.center, 68 - style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), 69 + style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), 69 70 ), 70 71 const SizedBox(height: 6), 71 72 Text( ··· 73 74 ? 'Hidden by your moderation settings. You can reveal it for this view.' 74 75 : 'Hidden by your moderation settings and cannot be revealed here.', 75 76 textAlign: TextAlign.center, 76 - style: Theme.of( 77 - context, 78 - ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, height: 1.35), 77 + style: context.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant, height: 1.35), 79 78 ), 80 79 if (canReveal) ...[ 81 80 const SizedBox(height: 14),
+3 -2
lib/features/moderation/presentation/widgets/moderation_badge_row.dart
··· 1 1 import 'package:bluesky/moderation.dart' as bsky_moderation; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; 4 + import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 5 6 class ModerationBadgeRow extends StatelessWidget { 6 7 const ModerationBadgeRow({super.key, required this.ui, this.padding = EdgeInsets.zero}); ··· 15 16 return const SizedBox.shrink(); 16 17 } 17 18 18 - final colorScheme = Theme.of(context).colorScheme; 19 + final colorScheme = context.colorScheme; 19 20 20 21 Widget chipFor(ModerationBadgeDescriptor descriptor, double maxWidth) { 21 22 final isAlert = descriptor.tone == ModerationBadgeTone.alert; ··· 45 46 descriptor.label, 46 47 maxLines: 2, 47 48 overflow: TextOverflow.ellipsis, 48 - style: Theme.of(context).textTheme.labelSmall?.copyWith( 49 + style: context.textTheme.labelSmall?.copyWith( 49 50 color: foreground, 50 51 fontWeight: FontWeight.w700, 51 52 letterSpacing: 0.2,
+4 -6
lib/features/notifications/presentation/widgets/grouped_notification_list_item.dart
··· 7 7 import 'package:lazurite/features/moderation/presentation/widgets/moderated_avatar.dart'; 8 8 import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; 9 9 import 'package:lazurite/shared/utils/format_utils.dart'; 10 + import 'package:lazurite/core/theme/theme_extensions.dart'; 10 11 11 12 class NotificationGroup { 12 13 const NotificationGroup({required this.notifications}); ··· 98 99 Container( 99 100 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 100 101 decoration: BoxDecoration( 101 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 102 + color: context.colorScheme.surfaceContainerHighest, 102 103 borderRadius: BorderRadius.circular(999), 103 104 ), 104 - child: Text( 105 - '${group.count}', 106 - style: Theme.of(context).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700), 107 - ), 105 + child: Text('${group.count}', style: context.textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700)), 108 106 ), 109 107 ], 110 108 ); ··· 119 117 return DecoratedBox( 120 118 decoration: BoxDecoration( 121 119 shape: BoxShape.circle, 122 - border: Border.all(color: Theme.of(context).colorScheme.surface, width: 2), 120 + border: Border.all(color: context.colorScheme.surface, width: 2), 123 121 ), 124 122 child: ModeratedAvatar( 125 123 size: 28,
+4 -7
lib/features/notifications/presentation/widgets/notifications_pane.dart
··· 9 9 import 'package:lazurite/shared/presentation/widgets/empty_state.dart'; 10 10 import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 11 11 import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 12 + import 'package:lazurite/core/theme/theme_extensions.dart'; 12 13 13 14 class NotificationsPane extends StatefulWidget { 14 15 const NotificationsPane({super.key}); ··· 229 230 child: Column( 230 231 mainAxisSize: MainAxisSize.min, 231 232 children: [ 232 - Icon(Icons.cloud_off_outlined, size: 48, color: Theme.of(context).colorScheme.outline), 233 + Icon(Icons.cloud_off_outlined, size: 48, color: context.colorScheme.outline), 233 234 const SizedBox(height: 12), 234 - Text('No connection', style: Theme.of(context).textTheme.titleMedium), 235 + Text('No connection', style: context.textTheme.titleMedium), 235 236 const SizedBox(height: 8), 236 - Text( 237 - 'Reconnect to load notifications.', 238 - textAlign: TextAlign.center, 239 - style: Theme.of(context).textTheme.bodyMedium, 240 - ), 237 + Text('Reconnect to load notifications.', textAlign: TextAlign.center, style: context.textTheme.bodyMedium), 241 238 ], 242 239 ), 243 240 ),
+27 -28
lib/features/profile/presentation/follow_audit_screen.dart
··· 1 1 import 'dart:math' as math; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:flutter/material.dart'; 4 5 import 'package:flutter/services.dart'; ··· 43 44 child: Text( 44 45 '${state.failedProfiles} profile(s) could not be loaded.', 45 46 key: const Key('follow_audit_failed_warning'), 46 - style: TextStyle(color: Theme.of(context).colorScheme.tertiary), 47 + style: TextStyle(color: context.colorScheme.tertiary), 47 48 ), 48 49 ), 49 50 ), ··· 57 58 child: Text( 58 59 'Unfollowed ${state.unfollowedCount} account(s)', 59 60 key: const Key('follow_audit_complete_message'), 60 - style: Theme.of(context).textTheme.titleMedium, 61 + style: context.textTheme.titleMedium, 61 62 ), 62 63 ), 63 64 ), ··· 83 84 return Row( 84 85 children: [ 85 86 SizedBox(width: 280, child: filters), 86 - VerticalDivider(width: 1, color: Theme.of(context).colorScheme.outlineVariant), 87 + VerticalDivider(width: 1, color: context.colorScheme.outlineVariant), 87 88 Expanded( 88 89 child: _ResultsPanel(state: state, visibleEntries: visibleEntries), 89 90 ), ··· 138 139 margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), 139 140 padding: const EdgeInsets.all(16), 140 141 decoration: BoxDecoration( 141 - color: Theme.of(context).colorScheme.surfaceContainerLowest, 142 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 142 + color: context.colorScheme.surfaceContainerLowest, 143 + border: Border.all(color: context.colorScheme.outlineVariant), 143 144 ), 144 145 child: Column( 145 146 crossAxisAlignment: CrossAxisAlignment.start, 146 147 children: [ 147 148 Text( 148 149 'AUDIT FOLLOWERS', 149 - style: Theme.of(context).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 1.1), 150 + style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700, letterSpacing: 1.1), 150 151 ), 151 152 const SizedBox(height: 6), 152 153 Text( 153 154 totalFollows > 0 154 155 ? '$totalFollows follows scanned for problematic accounts' 155 156 : 'Scan your follows for deleted, suspended, blocked, and hidden accounts.', 156 - style: Theme.of(context).textTheme.bodyMedium, 157 + style: context.textTheme.bodyMedium, 157 158 ), 158 159 ], 159 160 ), ··· 236 237 margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), 237 238 padding: const EdgeInsets.all(12), 238 239 decoration: BoxDecoration( 239 - border: Border.all(color: Theme.of(context).colorScheme.error), 240 - color: Theme.of(context).colorScheme.errorContainer, 240 + border: Border.all(color: context.colorScheme.error), 241 + color: context.colorScheme.errorContainer, 241 242 ), 242 243 child: Row( 243 244 crossAxisAlignment: CrossAxisAlignment.start, 244 245 children: [ 245 - Icon(Icons.error_outline, color: Theme.of(context).colorScheme.onErrorContainer), 246 + Icon(Icons.error_outline, color: context.colorScheme.onErrorContainer), 246 247 const SizedBox(width: 12), 247 248 Expanded( 248 249 child: Column( 249 250 crossAxisAlignment: CrossAxisAlignment.start, 250 251 children: [ 251 - Text(message, style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer)), 252 + Text(message, style: TextStyle(color: context.colorScheme.onErrorContainer)), 252 253 const SizedBox(height: 8), 253 254 OutlinedButton( 254 255 key: const Key('follow_audit_retry_button'), ··· 296 297 ) 297 298 : Container( 298 299 key: const Key('follow_audit_filter_sidebar'), 299 - color: Theme.of(context).colorScheme.surfaceContainerLowest, 300 + color: context.colorScheme.surfaceContainerLowest, 300 301 child: ListView( 301 302 padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), 302 303 children: [ ··· 304 305 padding: const EdgeInsets.fromLTRB(4, 8, 4, 8), 305 306 child: Text( 306 307 'FILTERS', 307 - style: Theme.of( 308 - context, 309 - ).textTheme.labelMedium?.copyWith(letterSpacing: 1.0, fontWeight: FontWeight.w700), 308 + style: context.textTheme.labelMedium?.copyWith(letterSpacing: 1.0, fontWeight: FontWeight.w700), 310 309 ), 311 310 ), 312 311 for (final status in FollowStatus.values) ··· 353 352 width: compact ? 220 : null, 354 353 padding: EdgeInsets.symmetric(horizontal: compact ? 10 : 12, vertical: compact ? 8 : 10), 355 354 decoration: BoxDecoration( 356 - color: Theme.of(context).colorScheme.surfaceContainerLowest, 357 - border: Border.all(color: Theme.of(context).colorScheme.outlineVariant), 355 + color: context.colorScheme.surfaceContainerLowest, 356 + border: Border.all(color: context.colorScheme.outlineVariant), 358 357 ), 359 358 child: Column( 360 359 crossAxisAlignment: CrossAxisAlignment.start, ··· 366 365 _labelForStatus(status).toUpperCase(), 367 366 maxLines: 1, 368 367 overflow: TextOverflow.ellipsis, 369 - style: Theme.of(context).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700), 368 + style: context.textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700), 370 369 ), 371 370 ), 372 371 Container( 373 372 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 374 373 decoration: BoxDecoration( 375 - color: Theme.of(context).colorScheme.surfaceContainerHigh, 374 + color: context.colorScheme.surfaceContainerHigh, 376 375 borderRadius: BorderRadius.circular(999), 377 376 ), 378 - child: Text('$count', style: Theme.of(context).textTheme.labelSmall), 377 + child: Text('$count', style: context.textTheme.labelSmall), 379 378 ), 380 379 ], 381 380 ), ··· 408 407 'Select All', 409 408 maxLines: 1, 410 409 overflow: TextOverflow.ellipsis, 411 - style: Theme.of(context).textTheme.bodySmall, 410 + style: context.textTheme.bodySmall, 412 411 ), 413 412 ), 414 413 ], ··· 481 480 482 481 @override 483 482 Widget build(BuildContext context) { 484 - final selectedTint = Theme.of(context).colorScheme.error.withValues(alpha: 0.08); 483 + final selectedTint = context.colorScheme.error.withValues(alpha: 0.08); 485 484 486 485 return Container( 487 486 key: Key('follow_audit_row_${item.record.rkey}'), 488 - color: item.selected ? selectedTint : Theme.of(context).colorScheme.surfaceContainerLowest, 487 + color: item.selected ? selectedTint : context.colorScheme.surfaceContainerLowest, 489 488 child: Material( 490 489 color: Colors.transparent, 491 490 child: InkWell( ··· 513 512 child: Text( 514 513 item.handle ?? item.record.subjectDid, 515 514 overflow: TextOverflow.ellipsis, 516 - style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), 515 + style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), 517 516 ), 518 517 ), 519 518 const SizedBox(height: 2), ··· 533 532 _truncateDid(item.record.subjectDid), 534 533 maxLines: 1, 535 534 overflow: TextOverflow.ellipsis, 536 - style: Theme.of(context).textTheme.bodySmall?.copyWith( 535 + style: context.textTheme.bodySmall?.copyWith( 537 536 fontFamily: 'JetBrains Mono', 538 - color: Theme.of(context).colorScheme.onSurfaceVariant, 537 + color: context.colorScheme.onSurfaceVariant, 539 538 ), 540 539 ), 541 540 ), ··· 576 575 width: double.infinity, 577 576 padding: const EdgeInsets.fromLTRB(16, 10, 16, 16), 578 577 decoration: BoxDecoration( 579 - border: Border(top: BorderSide(color: Theme.of(context).colorScheme.outlineVariant)), 578 + border: Border(top: BorderSide(color: context.colorScheme.outlineVariant)), 580 579 ), 581 580 child: Text( 582 581 'Selected: $selectedCount/$total', 583 - style: Theme.of(context).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), 582 + style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), 584 583 ), 585 584 ); 586 585 }
+15 -21
lib/features/profile/presentation/profile_context_screen.dart
··· 6 6 import 'package:lazurite/features/moderation/presentation/widgets/moderated_avatar.dart'; 7 7 import 'package:lazurite/features/profile/cubit/profile_context_cubit.dart'; 8 8 import 'package:lazurite/features/profile/data/profile_context_repository.dart'; 9 + import 'package:lazurite/core/theme/theme_extensions.dart'; 9 10 10 11 class ProfileContextScreen extends StatefulWidget { 11 12 const ProfileContextScreen({super.key, required this.handle}); ··· 68 69 const Text('Profile Context'), 69 70 Text( 70 71 '@${widget.handle}', 71 - style: Theme.of( 72 - context, 73 - ).textTheme.labelSmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 72 + style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 74 73 ), 75 74 ], 76 75 ), ··· 136 135 children: [ 137 136 Text( 138 137 '${state.blockedByCount} account${state.blockedByCount == 1 ? '' : 's'}', 139 - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 138 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 140 139 ), 141 140 const Spacer(), 142 141 if (state.blockedByStatus == ProfileContextTabStatus.initial) ··· 260 259 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 261 260 child: Text( 262 261 '${state.blockingCount} account${state.blockingCount == 1 ? '' : 's'}', 263 - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 262 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 264 263 ), 265 264 ), 266 265 ), ··· 410 409 411 410 @override 412 411 Widget build(BuildContext context) { 413 - final colorScheme = Theme.of(context).colorScheme; 412 + final colorScheme = context.colorScheme; 414 413 final displayName = profile.displayName?.isNotEmpty == true ? profile.displayName! : profile.handle; 415 414 final initials = _initials(displayName); 416 415 ··· 437 436 438 437 @override 439 438 Widget build(BuildContext context) { 440 - final colorScheme = Theme.of(context).colorScheme; 439 + final colorScheme = context.colorScheme; 441 440 return Card( 442 441 child: Padding( 443 442 padding: const EdgeInsets.symmetric(vertical: 8), ··· 471 470 472 471 @override 473 472 Widget build(BuildContext context) { 474 - final colorScheme = Theme.of(context).colorScheme; 473 + final colorScheme = context.colorScheme; 475 474 return ListTile( 476 475 leading: Icon(Icons.person_off_outlined, color: colorScheme.onSurfaceVariant), 477 476 title: Text(did, maxLines: 1, overflow: TextOverflow.ellipsis), ··· 515 514 SliverToBoxAdapter( 516 515 child: Padding( 517 516 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 518 - child: Text( 519 - section.title, 520 - style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), 521 - ), 517 + child: Text(section.title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700)), 522 518 ), 523 519 ), 524 520 SliverList.builder( ··· 575 571 576 572 @override 577 573 Widget build(BuildContext context) { 578 - final colorScheme = Theme.of(context).colorScheme; 574 + final colorScheme = context.colorScheme; 579 575 final description = list.description?.trim(); 580 576 581 577 return Padding( ··· 609 605 '@${list.creator.handle}', 610 606 maxLines: 1, 611 607 overflow: TextOverflow.ellipsis, 612 - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 608 + style: context.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), 613 609 ), 614 610 ], 615 611 ), ··· 643 639 644 640 @override 645 641 Widget build(BuildContext context) { 646 - final colorScheme = Theme.of(context).colorScheme; 642 + final colorScheme = context.colorScheme; 647 643 final (label, color) = switch (purpose) { 648 644 bsky_graph.KnownListPurpose.appBskyGraphDefsCuratelist => ('CURATE', colorScheme.primary), 649 645 bsky_graph.KnownListPurpose.appBskyGraphDefsModlist => ('MOD', colorScheme.error), ··· 660 656 ), 661 657 child: Text( 662 658 label, 663 - style: Theme.of( 664 - context, 665 - ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, color: color, letterSpacing: 0.6), 659 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700, color: color, letterSpacing: 0.6), 666 660 ), 667 661 ); 668 662 } ··· 676 670 677 671 @override 678 672 Widget build(BuildContext context) { 679 - final colorScheme = Theme.of(context).colorScheme; 673 + final colorScheme = context.colorScheme; 680 674 681 675 return DecoratedBox( 682 676 decoration: BoxDecoration(color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(999)), ··· 687 681 children: [ 688 682 Icon(icon, size: 16, color: colorScheme.onSurfaceVariant), 689 683 const SizedBox(width: 6), 690 - Text(label, style: Theme.of(context).textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), 684 + Text(label, style: context.textTheme.labelMedium?.copyWith(color: colorScheme.onSurfaceVariant)), 691 685 ], 692 686 ), 693 687 ), ··· 721 715 722 716 @override 723 717 Widget build(BuildContext context) { 724 - final colorScheme = Theme.of(context).colorScheme; 718 + final colorScheme = context.colorScheme; 725 719 return AnimatedBuilder( 726 720 animation: _animation, 727 721 builder: (context, _) {
+19 -46
lib/features/profile/presentation/profile_screen.dart
··· 7 7 import 'package:go_router/go_router.dart'; 8 8 import 'package:intl/intl.dart'; 9 9 import 'package:lazurite/core/router/app_shell.dart'; 10 + import 'package:lazurite/core/theme/color_filters.dart'; 10 11 import 'package:lazurite/core/theme/feed_layout.dart'; 12 + import 'package:lazurite/core/theme/spacing.dart'; 13 + import 'package:lazurite/core/theme/theme_extensions.dart'; 11 14 import 'package:lazurite/core/widgets/sliver_tab_bar_delegate.dart'; 12 15 import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 13 16 import 'package:lazurite/features/compose/presentation/compose_route_args.dart'; ··· 41 44 import 'package:share_plus/share_plus.dart'; 42 45 import 'package:url_launcher/url_launcher.dart'; 43 46 44 - const _greyscale = ColorFilter.matrix(<double>[ 45 - 0.2126, 46 - 0.7152, 47 - 0.0722, 48 - 0, 49 - 0, 50 - 0.2126, 51 - 0.7152, 52 - 0.0722, 53 - 0, 54 - 0, 55 - 0.2126, 56 - 0.7152, 57 - 0.0722, 58 - 0, 59 - 0, 60 - 0, 61 - 0, 62 - 0, 63 - 1, 64 - 0, 65 - ]); 66 - 67 47 class ProfileScreen extends StatefulWidget { 68 48 const ProfileScreen({super.key, this.actor, this.showBackButton = false}); 69 49 ··· 211 191 SliverToBoxAdapter( 212 192 child: switch (profileState.status) { 213 193 ProfileStatus.loading => const Padding( 214 - padding: EdgeInsets.all(24), 194 + padding: AppInsets.allLg, 215 195 child: Center(child: CircularProgressIndicator()), 216 196 ), 217 197 ProfileStatus.error => _buildProfileError(context, profileState.errorMessage), ··· 258 238 final width = MediaQuery.of(context).size.width; 259 239 final coverHeight = width >= 600 ? 256.0 : 192.0; 260 240 final avatarSize = width >= 600 ? 128.0 : 96.0; 261 - final colorScheme = Theme.of(context).colorScheme; 241 + final colorScheme = context.colorScheme; 262 242 263 243 Widget coverContent; 264 244 if (profile?.banner != null) { 265 245 coverContent = ColorFiltered( 266 - colorFilter: _greyscale, 246 + colorFilter: AppColorFilters.greyscale, 267 247 child: Image.network( 268 248 profile!.banner!, 269 249 fit: BoxFit.cover, ··· 305 285 } 306 286 307 287 Widget _buildSquareAvatar(BuildContext context, ProfileViewDetailed? profile, double size) { 308 - final colorScheme = Theme.of(context).colorScheme; 288 + final colorScheme = context.colorScheme; 309 289 final moderationService = maybeModerationService(context); 310 290 final avatarUi = profile == null 311 291 ? const bsky_moderation.ModerationUI() ··· 323 303 initials: formatInitials(profile?.displayName ?? profile?.handle ?? '?'), 324 304 shape: BoxShape.rectangle, 325 305 border: Border.all(color: colorScheme.surfaceContainerLowest, width: 4), 326 - placeholderTextStyle: Theme.of(context).textTheme.headlineSmall, 306 + placeholderTextStyle: context.textTheme.headlineSmall, 327 307 ), 328 308 ); 329 309 } ··· 334 314 child: Column( 335 315 crossAxisAlignment: CrossAxisAlignment.start, 336 316 children: [ 337 - Text('Unable to load profile', style: Theme.of(context).textTheme.titleLarge), 317 + Text('Unable to load profile', style: context.textTheme.titleLarge), 338 318 const SizedBox(height: 8), 339 - Text(errorMessage ?? 'Unknown error', style: Theme.of(context).textTheme.bodyMedium), 319 + Text(errorMessage ?? 'Unknown error', style: context.textTheme.bodyMedium), 340 320 const SizedBox(height: 12), 341 321 FilledButton(onPressed: _loadProfileAndFeed, child: const Text('Try again')), 342 322 ], ··· 347 327 Widget _buildProfileSummary(BuildContext context, ProfileViewDetailed? profile, bool isOwnProfile) { 348 328 if (profile == null) return const SizedBox.shrink(); 349 329 350 - final colorScheme = Theme.of(context).colorScheme; 351 - final textTheme = Theme.of(context).textTheme; 330 + final colorScheme = context.colorScheme; 331 + final textTheme = context.textTheme; 352 332 final moderationService = maybeModerationService(context); 353 333 final profileUi = 354 334 moderationService?.profileDetailedUi(profile, bsky_moderation.ModerationBehaviorContext.profileView) ?? ··· 427 407 final chip = Container( 428 408 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), 429 409 decoration: BoxDecoration( 430 - color: Theme.of(context).colorScheme.surfaceContainerHighest, 410 + color: context.colorScheme.surfaceContainerHighest, 431 411 borderRadius: BorderRadius.circular(999), 432 412 ), 433 413 child: Row( 434 414 mainAxisSize: MainAxisSize.min, 435 415 children: [ 436 - Icon(icon, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant), 416 + Icon(icon, size: 16, color: context.colorScheme.onSurfaceVariant), 437 417 const SizedBox(width: 6), 438 418 Flexible( 439 419 child: Text( 440 420 label, 441 - style: Theme.of( 442 - context, 443 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 421 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 444 422 ), 445 423 ), 446 424 ], ··· 452 430 } 453 431 454 432 Widget _buildStat(BuildContext context, int count, String label) { 455 - final colorScheme = Theme.of(context).colorScheme; 433 + final colorScheme = context.colorScheme; 456 434 return Column( 457 435 crossAxisAlignment: CrossAxisAlignment.start, 458 436 children: [ 459 - Text(formatCount(count), style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 437 + Text(formatCount(count), style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 460 438 Text( 461 439 label.toUpperCase(), 462 440 style: TextStyle(fontSize: 11, letterSpacing: 1.1, color: colorScheme.onSurfaceVariant), ··· 593 571 children: [ 594 572 Padding( 595 573 padding: const EdgeInsets.symmetric(vertical: 12), 596 - child: Text( 597 - 'Add to list', 598 - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 599 - ), 574 + child: Text('Add to list', style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), 600 575 ), 601 576 const Divider(height: 1), 602 577 Expanded( ··· 628 603 ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) 629 604 : Icon( 630 605 isMember ? Icons.check_circle : Icons.add_circle_outline, 631 - color: isMember 632 - ? Theme.of(context).colorScheme.primary 633 - : Theme.of(context).colorScheme.onSurfaceVariant, 606 + color: isMember ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, 634 607 ), 635 608 onTap: isToggling ? null : () => context.read<AddToListCubit>().toggleMembership(entry), 636 609 );
+4 -3
lib/features/profile/presentation/widgets/profile_action_buttons.dart
··· 1 1 import 'dart:async'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:flutter/material.dart'; 4 5 import 'package:flutter/services.dart'; ··· 61 62 label: 'Unblock', 62 63 onPressed: isOffline || onUnblock == null ? null : () => _confirmUnblock(context), 63 64 isLoading: isLoadingBlock, 64 - foregroundColor: Theme.of(context).colorScheme.onError, 65 - backgroundColor: Theme.of(context).colorScheme.error, 65 + foregroundColor: context.colorScheme.onError, 66 + backgroundColor: context.colorScheme.error, 66 67 tooltip: isOffline ? offlineActionMessage('unblock this account') : null, 67 68 ); 68 69 } ··· 192 193 context: context, 193 194 title: Row( 194 195 children: [ 195 - Icon(Icons.block, color: Theme.of(context).colorScheme.error), 196 + Icon(Icons.block, color: context.colorScheme.error), 196 197 const SizedBox(width: 8), 197 198 const Text('Block Account?'), 198 199 ],
+3 -2
lib/features/profile/presentation/widgets/report_dialog.dart
··· 4 4 import 'package:flutter/services.dart'; 5 5 import 'package:flutter_bloc/flutter_bloc.dart'; 6 6 import 'package:lazurite/features/profile/cubit/profile_action_cubit.dart'; 7 + import 'package:lazurite/core/theme/theme_extensions.dart'; 7 8 8 9 enum _ReportType { post, actor } 9 10 ··· 85 86 mainAxisSize: MainAxisSize.min, 86 87 crossAxisAlignment: CrossAxisAlignment.start, 87 88 children: [ 88 - Text('Reason', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), 89 + Text('Reason', style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), 89 90 const SizedBox(height: 8), 90 91 ..._reasonOptions.map((option) => _buildReasonOption(option)), 91 92 if (_requiresExplanation) ...[ 92 93 const SizedBox(height: 16), 93 94 Text( 94 95 'Explanation (required)', 95 - style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), 96 + style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), 96 97 ), 97 98 const SizedBox(height: 8), 98 99 TextField(
+2 -1
lib/features/profile/presentation/widgets/suggested_follows_sheet.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:go_router/go_router.dart'; 3 3 import 'package:lazurite/features/profile/presentation/widgets/suggested_follows_list.dart'; 4 + import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 5 6 class SuggestedFollowsSheet extends StatelessWidget { 6 7 const SuggestedFollowsSheet({super.key, required this.actor}); ··· 20 21 padding: const EdgeInsets.symmetric(vertical: 12), 21 22 child: Text( 22 23 'Suggested Follows', 23 - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 24 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), 24 25 ), 25 26 ), 26 27 const Divider(height: 1),
+8 -9
lib/features/search/presentation/hashtag_screen.dart
··· 14 14 import 'package:lazurite/features/search/data/hashtag_utils.dart'; 15 15 import 'package:lazurite/shared/presentation/widgets/options_sheet.dart'; 16 16 import 'package:lazurite/shared/utils/format_utils.dart'; 17 + import 'package:lazurite/core/theme/theme_extensions.dart'; 17 18 18 19 class HashtagScreen extends StatefulWidget { 19 20 const HashtagScreen({super.key, required this.tag}); ··· 88 89 mainAxisSize: MainAxisSize.min, 89 90 crossAxisAlignment: CrossAxisAlignment.start, 90 91 children: [ 91 - Text('Jump to hashtag', style: Theme.of(sheetContext).textTheme.titleMedium), 92 + Text('Jump to hashtag', style: sheetContext.textTheme.titleMedium), 92 93 const SizedBox(height: 12), 93 94 TextField( 94 95 controller: inputController, ··· 104 105 ), 105 106 if (suggestions.isNotEmpty) ...[ 106 107 const SizedBox(height: 16), 107 - Text('Related', style: Theme.of(sheetContext).textTheme.titleSmall), 108 + Text('Related', style: sheetContext.textTheme.titleSmall), 108 109 const SizedBox(height: 8), 109 110 Wrap( 110 111 spacing: 8, ··· 286 287 if (postUi.alert || postUi.inform) ...[const SizedBox(height: 10), ModerationBadgeRow(ui: postUi)], 287 288 if (record != null && record.text.isNotEmpty) ...[ 288 289 const SizedBox(height: 12), 289 - FacetText(text: record.text, facets: record.facets, style: Theme.of(context).textTheme.bodyLarge), 290 + FacetText(text: record.text, facets: record.facets, style: context.textTheme.bodyLarge), 290 291 ], 291 292 const SizedBox(height: 12), 292 293 _buildActions(context), ··· 322 323 children: [ 323 324 Text( 324 325 author.displayName ?? author.handle, 325 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 326 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 326 327 maxLines: 1, 327 328 overflow: TextOverflow.ellipsis, 328 329 ), 329 330 const SizedBox(height: 2), 330 331 Text( 331 332 '@${author.handle} · ${formatRelativeTime(createdAt)}', 332 - style: Theme.of( 333 - context, 334 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 333 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 335 334 ), 336 335 ], 337 336 ), ··· 354 353 } 355 354 356 355 Widget _buildActionButton(BuildContext context, IconData icon, String count) { 357 - final iconColor = Theme.of(context).colorScheme.onSurfaceVariant; 356 + final iconColor = context.colorScheme.onSurfaceVariant; 358 357 359 358 return InkWell( 360 359 onTap: () {}, ··· 366 365 Icon(icon, size: 18, color: iconColor), 367 366 if (count.isNotEmpty) ...[ 368 367 const SizedBox(width: 4), 369 - Text(count, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: iconColor)), 368 + Text(count, style: context.textTheme.bodySmall?.copyWith(color: iconColor)), 370 369 ], 371 370 ], 372 371 ),
+28 -40
lib/features/search/presentation/search_screen.dart
··· 19 19 import 'package:lazurite/shared/presentation/helpers/snackbar_helper.dart'; 20 20 import 'package:lazurite/shared/presentation/widgets/confirmation_dialog.dart'; 21 21 import 'package:lazurite/shared/utils/format_utils.dart'; 22 + import 'package:lazurite/core/theme/theme_extensions.dart'; 22 23 23 24 class SearchScreen extends StatefulWidget { 24 25 const SearchScreen({super.key}); ··· 164 165 if (showTypingHint) 165 166 Align( 166 167 alignment: Alignment.topLeft, 167 - child: Text( 168 - 'Start typing to search handles.', 169 - style: Theme.of(context).textTheme.bodySmall, 170 - ), 168 + child: Text('Start typing to search handles.', style: context.textTheme.bodySmall), 171 169 ), 172 170 AnimatedSize( 173 171 duration: const Duration(milliseconds: 180), ··· 416 414 child: Column( 417 415 mainAxisAlignment: MainAxisAlignment.center, 418 416 children: [ 419 - Text('Search failed', style: Theme.of(context).textTheme.titleMedium), 417 + Text('Search failed', style: context.textTheme.titleMedium), 420 418 const SizedBox(height: 8), 421 419 Text( 422 420 state.errorMessage ?? 'Unknown error', 423 421 textAlign: TextAlign.center, 424 - style: Theme.of(context).textTheme.bodySmall, 422 + style: context.textTheme.bodySmall, 425 423 ), 426 424 const SizedBox(height: 16), 427 425 FilledButton( ··· 457 455 child: Row( 458 456 mainAxisAlignment: MainAxisAlignment.spaceBetween, 459 457 children: [ 460 - Text('Recent Searches', style: Theme.of(context).textTheme.titleSmall), 458 + Text('Recent Searches', style: context.textTheme.titleSmall), 461 459 TextButton(onPressed: _onClearHistory, child: const Text('Clear All')), 462 460 ], 463 461 ), ··· 480 478 background: Container( 481 479 alignment: Alignment.centerRight, 482 480 padding: const EdgeInsets.only(right: 16), 483 - color: Theme.of(context).colorScheme.error, 484 - child: Icon(Icons.delete, color: Theme.of(context).colorScheme.onError), 481 + color: context.colorScheme.error, 482 + child: Icon(Icons.delete, color: context.colorScheme.onError), 485 483 ), 486 484 child: ListTile( 487 485 leading: const Icon(Icons.history), ··· 500 498 Widget _buildPostResults(BuildContext context, SearchState state) { 501 499 final posts = state.posts; 502 500 if (posts.isEmpty) { 503 - return Center(child: Text('No posts found', style: Theme.of(context).textTheme.bodyLarge)); 501 + return Center(child: Text('No posts found', style: context.textTheme.bodyLarge)); 504 502 } 505 503 506 504 return ListView.builder( ··· 520 518 Widget _buildActorResults(BuildContext context, SearchState state) { 521 519 final actors = state.actors; 522 520 if (actors.isEmpty) { 523 - return Center(child: Text('No people found', style: Theme.of(context).textTheme.bodyLarge)); 521 + return Center(child: Text('No people found', style: context.textTheme.bodyLarge)); 524 522 } 525 523 526 524 return ListView.builder( ··· 540 538 Widget _buildFeedResults(BuildContext context, SearchState state) { 541 539 final feeds = state.feeds; 542 540 if (feeds.isEmpty) { 543 - return Center(child: Text('No feeds found', style: Theme.of(context).textTheme.bodyLarge)); 541 + return Center(child: Text('No feeds found', style: context.textTheme.bodyLarge)); 544 542 } 545 543 546 544 return ListView.builder( ··· 572 570 Widget _buildStarterPackResults(BuildContext context, SearchState state) { 573 571 final packs = state.starterPacks; 574 572 if (packs.isEmpty) { 575 - return Center(child: Text('No starter packs found', style: Theme.of(context).textTheme.bodyLarge)); 573 + return Center(child: Text('No starter packs found', style: context.textTheme.bodyLarge)); 576 574 } 577 575 578 576 return ListView.builder( ··· 632 630 if (postUi.alert || postUi.inform) ...[const SizedBox(height: 10), ModerationBadgeRow(ui: postUi)], 633 631 if (record != null && record.text.isNotEmpty) ...[ 634 632 const SizedBox(height: 12), 635 - FacetText(text: record.text, facets: record.facets, style: Theme.of(context).textTheme.bodyLarge), 633 + FacetText(text: record.text, facets: record.facets, style: context.textTheme.bodyLarge), 636 634 ], 637 635 const SizedBox(height: 12), 638 636 _buildActions(context), ··· 667 665 children: [ 668 666 Text( 669 667 author.displayName ?? author.handle, 670 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 668 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w700), 671 669 maxLines: 1, 672 670 overflow: TextOverflow.ellipsis, 673 671 ), 674 672 const SizedBox(height: 2), 675 673 Text( 676 674 '@${author.handle} · ${formatRelativeTime(createdAt)}', 677 - style: Theme.of( 678 - context, 679 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 675 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 680 676 ), 681 677 ], 682 678 ), ··· 699 695 } 700 696 701 697 Widget _buildActionButton(BuildContext context, IconData icon, String count) { 702 - final iconColor = Theme.of(context).colorScheme.onSurfaceVariant; 698 + final iconColor = context.colorScheme.onSurfaceVariant; 703 699 704 700 return InkWell( 705 701 onTap: () {}, ··· 711 707 Icon(icon, size: 18, color: iconColor), 712 708 if (count.isNotEmpty) ...[ 713 709 const SizedBox(width: 4), 714 - Text(count, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: iconColor)), 710 + Text(count, style: context.textTheme.bodySmall?.copyWith(color: iconColor)), 715 711 ], 716 712 ], 717 713 ), ··· 773 769 children: [ 774 770 Text( 775 771 actor.displayName ?? actor.handle, 776 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), 772 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), 777 773 maxLines: 1, 778 774 overflow: TextOverflow.ellipsis, 779 775 ), 780 776 Text( 781 777 '@${actor.handle}', 782 - style: Theme.of( 783 - context, 784 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 778 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 785 779 ), 786 780 if (profileUi.alert || profileUi.inform) ...[ 787 781 const SizedBox(height: 8), ··· 793 787 actor.description!, 794 788 maxLines: 1, 795 789 overflow: TextOverflow.ellipsis, 796 - style: Theme.of( 797 - context, 798 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 790 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 799 791 ), 800 792 ], 801 793 ], ··· 845 837 imageUrl: actor.avatar, 846 838 initials: formatInitials(actor.displayName ?? actor.handle), 847 839 shape: BoxShape.circle, 848 - placeholderTextStyle: Theme.of(context).textTheme.labelMedium, 840 + placeholderTextStyle: context.textTheme.labelMedium, 849 841 ), 850 842 const SizedBox(width: 12), 851 843 Expanded( ··· 854 846 children: [ 855 847 Text( 856 848 actor.displayName ?? actor.handle, 857 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), 849 + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), 858 850 maxLines: 1, 859 851 overflow: TextOverflow.ellipsis, 860 852 ), 861 853 Text( 862 854 '@${actor.handle}', 863 - style: Theme.of( 864 - context, 865 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 855 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 866 856 ), 867 857 if (profileUi.alert || profileUi.inform) ...[ 868 858 const SizedBox(height: 8), ··· 925 915 children: [ 926 916 Text( 927 917 displayName, 928 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), 918 + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), 929 919 maxLines: 1, 930 920 overflow: TextOverflow.ellipsis, 931 921 ), 932 922 Text( 933 923 'by @${feed.creator.handle}', 934 - style: Theme.of( 935 - context, 936 - ).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 924 + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), 937 925 ), 938 926 if (feed.description != null && feed.description!.isNotEmpty) ...[ 939 927 const SizedBox(height: 4), ··· 941 929 feed.description!, 942 930 maxLines: 2, 943 931 overflow: TextOverflow.ellipsis, 944 - style: Theme.of(context).textTheme.bodySmall, 932 + style: context.textTheme.bodySmall, 945 933 ), 946 934 ], 947 935 ], ··· 1038 1026 child: Column( 1039 1027 mainAxisSize: MainAxisSize.min, 1040 1028 children: [ 1041 - Icon(Icons.search, size: 64, color: Theme.of(context).colorScheme.outline), 1029 + Icon(Icons.search, size: 64, color: context.colorScheme.outline), 1042 1030 const SizedBox(height: 16), 1043 - Text('Search', style: Theme.of(context).textTheme.titleMedium), 1031 + Text('Search', style: context.textTheme.titleMedium), 1044 1032 const SizedBox(height: 8), 1045 1033 Text( 1046 1034 'Find posts and people on the network.\n' 1047 1035 'Use Jump to profile to quickly open a user.', 1048 1036 textAlign: TextAlign.center, 1049 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.outline), 1037 + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.outline), 1050 1038 ), 1051 1039 ], 1052 1040 ),
+22 -22
lib/features/search/presentation/semantic_search_tab.dart
··· 1 1 import 'dart:convert'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:bluesky/app_bsky_feed_defs.dart'; 4 5 import 'package:flutter/material.dart'; ··· 76 77 77 78 @override 78 79 Widget build(BuildContext context) { 79 - final scheme = Theme.of(context).colorScheme; 80 + final scheme = context.colorScheme; 80 81 return Padding( 81 82 padding: const EdgeInsets.fromLTRB(12, 10, 12, 6), 82 83 child: TextField( ··· 142 143 143 144 @override 144 145 Widget build(BuildContext context) { 145 - final scheme = Theme.of(context).colorScheme; 146 + final scheme = context.colorScheme; 146 147 return GestureDetector( 147 148 onTap: onTap, 148 149 child: AnimatedContainer( ··· 155 156 ), 156 157 child: Text( 157 158 label, 158 - style: Theme.of(context).textTheme.labelMedium?.copyWith( 159 + style: context.textTheme.labelMedium?.copyWith( 159 160 color: isSelected ? scheme.onPrimary : scheme.onSurfaceVariant, 160 161 fontWeight: FontWeight.w600, 161 162 ), ··· 259 260 decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(99)), 260 261 child: Text( 261 262 '${score.round()}%', 262 - style: Theme.of( 263 - context, 264 - ).textTheme.labelSmall?.copyWith(color: color, fontWeight: FontWeight.w600, fontFamily: 'JetBrains Mono'), 263 + style: context.textTheme.labelSmall?.copyWith( 264 + color: color, 265 + fontWeight: FontWeight.w600, 266 + fontFamily: 'JetBrains Mono', 267 + ), 265 268 ), 266 269 ); 267 270 } ··· 280 283 281 284 @override 282 285 Widget build(BuildContext context) { 283 - final scheme = Theme.of(context).colorScheme; 286 + final scheme = context.colorScheme; 284 287 final (icon, label) = source == 'saved' ? (Icons.bookmark_outline, 'Saved') : (Icons.favorite_outline, 'Liked'); 285 288 286 289 return Container( ··· 291 294 children: [ 292 295 Icon(icon, size: 11, color: scheme.onSurfaceVariant), 293 296 const SizedBox(width: 3), 294 - Text(label, style: Theme.of(context).textTheme.labelSmall?.copyWith(color: scheme.onSurfaceVariant)), 297 + Text(label, style: context.textTheme.labelSmall?.copyWith(color: scheme.onSurfaceVariant)), 295 298 ], 296 299 ), 297 300 ); ··· 318 321 319 322 @override 320 323 Widget build(BuildContext context) { 321 - final scheme = Theme.of(context).colorScheme; 324 + final scheme = context.colorScheme; 322 325 return Center( 323 326 child: Padding( 324 327 padding: const EdgeInsets.all(32), ··· 327 330 children: [ 328 331 Icon(Icons.travel_explore_outlined, size: 64, color: scheme.outline), 329 332 const SizedBox(height: 16), 330 - Text( 331 - 'Search by meaning', 332 - style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: scheme.onSurfaceVariant), 333 - ), 333 + Text('Search by meaning', style: context.textTheme.headlineSmall?.copyWith(color: scheme.onSurfaceVariant)), 334 334 const SizedBox(height: 8), 335 335 Text( 336 336 'Search your saved and liked posts by meaning, not just keywords', 337 337 textAlign: TextAlign.center, 338 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant), 338 + style: context.textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant), 339 339 ), 340 340 ], 341 341 ), ··· 349 349 350 350 @override 351 351 Widget build(BuildContext context) { 352 - final scheme = Theme.of(context).colorScheme; 352 + final scheme = context.colorScheme; 353 353 return Center( 354 354 child: Column( 355 355 mainAxisAlignment: MainAxisAlignment.center, ··· 358 358 const SizedBox(height: 16), 359 359 Text( 360 360 'No similar posts found', 361 - style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: scheme.onSurfaceVariant), 361 + style: context.textTheme.headlineSmall?.copyWith(color: scheme.onSurfaceVariant), 362 362 ), 363 363 const SizedBox(height: 8), 364 364 Text( 365 365 'Try different keywords or a broader scope', 366 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant), 366 + style: context.textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant), 367 367 ), 368 368 ], 369 369 ), ··· 398 398 399 399 @override 400 400 Widget build(BuildContext context) { 401 - final scheme = Theme.of(context).colorScheme; 401 + final scheme = context.colorScheme; 402 402 return Center( 403 403 child: Padding( 404 404 padding: const EdgeInsets.all(32), ··· 409 409 const SizedBox(height: 16), 410 410 Text( 411 411 'Semantic search unavailable', 412 - style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: scheme.onSurfaceVariant), 412 + style: context.textTheme.headlineSmall?.copyWith(color: scheme.onSurfaceVariant), 413 413 ), 414 414 const SizedBox(height: 8), 415 415 Text( 416 416 'The on-device language model could not be loaded on this device.', 417 417 textAlign: TextAlign.center, 418 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant), 418 + style: context.textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant), 419 419 ), 420 420 ], 421 421 ), ··· 431 431 432 432 @override 433 433 Widget build(BuildContext context) { 434 - final scheme = Theme.of(context).colorScheme; 434 + final scheme = context.colorScheme; 435 435 final completed = indexState.backfillCompleted ?? 0; 436 436 final total = indexState.backfillTotal ?? 0; 437 437 final progress = (total > 0) ? completed / total : 0.0; ··· 447 447 children: [ 448 448 Text( 449 449 'Indexing: $completed/$total posts...', 450 - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant), 450 + style: context.textTheme.bodySmall?.copyWith(color: scheme.onSurfaceVariant), 451 451 ), 452 452 const SizedBox(height: 4), 453 453 LinearProgressIndicator(value: progress > 0 ? progress : null),
+12 -15
lib/features/settings/presentation/settings_screen.dart
··· 1 1 import 'dart:async'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:flutter/foundation.dart'; 4 5 import 'package:flutter/material.dart'; ··· 34 35 onPressed: () { 35 36 context.read<AuthBloc>().add(const LogoutRequested()); 36 37 }, 37 - icon: Icon(Icons.logout, color: Theme.of(context).colorScheme.error), 38 + icon: Icon(Icons.logout, color: context.colorScheme.error), 38 39 ), 39 40 ], 40 41 ), ··· 159 160 }, 160 161 ), 161 162 const SizedBox(height: 24), 162 - Center(child: Text('Lazurite v1.0.0', style: Theme.of(context).textTheme.bodySmall)), 163 + Center(child: Text('Lazurite v1.0.0', style: context.textTheme.bodySmall)), 163 164 const SizedBox(height: 24), 164 165 ], 165 166 ), ··· 171 172 padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), 172 173 child: Text( 173 174 title.toUpperCase(), 174 - style: Theme.of(context).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 175 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 175 176 ), 176 177 ); 177 178 } 178 179 179 - Widget _title(BuildContext context) => Text('Settings', style: Theme.of(context).textTheme.titleLarge); 180 + Widget _title(BuildContext context) => Text('Settings', style: context.textTheme.titleLarge); 180 181 181 182 Widget _buildThemeSelector(BuildContext context) { 182 183 final settingsCubit = context.read<SettingsCubit>(); ··· 198 199 child: Center( 199 200 child: SegmentedButton<_AppearanceMode>( 200 201 style: SegmentedButton.styleFrom( 201 - selectedBackgroundColor: Theme.of(context).colorScheme.primary, 202 - selectedForegroundColor: Theme.of(context).colorScheme.onPrimary, 202 + selectedBackgroundColor: context.colorScheme.primary, 203 + selectedForegroundColor: context.colorScheme.onPrimary, 203 204 ), 204 205 segments: const [ 205 206 ButtonSegment(value: _AppearanceMode.system, label: Text('System')), ··· 230 231 alignment: Alignment.centerLeft, 231 232 child: Text( 232 233 'THEME', 233 - style: Theme.of( 234 - context, 235 - ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 234 + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), 236 235 ), 237 236 ), 238 237 ), ··· 507 506 children: [ 508 507 Padding( 509 508 padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), 510 - child: Text('AT Protocol Connection', style: Theme.of(context).textTheme.titleMedium), 509 + child: Text('AT Protocol Connection', style: context.textTheme.titleMedium), 511 510 ), 512 511 const Divider(height: 1), 513 512 _ConnectionDetailRow(label: 'Handle', value: '@${tokens.handle}'), ··· 567 566 ), 568 567 if (isSelected) ...[ 569 568 const SizedBox(width: 12), 570 - Icon(Icons.check, color: Theme.of(context).colorScheme.primary, size: 20), 569 + Icon(Icons.check, color: context.colorScheme.primary, size: 20), 571 570 ], 572 571 ], 573 572 ), ··· 687 686 688 687 @override 689 688 Widget build(BuildContext context) { 690 - final color = isDestructive ? Theme.of(context).colorScheme.error : null; 689 + final color = isDestructive ? context.colorScheme.error : null; 691 690 692 691 return ListTile( 693 692 leading: icon != null ? Icon(icon, color: color) : null, ··· 740 739 subtitle: const Text('Maximum number of search results'), 741 740 trailing: Text( 742 741 '$value', 743 - style: Theme.of( 744 - context, 745 - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, fontFamily: 'JetBrains Mono'), 742 + style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, fontFamily: 'JetBrains Mono'), 746 743 ), 747 744 ), 748 745 Padding(
+5 -4
lib/features/settings/presentation/video_upload_limits_screen.dart
··· 2 2 import 'package:flutter_bloc/flutter_bloc.dart'; 3 3 import 'package:lazurite/features/settings/cubit/video_upload_limits_cubit.dart'; 4 4 import 'package:lazurite/features/settings/data/video_repository.dart'; 5 + import 'package:lazurite/core/theme/theme_extensions.dart'; 5 6 6 7 class VideoUploadLimitsScreen extends StatefulWidget { 7 8 const VideoUploadLimitsScreen({super.key}); ··· 34 35 child: Column( 35 36 mainAxisSize: MainAxisSize.min, 36 37 children: [ 37 - Icon(Icons.warning_amber_outlined, size: 48, color: Theme.of(context).colorScheme.error), 38 + Icon(Icons.warning_amber_outlined, size: 48, color: context.colorScheme.error), 38 39 const SizedBox(height: 16), 39 40 Text( 40 41 state.errorMessage ?? 'Failed to load video upload limits', 41 42 textAlign: TextAlign.center, 42 - style: TextStyle(color: Theme.of(context).colorScheme.error), 43 + style: TextStyle(color: context.colorScheme.error), 43 44 ), 44 45 ], 45 46 ), ··· 135 136 child: Row( 136 137 mainAxisAlignment: MainAxisAlignment.spaceBetween, 137 138 children: [ 138 - Text(label, style: Theme.of(context).textTheme.bodyLarge), 139 - Text(value, style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), 139 + Text(label, style: context.textTheme.bodyLarge), 140 + Text(value, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600)), 140 141 ], 141 142 ), 142 143 );
+7 -6
lib/features/starter_packs/presentation/create_edit_starter_pack_screen.dart
··· 7 7 import 'package:lazurite/features/lists/data/list_repository.dart'; 8 8 import 'package:lazurite/features/starter_packs/bloc/starter_pack_bloc.dart'; 9 9 import 'package:lazurite/features/starter_packs/data/starter_pack_repository.dart'; 10 + import 'package:lazurite/core/theme/theme_extensions.dart'; 10 11 11 12 /// Full-screen form for creating a new starter pack. 12 13 /// ··· 180 181 } 181 182 182 183 Widget _buildMembersSection(BuildContext context, bool isCreating) { 183 - final colorScheme = Theme.of(context).colorScheme; 184 - final textTheme = Theme.of(context).textTheme; 184 + final colorScheme = context.colorScheme; 185 + final textTheme = context.textTheme; 185 186 186 187 return Column( 187 188 crossAxisAlignment: CrossAxisAlignment.start, ··· 277 278 } 278 279 279 280 Widget _buildFeedsSection(BuildContext context, bool isCreating) { 280 - final textTheme = Theme.of(context).textTheme; 281 - final colorScheme = Theme.of(context).colorScheme; 281 + final textTheme = context.textTheme; 282 + final colorScheme = context.colorScheme; 282 283 283 284 return Column( 284 285 crossAxisAlignment: CrossAxisAlignment.start, ··· 353 354 354 355 @override 355 356 Widget build(BuildContext context) { 356 - final colorScheme = Theme.of(context).colorScheme; 357 + final colorScheme = context.colorScheme; 357 358 358 359 return DraggableScrollableSheet( 359 360 initialChildSize: 0.6, ··· 367 368 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 368 369 child: Row( 369 370 children: [ 370 - Text('Select a feed', style: Theme.of(context).textTheme.titleMedium), 371 + Text('Select a feed', style: context.textTheme.titleMedium), 371 372 const Spacer(), 372 373 IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), 373 374 ],
+10 -9
lib/features/starter_packs/presentation/starter_pack_detail_screen.dart
··· 8 8 import 'package:lazurite/features/starter_packs/bloc/starter_pack_bloc.dart'; 9 9 import 'package:lazurite/features/starter_packs/data/starter_pack_repository.dart'; 10 10 import 'package:lazurite/shared/utils/format_utils.dart'; 11 + import 'package:lazurite/core/theme/theme_extensions.dart'; 11 12 12 13 class StarterPackDetailScreen extends StatelessWidget { 13 14 const StarterPackDetailScreen({super.key, required this.packUri}); ··· 172 173 actions: [ 173 174 TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), 174 175 FilledButton( 175 - style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error), 176 + style: FilledButton.styleFrom(backgroundColor: context.colorScheme.error), 176 177 onPressed: () => Navigator.pop(context, true), 177 178 child: const Text('Delete'), 178 179 ), ··· 253 254 254 255 @override 255 256 Widget build(BuildContext context) { 256 - final colorScheme = Theme.of(context).colorScheme; 257 + final colorScheme = context.colorScheme; 257 258 258 259 return AlertDialog( 259 260 title: const Text('Edit starter pack'), ··· 279 280 textCapitalization: TextCapitalization.sentences, 280 281 ), 281 282 const SizedBox(height: 12), 282 - Text('Feeds', style: Theme.of(context).textTheme.labelLarge), 283 + Text('Feeds', style: context.textTheme.labelLarge), 283 284 const SizedBox(height: 8), 284 285 for (final feed in _feeds) 285 286 ListTile( ··· 342 343 343 344 @override 344 345 Widget build(BuildContext context) { 345 - final colorScheme = Theme.of(context).colorScheme; 346 + final colorScheme = context.colorScheme; 346 347 347 348 return DraggableScrollableSheet( 348 349 initialChildSize: 0.6, ··· 356 357 padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), 357 358 child: Row( 358 359 children: [ 359 - Text('Select a feed', style: Theme.of(context).textTheme.titleMedium), 360 + Text('Select a feed', style: context.textTheme.titleMedium), 360 361 const Spacer(), 361 362 IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), 362 363 ], ··· 407 408 408 409 @override 409 410 Widget build(BuildContext context) { 410 - final colorScheme = Theme.of(context).colorScheme; 411 - final textTheme = Theme.of(context).textTheme; 411 + final colorScheme = context.colorScheme; 412 + final textTheme = context.textTheme; 412 413 413 414 final name = (pack.record['name'] as String?) ?? 'Starter Pack'; 414 415 final description = pack.record['description'] as String?; ··· 504 505 } 505 506 506 507 Widget _buildMembersSection(BuildContext context) { 507 - final colorScheme = Theme.of(context).colorScheme; 508 - final textTheme = Theme.of(context).textTheme; 508 + final colorScheme = context.colorScheme; 509 + final textTheme = context.textTheme; 509 510 final sample = pack.listItemsSample ?? const []; 510 511 final refListUri = pack.list?.uri; 511 512
+5 -9
lib/features/starter_packs/presentation/widgets/starter_pack_card.dart
··· 1 1 import 'package:bluesky/app_bsky_graph_defs.dart'; 2 2 import 'package:flutter/material.dart'; 3 3 import 'package:lazurite/shared/utils/format_utils.dart'; 4 + import 'package:lazurite/core/theme/theme_extensions.dart'; 4 5 5 6 class StarterPackCard extends StatelessWidget { 6 7 const StarterPackCard({super.key, required this.pack, this.onTap}); ··· 10 11 11 12 @override 12 13 Widget build(BuildContext context) { 13 - final colorScheme = Theme.of(context).colorScheme; 14 - final textTheme = Theme.of(context).textTheme; 14 + final colorScheme = context.colorScheme; 15 + final textTheme = context.textTheme; 15 16 16 17 final name = (pack.record['name'] as String?) ?? 'Starter Pack'; 17 18 final memberCount = pack.listItemCount; ··· 76 77 return Column( 77 78 crossAxisAlignment: CrossAxisAlignment.start, 78 79 children: [ 79 - Text(formatCount(count), style: Theme.of(context).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700)), 80 - Text( 81 - label, 82 - style: Theme.of( 83 - context, 84 - ).textTheme.labelSmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), 85 - ), 80 + Text(formatCount(count), style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700)), 81 + Text(label, style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurfaceVariant)), 86 82 ], 87 83 ); 88 84 }
+2 -1
lib/shared/presentation/helpers/snackbar_helper.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showAppSnackBar( 4 5 BuildContext context, ··· 11 12 VoidCallback? onAction, 12 13 }) { 13 14 final messenger = ScaffoldMessenger.of(context); 14 - final colorScheme = Theme.of(context).colorScheme; 15 + final colorScheme = context.colorScheme; 15 16 16 17 if (hideCurrent) { 17 18 messenger.hideCurrentSnackBar();
+3 -2
lib/shared/presentation/widgets/confirmation_dialog.dart
··· 1 1 import 'dart:async'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:flutter/material.dart'; 4 5 ··· 38 39 onPressed: confirmEnabled ? onConfirm : null, 39 40 style: confirmDestructive 40 41 ? FilledButton.styleFrom( 41 - backgroundColor: Theme.of(context).colorScheme.error, 42 - foregroundColor: Theme.of(context).colorScheme.onError, 42 + backgroundColor: context.colorScheme.error, 43 + foregroundColor: context.colorScheme.onError, 43 44 ) 44 45 : null, 45 46 child: Text(confirmLabel),
+3 -2
lib/shared/presentation/widgets/empty_state.dart
··· 1 1 import 'package:flutter/material.dart'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 class EmptyState extends StatelessWidget { 4 5 const EmptyState({ ··· 18 19 19 20 @override 20 21 Widget build(BuildContext context) { 21 - final colorScheme = Theme.of(context).colorScheme; 22 - final textTheme = Theme.of(context).textTheme; 22 + final colorScheme = context.colorScheme; 23 + final textTheme = context.textTheme; 23 24 24 25 return Center( 25 26 child: Padding(
+2 -1
lib/shared/presentation/widgets/options_sheet.dart
··· 1 1 import 'dart:async'; 2 + import 'package:lazurite/core/theme/theme_extensions.dart'; 2 3 3 4 import 'package:flutter/material.dart'; 4 5 ··· 32 33 33 34 @override 34 35 Widget build(BuildContext context) { 35 - final colorScheme = Theme.of(context).colorScheme; 36 + final colorScheme = context.colorScheme; 36 37 return SafeArea( 37 38 child: Column( 38 39 mainAxisSize: MainAxisSize.min,
+79
test/core/theme/theme_extensions_test.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_test/flutter_test.dart'; 3 + import 'package:lazurite/core/theme/theme_extensions.dart'; 4 + 5 + void main() { 6 + group('ThemeX', () { 7 + testWidgets('exposes the current ColorScheme from BuildContext', (tester) async { 8 + final theme = ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1565C0))); 9 + late ColorScheme observed; 10 + late TextTheme observedTextTheme; 11 + late TextTheme expectedTextTheme; 12 + 13 + await tester.pumpWidget( 14 + MaterialApp( 15 + theme: theme, 16 + home: Builder( 17 + builder: (context) { 18 + observed = context.colorScheme; 19 + observedTextTheme = context.textTheme; 20 + expectedTextTheme = Theme.of(context).textTheme; 21 + return const SizedBox.shrink(); 22 + }, 23 + ), 24 + ), 25 + ); 26 + 27 + expect(observed.primary, theme.colorScheme.primary); 28 + expect(observed.onPrimary, theme.colorScheme.onPrimary); 29 + expect(observed.surface, theme.colorScheme.surface); 30 + expect(observedTextTheme.titleMedium?.fontSize, expectedTextTheme.titleMedium?.fontSize); 31 + expect(observedTextTheme.bodySmall?.fontSize, expectedTextTheme.bodySmall?.fontSize); 32 + expect(observedTextTheme.labelLarge?.fontWeight, expectedTextTheme.labelLarge?.fontWeight); 33 + }); 34 + 35 + testWidgets('tracks updated inherited theme values', (tester) async { 36 + final light = ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D32))); 37 + final dark = ThemeData( 38 + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6A1B9A), brightness: Brightness.dark), 39 + ); 40 + 41 + late Color initialPrimary; 42 + late Color updatedPrimary; 43 + 44 + await tester.pumpWidget( 45 + Directionality( 46 + textDirection: TextDirection.ltr, 47 + child: Theme( 48 + data: light, 49 + child: Builder( 50 + builder: (context) { 51 + initialPrimary = context.colorScheme.primary; 52 + return const SizedBox.shrink(); 53 + }, 54 + ), 55 + ), 56 + ), 57 + ); 58 + 59 + await tester.pumpWidget( 60 + Directionality( 61 + textDirection: TextDirection.ltr, 62 + child: Theme( 63 + data: dark, 64 + child: Builder( 65 + builder: (context) { 66 + updatedPrimary = context.colorScheme.primary; 67 + return const SizedBox.shrink(); 68 + }, 69 + ), 70 + ), 71 + ), 72 + ); 73 + 74 + expect(initialPrimary, light.colorScheme.primary); 75 + expect(updatedPrimary, dark.colorScheme.primary); 76 + expect(updatedPrimary, isNot(initialPrimary)); 77 + }); 78 + }); 79 + }