This commit is contained in:
2026-10-16 01:25:50 +05:30
commit c60ea2d370
89 changed files with 4661 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
class Account {
const Account({
required this.id,
required this.username,
required this.isAdmin,
required this.permissions,
});
final String id;
final String username;
final bool isAdmin;
final Map<String, dynamic> permissions;
bool get canReadSupport => isAdmin || _supportFlag('read');
bool get canWriteSupport => isAdmin || _supportFlag('write');
bool _supportFlag(String key) {
final support = permissions['support'];
if (support is Map) {
return support[key] == true;
}
return false;
}
factory Account.fromJson(Map<String, dynamic> json) {
return Account(
id: json['id']?.toString() ?? '',
username: json['username'] as String? ?? '',
isAdmin: json['isAdmin'] == true,
permissions: _stringKeyMap(json['permissions']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'username': username,
'isAdmin': isAdmin,
'permissions': permissions,
};
}
}
Map<String, dynamic> _stringKeyMap(Object? value) {
if (value is! Map) {
return {};
}
return value.map((key, item) => MapEntry(key.toString(), item));
}
+45
View File
@@ -0,0 +1,45 @@
class ApiException implements Exception {
const ApiException({
this.statusCode,
required this.message,
this.retryAfterSeconds,
});
final int? statusCode;
final String message;
final int? retryAfterSeconds;
bool get isUnauthorized => statusCode == 401;
@override
String toString() => message;
static ApiException fromBody({
int? statusCode,
Object? body,
String fallback = 'Something went wrong.',
}) {
var message = fallback;
int? retryAfterSeconds;
if (body is Map) {
final error = body['error'];
if (error is String && error.trim().isNotEmpty) {
message = error.trim();
}
final retry = body['retryAfterSeconds'];
if (retry is int) {
retryAfterSeconds = retry;
} else if (retry is num) {
retryAfterSeconds = retry.toInt();
}
}
if (retryAfterSeconds != null) {
message = '$message Retry after $retryAfterSeconds seconds.';
}
return ApiException(
statusCode: statusCode,
message: message,
retryAfterSeconds: retryAfterSeconds,
);
}
}
+20
View File
@@ -0,0 +1,20 @@
import 'account.dart';
class LoginResult {
const LoginResult({required this.token, required this.account});
final String token;
final Account account;
factory LoginResult.fromJson(Map<String, dynamic> json) {
final token = json['token'];
final account = json['account'];
if (token is! String || token.isEmpty || account is! Map) {
throw const FormatException('Unexpected login payload');
}
return LoginResult(
token: token,
account: Account.fromJson(Map<String, dynamic>.from(account)),
);
}
}
+69
View File
@@ -0,0 +1,69 @@
import 'support_ticket.dart';
const maxReplyLength = 4000;
class SupportMessage {
const SupportMessage({
required this.id,
required this.ticketId,
required this.authorKind,
required this.authorName,
required this.body,
required this.createdAt,
});
final String id;
final String ticketId;
final String authorKind;
final String authorName;
final String body;
final DateTime createdAt;
bool get isStaff => authorKind == 'staff';
factory SupportMessage.fromJson(Map<String, dynamic> json) {
return SupportMessage(
id: json['id'] as String,
ticketId: json['ticketId'] as String,
authorKind: json['authorKind'] as String? ?? 'player',
authorName: json['authorName'] as String? ?? '',
body: json['body'] as String? ?? '',
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
}
class SupportThread {
const SupportThread({required this.ticket, required this.messages});
final SupportTicket ticket;
final List<SupportMessage> messages;
factory SupportThread.fromJson(Map<String, dynamic> json) {
final ticketJson = json['ticket'];
final messagesJson = json['messages'];
if (ticketJson is! Map || messagesJson is! List) {
throw const FormatException('Unexpected thread payload');
}
return SupportThread(
ticket: SupportTicket.fromJson(Map<String, dynamic>.from(ticketJson)),
messages: [
for (final item in messagesJson)
if (item is Map)
SupportMessage.fromJson(Map<String, dynamic>.from(item)),
],
);
}
}
/// Returns an error message when [raw] is not 1–4000 characters after trim.
String? validateReplyBody(String raw) {
final body = raw.trim();
if (body.isEmpty) {
return 'Enter a message.';
}
if (body.length > maxReplyLength) {
return 'Message must be $maxReplyLength characters or fewer.';
}
return null;
}
+47
View File
@@ -0,0 +1,47 @@
class SupportTicket {
const SupportTicket({
required this.id,
required this.userId,
required this.username,
required this.category,
required this.categoryLabel,
required this.status,
required this.createdAt,
required this.updatedAt,
required this.lastMessageAt,
required this.lastMessagePreview,
});
final String id;
final int userId;
final String username;
final String category;
final String categoryLabel;
final String status;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime lastMessageAt;
final String lastMessagePreview;
bool get isClosed => status == 'closed';
String get displayCategory =>
categoryLabel.isNotEmpty ? categoryLabel : category;
String get statusLabel => isClosed ? 'Closed' : 'Open';
factory SupportTicket.fromJson(Map<String, dynamic> json) {
return SupportTicket(
id: json['id'] as String,
userId: (json['userId'] as num).toInt(),
username: json['username'] as String? ?? '',
category: json['category'] as String? ?? '',
categoryLabel: json['categoryLabel'] as String? ?? '',
status: json['status'] as String? ?? 'open',
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
lastMessageAt: DateTime.parse(json['lastMessageAt'] as String),
lastMessagePreview: json['lastMessagePreview'] as String? ?? '',
);
}
}