feat: 重构 Reminder Notification 系统并更新应用包名

This commit is contained in:
qzl
2026-03-30 18:36:57 +08:00
parent 9fb2a6857b
commit 91bf3c3f96
90 changed files with 5133 additions and 3017 deletions
@@ -83,14 +83,24 @@ class UserBasicInfo {
final String id;
final String username;
final String? avatarUrl;
final String? phone;
final String? bio;
UserBasicInfo({required this.id, required this.username, this.avatarUrl});
UserBasicInfo({
required this.id,
required this.username,
this.avatarUrl,
this.phone,
this.bio,
});
factory UserBasicInfo.fromJson(Map<String, dynamic> json) {
return UserBasicInfo(
id: json['id'] as String,
username: json['username'] as String,
avatarUrl: json['avatar_url'] as String?,
phone: json['phone'] as String?,
bio: json['bio'] as String?,
);
}
}
@@ -2,22 +2,9 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:social_app/data/network/i_api_client.dart';
import '../models/user_profile.dart';
import 'friends_api.dart';
class UserBasicInfo {
final String id;
final String username;
final String? avatarUrl;
UserBasicInfo({required this.id, required this.username, this.avatarUrl});
factory UserBasicInfo.fromJson(Map<String, dynamic> json) {
return UserBasicInfo(
id: json['id'] as String,
username: json['username'] as String,
avatarUrl: json['avatar_url'] as String?,
);
}
}
export 'friends_api.dart' show UserBasicInfo;
class UsersApi {
final IApiClient _client;
@@ -1,207 +0,0 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/l10n/l10n.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/back_title_page_header.dart';
import '../../../../shared/widgets/app_input.dart';
import '../../../../shared/widgets/link_button.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
class AddContactScreen extends StatefulWidget {
final String? contactId;
const AddContactScreen({super.key, this.contactId});
@override
State<AddContactScreen> createState() => _AddContactScreenState();
}
class _AddContactScreenState extends State<AddContactScreen> {
final _nameController = TextEditingController();
final _phoneController = TextEditingController();
final _remarkController = TextEditingController();
bool get isEditing => widget.contactId != null;
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_remarkController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surfaceContainerLow,
resizeToAvoidBottomInset: false,
body: SafeArea(
maintainBottomViewPadding: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BackTitlePageHeader(
title: isEditing
? context.l10n.contactEditTitle
: context.l10n.contactAddTitle,
onBack: () => context.pop(),
trailing: _buildConfirmButton(),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAvatarSection(),
const SizedBox(height: 14),
_buildFormCard(),
if (isEditing) ...[
const SizedBox(height: 14),
_buildDeleteRow(),
],
],
),
),
),
],
),
),
);
}
Widget _buildConfirmButton() {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox(
width: AppSpacing.xxl * 2,
height: AppSpacing.xxl * 2,
child: TextButton(
onPressed: _handleConfirm,
style: TextButton.styleFrom(
padding: const EdgeInsets.all(AppSpacing.none),
backgroundColor: colorScheme.primaryContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.full),
side: BorderSide(color: colorScheme.outlineVariant),
),
),
child: Icon(
Icons.check,
size: AppSpacing.lg,
color: colorScheme.primary,
),
),
);
}
Widget _buildAvatarSection() {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(36),
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: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
AppInput(
label: context.l10n.contactNickname,
hint: context.l10n.contactNicknameHint,
controller: _nameController,
),
const SizedBox(height: 14),
AppInput(
label: context.l10n.contactPhone,
hint: context.l10n.contactPhoneHint,
controller: _phoneController,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 14),
AppInput(
label: context.l10n.contactRemark,
hint: context.l10n.contactRemarkHint,
controller: _remarkController,
maxLines: 3,
),
],
),
);
}
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: colorScheme.error,
),
);
}
void _handleConfirm() {
final name = _nameController.text.trim();
final phone = _phoneController.text.trim();
if (name.isEmpty || phone.isEmpty) {
Toast.show(
context,
context.l10n.contactFillRequired,
type: ToastType.warning,
);
return;
}
// TODO: Implement save logic
context.pop();
}
void _handleDelete() {
final colorScheme = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.contactDeleteConfirmTitle),
content: Text(context.l10n.contactDeleteConfirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.commonCancel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// TODO: Implement delete logic
context.pop();
},
child: Text(
context.l10n.commonDelete,
style: TextStyle(color: colorScheme.error),
),
),
],
),
);
}
}
@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:social_app/core/l10n/l10n.dart';
import 'package:social_app/core/theme/design_tokens.dart';
import 'package:social_app/shared/widgets/back_title_page_header.dart';
import 'package:social_app/features/contacts/data/apis/users_api.dart';
class ContactDetailScreen extends StatelessWidget {
final UserBasicInfo user;
const ContactDetailScreen({super.key, required this.user});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surfaceContainerLow,
resizeToAvoidBottomInset: false,
body: SafeArea(
maintainBottomViewPadding: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
BackTitlePageHeader(
title: context.l10n.contactDetailTitle,
onBack: () => context.pop(),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAvatarSection(context, colorScheme),
const SizedBox(height: 14),
_buildInfoCard(context, colorScheme),
],
),
),
),
],
),
),
);
}
Widget _buildAvatarSection(BuildContext context, ColorScheme colorScheme) {
final palette = Theme.of(context).extension<AppColorPalette>()!;
final avatarColor = palette
.avatarColors[user.id.hashCode.abs() % palette.avatarColors.length];
return Center(
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.45),
borderRadius: BorderRadius.circular(40),
border: Border.all(color: colorScheme.surface.withValues(alpha: 0)),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withValues(alpha: 0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: user.avatarUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(40),
child: Image.network(
user.avatarUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Icon(Icons.person, size: 32, color: avatarColor),
),
)
: Icon(Icons.person, size: 32, color: avatarColor),
),
);
}
Widget _buildInfoCard(BuildContext context, ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
context.l10n.contactDetailUsername,
user.username,
Icons.person_outline,
colorScheme,
),
const SizedBox(height: 14),
_buildInfoRow(
context.l10n.contactDetailPhone,
user.phone ?? context.l10n.commonNone,
Icons.phone_outlined,
colorScheme,
),
const SizedBox(height: 14),
_buildInfoRow(
context.l10n.contactDetailBio,
user.bio ?? context.l10n.commonNone,
Icons.info_outline,
colorScheme,
),
],
),
);
}
Widget _buildInfoRow(
String label,
String value,
IconData icon,
ColorScheme colorScheme,
) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: colorScheme.onSurfaceVariant),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
],
),
),
],
);
}
}
@@ -8,6 +8,7 @@ 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 '../../../../shared/widgets/shared_divider.dart';
import '../../../contacts/data/apis/friends_api.dart';
import '../../../contacts/data/apis/users_api.dart';
@@ -448,7 +449,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
children: [
for (int i = 0; i < _searchResults.length; i++) ...[
_buildSearchResultItem(_searchResults[i]),
if (i < _searchResults.length - 1) _buildDivider(),
if (i < _searchResults.length - 1) SharedDivider(),
],
],
),
@@ -620,7 +621,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
children: [
for (int i = 0; i < requests.length; i++) ...[
_buildPendingRequestItem(requests[i]),
if (i < requests.length - 1) _buildDivider(),
if (i < requests.length - 1) SharedDivider(),
],
],
),
@@ -679,7 +680,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
children: [
for (int i = 0; i < friends.length; i++) ...[
_buildContactItem(friends[i]),
if (i < friends.length - 1) _buildDivider(),
if (i < friends.length - 1) SharedDivider(),
],
],
),
@@ -691,7 +692,8 @@ class _ContactsScreenState extends State<ContactsScreen> {
final friendInfo = friend.friend;
return GestureDetector(
onTap: () => context.push('/contacts/add?id=${friendInfo.id}'),
onTap: () =>
context.push('/contacts/${friendInfo.id}', extra: friendInfo),
child: Container(
height: 70,
padding: const EdgeInsets.symmetric(horizontal: 14),
@@ -753,15 +755,6 @@ class _ContactsScreenState extends State<ContactsScreen> {
: colorScheme.primary.withValues(alpha: opacity);
}
Widget _buildDivider() {
final colorScheme = Theme.of(context).colorScheme;
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 14),
color: colorScheme.outlineVariant,
);
}
Widget _buildAvatar(
String? avatarUrl,
String userId,