refactor(apps): 主题系统迁移至 ColorScheme + 扩展架构并支持 Dark Mode
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import '../../core/network/i_api_client.dart';
|
||||
import 'models/calendar_event.dart';
|
||||
|
||||
abstract class CalendarEventRepository {
|
||||
Future<List<CalendarEvent>> listByRange({
|
||||
required DateTime startAt,
|
||||
required DateTime endAt,
|
||||
});
|
||||
|
||||
Future<CalendarEvent> getById(String id);
|
||||
Future<void> acceptSubscription(String itemId);
|
||||
Future<void> rejectSubscription(String itemId);
|
||||
}
|
||||
|
||||
class CalendarEventRepositoryImpl implements CalendarEventRepository {
|
||||
final IApiClient _apiClient;
|
||||
static const _prefix = '/api/v1/schedule-items';
|
||||
|
||||
CalendarEventRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<List<CalendarEvent>> listByRange({
|
||||
required DateTime startAt,
|
||||
required DateTime endAt,
|
||||
}) async {
|
||||
final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String());
|
||||
final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String());
|
||||
final response = await _apiClient.get<List<dynamic>>(
|
||||
'$_prefix?start_at=$start&end_at=$end',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid listByRange response: empty payload');
|
||||
}
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(CalendarEvent.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CalendarEvent> getById(String id) async {
|
||||
final response = await _apiClient.get<Map<String, dynamic>>('$_prefix/$id');
|
||||
final event = response.data;
|
||||
if (event == null) {
|
||||
throw StateError('Invalid getById response: empty payload');
|
||||
}
|
||||
return CalendarEvent.fromJson(event);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> acceptSubscription(String itemId) {
|
||||
return _apiClient.post<void>('$_prefix/$itemId/accept');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rejectSubscription(String itemId) {
|
||||
return _apiClient.post<void>('$_prefix/$itemId/reject');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import '../cache/cache_policy.dart';
|
||||
import '../cache/cached_repository.dart';
|
||||
import '../../core/network/i_api_client.dart';
|
||||
import 'models/schedule_item_model.dart';
|
||||
|
||||
class CalendarRepository extends CachedRepository<List<ScheduleItemModel>> {
|
||||
final IApiClient _apiClient;
|
||||
static const _prefix = '/api/v1/schedule-items';
|
||||
|
||||
CalendarRepository({
|
||||
required super.store,
|
||||
required IApiClient apiClient,
|
||||
CachePolicy? policy,
|
||||
super.now,
|
||||
}) : _apiClient = apiClient,
|
||||
super(
|
||||
policy:
|
||||
policy ??
|
||||
const CachePolicy(
|
||||
softTtl: Duration(minutes: 2),
|
||||
hardTtl: Duration(minutes: 30),
|
||||
minRefreshInterval: Duration(minutes: 1),
|
||||
),
|
||||
);
|
||||
|
||||
static String dayKey(DateTime date) {
|
||||
final day =
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
return 'calendar:day:$day';
|
||||
}
|
||||
|
||||
static String monthKey(DateTime date) {
|
||||
return 'calendar:month:${date.year}-${date.month.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Future<List<ScheduleItemModel>> getDayEvents(
|
||||
DateTime date, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
final key = dayKey(date);
|
||||
final start = DateTime(date.year, date.month, date.day);
|
||||
final end = DateTime(date.year, date.month, date.day, 23, 59, 59);
|
||||
return getOrLoad(
|
||||
key: key,
|
||||
forceRefresh: forceRefresh,
|
||||
loadFromRemote: () => _listByRange(startAt: start, endAt: end),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ScheduleItemModel>> getMonthEvents(
|
||||
DateTime monthStart, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
final key = monthKey(monthStart);
|
||||
final start = DateTime(monthStart.year, monthStart.month, 1);
|
||||
final end = DateTime(monthStart.year, monthStart.month + 1, 0, 23, 59, 59);
|
||||
return getOrLoad(
|
||||
key: key,
|
||||
forceRefresh: forceRefresh,
|
||||
loadFromRemote: () => _listByRange(startAt: start, endAt: end),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ScheduleItemModel> getEventById(String id) async {
|
||||
final response = await _apiClient.get<Map<String, dynamic>>('$_prefix/$id');
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid getEventById response: empty payload');
|
||||
}
|
||||
return ScheduleItemModel.fromJson(data);
|
||||
}
|
||||
|
||||
Future<List<ScheduleItemModel>> listEventsByRange({
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
}) {
|
||||
return _listByRange(startAt: start, endAt: end);
|
||||
}
|
||||
|
||||
Future<List<ScheduleItemModel>> _listByRange({
|
||||
required DateTime startAt,
|
||||
required DateTime endAt,
|
||||
}) async {
|
||||
final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String());
|
||||
final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String());
|
||||
final response = await _apiClient.get<List<dynamic>>(
|
||||
'$_prefix?start_at=$start&end_at=$end',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid listByRange response: empty payload');
|
||||
}
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ScheduleItemModel.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import '../../core/network/i_api_client.dart';
|
||||
import 'models/friend_request.dart';
|
||||
|
||||
abstract class FriendRepository {
|
||||
Future<List<FriendUser>> getFriends();
|
||||
Future<FriendRequest> getRequestById(String friendshipId);
|
||||
Future<Map<String, FriendRequest>> getRequestsByIds(
|
||||
List<String> friendshipIds,
|
||||
);
|
||||
Future<FriendRequest> acceptRequest(String friendshipId);
|
||||
Future<FriendRequest> declineRequest(String friendshipId);
|
||||
}
|
||||
|
||||
class FriendRepositoryImpl implements FriendRepository {
|
||||
final IApiClient _apiClient;
|
||||
static const _prefix = '/api/v1/friends';
|
||||
|
||||
FriendRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<List<FriendUser>> getFriends() async {
|
||||
final response = await _apiClient.get<List<dynamic>>(_prefix);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid getFriends response: empty payload');
|
||||
}
|
||||
return data
|
||||
.map((item) => item as Map<String, dynamic>)
|
||||
.map(
|
||||
(item) => FriendUser.fromJson(item['friend'] as Map<String, dynamic>),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FriendRequest> getRequestById(String friendshipId) async {
|
||||
final response = await _apiClient.get<Map<String, dynamic>>(
|
||||
'$_prefix/requests/$friendshipId',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid getRequestById response: empty payload');
|
||||
}
|
||||
return FriendRequest.fromJson(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, FriendRequest>> getRequestsByIds(
|
||||
List<String> friendshipIds,
|
||||
) async {
|
||||
if (friendshipIds.isEmpty) {
|
||||
return const <String, FriendRequest>{};
|
||||
}
|
||||
|
||||
final pairs = await Future.wait(
|
||||
friendshipIds.map((id) async {
|
||||
final request = await getRequestById(id);
|
||||
return MapEntry(id, request);
|
||||
}),
|
||||
);
|
||||
|
||||
return Map<String, FriendRequest>.fromEntries(pairs);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FriendRequest> acceptRequest(String friendshipId) async {
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'$_prefix/requests/$friendshipId/accept',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid acceptRequest response: empty payload');
|
||||
}
|
||||
return FriendRequest.fromJson(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FriendRequest> declineRequest(String friendshipId) async {
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'$_prefix/requests/$friendshipId/decline',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid declineRequest response: empty payload');
|
||||
}
|
||||
return FriendRequest.fromJson(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import '../../core/network/i_api_client.dart';
|
||||
import 'models/inbox_message.dart';
|
||||
|
||||
abstract class InboxRepository {
|
||||
Future<List<InboxMessage>> getMessages({bool? isRead});
|
||||
Future<InboxMessage> markAsRead(String messageId);
|
||||
}
|
||||
|
||||
class InboxRepositoryImpl implements InboxRepository {
|
||||
final IApiClient _apiClient;
|
||||
static const _prefix = '/api/v1/inbox/messages';
|
||||
|
||||
InboxRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<List<InboxMessage>> getMessages({bool? isRead}) async {
|
||||
final queryParams = isRead != null ? '?is_read=$isRead' : '';
|
||||
final response = await _apiClient.get<List<dynamic>>(
|
||||
'$_prefix$queryParams',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid getMessages response: empty payload');
|
||||
}
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(InboxMessage.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InboxMessage> markAsRead(String messageId) async {
|
||||
final response = await _apiClient.patch<Map<String, dynamic>>(
|
||||
'$_prefix/$messageId/read',
|
||||
);
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw StateError('Invalid markAsRead response: empty payload');
|
||||
}
|
||||
return InboxMessage.fromJson(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
enum CalendarEventStatus { active, archived }
|
||||
|
||||
class CalendarEvent {
|
||||
final String id;
|
||||
final String title;
|
||||
final DateTime startAt;
|
||||
final DateTime? endAt;
|
||||
final CalendarEventStatus status;
|
||||
|
||||
const CalendarEvent({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.startAt,
|
||||
required this.endAt,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory CalendarEvent.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEvent(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
startAt: DateTime.parse(json['start_at'] as String).toLocal(),
|
||||
endAt: json['end_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['end_at'] as String).toLocal(),
|
||||
status: _calendarEventStatusFromApi(json['status'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CalendarEventStatus _calendarEventStatusFromApi(String raw) {
|
||||
switch (raw) {
|
||||
case 'active':
|
||||
return CalendarEventStatus.active;
|
||||
case 'archived':
|
||||
return CalendarEventStatus.archived;
|
||||
default:
|
||||
throw StateError('Unsupported calendar event status: $raw');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
enum FriendRequestStatus { pending, accepted, rejected }
|
||||
|
||||
class FriendUser {
|
||||
final String id;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
|
||||
const FriendUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.avatarUrl,
|
||||
});
|
||||
|
||||
factory FriendUser.fromJson(Map<String, dynamic> json) {
|
||||
return FriendUser(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String,
|
||||
avatarUrl: json['avatar_url'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FriendRequest {
|
||||
final String id;
|
||||
final FriendUser sender;
|
||||
final FriendUser recipient;
|
||||
final String? content;
|
||||
final FriendRequestStatus status;
|
||||
final DateTime createdAt;
|
||||
|
||||
const FriendRequest({
|
||||
required this.id,
|
||||
required this.sender,
|
||||
required this.recipient,
|
||||
required this.content,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory FriendRequest.fromJson(Map<String, dynamic> json) {
|
||||
return FriendRequest(
|
||||
id: json['id'] as String,
|
||||
sender: FriendUser.fromJson(json['sender'] as Map<String, dynamic>),
|
||||
recipient: FriendUser.fromJson(json['recipient'] as Map<String, dynamic>),
|
||||
content: json['content'] as String?,
|
||||
status: _friendRequestStatusFromApi(json['status'] as String),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FriendRequestStatus _friendRequestStatusFromApi(String raw) {
|
||||
switch (raw) {
|
||||
case 'pending':
|
||||
return FriendRequestStatus.pending;
|
||||
case 'accepted':
|
||||
return FriendRequestStatus.accepted;
|
||||
case 'rejected':
|
||||
return FriendRequestStatus.rejected;
|
||||
default:
|
||||
throw StateError('Unsupported friend request status: $raw');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
enum InboxMessageType { friendRequest, calendar, system, group }
|
||||
|
||||
enum InboxMessageStatus { pending, accepted, rejected, dismissed }
|
||||
|
||||
class InboxMessage {
|
||||
final String id;
|
||||
final String recipientId;
|
||||
final String? senderId;
|
||||
final InboxMessageType messageType;
|
||||
final String? scheduleItemId;
|
||||
final String? friendshipId;
|
||||
final Map<String, dynamic>? content;
|
||||
final bool isRead;
|
||||
final InboxMessageStatus status;
|
||||
final DateTime createdAt;
|
||||
|
||||
const InboxMessage({
|
||||
required this.id,
|
||||
required this.recipientId,
|
||||
required this.senderId,
|
||||
required this.messageType,
|
||||
required this.scheduleItemId,
|
||||
required this.friendshipId,
|
||||
required this.content,
|
||||
required this.isRead,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory InboxMessage.fromJson(Map<String, dynamic> json) {
|
||||
return InboxMessage(
|
||||
id: json['id'] as String,
|
||||
recipientId: json['recipient_id'] as String,
|
||||
senderId: json['sender_id'] as String?,
|
||||
messageType: _messageTypeFromApi(json['message_type'] as String),
|
||||
scheduleItemId: json['schedule_item_id'] as String?,
|
||||
friendshipId: json['friendship_id'] as String?,
|
||||
content: json['content'] as Map<String, dynamic>?,
|
||||
isRead: json['is_read'] as bool,
|
||||
status: _messageStatusFromApi(json['status'] as String),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InboxMessageType _messageTypeFromApi(String raw) {
|
||||
switch (raw) {
|
||||
case 'friend_request':
|
||||
return InboxMessageType.friendRequest;
|
||||
case 'calendar':
|
||||
return InboxMessageType.calendar;
|
||||
case 'system':
|
||||
return InboxMessageType.system;
|
||||
case 'group':
|
||||
return InboxMessageType.group;
|
||||
default:
|
||||
throw StateError('Unsupported inbox message type: $raw');
|
||||
}
|
||||
}
|
||||
|
||||
InboxMessageStatus _messageStatusFromApi(String raw) {
|
||||
switch (raw) {
|
||||
case 'pending':
|
||||
return InboxMessageStatus.pending;
|
||||
case 'accepted':
|
||||
return InboxMessageStatus.accepted;
|
||||
case 'rejected':
|
||||
return InboxMessageStatus.rejected;
|
||||
case 'dismissed':
|
||||
return InboxMessageStatus.dismissed;
|
||||
default:
|
||||
throw StateError('Unsupported inbox message status: $raw');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
enum ScheduleSourceType { manual, imported, agentGenerated }
|
||||
|
||||
enum ScheduleStatus { active, archived }
|
||||
|
||||
class ScheduleItemModel {
|
||||
final String id;
|
||||
final String ownerId;
|
||||
final int permission;
|
||||
final bool isOwner;
|
||||
final String title;
|
||||
final String? description;
|
||||
final DateTime startAt;
|
||||
final DateTime? endAt;
|
||||
final String timezone;
|
||||
final ScheduleMetadata? metadata;
|
||||
final ScheduleSourceType sourceType;
|
||||
final ScheduleStatus status;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
static const int permissionView = 1;
|
||||
static const int permissionInvite = 2;
|
||||
static const int permissionEdit = 4;
|
||||
|
||||
bool get canEdit => isOwner || (permission & permissionEdit) != 0;
|
||||
bool get canInvite => isOwner || (permission & permissionInvite) != 0;
|
||||
bool get canDelete => isOwner;
|
||||
|
||||
ScheduleItemModel({
|
||||
required this.id,
|
||||
required this.ownerId,
|
||||
this.permission = 1,
|
||||
this.isOwner = false,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.startAt,
|
||||
this.endAt,
|
||||
this.timezone = 'Asia/Shanghai',
|
||||
this.metadata,
|
||||
this.sourceType = ScheduleSourceType.manual,
|
||||
this.status = ScheduleStatus.active,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) : createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
||||
ScheduleItemModel copyWith({
|
||||
String? id,
|
||||
String? ownerId,
|
||||
int? permission,
|
||||
bool? isOwner,
|
||||
String? title,
|
||||
String? description,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
String? timezone,
|
||||
ScheduleMetadata? metadata,
|
||||
ScheduleSourceType? sourceType,
|
||||
ScheduleStatus? status,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return ScheduleItemModel(
|
||||
id: id ?? this.id,
|
||||
ownerId: ownerId ?? this.ownerId,
|
||||
permission: permission ?? this.permission,
|
||||
isOwner: isOwner ?? this.isOwner,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
startAt: startAt ?? this.startAt,
|
||||
endAt: endAt ?? this.endAt,
|
||||
timezone: timezone ?? this.timezone,
|
||||
metadata: metadata ?? this.metadata,
|
||||
sourceType: sourceType ?? this.sourceType,
|
||||
status: status ?? this.status,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
factory ScheduleItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return ScheduleItemModel(
|
||||
id: json['id'] as String,
|
||||
ownerId: json['owner_id'] as String,
|
||||
permission: json['permission'] as int,
|
||||
isOwner: json['is_owner'] as bool,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
startAt: DateTime.parse(json['start_at'] as String).toLocal(),
|
||||
endAt: json['end_at'] != null
|
||||
? DateTime.parse(json['end_at'] as String).toLocal()
|
||||
: null,
|
||||
timezone: json['timezone'] as String,
|
||||
metadata: json['metadata'] is Map<String, dynamic>
|
||||
? ScheduleMetadata.fromJson(json['metadata'] as Map<String, dynamic>)
|
||||
: null,
|
||||
sourceType: _sourceTypeFromApi(json['source_type'] as String),
|
||||
status: _statusFromApi(json['status'] as String),
|
||||
createdAt: DateTime.parse(json['created_at'] as String).toLocal(),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String).toLocal(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toCreateJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'start_at': startAt.toUtc().toIso8601String(),
|
||||
'end_at': endAt?.toUtc().toIso8601String(),
|
||||
'timezone': timezone,
|
||||
'metadata': metadata?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> toUpdateJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'start_at': startAt.toUtc().toIso8601String(),
|
||||
'end_at': endAt?.toUtc().toIso8601String(),
|
||||
'timezone': timezone,
|
||||
'metadata': metadata?.toJson(),
|
||||
'status': _statusToApi(status),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ScheduleMetadata {
|
||||
final String? color;
|
||||
final String? location;
|
||||
final String? notes;
|
||||
final int? reminderMinutes;
|
||||
final List<Attachment> attachments;
|
||||
final int version;
|
||||
final Map<String, dynamic> raw;
|
||||
|
||||
ScheduleMetadata({
|
||||
this.color,
|
||||
this.location,
|
||||
this.notes,
|
||||
this.reminderMinutes,
|
||||
List<Attachment>? attachments,
|
||||
this.version = 1,
|
||||
Map<String, dynamic>? raw,
|
||||
}) : attachments = attachments ?? const [],
|
||||
raw = raw ?? const {};
|
||||
|
||||
ScheduleMetadata copyWith({
|
||||
String? color,
|
||||
String? location,
|
||||
String? notes,
|
||||
int? reminderMinutes,
|
||||
List<Attachment>? attachments,
|
||||
int? version,
|
||||
Map<String, dynamic>? raw,
|
||||
}) {
|
||||
return ScheduleMetadata(
|
||||
color: color ?? this.color,
|
||||
location: location ?? this.location,
|
||||
notes: notes ?? this.notes,
|
||||
reminderMinutes: reminderMinutes ?? this.reminderMinutes,
|
||||
attachments: attachments ?? this.attachments,
|
||||
version: version ?? this.version,
|
||||
raw: raw ?? this.raw,
|
||||
);
|
||||
}
|
||||
|
||||
factory ScheduleMetadata.fromJson(Map<String, dynamic> json) {
|
||||
final rawAttachments = json['attachments'] as List<dynamic>;
|
||||
final attachments = rawAttachments
|
||||
.map((item) => Attachment.fromJson(item as Map<String, dynamic>))
|
||||
.toList(growable: false);
|
||||
return ScheduleMetadata(
|
||||
color: json['color'] as String?,
|
||||
location: json['location'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
reminderMinutes: json['reminder_minutes'] as int?,
|
||||
attachments: attachments,
|
||||
version: json['version'] as int,
|
||||
raw: Map<String, dynamic>.from(json),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'color': color,
|
||||
'location': location,
|
||||
'notes': notes,
|
||||
'reminder_minutes': reminderMinutes,
|
||||
'attachments': attachments.map((item) => item.toJson()).toList(),
|
||||
'version': version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Attachment {
|
||||
final String name;
|
||||
final List<String> visibleTo;
|
||||
final String? url;
|
||||
final String? note;
|
||||
final String? content;
|
||||
final String type;
|
||||
|
||||
Attachment({
|
||||
required this.name,
|
||||
this.visibleTo = const [],
|
||||
this.url,
|
||||
this.note,
|
||||
this.content,
|
||||
this.type = 'document',
|
||||
});
|
||||
|
||||
Attachment copyWith({
|
||||
String? name,
|
||||
List<String>? visibleTo,
|
||||
String? url,
|
||||
String? note,
|
||||
String? content,
|
||||
String? type,
|
||||
}) {
|
||||
return Attachment(
|
||||
name: name ?? this.name,
|
||||
visibleTo: visibleTo ?? this.visibleTo,
|
||||
url: url ?? this.url,
|
||||
note: note ?? this.note,
|
||||
content: content ?? this.content,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) {
|
||||
final rawVisibleTo = json['visible_to'] as List<dynamic>;
|
||||
final visibleTo = rawVisibleTo.map((item) => item.toString()).toList();
|
||||
return Attachment(
|
||||
name: json['name'] as String,
|
||||
visibleTo: visibleTo,
|
||||
url: json['url'] as String?,
|
||||
note: json['note'] as String?,
|
||||
content: json['content'] as String?,
|
||||
type: json['type'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'visible_to': visibleTo,
|
||||
'url': url,
|
||||
'note': note,
|
||||
'content': content,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ScheduleSourceType _sourceTypeFromApi(String raw) {
|
||||
switch (raw) {
|
||||
case 'imported':
|
||||
return ScheduleSourceType.imported;
|
||||
case 'agent_generated':
|
||||
return ScheduleSourceType.agentGenerated;
|
||||
case 'manual':
|
||||
return ScheduleSourceType.manual;
|
||||
default:
|
||||
throw StateError('Unsupported schedule source type: $raw');
|
||||
}
|
||||
}
|
||||
|
||||
ScheduleStatus _statusFromApi(String raw) {
|
||||
switch (raw) {
|
||||
case 'archived':
|
||||
return ScheduleStatus.archived;
|
||||
case 'active':
|
||||
return ScheduleStatus.active;
|
||||
default:
|
||||
throw StateError('Unsupported schedule status: $raw');
|
||||
}
|
||||
}
|
||||
|
||||
String _statusToApi(ScheduleStatus status) {
|
||||
switch (status) {
|
||||
case ScheduleStatus.active:
|
||||
return 'active';
|
||||
case ScheduleStatus.archived:
|
||||
return 'archived';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
class UserSummary {
|
||||
final String id;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
|
||||
const UserSummary({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.avatarUrl,
|
||||
});
|
||||
|
||||
factory UserSummary.fromJson(Map<String, dynamic> json) {
|
||||
return UserSummary(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String,
|
||||
avatarUrl: json['avatar_url'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import '../../core/network/i_api_client.dart';
|
||||
import 'models/user_summary.dart';
|
||||
|
||||
abstract class UserRepository {
|
||||
Future<UserSummary> getById(String userId);
|
||||
Future<UserSummary> getMe();
|
||||
}
|
||||
|
||||
class UserRepositoryImpl implements UserRepository {
|
||||
final IApiClient _apiClient;
|
||||
static const _prefix = '/api/v1/users';
|
||||
|
||||
UserRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<UserSummary> getById(String userId) async {
|
||||
final response = await _apiClient.get<Map<String, dynamic>>(
|
||||
'$_prefix/$userId',
|
||||
);
|
||||
final user = response.data;
|
||||
if (user == null) {
|
||||
throw StateError('Invalid getById response: empty payload');
|
||||
}
|
||||
return UserSummary.fromJson(user);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserSummary> getMe() async {
|
||||
final response = await _apiClient.get<Map<String, dynamic>>('$_prefix/me');
|
||||
final user = response.data;
|
||||
if (user == null) {
|
||||
throw StateError('Invalid getMe response: empty payload');
|
||||
}
|
||||
return UserSummary.fromJson(user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user