二次开发源码提交
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// 管理员相关数据模型
|
||||
|
||||
class PaginationModel {
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final int totalItems;
|
||||
|
||||
PaginationModel({
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
required this.totalItems,
|
||||
});
|
||||
|
||||
factory PaginationModel.fromJson(Map<String, dynamic> json) {
|
||||
return PaginationModel(
|
||||
page: json['page'] as int? ?? 0,
|
||||
pageSize: json['page_size'] as int? ?? 10,
|
||||
totalItems: json['total_items'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理员用户组模型
|
||||
class AdminGroupModel {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? permissions;
|
||||
final Map<String, dynamic>? settings;
|
||||
final int? maxStorage;
|
||||
final int? storagePolicyId;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final Map<String, dynamic>? storagePolicy;
|
||||
final int? totalUsers;
|
||||
|
||||
AdminGroupModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.permissions,
|
||||
this.settings,
|
||||
this.maxStorage,
|
||||
this.storagePolicyId,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.storagePolicy,
|
||||
this.totalUsers,
|
||||
});
|
||||
|
||||
factory AdminGroupModel.fromJson(Map<String, dynamic> json) {
|
||||
Map<String, dynamic>? storagePolicy;
|
||||
if (json['edges'] is Map && json['edges']['storage_policies'] is Map) {
|
||||
storagePolicy = json['edges']['storage_policies'] as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
return AdminGroupModel(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
permissions: json['permissions'] as String?,
|
||||
settings: json['settings'] as Map<String, dynamic>?,
|
||||
maxStorage: json['max_storage'] as int?,
|
||||
storagePolicyId: json['storage_policy_id'] as int?,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
storagePolicy: storagePolicy,
|
||||
totalUsers: json['total_users'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式化最大存储空间
|
||||
String get formattedMaxStorage {
|
||||
if (maxStorage == null) return '无限制';
|
||||
return _formatBytes(maxStorage!);
|
||||
}
|
||||
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理员用户模型
|
||||
class AdminUserModel {
|
||||
final int id;
|
||||
final String email;
|
||||
final String nick;
|
||||
final String status;
|
||||
final String? avatar;
|
||||
final int? storage;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? hashId;
|
||||
final int? groupUsers;
|
||||
final AdminGroupModel? group;
|
||||
|
||||
AdminUserModel({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.nick,
|
||||
required this.status,
|
||||
this.avatar,
|
||||
this.storage,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.hashId,
|
||||
this.groupUsers,
|
||||
this.group,
|
||||
});
|
||||
|
||||
factory AdminUserModel.fromJson(Map<String, dynamic> json) {
|
||||
AdminGroupModel? group;
|
||||
if (json['edges'] is Map && json['edges']['group'] is Map) {
|
||||
group = AdminGroupModel.fromJson(
|
||||
json['edges']['group'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
return AdminUserModel(
|
||||
id: json['id'] as int,
|
||||
email: json['email'] as String? ?? '',
|
||||
nick: json['nick'] as String? ?? '',
|
||||
status: json['status'] as String? ?? 'active',
|
||||
avatar: json['avatar'] as String?,
|
||||
storage: json['storage'] as int?,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
hashId: json['hash_id'] as String?,
|
||||
groupUsers: json['group_users'] as int?,
|
||||
group: group,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取用户头像首字母
|
||||
String get initial => nick.isNotEmpty ? nick[0].toUpperCase() : 'U';
|
||||
|
||||
/// 格式化已用存储空间
|
||||
String get formattedStorage {
|
||||
if (storage == null) return '未知';
|
||||
return _formatBytes(storage!);
|
||||
}
|
||||
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理员用户组列表响应
|
||||
class AdminGroupListResponse {
|
||||
final List<AdminGroupModel> groups;
|
||||
final PaginationModel pagination;
|
||||
|
||||
AdminGroupListResponse({required this.groups, required this.pagination});
|
||||
|
||||
factory AdminGroupListResponse.fromJson(Map<String, dynamic> json) {
|
||||
return AdminGroupListResponse(
|
||||
groups: (json['groups'] as List?)
|
||||
?.map((e) => AdminGroupModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
pagination: PaginationModel.fromJson(
|
||||
json['pagination'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理员用户列表响应
|
||||
class AdminUserListResponse {
|
||||
final List<AdminUserModel> users;
|
||||
final PaginationModel pagination;
|
||||
|
||||
AdminUserListResponse({required this.users, required this.pagination});
|
||||
|
||||
factory AdminUserListResponse.fromJson(Map<String, dynamic> json) {
|
||||
return AdminUserListResponse(
|
||||
users: (json['users'] as List?)
|
||||
?.map((e) => AdminUserModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
pagination: PaginationModel.fromJson(
|
||||
json['pagination'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/// 缓存设置模型
|
||||
class CacheSettingsModel {
|
||||
/// 最大缓存大小(字节)
|
||||
final int maxCacheSize;
|
||||
|
||||
/// 缓存过期时间(毫秒)
|
||||
final int cacheExpireDuration;
|
||||
|
||||
/// 是否自动清理最旧文件
|
||||
final bool autoCleanOldFiles;
|
||||
|
||||
CacheSettingsModel({
|
||||
this.maxCacheSize = 500 * 1024 * 1024, // 默认500MB
|
||||
this.cacheExpireDuration = 7 * 24 * 60 * 60 * 1000, // 默认7天
|
||||
this.autoCleanOldFiles = true,
|
||||
});
|
||||
|
||||
factory CacheSettingsModel.fromJson(Map<String, dynamic> json) {
|
||||
return CacheSettingsModel(
|
||||
maxCacheSize: json['max_cache_size'] as int? ?? 500 * 1024 * 1024,
|
||||
cacheExpireDuration:
|
||||
json['cache_expire_duration'] as int? ?? 7 * 24 * 60 * 60 * 1000,
|
||||
autoCleanOldFiles: json['auto_clean_old_files'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'max_cache_size': maxCacheSize,
|
||||
'cache_expire_duration': cacheExpireDuration,
|
||||
'auto_clean_old_files': autoCleanOldFiles,
|
||||
};
|
||||
}
|
||||
|
||||
/// 获取人类可读的最大缓存大小
|
||||
String get maxCacheSizeReadable {
|
||||
return _formatBytes(maxCacheSize);
|
||||
}
|
||||
|
||||
/// 获取人类可读的缓存过期时间
|
||||
String get cacheExpireDurationReadable {
|
||||
final days = cacheExpireDuration ~/ (24 * 60 * 60 * 1000);
|
||||
if (days > 0) {
|
||||
return '$days天';
|
||||
}
|
||||
final hours = cacheExpireDuration ~/ (60 * 60 * 1000);
|
||||
if (hours > 0) {
|
||||
return '$hours小时';
|
||||
}
|
||||
final minutes = cacheExpireDuration ~/ (60 * 1000);
|
||||
return '$minutes分钟';
|
||||
}
|
||||
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) {
|
||||
return '$bytes B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
} else {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
/// 从MB创建
|
||||
static CacheSettingsModel fromMB(int maxCacheSizeMB) {
|
||||
return CacheSettingsModel(maxCacheSize: maxCacheSizeMB * 1024 * 1024);
|
||||
}
|
||||
|
||||
/// 从天数创建
|
||||
static CacheSettingsModel fromDays(int days) {
|
||||
return CacheSettingsModel(
|
||||
cacheExpireDuration: days * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建副本
|
||||
CacheSettingsModel copyWith({
|
||||
int? maxCacheSize,
|
||||
int? cacheExpireDuration,
|
||||
bool? autoCleanOldFiles,
|
||||
}) {
|
||||
return CacheSettingsModel(
|
||||
maxCacheSize: maxCacheSize ?? this.maxCacheSize,
|
||||
cacheExpireDuration: cacheExpireDuration ?? this.cacheExpireDuration,
|
||||
autoCleanOldFiles: autoCleanOldFiles ?? this.autoCleanOldFiles,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取可用预设大小选项
|
||||
static List<int> get availableSizes => [100, 250, 500, 1000, 2000]; // MB
|
||||
|
||||
/// 获取可用预设过期时间选项
|
||||
static List<int> get availableDurations => [1, 3, 7, 15, 30]; // 天
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/// WebDAV 账户模型
|
||||
class DavAccountModel {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
final String uri;
|
||||
final String password;
|
||||
final String options;
|
||||
|
||||
DavAccountModel({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.name,
|
||||
required this.uri,
|
||||
required this.password,
|
||||
required this.options,
|
||||
});
|
||||
|
||||
factory DavAccountModel.fromJson(Map<String, dynamic> json) {
|
||||
return DavAccountModel(
|
||||
id: json['id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
name: json['name'] as String,
|
||||
uri: json['uri'] as String,
|
||||
password: json['password'] as String,
|
||||
options: json['options'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'name': name,
|
||||
'uri': uri,
|
||||
'password': password,
|
||||
'options': options,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// 下载状态
|
||||
enum DownloadStatus {
|
||||
waiting, // 等待中
|
||||
downloading, // 下载中
|
||||
completed, // 已完成
|
||||
paused, // 已暂停
|
||||
failed, // 失败
|
||||
cancelled, // 已取消
|
||||
}
|
||||
|
||||
/// 下载任务模型
|
||||
class DownloadTaskModel {
|
||||
final String id;
|
||||
final String fileName;
|
||||
final String fileUri; // cloudreve URI
|
||||
final String? downloadUrl; // 实际下载URL
|
||||
final int fileSize;
|
||||
final String savePath;
|
||||
String? backgroundTaskId; // background_downloader 的 task ID(可变,用于重启后恢复映射)
|
||||
DownloadStatus status;
|
||||
int downloadedBytes;
|
||||
int speed; // 下载速度,字节/秒
|
||||
bool waitingForWifi; // 是否在等待WiFi(非持久化)
|
||||
final DateTime createdAt;
|
||||
DateTime? completedAt;
|
||||
String? errorMessage;
|
||||
|
||||
double get progress => fileSize > 0 ? downloadedBytes / fileSize : 0.0;
|
||||
int get remainingBytes => fileSize - downloadedBytes;
|
||||
String get speedText {
|
||||
if (speed <= 0) return '';
|
||||
if (speed < 1024) return '$speed B/s';
|
||||
if (speed < 1024 * 1024) return '${(speed / 1024).toStringAsFixed(1)} KB/s';
|
||||
return '${(speed / (1024 * 1024)).toStringAsFixed(1)} MB/s';
|
||||
}
|
||||
String get progressText {
|
||||
if (status == DownloadStatus.completed) {
|
||||
return '100%';
|
||||
}
|
||||
AppLogger.d('DownloadTaskModel -> 已经初始化, status: $status');
|
||||
final percent = (progress * 100).toStringAsFixed(1);
|
||||
AppLogger.d('DownloadTaskModel -> 已经初始化, percent: $percent');
|
||||
return '$percent%';
|
||||
}
|
||||
String get statusText {
|
||||
switch (status) {
|
||||
case DownloadStatus.waiting:
|
||||
return waitingForWifi ? '等待WiFi' : '等待中';
|
||||
case DownloadStatus.downloading:
|
||||
return '下载中';
|
||||
case DownloadStatus.completed:
|
||||
return '已完成';
|
||||
case DownloadStatus.paused:
|
||||
return '已暂停';
|
||||
case DownloadStatus.failed:
|
||||
return '下载失败';
|
||||
case DownloadStatus.cancelled:
|
||||
return '已取消';
|
||||
}
|
||||
}
|
||||
|
||||
DownloadTaskModel({
|
||||
required this.id,
|
||||
required this.fileName,
|
||||
required this.fileUri,
|
||||
this.downloadUrl,
|
||||
required this.fileSize,
|
||||
required this.savePath,
|
||||
this.backgroundTaskId,
|
||||
this.status = DownloadStatus.waiting,
|
||||
this.downloadedBytes = 0,
|
||||
this.speed = 0,
|
||||
this.waitingForWifi = false,
|
||||
DateTime? createdAt,
|
||||
this.completedAt,
|
||||
this.errorMessage,
|
||||
}) : createdAt = createdAt ?? DateTime.now();
|
||||
|
||||
DownloadTaskModel copyWith({
|
||||
String? id,
|
||||
String? fileName,
|
||||
String? fileUri,
|
||||
String? downloadUrl,
|
||||
int? fileSize,
|
||||
String? savePath,
|
||||
String? backgroundTaskId,
|
||||
DownloadStatus? status,
|
||||
int? downloadedBytes,
|
||||
int? speed,
|
||||
bool? waitingForWifi,
|
||||
DateTime? createdAt,
|
||||
DateTime? completedAt,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return DownloadTaskModel(
|
||||
id: id ?? this.id,
|
||||
fileName: fileName ?? this.fileName,
|
||||
fileUri: fileUri ?? this.fileUri,
|
||||
downloadUrl: downloadUrl ?? this.downloadUrl,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
savePath: savePath ?? this.savePath,
|
||||
backgroundTaskId: backgroundTaskId ?? this.backgroundTaskId,
|
||||
status: status ?? this.status,
|
||||
downloadedBytes: downloadedBytes ?? this.downloadedBytes,
|
||||
speed: speed ?? this.speed,
|
||||
waitingForWifi: waitingForWifi ?? this.waitingForWifi,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'fileName': fileName,
|
||||
'fileUri': fileUri,
|
||||
'downloadUrl': downloadUrl,
|
||||
'fileSize': fileSize,
|
||||
'savePath': savePath,
|
||||
'backgroundTaskId': backgroundTaskId,
|
||||
'status': status.index,
|
||||
'downloadedBytes': downloadedBytes,
|
||||
'speed': speed,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'completedAt': completedAt?.toIso8601String(),
|
||||
'errorMessage': errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
factory DownloadTaskModel.fromJson(Map<String, dynamic> json) {
|
||||
return DownloadTaskModel(
|
||||
id: json['id'] as String,
|
||||
fileName: json['fileName'] as String,
|
||||
fileUri: json['fileUri'] as String,
|
||||
downloadUrl: json['downloadUrl'] as String?,
|
||||
fileSize: json['fileSize'] as int,
|
||||
savePath: json['savePath'] as String,
|
||||
backgroundTaskId: json['backgroundTaskId'] as String?,
|
||||
status: DownloadStatus.values[json['status'] as int? ?? 0],
|
||||
downloadedBytes: json['downloadedBytes'] as int? ?? 0,
|
||||
speed: json['speed'] as int? ?? 0,
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: null,
|
||||
completedAt: json['completedAt'] != null
|
||||
? DateTime.parse(json['completedAt'] as String)
|
||||
: null,
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import 'share_model.dart';
|
||||
|
||||
/// 文件模型
|
||||
class FileModel {
|
||||
final int type; // 0:文件, 1:文件夹
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int size;
|
||||
final String path;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final String? permission;
|
||||
final String? primaryEntity;
|
||||
final String? capability;
|
||||
final bool? owned;
|
||||
|
||||
FileModel({
|
||||
required this.type,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.size,
|
||||
required this.path,
|
||||
this.metadata,
|
||||
this.permission,
|
||||
this.primaryEntity,
|
||||
this.capability,
|
||||
this.owned,
|
||||
});
|
||||
|
||||
factory FileModel.fromJson(Map<String, dynamic> json) {
|
||||
return FileModel(
|
||||
type: json['type'] as int,
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||
path: json['path'] as String,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
permission: json['permission'] as String?,
|
||||
primaryEntity: json['primary_entity'] as String?,
|
||||
capability: json['capability'] as String?,
|
||||
owned: json['owned'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type,
|
||||
'id': id,
|
||||
'name': name,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'size': size,
|
||||
'path': path,
|
||||
'metadata': metadata,
|
||||
'permission': permission,
|
||||
'primary_entity': primaryEntity,
|
||||
'capability': capability,
|
||||
'owned': owned,
|
||||
};
|
||||
}
|
||||
|
||||
bool get isFile => type == 0;
|
||||
|
||||
bool get isFolder => type == 1;
|
||||
|
||||
FileModel copyWith({
|
||||
int? type,
|
||||
String? id,
|
||||
String? name,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
int? size,
|
||||
String? path,
|
||||
Map<String, dynamic>? metadata,
|
||||
String? permission,
|
||||
String? primaryEntity,
|
||||
String? capability,
|
||||
bool? owned,
|
||||
}) {
|
||||
return FileModel(
|
||||
type: type ?? this.type,
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
size: size ?? this.size,
|
||||
path: path ?? this.path,
|
||||
metadata: metadata ?? this.metadata,
|
||||
permission: permission ?? this.permission,
|
||||
primaryEntity: primaryEntity ?? this.primaryEntity,
|
||||
capability: capability ?? this.capability,
|
||||
owned: owned ?? this.owned,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取相对于 cloudreve://my 的路径
|
||||
/// 例如: cloudreve://my/Games -> /Games
|
||||
/// cloudreve://my/sub/folder -> /sub/folder
|
||||
String get relativePath {
|
||||
if (!path.startsWith('cloudreve://my')) {
|
||||
// 如果不是 cloudreve://my 开头,返回空
|
||||
return '/';
|
||||
}
|
||||
final prefix = 'cloudreve://my';
|
||||
final relative = path.substring(prefix.length);
|
||||
return relative.isEmpty ? '/' : relative;
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件夹摘要模型
|
||||
class FolderSummaryModel {
|
||||
final int size;
|
||||
final int files;
|
||||
final int folders;
|
||||
final bool completed;
|
||||
final DateTime calculatedAt;
|
||||
|
||||
FolderSummaryModel({
|
||||
required this.size,
|
||||
required this.files,
|
||||
required this.folders,
|
||||
required this.completed,
|
||||
required this.calculatedAt,
|
||||
});
|
||||
|
||||
factory FolderSummaryModel.fromJson(Map<String, dynamic> json) {
|
||||
return FolderSummaryModel(
|
||||
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||
files: (json['files'] as num?)?.toInt() ?? 0,
|
||||
folders: (json['folders'] as num?)?.toInt() ?? 0,
|
||||
completed: json['completed'] as bool,
|
||||
calculatedAt: DateTime.parse(json['calculated_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'size': size,
|
||||
'files': files,
|
||||
'folders': folders,
|
||||
'completed': completed,
|
||||
'calculated_at': calculatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 扩展信息模型
|
||||
class ExtendedInfoModel {
|
||||
final StoragePolicyModel? storagePolicy;
|
||||
final int? storageUsed;
|
||||
final List<ShareModel>? shares;
|
||||
final List<EntityModel>? entities;
|
||||
final List<DirectLinkModel>? directLinks;
|
||||
|
||||
ExtendedInfoModel({
|
||||
this.storagePolicy,
|
||||
this.storageUsed,
|
||||
this.shares,
|
||||
this.entities,
|
||||
this.directLinks,
|
||||
});
|
||||
|
||||
factory ExtendedInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return ExtendedInfoModel(
|
||||
storagePolicy: json['storage_policy'] is Map<String, dynamic>
|
||||
? StoragePolicyModel.fromJson(json['storage_policy'] as Map<String, dynamic>)
|
||||
: null,
|
||||
storageUsed: (json['storage_used'] as num?)?.toInt(),
|
||||
shares: json['shares'] != null
|
||||
? (json['shares'] as List)
|
||||
.map((e) => ShareModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
entities: json['entities'] != null
|
||||
? (json['entities'] as List)
|
||||
.map((e) => EntityModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
directLinks: json['direct_links'] != null
|
||||
? (json['direct_links'] as List)
|
||||
.map((e) => DirectLinkModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'storage_policy': storagePolicy?.toJson(),
|
||||
'storage_used': storageUsed,
|
||||
'shares': shares?.map((e) => e.toJson()).toList(),
|
||||
'entities': entities?.map((e) => e.toJson()).toList(),
|
||||
'direct_links': directLinks?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 存储策略模型
|
||||
class StoragePolicyModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String type;
|
||||
final int? maxSize;
|
||||
|
||||
StoragePolicyModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.maxSize,
|
||||
});
|
||||
|
||||
factory StoragePolicyModel.fromJson(Map<String, dynamic> json) {
|
||||
return StoragePolicyModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
type: json['type'] as String,
|
||||
maxSize: (json['max_size'] as num?)?.toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'max_size': maxSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 实体创建者模型
|
||||
class EntityCreatedByModel {
|
||||
final String id;
|
||||
final String nickname;
|
||||
final String? avatar;
|
||||
final DateTime createdAt;
|
||||
|
||||
EntityCreatedByModel({
|
||||
required this.id,
|
||||
required this.nickname,
|
||||
this.avatar,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory EntityCreatedByModel.fromJson(Map<String, dynamic> json) {
|
||||
return EntityCreatedByModel(
|
||||
id: json['id'] as String,
|
||||
nickname: json['nickname'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'nickname': nickname,
|
||||
'avatar': avatar,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 实体模型(文件版本/Blob)
|
||||
class EntityModel {
|
||||
final String id;
|
||||
final int type;
|
||||
final DateTime createdAt;
|
||||
final int size;
|
||||
final String? encryptedWith;
|
||||
final StoragePolicyModel? storagePolicy;
|
||||
final EntityCreatedByModel? createdBy;
|
||||
|
||||
EntityModel({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.size,
|
||||
this.encryptedWith,
|
||||
this.storagePolicy,
|
||||
this.createdBy,
|
||||
});
|
||||
|
||||
factory EntityModel.fromJson(Map<String, dynamic> json) {
|
||||
return EntityModel(
|
||||
id: json['id'] as String,
|
||||
type: json['type'] as int,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||
encryptedWith: json['encrypted_with'] as String?,
|
||||
storagePolicy: json['storage_policy'] is Map<String, dynamic>
|
||||
? StoragePolicyModel.fromJson(json['storage_policy'] as Map<String, dynamic>)
|
||||
: null,
|
||||
createdBy: json['created_by'] is Map<String, dynamic>
|
||||
? EntityCreatedByModel.fromJson(json['created_by'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'size': size,
|
||||
'encrypted_with': encryptedWith,
|
||||
'storage_policy': storagePolicy?.toJson(),
|
||||
'created_by': createdBy?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 直链模型
|
||||
class DirectLinkModel {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String url;
|
||||
final int downloaded;
|
||||
|
||||
DirectLinkModel({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.url,
|
||||
required this.downloaded,
|
||||
});
|
||||
|
||||
factory DirectLinkModel.fromJson(Map<String, dynamic> json) {
|
||||
return DirectLinkModel(
|
||||
id: json['id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
url: json['url'] as String,
|
||||
downloaded: (json['downloaded'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'url': url,
|
||||
'downloaded': downloaded,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件详情模型(/file/info 接口返回)
|
||||
class FileInfoModel {
|
||||
final FileModel file;
|
||||
final FolderSummaryModel? folderSummary;
|
||||
final ExtendedInfoModel? extendedInfo;
|
||||
|
||||
FileInfoModel({
|
||||
required this.file,
|
||||
this.folderSummary,
|
||||
this.extendedInfo,
|
||||
});
|
||||
|
||||
factory FileInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return FileInfoModel(
|
||||
file: FileModel.fromJson(json),
|
||||
folderSummary: json['folder_summary'] is Map<String, dynamic>
|
||||
? FolderSummaryModel.fromJson(json['folder_summary'] as Map<String, dynamic>)
|
||||
: null,
|
||||
extendedInfo: json['extended_info'] is Map<String, dynamic>
|
||||
? ExtendedInfoModel.fromJson(json['extended_info'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/// 离线下载任务状态
|
||||
enum RemoteDownloadStatus {
|
||||
queued,
|
||||
running,
|
||||
completed,
|
||||
error,
|
||||
suspending,
|
||||
suspended;
|
||||
|
||||
static RemoteDownloadStatus fromString(String value) {
|
||||
return RemoteDownloadStatus.values.firstWhere(
|
||||
(e) => e.name == value,
|
||||
orElse: () => RemoteDownloadStatus.queued,
|
||||
);
|
||||
}
|
||||
|
||||
String get text {
|
||||
switch (this) {
|
||||
case RemoteDownloadStatus.queued:
|
||||
return '排队中';
|
||||
case RemoteDownloadStatus.running:
|
||||
return '下载中';
|
||||
case RemoteDownloadStatus.completed:
|
||||
return '已完成';
|
||||
case RemoteDownloadStatus.error:
|
||||
return '出错';
|
||||
case RemoteDownloadStatus.suspending:
|
||||
return '暂停中';
|
||||
case RemoteDownloadStatus.suspended:
|
||||
return '已暂停';
|
||||
}
|
||||
}
|
||||
|
||||
bool get isOngoing =>
|
||||
this == RemoteDownloadStatus.queued ||
|
||||
this == RemoteDownloadStatus.running ||
|
||||
this == RemoteDownloadStatus.suspending ||
|
||||
this == RemoteDownloadStatus.suspended;
|
||||
}
|
||||
|
||||
/// 种子文件信息
|
||||
class DownloadFileInfo {
|
||||
final int index;
|
||||
final String name;
|
||||
final int size;
|
||||
final double progress;
|
||||
final bool selected;
|
||||
|
||||
const DownloadFileInfo({
|
||||
required this.index,
|
||||
required this.name,
|
||||
required this.size,
|
||||
required this.progress,
|
||||
required this.selected,
|
||||
});
|
||||
|
||||
factory DownloadFileInfo.fromJson(Map<String, dynamic> json) {
|
||||
return DownloadFileInfo(
|
||||
index: json['index'] as int,
|
||||
name: json['name'] as String,
|
||||
size: json['size'] as int,
|
||||
progress: (json['progress'] as num).toDouble(),
|
||||
selected: json['selected'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载详情(aria2 返回的信息)
|
||||
class DownloadInfo {
|
||||
final String name;
|
||||
final String state;
|
||||
final int total;
|
||||
final int downloaded;
|
||||
final int downloadSpeed;
|
||||
final int uploaded;
|
||||
final int uploadSpeed;
|
||||
final String hash;
|
||||
final List<DownloadFileInfo> files;
|
||||
final int numPieces;
|
||||
|
||||
const DownloadInfo({
|
||||
required this.name,
|
||||
required this.state,
|
||||
required this.total,
|
||||
required this.downloaded,
|
||||
required this.downloadSpeed,
|
||||
required this.uploaded,
|
||||
required this.uploadSpeed,
|
||||
required this.hash,
|
||||
required this.files,
|
||||
this.numPieces = 0,
|
||||
});
|
||||
|
||||
double get progress => total > 0 ? downloaded / total : 0.0;
|
||||
|
||||
String get speedText {
|
||||
if (downloadSpeed <= 0) return '';
|
||||
if (downloadSpeed < 1024) return '$downloadSpeed B/s';
|
||||
if (downloadSpeed < 1024 * 1024) {
|
||||
return '${(downloadSpeed / 1024).toStringAsFixed(1)} KB/s';
|
||||
}
|
||||
return '${(downloadSpeed / (1024 * 1024)).toStringAsFixed(1)} MB/s';
|
||||
}
|
||||
|
||||
factory DownloadInfo.fromJson(Map<String, dynamic> json) {
|
||||
final filesList = json['files'] as List<dynamic>? ?? [];
|
||||
return DownloadInfo(
|
||||
name: json['name'] as String? ?? '',
|
||||
state: json['state'] as String? ?? '',
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
downloaded: (json['downloaded'] as num?)?.toInt() ?? 0,
|
||||
downloadSpeed: (json['download_speed'] as num?)?.toInt() ?? 0,
|
||||
uploaded: (json['uploaded'] as num?)?.toInt() ?? 0,
|
||||
uploadSpeed: (json['upload_speed'] as num?)?.toInt() ?? 0,
|
||||
hash: json['hash'] as String? ?? '',
|
||||
files: filesList
|
||||
.map((f) => DownloadFileInfo.fromJson(f as Map<String, dynamic>))
|
||||
.toList(),
|
||||
numPieces: (json['num_pieces'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理节点信息
|
||||
class TaskNode {
|
||||
final String id;
|
||||
final String name;
|
||||
final String type;
|
||||
|
||||
const TaskNode({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
String get displayName {
|
||||
switch (type) {
|
||||
case 'master':
|
||||
return '$name(本机)';
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
factory TaskNode.fromJson(Map<String, dynamic> json) {
|
||||
return TaskNode(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务摘要
|
||||
class TaskSummary {
|
||||
final String phase;
|
||||
final String dst;
|
||||
final int failed;
|
||||
final String src;
|
||||
final String srcStr;
|
||||
final DownloadInfo? download;
|
||||
|
||||
const TaskSummary({
|
||||
required this.phase,
|
||||
required this.dst,
|
||||
required this.failed,
|
||||
required this.src,
|
||||
required this.srcStr,
|
||||
this.download,
|
||||
});
|
||||
|
||||
factory TaskSummary.fromJson(Map<String, dynamic> json) {
|
||||
return TaskSummary(
|
||||
phase: json['phase'] as String? ?? '',
|
||||
dst: json['props']?['dst'] as String? ?? '',
|
||||
failed: (json['props']?['failed'] as num?)?.toInt() ?? 0,
|
||||
src: json['props']?['src'] as String? ?? '',
|
||||
srcStr: json['props']?['src_str'] as String? ?? '',
|
||||
download: json['props']?['download'] != null
|
||||
? DownloadInfo.fromJson(
|
||||
json['props']!['download'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 离线下载任务模型
|
||||
class RemoteDownloadTaskModel {
|
||||
final String id;
|
||||
final RemoteDownloadStatus status;
|
||||
final String type;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int duration;
|
||||
final String? error;
|
||||
final TaskNode? node;
|
||||
final TaskSummary? summary;
|
||||
final int resumeTime;
|
||||
final int retryCount;
|
||||
|
||||
const RemoteDownloadTaskModel({
|
||||
required this.id,
|
||||
required this.status,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.duration,
|
||||
this.error,
|
||||
this.node,
|
||||
this.summary,
|
||||
this.resumeTime = 0,
|
||||
this.retryCount = 0,
|
||||
});
|
||||
|
||||
String getFileNameFromUrl(String url) {
|
||||
if (url.isEmpty) return '';
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.isEmpty) return '';
|
||||
final fileName = segments.last;
|
||||
return fileName.isNotEmpty ? fileName : '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示名称:优先使用 download.name,其次 srcStr,最后 id
|
||||
String get displayName {
|
||||
final dlName = summary?.download?.name;
|
||||
if (dlName != null && dlName.isNotEmpty) {
|
||||
return dlName;
|
||||
} else {
|
||||
final srcStr = summary?.srcStr;
|
||||
if (srcStr == null || srcStr.isEmpty) return id;
|
||||
|
||||
final fileName = getFileNameFromUrl(srcStr);
|
||||
if (fileName.isEmpty) return id;
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化耗时(duration 单位为毫秒)
|
||||
String get durationText {
|
||||
final seconds = duration ~/ 1000;
|
||||
if (seconds <= 0) return '-';
|
||||
if (seconds < 60) return '$seconds 秒';
|
||||
if (seconds < 3600) return '${seconds ~/ 60} 分 ${seconds % 60} 秒';
|
||||
final hours = seconds ~/ 3600;
|
||||
final minutes = (seconds % 3600) ~/ 60;
|
||||
return '$hours 小时 $minutes 分';
|
||||
}
|
||||
|
||||
/// 输入源显示文本
|
||||
String get srcDisplayText {
|
||||
final srcStr = summary?.srcStr;
|
||||
if (srcStr != null && srcStr.isNotEmpty) return srcStr;
|
||||
final src = summary?.src;
|
||||
if (src!.isNotEmpty) {
|
||||
// 从 cloudreve URI 提取文件名
|
||||
final parts = src.split('/');
|
||||
return parts.isNotEmpty ? parts.last : src;
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
/// 输出目标显示文本(从 cloudreve URI 转换为相对路径)
|
||||
String get dstDisplayText {
|
||||
final dst = summary?.dst ?? '';
|
||||
if (dst.isEmpty) return '-';
|
||||
if (dst.startsWith('cloudreve://my')) {
|
||||
final relative = dst.replaceFirst('cloudreve://my', '');
|
||||
return relative.isEmpty ? '/' : relative;
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
factory RemoteDownloadTaskModel.fromJson(Map<String, dynamic> json) {
|
||||
return RemoteDownloadTaskModel(
|
||||
id: json['id'] as String,
|
||||
status: RemoteDownloadStatus.fromString(json['status'] as String),
|
||||
type: json['type'] as String? ?? '',
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String).toLocal()
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String).toLocal()
|
||||
: DateTime.now(),
|
||||
duration: (json['duration'] as num?)?.toInt() ?? 0,
|
||||
error: json['error'] as String?,
|
||||
node: json['node'] != null
|
||||
? TaskNode.fromJson(json['node'] as Map<String, dynamic>)
|
||||
: null,
|
||||
summary: json['summary'] != null
|
||||
? TaskSummary.fromJson(json['summary'] as Map<String, dynamic>)
|
||||
: null,
|
||||
resumeTime: (json['resume_time'] as num?)?.toInt() ?? 0,
|
||||
retryCount: (json['retry_count'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:cloudreve4_flutter/data/models/user_model.dart';
|
||||
|
||||
/// 服务器模型
|
||||
class ServerModel {
|
||||
final String label;
|
||||
final String baseUrl;
|
||||
bool rememberMe;
|
||||
String? email;
|
||||
String? password;
|
||||
UserModel? user;
|
||||
|
||||
ServerModel({
|
||||
required this.label,
|
||||
required this.baseUrl,
|
||||
this.rememberMe = true,
|
||||
this.email,
|
||||
this.password,
|
||||
this.user,
|
||||
});
|
||||
|
||||
ServerModel copyWith({
|
||||
String? label,
|
||||
String? baseUrl,
|
||||
bool? rememberMe,
|
||||
String? email,
|
||||
String? password,
|
||||
UserModel? user,
|
||||
}) {
|
||||
return ServerModel(
|
||||
label: label ?? this.label,
|
||||
baseUrl: baseUrl ?? this.baseUrl,
|
||||
rememberMe: rememberMe ?? this.rememberMe,
|
||||
email: email ?? this.email,
|
||||
password: password ?? this.password,
|
||||
user: user ?? this.user,
|
||||
);
|
||||
}
|
||||
|
||||
factory ServerModel.fromJson(Map<String, dynamic> json) {
|
||||
return ServerModel(
|
||||
label: json['label'] as String,
|
||||
baseUrl: json['baseUrl'] as String,
|
||||
rememberMe: json['rememberMe'] as bool? ?? true,
|
||||
email: json['email'] as String?,
|
||||
password: json['password'] as String?,
|
||||
user: json['user'] != null
|
||||
? UserModel.fromJson(json['user'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'label': label,
|
||||
'baseUrl': baseUrl,
|
||||
'rememberMe': rememberMe,
|
||||
'email': email,
|
||||
'password': password,
|
||||
'user': user?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/// 分享模型
|
||||
class ShareModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final int visited;
|
||||
final int? downloaded;
|
||||
final int? price;
|
||||
final bool unlocked;
|
||||
final int sourceType;
|
||||
final ShareOwner? owner;
|
||||
final DateTime createdAt;
|
||||
final DateTime? expires;
|
||||
final bool expired;
|
||||
final String url;
|
||||
final int? size;
|
||||
final SharePermissionSetting? permissionSetting;
|
||||
final bool? isPrivate;
|
||||
final String? password;
|
||||
final bool? shareView;
|
||||
final String? sourceUri;
|
||||
final bool? showReadme;
|
||||
final bool? passwordProtected;
|
||||
|
||||
ShareModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.visited,
|
||||
this.downloaded,
|
||||
this.price,
|
||||
required this.unlocked,
|
||||
required this.sourceType,
|
||||
this.owner,
|
||||
required this.createdAt,
|
||||
this.expires,
|
||||
required this.expired,
|
||||
required this.url,
|
||||
this.size,
|
||||
this.permissionSetting,
|
||||
this.isPrivate,
|
||||
this.password,
|
||||
this.shareView,
|
||||
this.sourceUri,
|
||||
this.showReadme,
|
||||
this.passwordProtected,
|
||||
});
|
||||
|
||||
factory ShareModel.fromJson(Map<String, dynamic> json) {
|
||||
return ShareModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
visited: (json['visited'] as num?)?.toInt() ?? 0,
|
||||
downloaded: (json['downloaded'] as num?)?.toInt(),
|
||||
price: (json['price'] as num?)?.toInt(),
|
||||
unlocked: json['unlocked'] as bool? ?? false,
|
||||
sourceType: (json['source_type'] as num?)?.toInt() ?? 0,
|
||||
owner: json['owner'] is Map<String, dynamic>
|
||||
? ShareOwner.fromJson(json['owner'] as Map<String, dynamic>)
|
||||
: null,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: DateTime.now(),
|
||||
expires: json['expires'] != null
|
||||
? DateTime.parse(json['expires'] as String)
|
||||
: null,
|
||||
expired: json['expired'] as bool? ?? false,
|
||||
url: json['url'] as String? ?? '',
|
||||
size: (json['size'] as num?)?.toInt(),
|
||||
permissionSetting: json['permission_setting'] is Map<String, dynamic>
|
||||
? SharePermissionSetting.fromJson(json['permission_setting'] as Map<String, dynamic>)
|
||||
: null,
|
||||
isPrivate: json['is_private'] as bool?,
|
||||
password: json['password'] as String?,
|
||||
shareView: json['share_view'] as bool?,
|
||||
sourceUri: json['source_uri'] as String?,
|
||||
showReadme: json['show_readme'] as bool?,
|
||||
passwordProtected: json['password_protected'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'visited': visited,
|
||||
'downloaded': downloaded,
|
||||
'price': price,
|
||||
'unlocked': unlocked,
|
||||
'source_type': sourceType,
|
||||
'owner': owner?.toJson(),
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'expires': expires?.toIso8601String(),
|
||||
'expired': expired,
|
||||
'url': url,
|
||||
'size': size,
|
||||
'permission_setting': permissionSetting?.toJson(),
|
||||
'is_private': isPrivate,
|
||||
'password': password,
|
||||
'share_view': shareView,
|
||||
'source_uri': sourceUri,
|
||||
'show_readme': showReadme,
|
||||
'password_protected': passwordProtected,
|
||||
};
|
||||
}
|
||||
|
||||
bool get isFolder => sourceType == 1;
|
||||
bool get isFile => sourceType == 0;
|
||||
}
|
||||
|
||||
/// 分享所有者信息
|
||||
class ShareOwner {
|
||||
final String id;
|
||||
final String? email;
|
||||
final String nickname;
|
||||
final DateTime createdAt;
|
||||
final ShareOwnerGroup? group;
|
||||
final String? shareLinksInProfile;
|
||||
|
||||
ShareOwner({
|
||||
required this.id,
|
||||
this.email,
|
||||
required this.nickname,
|
||||
required this.createdAt,
|
||||
this.group,
|
||||
this.shareLinksInProfile,
|
||||
});
|
||||
|
||||
factory ShareOwner.fromJson(Map<String, dynamic> json) {
|
||||
return ShareOwner(
|
||||
id: json['id'] as String,
|
||||
email: json['email'] as String?,
|
||||
nickname: json['nickname'] as String,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: DateTime.now(),
|
||||
group: json['group'] is Map<String, dynamic>
|
||||
? ShareOwnerGroup.fromJson(json['group'] as Map<String, dynamic>)
|
||||
: null,
|
||||
shareLinksInProfile: json['share_links_in_profile'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'nickname': nickname,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'group': group?.toJson(),
|
||||
'share_links_in_profile': shareLinksInProfile,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 分享所有者所属组
|
||||
class ShareOwnerGroup {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
ShareOwnerGroup({required this.id, required this.name});
|
||||
|
||||
factory ShareOwnerGroup.fromJson(Map<String, dynamic> json) {
|
||||
return ShareOwnerGroup(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'id': id, 'name': name};
|
||||
}
|
||||
|
||||
/// 权限设置
|
||||
class SharePermissionSetting {
|
||||
final String? sameGroup;
|
||||
final String? other;
|
||||
final String? anonymous;
|
||||
final String? everyone;
|
||||
final Map<String, String>? groupExplicit;
|
||||
final Map<String, String>? userExplicit;
|
||||
|
||||
SharePermissionSetting({
|
||||
this.sameGroup,
|
||||
this.other,
|
||||
this.anonymous,
|
||||
this.everyone,
|
||||
this.groupExplicit,
|
||||
this.userExplicit,
|
||||
});
|
||||
|
||||
factory SharePermissionSetting.fromJson(Map<String, dynamic> json) {
|
||||
return SharePermissionSetting(
|
||||
sameGroup: json['same_group'] as String?,
|
||||
other: json['other'] as String?,
|
||||
anonymous: json['anonymous'] as String?,
|
||||
everyone: json['everyone'] as String?,
|
||||
groupExplicit: (json['group_explicit'] as Map<String, dynamic>?)
|
||||
?.cast<String, String>(),
|
||||
userExplicit: (json['user_explicit'] as Map<String, dynamic>?)
|
||||
?.cast<String, String>(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'same_group': sameGroup,
|
||||
'other': other,
|
||||
'anonymous': anonymous,
|
||||
'everyone': everyone,
|
||||
'group_explicit': groupExplicit,
|
||||
'user_explicit': userExplicit,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// 上传状态
|
||||
enum UploadStatus {
|
||||
waiting, // 等待中
|
||||
uploading, // 上传中
|
||||
completed, // 已完成
|
||||
paused, // 已暂停
|
||||
failed, // 失败
|
||||
cancelled, // 已取消
|
||||
}
|
||||
|
||||
/// 上传会话模型(从 API 返回的上传会话信息)
|
||||
class UploadSessionModel {
|
||||
final String sessionId;
|
||||
final String? uploadId;
|
||||
final int chunkSize;
|
||||
final int expires;
|
||||
final List<String>? uploadUrls;
|
||||
final String? credential;
|
||||
final String? completeUrl;
|
||||
final StoragePolicyModel storagePolicy;
|
||||
final String? mimeType;
|
||||
final String? uploadPolicy;
|
||||
final String? callbackSecret;
|
||||
|
||||
UploadSessionModel({
|
||||
required this.sessionId,
|
||||
this.uploadId,
|
||||
required this.chunkSize,
|
||||
required this.expires,
|
||||
this.uploadUrls,
|
||||
this.credential,
|
||||
this.completeUrl,
|
||||
required this.storagePolicy,
|
||||
this.mimeType,
|
||||
this.uploadPolicy,
|
||||
this.callbackSecret,
|
||||
});
|
||||
|
||||
factory UploadSessionModel.fromJson(Map<String, dynamic> json) {
|
||||
return UploadSessionModel(
|
||||
sessionId: json['session_id'] as String,
|
||||
uploadId: json['upload_id'] as String?,
|
||||
chunkSize: json['chunk_size'] as int,
|
||||
expires: json['expires'] as int,
|
||||
uploadUrls: json['upload_urls'] != null
|
||||
? (json['upload_urls'] as List).map((e) => e as String).toList()
|
||||
: null,
|
||||
credential: json['credential'] as String?,
|
||||
completeUrl: json['completeURL'] as String?,
|
||||
storagePolicy: StoragePolicyModel.fromJson(json['storage_policy'] as Map<String, dynamic>),
|
||||
mimeType: json['mime_type'] as String?,
|
||||
uploadPolicy: json['upload_policy'] as String?,
|
||||
callbackSecret: json['callback_secret'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 是否支持分片上传
|
||||
bool get isMultipartEnabled => chunkSize > 0;
|
||||
|
||||
/// 是否使用中继上传(上传到 Cloudreve 服务器)
|
||||
bool get isRelayUpload => storagePolicy.relay == true || storagePolicy.type == 'local';
|
||||
}
|
||||
|
||||
/// 存储策略模型
|
||||
class StoragePolicyModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String type;
|
||||
final int maxSize;
|
||||
final bool? relay;
|
||||
|
||||
StoragePolicyModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.maxSize,
|
||||
this.relay,
|
||||
});
|
||||
|
||||
factory StoragePolicyModel.fromJson(Map<String, dynamic> json) {
|
||||
return StoragePolicyModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
type: json['type'] as String,
|
||||
maxSize: json['max_size'] as int,
|
||||
relay: json['relay'] as bool?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传任务模型
|
||||
class UploadTaskModel {
|
||||
final String id;
|
||||
final File file;
|
||||
final String fileName;
|
||||
final int fileSize;
|
||||
final String targetPath; // 目标路径,例如 cloudreve://my/subfolder
|
||||
final DateTime createdAt;
|
||||
DateTime? completedAt;
|
||||
final UploadStatus status;
|
||||
final int uploadedBytes;
|
||||
final double progress;
|
||||
final int uploadedChunks; // 已上传的分片数量
|
||||
final int totalChunks; // 总分片数量
|
||||
final String? errorMessage;
|
||||
final UploadSessionModel? session; // 上传会话信息
|
||||
final int speed; // 上传速度,字节/秒
|
||||
|
||||
UploadTaskModel({
|
||||
required this.id,
|
||||
required this.file,
|
||||
required this.fileName,
|
||||
required this.fileSize,
|
||||
required this.targetPath,
|
||||
DateTime? createdAt,
|
||||
this.completedAt,
|
||||
UploadStatus? status,
|
||||
this.uploadedBytes = 0,
|
||||
this.progress = 0,
|
||||
this.uploadedChunks = 0,
|
||||
this.totalChunks = 1,
|
||||
this.errorMessage,
|
||||
this.session,
|
||||
this.speed = 0,
|
||||
}) : createdAt = createdAt ?? DateTime.now(),
|
||||
status = status ?? UploadStatus.waiting;
|
||||
|
||||
/// 计算总分片数
|
||||
int calculateTotalChunks(int chunkSize) {
|
||||
if (chunkSize <= 0) return 1;
|
||||
return (fileSize / chunkSize).ceil();
|
||||
}
|
||||
|
||||
/// 获取状态文本
|
||||
String get statusText {
|
||||
switch (status) {
|
||||
case UploadStatus.waiting:
|
||||
return '等待中';
|
||||
case UploadStatus.uploading:
|
||||
return '上传中...';
|
||||
case UploadStatus.completed:
|
||||
return '上传完成';
|
||||
case UploadStatus.paused:
|
||||
return '已暂停';
|
||||
case UploadStatus.failed:
|
||||
return '上传失败';
|
||||
case UploadStatus.cancelled:
|
||||
return '已取消';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取进度文本
|
||||
String get progressText {
|
||||
if (status == UploadStatus.completed) {
|
||||
return '100%';
|
||||
}
|
||||
return '${(progress * 100).toStringAsFixed(1)}%';
|
||||
}
|
||||
|
||||
/// 获取可读的文件大小
|
||||
String get readableFileSize {
|
||||
if (fileSize < 1024) {
|
||||
return '$fileSize B';
|
||||
} else if (fileSize < 1024 * 1024) {
|
||||
return '${(fileSize / 1024).toStringAsFixed(1)} KB';
|
||||
} else if (fileSize < 1024 * 1024 * 1024) {
|
||||
return '${(fileSize / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
} else {
|
||||
return '${(fileSize / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取可读的上传速度
|
||||
String get speedText {
|
||||
if (speed <= 0) return '';
|
||||
if (speed < 1024) return '$speed B/s';
|
||||
if (speed < 1024 * 1024) return '${(speed / 1024).toStringAsFixed(1)} KB/s';
|
||||
return '${(speed / (1024 * 1024)).toStringAsFixed(1)} MB/s';
|
||||
}
|
||||
|
||||
/// 克隆任务
|
||||
UploadTaskModel copyWith({
|
||||
UploadStatus? status,
|
||||
int? uploadedBytes,
|
||||
double? progress,
|
||||
int? uploadedChunks,
|
||||
int? totalChunks,
|
||||
String? errorMessage,
|
||||
UploadSessionModel? session,
|
||||
DateTime? completedAt,
|
||||
int? speed,
|
||||
}) {
|
||||
return UploadTaskModel(
|
||||
id: id,
|
||||
file: file,
|
||||
fileName: fileName,
|
||||
fileSize: fileSize,
|
||||
targetPath: targetPath,
|
||||
createdAt: createdAt,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
status: status ?? this.status,
|
||||
uploadedBytes: uploadedBytes ?? this.uploadedBytes,
|
||||
progress: progress ?? this.progress,
|
||||
uploadedChunks: uploadedChunks ?? this.uploadedChunks,
|
||||
totalChunks: totalChunks ?? this.totalChunks,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
session: session ?? this.session,
|
||||
speed: speed ?? this.speed,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为 JSON(用于持久化)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'file_path': file.path,
|
||||
'file_name': fileName,
|
||||
'file_size': fileSize,
|
||||
'target_path': targetPath,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'completed_at': completedAt?.toIso8601String(),
|
||||
'status': status.index,
|
||||
'uploaded_bytes': uploadedBytes,
|
||||
'progress': progress,
|
||||
'uploaded_chunks': uploadedChunks,
|
||||
'total_chunks': totalChunks,
|
||||
'error_message': errorMessage,
|
||||
'speed': speed,
|
||||
};
|
||||
}
|
||||
|
||||
/// 从 JSON 创建(用于持久化恢复)
|
||||
factory UploadTaskModel.fromJson(Map<String, dynamic> json) {
|
||||
return UploadTaskModel(
|
||||
id: json['id'] as String,
|
||||
file: File(json['file_path'] as String),
|
||||
fileName: json['file_name'] as String,
|
||||
fileSize: json['file_size'] as int,
|
||||
targetPath: json['target_path'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
completedAt: json['completed_at'] != null
|
||||
? DateTime.parse(json['completed_at'] as String)
|
||||
: null,
|
||||
status: UploadStatus.values[json['status'] as int],
|
||||
uploadedBytes: json['uploaded_bytes'] as int,
|
||||
progress: json['progress'] as double,
|
||||
uploadedChunks: json['uploaded_chunks'] as int,
|
||||
totalChunks: json['total_chunks'] as int,
|
||||
errorMessage: json['error_message'] as String?,
|
||||
speed: json['speed'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/// 用户模型
|
||||
class UserModel {
|
||||
final String id;
|
||||
final String? email;
|
||||
final String nickname;
|
||||
final String? avatar;
|
||||
final DateTime createdAt;
|
||||
final String? preferredTheme;
|
||||
final String? language;
|
||||
final bool? anonymous;
|
||||
final GroupModel? group;
|
||||
final List<PinedFileModel>? pined;
|
||||
|
||||
// Token 信息
|
||||
final TokenModel? token;
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
this.email,
|
||||
required this.nickname,
|
||||
this.avatar,
|
||||
required this.createdAt,
|
||||
this.preferredTheme,
|
||||
this.language,
|
||||
this.anonymous,
|
||||
this.group,
|
||||
this.pined,
|
||||
this.token,
|
||||
});
|
||||
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
id: json['id'] as String,
|
||||
email: json['email'] as String?,
|
||||
nickname: json['nickname'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
preferredTheme: json['preferred_theme'] as String?,
|
||||
language: json['language'] as String?,
|
||||
anonymous: json['anonymous'] as bool?,
|
||||
group: json['group'] != null
|
||||
? GroupModel.fromJson(json['group'] as Map<String, dynamic>)
|
||||
: null,
|
||||
pined: json['pined'] != null
|
||||
? (json['pined'] as List)
|
||||
.map((e) => PinedFileModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
token: json['token'] != null
|
||||
? TokenModel.fromJson(json['token'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
UserModel copyWith({
|
||||
String? id,
|
||||
String? email,
|
||||
String? nickname,
|
||||
String? avatar,
|
||||
DateTime? createdAt,
|
||||
String? preferredTheme,
|
||||
String? language,
|
||||
bool? anonymous,
|
||||
GroupModel? group,
|
||||
List<PinedFileModel>? pined,
|
||||
TokenModel? token,
|
||||
}) {
|
||||
return UserModel(
|
||||
id: id ?? this.id,
|
||||
email: email ?? this.email,
|
||||
nickname: nickname ?? this.nickname,
|
||||
avatar: avatar ?? this.avatar,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
preferredTheme: preferredTheme ?? this.preferredTheme,
|
||||
language: language ?? this.language,
|
||||
anonymous: anonymous ?? this.anonymous,
|
||||
group: group ?? this.group,
|
||||
pined: pined ?? this.pined,
|
||||
token: token ?? this.token,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'nickname': nickname,
|
||||
'avatar': avatar,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'preferred_theme': preferredTheme,
|
||||
'language': language,
|
||||
'anonymous': anonymous,
|
||||
'group': group?.toJson(),
|
||||
'pined': pined?.map((e) => e.toJson()).toList(),
|
||||
'token': token?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户组模型
|
||||
class GroupModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? permission;
|
||||
final int? directLinkBatchSize;
|
||||
final int? trashRetention;
|
||||
|
||||
GroupModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.permission,
|
||||
this.directLinkBatchSize,
|
||||
this.trashRetention,
|
||||
});
|
||||
|
||||
factory GroupModel.fromJson(Map<String, dynamic> json) {
|
||||
return GroupModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
permission: json['permission'] as String?,
|
||||
directLinkBatchSize: json['direct_link_batch_size'] as int?,
|
||||
trashRetention: json['trash_retention'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'permission': permission,
|
||||
'direct_link_batch_size': directLinkBatchSize,
|
||||
'trash_retention': trashRetention,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 固定文件模型
|
||||
class PinedFileModel {
|
||||
final String uri;
|
||||
final String? name;
|
||||
|
||||
PinedFileModel({required this.uri, this.name});
|
||||
|
||||
factory PinedFileModel.fromJson(Map<String, dynamic> json) {
|
||||
return PinedFileModel(
|
||||
uri: json['uri'] as String,
|
||||
name: json['name'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'uri': uri, 'name': name};
|
||||
}
|
||||
}
|
||||
|
||||
/// Token模型
|
||||
class TokenModel {
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
final DateTime accessExpires;
|
||||
final DateTime refreshExpires;
|
||||
|
||||
TokenModel({
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
required this.accessExpires,
|
||||
required this.refreshExpires,
|
||||
});
|
||||
|
||||
factory TokenModel.fromJson(Map<String, dynamic> json) {
|
||||
// 支持两种格式:直接是 token 数据或嵌套在 data 中
|
||||
Map<String, dynamic> data;
|
||||
|
||||
if (json['data'] != null) {
|
||||
data = json['data'] as Map<String, dynamic>;
|
||||
} else {
|
||||
data = json;
|
||||
}
|
||||
|
||||
return TokenModel(
|
||||
accessToken: data['access_token'] as String,
|
||||
refreshToken: data['refresh_token'] as String,
|
||||
accessExpires: DateTime.parse(data['access_expires'] as String),
|
||||
refreshExpires: DateTime.parse(data['refresh_expires'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'access_token': accessToken,
|
||||
'refresh_token': refreshToken,
|
||||
'access_expires': accessExpires.toIso8601String(),
|
||||
'refresh_expires': refreshExpires.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 检查 access token 是否过期
|
||||
bool get isAccessTokenExpired => DateTime.now().isAfter(accessExpires);
|
||||
|
||||
/// 检查 refresh token 是否过期
|
||||
bool get isRefreshTokenExpired => DateTime.now().isAfter(refreshExpires);
|
||||
}
|
||||
|
||||
/// 用户容量模型
|
||||
class CapacityModel {
|
||||
final int total;
|
||||
final int used;
|
||||
double get usagePercentage => total > 0 ? (used / total) * 100 : 0;
|
||||
|
||||
int get remaining => total - used;
|
||||
|
||||
CapacityModel({required this.total, required this.used});
|
||||
|
||||
factory CapacityModel.fromJson(Map<String, dynamic> json) {
|
||||
return CapacityModel(
|
||||
total: json['total'] as int,
|
||||
used: json['used'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'total': total, 'used': used};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/// 用户设置模型 - 对应 GET /user/setting 响应
|
||||
class UserSettingModel {
|
||||
final DateTime? groupExpires;
|
||||
final List<OpenIdProvider> openId;
|
||||
final bool versionRetentionEnabled;
|
||||
final List<String>? versionRetentionExt;
|
||||
final int versionRetentionMax;
|
||||
final bool passwordless;
|
||||
final bool twoFaEnabled;
|
||||
final List<PasskeyModel> passkeys;
|
||||
final List<LoginActivity> loginActivity;
|
||||
final List<StoragePack> storagePacks;
|
||||
final int credit;
|
||||
final bool disableViewSync;
|
||||
final String shareLinksInProfile;
|
||||
final List<OAuthGrant> oauthGrants;
|
||||
|
||||
UserSettingModel({
|
||||
this.groupExpires,
|
||||
this.openId = const [],
|
||||
this.versionRetentionEnabled = false,
|
||||
this.versionRetentionExt,
|
||||
this.versionRetentionMax = 0,
|
||||
this.passwordless = false,
|
||||
this.twoFaEnabled = false,
|
||||
this.passkeys = const [],
|
||||
this.loginActivity = const [],
|
||||
this.storagePacks = const [],
|
||||
this.credit = 0,
|
||||
this.disableViewSync = false,
|
||||
this.shareLinksInProfile = '',
|
||||
this.oauthGrants = const [],
|
||||
});
|
||||
|
||||
factory UserSettingModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserSettingModel(
|
||||
groupExpires: json['group_expires'] != null
|
||||
? DateTime.parse(json['group_expires'] as String)
|
||||
: null,
|
||||
openId: (json['open_id'] as List?)
|
||||
?.map((e) => OpenIdProvider.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
versionRetentionEnabled: json['version_retention_enabled'] as bool? ?? false,
|
||||
versionRetentionExt: (json['version_retention_ext'] as List?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
versionRetentionMax: json['version_retention_max'] as int? ?? 0,
|
||||
passwordless: json['passwordless'] as bool? ?? false,
|
||||
twoFaEnabled: json['two_fa_enabled'] as bool? ?? false,
|
||||
passkeys: (json['passkeys'] as List?)
|
||||
?.map((e) => PasskeyModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
loginActivity: (json['login_activity'] as List?)
|
||||
?.map((e) => LoginActivity.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
storagePacks: (json['storage_packs'] as List?)
|
||||
?.map((e) => StoragePack.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
credit: json['credit'] as int? ?? 0,
|
||||
disableViewSync: json['disable_view_sync'] as bool? ?? false,
|
||||
shareLinksInProfile: json['share_links_in_profile'] as String? ?? '',
|
||||
oauthGrants: (json['oauth_grants'] as List?)
|
||||
?.map((e) => OAuthGrant.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
UserSettingModel copyWith({
|
||||
DateTime? groupExpires,
|
||||
List<OpenIdProvider>? openId,
|
||||
bool? versionRetentionEnabled,
|
||||
List<String>? versionRetentionExt,
|
||||
int? versionRetentionMax,
|
||||
bool? passwordless,
|
||||
bool? twoFaEnabled,
|
||||
List<PasskeyModel>? passkeys,
|
||||
List<LoginActivity>? loginActivity,
|
||||
List<StoragePack>? storagePacks,
|
||||
int? credit,
|
||||
bool? disableViewSync,
|
||||
String? shareLinksInProfile,
|
||||
List<OAuthGrant>? oauthGrants,
|
||||
}) {
|
||||
return UserSettingModel(
|
||||
groupExpires: groupExpires ?? this.groupExpires,
|
||||
openId: openId ?? this.openId,
|
||||
versionRetentionEnabled: versionRetentionEnabled ?? this.versionRetentionEnabled,
|
||||
versionRetentionExt: versionRetentionExt ?? this.versionRetentionExt,
|
||||
versionRetentionMax: versionRetentionMax ?? this.versionRetentionMax,
|
||||
passwordless: passwordless ?? this.passwordless,
|
||||
twoFaEnabled: twoFaEnabled ?? this.twoFaEnabled,
|
||||
passkeys: passkeys ?? this.passkeys,
|
||||
loginActivity: loginActivity ?? this.loginActivity,
|
||||
storagePacks: storagePacks ?? this.storagePacks,
|
||||
credit: credit ?? this.credit,
|
||||
disableViewSync: disableViewSync ?? this.disableViewSync,
|
||||
shareLinksInProfile: shareLinksInProfile ?? this.shareLinksInProfile,
|
||||
oauthGrants: oauthGrants ?? this.oauthGrants,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 已关联的外部身份提供商
|
||||
class OpenIdProvider {
|
||||
final int provider;
|
||||
final DateTime linkedAt;
|
||||
|
||||
OpenIdProvider({required this.provider, required this.linkedAt});
|
||||
|
||||
factory OpenIdProvider.fromJson(Map<String, dynamic> json) {
|
||||
return OpenIdProvider(
|
||||
provider: json['provider'] as int,
|
||||
linkedAt: DateTime.parse(json['linked_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
String get providerName {
|
||||
switch (provider) {
|
||||
case 0:
|
||||
return 'Logto';
|
||||
case 1:
|
||||
return 'QQ';
|
||||
case 2:
|
||||
return 'OIDC';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Passkey 模型
|
||||
class PasskeyModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime? usedAt;
|
||||
final DateTime createdAt;
|
||||
|
||||
PasskeyModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.usedAt,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PasskeyModel.fromJson(Map<String, dynamic> json) {
|
||||
return PasskeyModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
usedAt: json['used_at'] != null
|
||||
? DateTime.parse(json['used_at'] as String)
|
||||
: null,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录活动记录
|
||||
class LoginActivity {
|
||||
final DateTime createdAt;
|
||||
final String ip;
|
||||
final String browser;
|
||||
final String device;
|
||||
final String os;
|
||||
final String loginWith;
|
||||
final int? openIdProvider;
|
||||
final bool success;
|
||||
final bool webdav;
|
||||
|
||||
LoginActivity({
|
||||
required this.createdAt,
|
||||
required this.ip,
|
||||
required this.browser,
|
||||
required this.device,
|
||||
required this.os,
|
||||
required this.loginWith,
|
||||
this.openIdProvider,
|
||||
required this.success,
|
||||
required this.webdav,
|
||||
});
|
||||
|
||||
factory LoginActivity.fromJson(Map<String, dynamic> json) {
|
||||
return LoginActivity(
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
ip: json['ip'] as String,
|
||||
browser: json['browser'] as String,
|
||||
device: json['device'] as String,
|
||||
os: json['os'] as String,
|
||||
loginWith: json['login_with'] as String? ?? '',
|
||||
openIdProvider: json['open_id_provider'] as int?,
|
||||
success: json['success'] as bool? ?? false,
|
||||
webdav: json['webdav'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
String get loginMethodName {
|
||||
if (webdav) return 'WebDAV';
|
||||
switch (loginWith) {
|
||||
case 'passkey':
|
||||
return 'Passkey';
|
||||
case 'openid':
|
||||
return '第三方登录';
|
||||
default:
|
||||
return '密码';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储包
|
||||
class StoragePack {
|
||||
final String name;
|
||||
final DateTime activeSince;
|
||||
final DateTime? expireAt;
|
||||
final int size;
|
||||
|
||||
StoragePack({
|
||||
required this.name,
|
||||
required this.activeSince,
|
||||
this.expireAt,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
factory StoragePack.fromJson(Map<String, dynamic> json) {
|
||||
return StoragePack(
|
||||
name: json['name'] as String,
|
||||
activeSince: DateTime.parse(json['active_since'] as String),
|
||||
expireAt: json['expire_at'] != null
|
||||
? DateTime.parse(json['expire_at'] as String)
|
||||
: null,
|
||||
size: json['size'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isExpired => expireAt != null && DateTime.now().isAfter(expireAt!);
|
||||
}
|
||||
|
||||
/// OAuth 授权应用
|
||||
class OAuthGrant {
|
||||
final String clientId;
|
||||
final String clientName;
|
||||
final String? clientLogo;
|
||||
final List<String> scopes;
|
||||
final DateTime? lastUsedAt;
|
||||
|
||||
OAuthGrant({
|
||||
required this.clientId,
|
||||
required this.clientName,
|
||||
this.clientLogo,
|
||||
this.scopes = const [],
|
||||
this.lastUsedAt,
|
||||
});
|
||||
|
||||
factory OAuthGrant.fromJson(Map<String, dynamic> json) {
|
||||
return OAuthGrant(
|
||||
clientId: json['client_id'] as String,
|
||||
clientName: json['client_name'] as String,
|
||||
clientLogo: json['client_logo'] as String?,
|
||||
scopes: (json['scopes'] as List?)?.map((e) => e as String).toList() ?? [],
|
||||
lastUsedAt: json['last_used_at'] != null
|
||||
? DateTime.parse(json['last_used_at'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户容量模型(扩展版,含存储包)
|
||||
class UserCapacityModel {
|
||||
final int total;
|
||||
final int used;
|
||||
final int storagePackTotal;
|
||||
|
||||
UserCapacityModel({
|
||||
required this.total,
|
||||
required this.used,
|
||||
this.storagePackTotal = 0,
|
||||
});
|
||||
|
||||
factory UserCapacityModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserCapacityModel(
|
||||
total: json['total'] as int,
|
||||
used: json['used'] as int,
|
||||
storagePackTotal: json['storage_pack_total'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
double get usagePercentage => total > 0 ? (used / total) * 100 : 0;
|
||||
int get remaining => total - used;
|
||||
}
|
||||
|
||||
/// 积分变动记录
|
||||
class CreditChange {
|
||||
final DateTime changedAt;
|
||||
final int diff;
|
||||
final String reason;
|
||||
|
||||
CreditChange({
|
||||
required this.changedAt,
|
||||
required this.diff,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
factory CreditChange.fromJson(Map<String, dynamic> json) {
|
||||
return CreditChange(
|
||||
changedAt: DateTime.parse(json['changed_at'] as String),
|
||||
diff: json['diff'] as int,
|
||||
reason: json['reason'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String get reasonLabel {
|
||||
switch (reason) {
|
||||
case 'share_purchased':
|
||||
return '分享被购买';
|
||||
case 'pay':
|
||||
return '支付';
|
||||
case 'adjust':
|
||||
return '管理员调整';
|
||||
default:
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 积分变动列表响应
|
||||
class CreditChangeList {
|
||||
final List<CreditChange> changes;
|
||||
final String? nextToken;
|
||||
|
||||
CreditChangeList({required this.changes, this.nextToken});
|
||||
|
||||
factory CreditChangeList.fromJson(Map<String, dynamic> json) {
|
||||
return CreditChangeList(
|
||||
changes: (json['changes'] as List?)
|
||||
?.map((e) => CreditChange.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
nextToken: (json['pagination'] as Map<String, dynamic>?)?['next_token'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasMore => nextToken != null && nextToken!.isNotEmpty;
|
||||
}
|
||||
Reference in New Issue
Block a user