refactor(apps): 主题系统迁移至 ColorScheme + 扩展架构并支持 Dark Mode

This commit is contained in:
qzl
2026-03-27 19:07:39 +08:00
parent ecc1ec6ce4
commit ae29a8209b
146 changed files with 4301 additions and 3200 deletions
@@ -34,8 +34,9 @@ class _AddContactScreenState extends State<AddContactScreen> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.surfaceSecondary,
backgroundColor: colorScheme.surfaceContainerLow,
resizeToAvoidBottomInset: false,
body: SafeArea(
maintainBottomViewPadding: true,
@@ -73,6 +74,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
}
Widget _buildConfirmButton() {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox(
width: AppSpacing.xxl * 2,
height: AppSpacing.xxl * 2,
@@ -80,47 +82,45 @@ class _AddContactScreenState extends State<AddContactScreen> {
onPressed: _handleConfirm,
style: TextButton.styleFrom(
padding: const EdgeInsets.all(AppSpacing.none),
backgroundColor: AppColors.surfaceInfo,
backgroundColor: colorScheme.primaryContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.full),
side: const BorderSide(color: AppColors.borderQuaternary),
side: BorderSide(color: colorScheme.outlineVariant),
),
),
child: const Icon(
child: Icon(
Icons.check,
size: AppSpacing.lg,
color: AppColors.blue600,
color: colorScheme.primary,
),
),
);
}
Widget _buildAvatarSection() {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.surfaceInfoLight,
color: colorScheme.primaryContainer.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(36),
border: Border.all(color: Colors.transparent),
),
child: const Icon(
Icons.person_outline,
size: 24,
color: AppColors.slate400,
border: Border.all(color: colorScheme.surface.withValues(alpha: 0)),
),
child: Icon(Icons.person_outline, size: 24, color: colorScheme.outline),
),
);
}
Widget _buildFormCard() {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.messageCardBorder),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
@@ -149,12 +149,13 @@ class _AddContactScreenState extends State<AddContactScreen> {
}
Widget _buildDeleteRow() {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: LinkButton(
text: context.l10n.contactDelete,
onTap: _handleDelete,
foregroundColor: AppColors.red600,
foregroundColor: colorScheme.error,
),
);
}
@@ -177,6 +178,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
}
void _handleDelete() {
final colorScheme = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -195,7 +197,7 @@ class _AddContactScreenState extends State<AddContactScreen> {
},
child: Text(
context.l10n.commonDelete,
style: const TextStyle(color: AppColors.red600),
style: TextStyle(color: colorScheme.error),
),
),
],
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../app/di/injection.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../data/models/user_profile.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/toast/index.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/back_title_page_header.dart';
import '../../../contacts/data/friends_api.dart';
import '../../../contacts/data/users/models/user_response.dart';
import '../../../contacts/data/users/users_api.dart';
class ContactsScreen extends StatefulWidget {
@@ -24,7 +24,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
List<FriendResponse> _friends = [];
List<FriendRequestResponse> _pendingRequests = [];
List<UserResponse> _searchResults = [];
List<UserProfile> _searchResults = [];
bool _isLoading = true;
bool _isSearching = false;
bool _hasSearched = false;
@@ -142,13 +142,16 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
}
void _showAddFriendDialog(UserResponse user) {
void _showAddFriendDialog(UserProfile user) {
final controller = TextEditingController();
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
backgroundColor: Theme.of(
context,
).colorScheme.surface.withValues(alpha: 0),
builder: (sheetContext) {
final colorScheme = Theme.of(sheetContext).colorScheme;
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
@@ -164,8 +167,8 @@ class _ContactsScreenState extends State<ContactsScreen> {
AppSpacing.xxl,
AppSpacing.lg,
),
decoration: const BoxDecoration(
color: AppColors.white,
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.vertical(
top: Radius.circular(AppRadius.xxl),
),
@@ -180,7 +183,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
width: 40,
height: AppSpacing.xs,
decoration: BoxDecoration(
color: AppColors.slate300,
color: colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(AppRadius.full),
),
),
@@ -188,23 +191,26 @@ class _ContactsScreenState extends State<ContactsScreen> {
const SizedBox(height: AppSpacing.lg),
Text(
context.l10n.contactsAddSheetTitle(user.username),
style: const TextStyle(
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: AppSpacing.sm),
Text(
context.l10n.contactsAddSheetDesc,
style: TextStyle(fontSize: 13, color: AppColors.slate500),
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.lg),
Container(
decoration: BoxDecoration(
color: AppColors.slate50,
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.borderSecondary),
border: Border.all(color: colorScheme.outlineVariant),
),
child: TextField(
controller: controller,
@@ -215,13 +221,13 @@ class _ContactsScreenState extends State<ContactsScreen> {
hintText: context.l10n.contactsAddSheetMessageHint,
hintStyle: TextStyle(
fontSize: 13,
color: AppColors.slate400,
color: colorScheme.outline,
),
border: InputBorder.none,
contentPadding: EdgeInsets.all(AppSpacing.lg),
counterStyle: TextStyle(
fontSize: 11,
color: AppColors.slate400,
color: colorScheme.outline,
),
),
),
@@ -270,8 +276,9 @@ class _ContactsScreenState extends State<ContactsScreen> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.surfaceSecondary,
backgroundColor: colorScheme.surfaceContainerLow,
resizeToAvoidBottomInset: false,
body: SafeArea(
maintainBottomViewPadding: true,
@@ -321,15 +328,16 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildSearchRow() {
final colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Expanded(
child: Container(
height: 40,
decoration: BoxDecoration(
color: AppColors.surfaceTertiary,
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE4EBF7)),
border: Border.all(color: colorScheme.outlineVariant),
),
child: TextField(
controller: _searchController,
@@ -339,12 +347,12 @@ class _ContactsScreenState extends State<ContactsScreen> {
hintStyle: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.slate400,
color: colorScheme.outline,
),
prefixIcon: Icon(
Icons.search,
size: 16,
color: AppColors.slate400,
color: colorScheme.outline,
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
@@ -374,22 +382,22 @@ class _ContactsScreenState extends State<ContactsScreen> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFF1F7FF),
color: colorScheme.primaryContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFD7E6FF)),
border: Border.all(color: colorScheme.primaryContainer),
),
child: _isSearching
? const Padding(
? Padding(
padding: EdgeInsets.all(10),
child: AppLoadingIndicator(
size: 16,
strokeWidth: 2,
color: AppColors.blue500,
trackColor: AppColors.blue100,
color: colorScheme.primary,
trackColor: colorScheme.primaryContainer,
withContainer: false,
),
)
: const Icon(Icons.search, size: 16, color: AppColors.blue500),
: Icon(Icons.search, size: 16, color: colorScheme.primary),
),
),
],
@@ -397,6 +405,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildSearchResults() {
final colorScheme = Theme.of(context).colorScheme;
if (!_hasSearched) {
return const SizedBox.shrink();
}
@@ -404,11 +413,11 @@ class _ContactsScreenState extends State<ContactsScreen> {
return Container(
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -427,7 +436,10 @@ class _ContactsScreenState extends State<ContactsScreen> {
child: Center(
child: Text(
context.l10n.contactsSearchNoUser,
style: TextStyle(fontSize: 14, color: AppColors.slate500),
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
),
),
)
@@ -445,17 +457,17 @@ class _ContactsScreenState extends State<ContactsScreen> {
);
}
Widget _buildSearchResultItem(UserResponse user) {
Widget _buildSearchResultItem(UserProfile user) {
final colorScheme = Theme.of(context).colorScheme;
final isFriend = _friendIds.contains(user.id);
final isSent = _sentRequestIds.contains(user.id);
final avatarColor = _getAvatarColor(user.id);
return Container(
height: 70,
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
_buildAvatar(user.avatarUrl, user.id, avatarColor),
_buildAvatar(user.avatarUrl, user.id, colorScheme),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -464,19 +476,19 @@ class _ContactsScreenState extends State<ContactsScreen> {
children: [
Text(
user.username,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
color: colorScheme.onSurface,
),
),
if (user.bio != null) ...[
const SizedBox(height: 2),
Text(
user.bio!,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.slate500,
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -492,16 +504,17 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildAddButton(String userId, bool isFriend, bool isSent) {
final colorScheme = Theme.of(context).colorScheme;
if (isFriend) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.slate300,
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
context.l10n.contactsStatusAlreadyFriend,
style: TextStyle(fontSize: 12, color: AppColors.slate500),
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
),
);
}
@@ -510,12 +523,12 @@ class _ContactsScreenState extends State<ContactsScreen> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.slate300,
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(
context.l10n.contactsStatusSent,
style: TextStyle(fontSize: 12, color: AppColors.slate500),
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
),
);
}
@@ -528,21 +541,21 @@ class _ContactsScreenState extends State<ContactsScreen> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFF1F7FF),
color: colorScheme.primaryContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFD7E6FF)),
border: Border.all(color: colorScheme.primaryContainer),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.person_add, size: 14, color: AppColors.blue500),
Icon(Icons.person_add, size: 14, color: colorScheme.primary),
const SizedBox(width: 4),
Text(
context.l10n.contactsAdd,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.blue500,
color: colorScheme.primary,
),
),
],
@@ -552,30 +565,31 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildEmptyState() {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE3EAF6)),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
const Icon(Icons.person_outline, size: 48, color: AppColors.slate400),
Icon(Icons.person_outline, size: 48, color: colorScheme.outline),
const SizedBox(height: 12),
Text(
context.l10n.contactsEmptyTitle,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.slate500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
context.l10n.contactsEmptyDesc,
style: TextStyle(fontSize: 13, color: AppColors.slate400),
style: TextStyle(fontSize: 13, color: colorScheme.outline),
),
],
),
@@ -583,22 +597,24 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildSectionTitle(String title) {
final colorScheme = Theme.of(context).colorScheme;
return Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.slate500,
color: colorScheme.onSurfaceVariant,
),
);
}
Widget _buildPendingRequestCard(List<FriendRequestResponse> requests) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE3EAF6)),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
@@ -612,15 +628,15 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildPendingRequestItem(FriendRequestResponse request) {
final colorScheme = Theme.of(context).colorScheme;
final recipient = request.recipient;
final color = _getAvatarColor(recipient.id);
return Container(
height: 70,
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
_buildAvatar(recipient.avatarUrl, recipient.id, color),
_buildAvatar(recipient.avatarUrl, recipient.id, colorScheme),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -629,16 +645,19 @@ class _ContactsScreenState extends State<ContactsScreen> {
children: [
Text(
recipient.username,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
context.l10n.contactsPendingConfirm,
style: TextStyle(fontSize: 12, color: AppColors.slate500),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
),
@@ -649,11 +668,12 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildContactCard(List<FriendResponse> friends) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE3EAF6)),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
@@ -667,8 +687,8 @@ class _ContactsScreenState extends State<ContactsScreen> {
}
Widget _buildContactItem(FriendResponse friend) {
final colorScheme = Theme.of(context).colorScheme;
final friendInfo = friend.friend;
final color = _getAvatarColor(friendInfo.id);
return GestureDetector(
onTap: () => context.push('/contacts/add?id=${friendInfo.id}'),
@@ -677,7 +697,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Row(
children: [
_buildAvatar(friendInfo.avatarUrl, friendInfo.id, color),
_buildAvatar(friendInfo.avatarUrl, friendInfo.id, colorScheme),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -686,10 +706,10 @@ class _ContactsScreenState extends State<ContactsScreen> {
children: [
Text(
friendInfo.username,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
color: colorScheme.onSurface,
),
),
],
@@ -701,52 +721,65 @@ class _ContactsScreenState extends State<ContactsScreen> {
);
}
Color _getAvatarColor(String id) {
final colors = [
AppColors.blue500,
AppColors.violet600,
AppColors.blue600,
const Color(0xFF0EA5E9),
AppColors.violet500,
];
final index = id.hashCode.abs() % colors.length;
return colors[index];
Color _getAvatarBackground(
Color color,
ColorScheme colorScheme,
AppColorPalette palette,
) {
final avatarIndex = palette.avatarColors.indexOf(color);
final opacities = [0.30, 0.20, 0.35, 0.15, 0.30];
final opacity = avatarIndex >= 0 && avatarIndex < opacities.length
? opacities[avatarIndex]
: 0.30;
final isTertiary = avatarIndex == 4;
return isTertiary
? colorScheme.tertiaryContainer.withValues(alpha: opacity)
: colorScheme.primaryContainer.withValues(alpha: opacity);
}
Color _getAvatarBackground(Color color) {
if (color == AppColors.blue500) return const Color(0xFFEEF4FF);
if (color == AppColors.violet600) return AppColors.surfaceInfoLight;
if (color == AppColors.blue600) return const Color(0xFFEDF5FF);
if (color == const Color(0xFF0EA5E9)) return const Color(0xFFF2F8FF);
if (color == AppColors.violet500) return const Color(0xFFF5F7FF);
return const Color(0xFFEEF4FF);
}
Color _getAvatarBorder(Color color) {
if (color == AppColors.blue500) return const Color(0xFFDDE8FB);
if (color == AppColors.violet600) return const Color(0xFFE2EAFB);
if (color == AppColors.blue600) return const Color(0xFFDCE9FB);
if (color == const Color(0xFF0EA5E9)) return const Color(0xFFDFEAFA);
if (color == AppColors.violet500) return const Color(0xFFE4E8FA);
return const Color(0xFFDDE8FB);
Color _getAvatarBorder(
Color color,
ColorScheme colorScheme,
AppColorPalette palette,
) {
final avatarIndex = palette.avatarColors.indexOf(color);
final opacities = [0.25, 0.20, 0.30, 0.15, 0.25];
final opacity = avatarIndex >= 0 && avatarIndex < opacities.length
? opacities[avatarIndex]
: 0.25;
final isTertiary = avatarIndex == 4;
return isTertiary
? colorScheme.tertiary.withValues(alpha: opacity)
: colorScheme.primary.withValues(alpha: opacity);
}
Widget _buildDivider() {
final colorScheme = Theme.of(context).colorScheme;
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 14),
color: const Color(0xFFEEF2F7),
color: colorScheme.outlineVariant,
);
}
Widget _buildAvatar(String? avatarUrl, String userId, Color color) {
Widget _buildAvatar(
String? avatarUrl,
String userId,
ColorScheme colorScheme,
) {
final palette = Theme.of(context).extension<AppColorPalette>()!;
final avatarColor = palette
.avatarColors[userId.hashCode.abs() % palette.avatarColors.length];
return Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: _getAvatarBackground(color),
color: _getAvatarBackground(avatarColor, colorScheme, palette),
borderRadius: BorderRadius.circular(21),
border: Border.all(color: _getAvatarBorder(color)),
border: Border.all(
color: _getAvatarBorder(avatarColor, colorScheme, palette),
),
),
child: avatarUrl != null
? ClipRRect(
@@ -756,14 +789,11 @@ class _ContactsScreenState extends State<ContactsScreen> {
width: 42,
height: 42,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Icon(
Icons.person,
size: 18,
color: _getAvatarColor(userId),
),
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.person, size: 18, color: avatarColor),
),
)
: Icon(Icons.person, size: 18, color: _getAvatarColor(userId)),
: Icon(Icons.person, size: 18, color: avatarColor),
);
}
@@ -772,6 +802,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
TextEditingController controller,
BuildContext sheetContext,
) {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox(
height: 44,
child: ElevatedButton(
@@ -786,18 +817,18 @@ class _ContactsScreenState extends State<ContactsScreen> {
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.blue500,
foregroundColor: AppColors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
),
),
child: _sendingRequestUserId == userId
? const AppLoadingIndicator(
? AppLoadingIndicator(
size: 16,
strokeWidth: 2,
color: AppColors.white,
trackColor: AppColors.blue300,
color: colorScheme.onPrimary,
trackColor: colorScheme.primary.withValues(alpha: 0.3),
withContainer: false,
)
: Text(