二次开发源码提交
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../data/models/admin_model.dart';
|
||||
import '../../services/admin_service.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
enum AdminState { idle, loading, error }
|
||||
|
||||
/// 管理员数据 Provider
|
||||
class AdminProvider extends ChangeNotifier {
|
||||
AdminState _state = AdminState.idle;
|
||||
List<AdminGroupModel> _groups = [];
|
||||
List<AdminUserModel> _users = [];
|
||||
PaginationModel? _groupsPagination;
|
||||
PaginationModel? _usersPagination;
|
||||
String? _errorMessage;
|
||||
|
||||
// 用户多选状态
|
||||
final Set<int> _selectedUserIds = {};
|
||||
bool _isSelectingUsers = false;
|
||||
|
||||
AdminState get state => _state;
|
||||
List<AdminGroupModel> get groups => _groups;
|
||||
List<AdminUserModel> get users => _users;
|
||||
PaginationModel? get groupsPagination => _groupsPagination;
|
||||
PaginationModel? get usersPagination => _usersPagination;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get isLoading => _state == AdminState.loading;
|
||||
Set<int> get selectedUserIds => _selectedUserIds;
|
||||
bool get isSelectingUsers => _isSelectingUsers;
|
||||
bool get hasSelectedUsers => _selectedUserIds.isNotEmpty;
|
||||
|
||||
final AdminService _service = AdminService.instance;
|
||||
|
||||
/// 加载用户组列表
|
||||
Future<void> loadGroups({int page = 1}) async {
|
||||
try {
|
||||
final response = await _service.getGroups(page: page);
|
||||
_groups = response.groups;
|
||||
_groupsPagination = response.pagination;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
AppLogger.d('加载用户组失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载用户列表
|
||||
Future<void> loadUsers({int page = 1}) async {
|
||||
try {
|
||||
final response = await _service.getUsers(page: page);
|
||||
_users = response.users;
|
||||
_usersPagination = response.pagination;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
AppLogger.d('加载用户列表失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载全部管理员数据
|
||||
Future<void> loadAll() async {
|
||||
_setState(AdminState.loading);
|
||||
try {
|
||||
await Future.wait([
|
||||
loadGroups(),
|
||||
loadUsers(),
|
||||
]);
|
||||
_setState(AdminState.idle);
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
_setState(AdminState.error);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建用户组
|
||||
Future<bool> createGroup(String name) async {
|
||||
try {
|
||||
final group = await _service.createGroup(name);
|
||||
_groups.insert(0, group);
|
||||
if (_groupsPagination != null) {
|
||||
_groupsPagination = PaginationModel(
|
||||
page: _groupsPagination!.page,
|
||||
pageSize: _groupsPagination!.pageSize,
|
||||
totalItems: _groupsPagination!.totalItems + 1,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('创建用户组失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除用户组(会先检查组下是否有用户)
|
||||
Future<String?> deleteGroup(int groupId) async {
|
||||
try {
|
||||
final detail = await _service.getGroupDetail(groupId);
|
||||
if ((detail.totalUsers ?? 0) > 0) {
|
||||
return '该组下有 ${detail.totalUsers} 个用户,请先删除或迁移用户';
|
||||
}
|
||||
await _service.deleteGroup(groupId);
|
||||
_groups.removeWhere((g) => g.id == groupId);
|
||||
if (_groupsPagination != null) {
|
||||
_groupsPagination = PaginationModel(
|
||||
page: _groupsPagination!.page,
|
||||
pageSize: _groupsPagination!.pageSize,
|
||||
totalItems: _groupsPagination!.totalItems - 1,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
return null;
|
||||
} catch (e) {
|
||||
AppLogger.d('删除用户组失败: $e');
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建用户
|
||||
Future<bool> createUser({
|
||||
required String email,
|
||||
required String nick,
|
||||
required String password,
|
||||
required int groupId,
|
||||
}) async {
|
||||
try {
|
||||
final user = await _service.createUser(
|
||||
email: email,
|
||||
nick: nick,
|
||||
password: password,
|
||||
groupId: groupId,
|
||||
);
|
||||
_users.insert(0, user);
|
||||
if (_usersPagination != null) {
|
||||
_usersPagination = PaginationModel(
|
||||
page: _usersPagination!.page,
|
||||
pageSize: _usersPagination!.pageSize,
|
||||
totalItems: _usersPagination!.totalItems + 1,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('创建用户失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量删除用户
|
||||
Future<bool> batchDeleteUsers(List<int> ids) async {
|
||||
try {
|
||||
await _service.batchDeleteUsers(ids);
|
||||
_users.removeWhere((u) => ids.contains(u.id));
|
||||
if (_usersPagination != null) {
|
||||
_usersPagination = PaginationModel(
|
||||
page: _usersPagination!.page,
|
||||
pageSize: _usersPagination!.pageSize,
|
||||
totalItems: _usersPagination!.totalItems - ids.length,
|
||||
);
|
||||
}
|
||||
exitSelectMode();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('批量删除用户失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 多选模式 ---
|
||||
|
||||
void toggleSelectMode() {
|
||||
_isSelectingUsers = !_isSelectingUsers;
|
||||
if (!_isSelectingUsers) {
|
||||
_selectedUserIds.clear();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void exitSelectMode() {
|
||||
_isSelectingUsers = false;
|
||||
_selectedUserIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleUserSelection(int userId) {
|
||||
if (_selectedUserIds.contains(userId)) {
|
||||
_selectedUserIds.remove(userId);
|
||||
} else {
|
||||
_selectedUserIds.add(userId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void selectAllUsers() {
|
||||
_selectedUserIds.clear();
|
||||
_selectedUserIds.addAll(_users.map((u) => u.id));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearUserSelection() {
|
||||
_selectedUserIds.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isUserSelected(int userId) => _selectedUserIds.contains(userId);
|
||||
|
||||
void _setState(AdminState state) {
|
||||
_state = state;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/toast_helper.dart';
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/user_avatar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../data/models/server_model.dart';
|
||||
import '../../data/models/user_model.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../services/server_service.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
import '../../services/api_service.dart';
|
||||
import '../../core/exceptions/app_exception.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// 认证状态
|
||||
enum AuthState { loading, authenticated, unauthenticated, error }
|
||||
|
||||
/// 认证Provider
|
||||
class AuthProvider extends ChangeNotifier {
|
||||
AuthState _state = AuthState.unauthenticated;
|
||||
UserModel? _user;
|
||||
String? _errorMessage;
|
||||
bool _hasRefreshTokenExpired = false;
|
||||
|
||||
AuthState get state => _state;
|
||||
UserModel? get user => _user;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get isAuthenticated => _state == AuthState.authenticated;
|
||||
bool get isLoading => _state == AuthState.loading;
|
||||
bool get hasRefreshTokenExpired => _hasRefreshTokenExpired;
|
||||
bool get isAdmin {
|
||||
final name = _user?.group?.name.toLowerCase();
|
||||
return name == 'admin' || name == '管理员';
|
||||
}
|
||||
|
||||
/// 当前选中的服务器
|
||||
ServerModel? get currentServer => ServerService.instance.currentServer;
|
||||
|
||||
/// 获取当前用户的 token
|
||||
TokenModel? get token => _user?.token;
|
||||
|
||||
/// 初始化
|
||||
Future<void> init() async {
|
||||
try {
|
||||
setState(AuthState.loading);
|
||||
|
||||
// 初始化服务器服务
|
||||
await ServerService.instance.init();
|
||||
|
||||
// 获取当前服务器
|
||||
final server = ServerService.instance.currentServer;
|
||||
if (server == null) {
|
||||
setState(AuthState.unauthenticated);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 API 的 baseUrl
|
||||
await _setApiBaseUrl(server.baseUrl);
|
||||
|
||||
// 设置 ApiService 的认证回调
|
||||
_setupApiCallbacks();
|
||||
|
||||
// 检查是否有保存的登录信息
|
||||
if (server.user != null && server.user!.token != null) {
|
||||
// 有保存的用户信息,检查 token 是否过期
|
||||
if (!server.user!.token!.isRefreshTokenExpired) {
|
||||
// Refresh token 未过期,设置用户信息
|
||||
setUser(server.user);
|
||||
setState(AuthState.authenticated);
|
||||
return;
|
||||
} else {
|
||||
// Refresh token 已过期,清除登录信息
|
||||
await ServerService.instance.clearCurrentServerLogin();
|
||||
}
|
||||
}
|
||||
|
||||
_user = null;
|
||||
setState(AuthState.unauthenticated);
|
||||
} catch (e) {
|
||||
AppLogger.d('AuthProvider 初始化失败: $e');
|
||||
_user = null;
|
||||
setState(AuthState.unauthenticated);
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 ApiService 的认证回调
|
||||
void _setupApiCallbacks() {
|
||||
ApiService.setAuthCallbacks(
|
||||
getToken: () async {
|
||||
// 返回当前的 access token
|
||||
return _user?.token?.accessToken;
|
||||
},
|
||||
refreshToken: () async {
|
||||
// 刷新 token
|
||||
try {
|
||||
await refreshToken();
|
||||
} catch (e) {
|
||||
// 刷新失败,设置过期标志
|
||||
if (e is RefreshTokenExpiredException) {
|
||||
setRefreshTokenExpired();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
clearAuth: () async {
|
||||
// 清除认证数据
|
||||
await ServerService.instance.clearCurrentServerLogin();
|
||||
setRefreshTokenExpired();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 设置 API baseUrl
|
||||
Future<void> _setApiBaseUrl(String baseUrl) async {
|
||||
// 同时更新存储和 ApiService 的 baseUrl
|
||||
final storageService = StorageService.instance;
|
||||
await storageService.setCustomBaseUrl(baseUrl);
|
||||
await ApiService.instance.setBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
/// 密码登录
|
||||
Future<bool> passwordLogin({
|
||||
required String email,
|
||||
required String password,
|
||||
bool rememberMe = false,
|
||||
}) async {
|
||||
try {
|
||||
setState(AuthState.loading);
|
||||
|
||||
// 获取当前服务器
|
||||
final server = ServerService.instance.currentServer;
|
||||
if (server == null) {
|
||||
_errorMessage = '服务器初始化失败';
|
||||
_user = null;
|
||||
setState(AuthState.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 每次登录时都重新设置 API 的 baseUrl,确保使用最新的服务器地址
|
||||
await _setApiBaseUrl(server.baseUrl);
|
||||
|
||||
// 执行登录
|
||||
final response = await AuthService.instance.passwordLogin(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
AppLogger.d('AuthProvider 登录成功: $response');
|
||||
// 保存登录信息到当前服务器(包含完整 user 和 token)
|
||||
await ServerService.instance.updateCurrentServerLogin(
|
||||
email: rememberMe ? email : null,
|
||||
password: rememberMe ? password : null,
|
||||
user: response.user,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
|
||||
setUser(response.user);
|
||||
setState(AuthState.authenticated);
|
||||
|
||||
return true;
|
||||
} on TwoFactorRequiredException {
|
||||
// 两步验证需要,不设置 error 状态,重新抛出让调用方处理
|
||||
setState(AuthState.unauthenticated);
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
_user = null;
|
||||
setState(AuthState.error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 两步验证登录
|
||||
Future<bool> twoFactorLogin({
|
||||
required String otp,
|
||||
required String sessionId,
|
||||
required String email,
|
||||
required String password,
|
||||
bool rememberMe = false,
|
||||
}) async {
|
||||
try {
|
||||
setState(AuthState.loading);
|
||||
|
||||
final response = await AuthService.instance.twoFactorLogin(
|
||||
otp: otp,
|
||||
sessionId: sessionId,
|
||||
);
|
||||
|
||||
await ServerService.instance.updateCurrentServerLogin(
|
||||
email: rememberMe ? email : null,
|
||||
password: rememberMe ? password : null,
|
||||
user: response.user,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
|
||||
setUser(response.user);
|
||||
setState(AuthState.authenticated);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
_user = null;
|
||||
setState(AuthState.error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 登出
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
// 调用登出 API(需要 token)
|
||||
if (token?.refreshToken != null) {
|
||||
try {
|
||||
await AuthService.instance.logout();
|
||||
} catch (e) {
|
||||
// 登出 API 调用失败不影响本地清理
|
||||
AppLogger.d('登出 API 调用失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 清除当前服务器的登录信息
|
||||
await ServerService.instance.clearCurrentServerLogin();
|
||||
|
||||
_clearUserData();
|
||||
// 清除头像缓存
|
||||
await UserAvatar.clearAllCache();
|
||||
setState(AuthState.unauthenticated);
|
||||
} catch (e) {
|
||||
// 即使出错也要清除本地状态
|
||||
_clearUserData();
|
||||
setState(AuthState.unauthenticated);
|
||||
_errorMessage = e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新用户信息
|
||||
Future<void> refreshUser() async {
|
||||
try {
|
||||
final user = await AuthService.instance.getCurrentUser();
|
||||
setUser(user);
|
||||
|
||||
// 更新服务器中的用户信息(保留 token)
|
||||
final server = ServerService.instance.currentServer;
|
||||
if (server != null && token != null) {
|
||||
await ServerService.instance.updateCurrentServerLogin(
|
||||
user: user.copyWith(token: token),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_errorMessage = e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新 token
|
||||
Future<void> refreshToken() async {
|
||||
try {
|
||||
final currentToken = token;
|
||||
if (currentToken == null || currentToken.isRefreshTokenExpired) {
|
||||
ToastHelper.failure('已注销或Refresh token 已过期,需要重新登录');
|
||||
throw Exception('Refresh token 已过期,需要重新登录');
|
||||
}
|
||||
|
||||
// 调用刷新 token 接口
|
||||
final newToken = await AuthService.instance.refreshToken(currentToken.refreshToken);
|
||||
|
||||
// 更新用户信息中的 token
|
||||
final updatedUser = _user!.copyWith(token: newToken);
|
||||
|
||||
// 保存到服务器
|
||||
await ServerService.instance.updateCurrentServerLogin(user: updatedUser);
|
||||
|
||||
setUser(updatedUser);
|
||||
} catch (e) {
|
||||
ToastHelper.error('刷新 token 失败: $e');
|
||||
AppLogger.d('刷新 token 失败: $e');
|
||||
_user = null;
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置用户
|
||||
void setUser(UserModel? user) {
|
||||
_user = user;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 设置状态
|
||||
void setState(AuthState state) {
|
||||
_state = state;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 清除错误
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 清除用户数据
|
||||
void _clearUserData() {
|
||||
_user = null;
|
||||
_errorMessage = null;
|
||||
_hasRefreshTokenExpired = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 处理 RefreshTokenExpiredException
|
||||
void setRefreshTokenExpired() {
|
||||
_hasRefreshTokenExpired = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearRefreshTokenExpired() {
|
||||
_hasRefreshTokenExpired = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取上次登录的邮箱(用于填充输入框)
|
||||
String? get rememberedEmail {
|
||||
final server = ServerService.instance.currentServer;
|
||||
return server?.email;
|
||||
}
|
||||
|
||||
/// 获取上次登录的密码(用于填充输入框)
|
||||
String? get rememberedPassword {
|
||||
final server = ServerService.instance.currentServer;
|
||||
return server?.password;
|
||||
}
|
||||
|
||||
/// 是否记住密码
|
||||
bool get rememberMe {
|
||||
final server = ServerService.instance.currentServer;
|
||||
return server?.rememberMe ?? false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../core/constants/storage_keys.dart';
|
||||
import '../../data/models/download_task_model.dart';
|
||||
import '../../services/download_service.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// 下载管理Provider
|
||||
class DownloadManagerProvider extends ChangeNotifier {
|
||||
final DownloadService _downloadService = DownloadService();
|
||||
final Map<String, DownloadTaskModel> _tasks = {};
|
||||
bool _isInitialized = false;
|
||||
bool _isWifiOnlyEnabled = false;
|
||||
|
||||
// 速度追踪:记录每个任务的上次进度更新时间和字节数
|
||||
final Map<String, DateTime> _lastProgressTime = {};
|
||||
final Map<String, int> _lastProgressBytes = {};
|
||||
|
||||
/// 获取所有下载任务
|
||||
List<DownloadTaskModel> get tasks => _tasks.values.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
/// 获取指定状态的任务
|
||||
List<DownloadTaskModel> getTasksByStatus(DownloadStatus status) {
|
||||
return tasks.where((task) => task.status == status).toList();
|
||||
}
|
||||
|
||||
/// 下载中的任务数
|
||||
int get downloadingCount =>
|
||||
getTasksByStatus(DownloadStatus.downloading).length;
|
||||
|
||||
/// 活跃任务数(下载中 + 等待中 + 暂停)
|
||||
int get activeTaskCount => tasks
|
||||
.where((t) =>
|
||||
t.status == DownloadStatus.downloading ||
|
||||
t.status == DownloadStatus.waiting ||
|
||||
t.status == DownloadStatus.paused)
|
||||
.length;
|
||||
|
||||
/// WiFi-only 设置
|
||||
bool get isWifiOnlyEnabled => _isWifiOnlyEnabled;
|
||||
|
||||
/// 初始化下载服务
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
await _downloadService.initialize(callbackHandler: _handleDownloadCallback);
|
||||
|
||||
// 加载 WiFi-only 设置
|
||||
_isWifiOnlyEnabled =
|
||||
await StorageService.instance.getBool(StorageKeys.downloadWifiOnly) ??
|
||||
false;
|
||||
|
||||
// 从本地存储加载已保存的下载任务
|
||||
await _loadTasks();
|
||||
|
||||
_isInitialized = true;
|
||||
AppLogger.d('DownloadManagerProvider 初始化完成');
|
||||
}
|
||||
|
||||
/// 更新 WiFi-only 设置,并同步等待中的任务
|
||||
Future<void> setWifiOnlyEnabled(bool value) async {
|
||||
_isWifiOnlyEnabled = value;
|
||||
await StorageService.instance
|
||||
.setBool(StorageKeys.downloadWifiOnly, value);
|
||||
|
||||
// 如果关闭了WiFi-only,需要将等待WiFi的任务重新入队
|
||||
if (!value) {
|
||||
for (final task in _tasks.values.toList()) {
|
||||
if (task.waitingForWifi) {
|
||||
// 取消当前等待WiFi的任务,重新入队(不需要WiFi)
|
||||
await _downloadService.cancelDownload(task.id);
|
||||
_tasks[task.id] = task.copyWith(
|
||||
status: DownloadStatus.waiting,
|
||||
waitingForWifi: false,
|
||||
);
|
||||
await _saveTasks();
|
||||
// 重新开始下载
|
||||
await _downloadService.startDownload(_tasks[task.id]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 添加下载任务
|
||||
Future<DownloadTaskModel?> addDownloadTask({
|
||||
required String fileName,
|
||||
required String fileUri,
|
||||
required int fileSize,
|
||||
String? savePath,
|
||||
}) async {
|
||||
// 如果已存在相同文件的任务,返回null
|
||||
DownloadTaskModel? existingTask;
|
||||
for (final task in _tasks.values) {
|
||||
if (task.fileUri == fileUri) {
|
||||
existingTask = task;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingTask != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保下载服务已初始化
|
||||
await initialize();
|
||||
|
||||
// 获取保存路径
|
||||
if (savePath == null) {
|
||||
final dir = await _downloadService.getDownloadDirectory();
|
||||
savePath = '${dir.path}/$fileName';
|
||||
}
|
||||
|
||||
// 创建任务ID
|
||||
final id = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
final task = DownloadTaskModel(
|
||||
id: id,
|
||||
fileName: fileName,
|
||||
fileUri: fileUri,
|
||||
fileSize: fileSize,
|
||||
savePath: savePath,
|
||||
status: DownloadStatus.waiting,
|
||||
);
|
||||
|
||||
_tasks[id] = task;
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
|
||||
// 开始下载
|
||||
AppLogger.d(
|
||||
'准备开始下载任务: ${task.id}, 文件: ${task.fileName}, 下载状态: ${task.status}');
|
||||
final bdTaskId = await _downloadService.startDownload(task);
|
||||
AppLogger.d('startDownload 返回: bdTaskId=$bdTaskId');
|
||||
|
||||
if (bdTaskId == null) {
|
||||
// 下载失败,更新任务状态
|
||||
_tasks[id] = task.copyWith(
|
||||
status: DownloadStatus.failed,
|
||||
errorMessage: '无法创建下载任务',
|
||||
);
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/// 批量添加下载任务
|
||||
Future<void> addBatchDownloadTasks(List<Map<String, dynamic>> files) async {
|
||||
await initialize();
|
||||
final dir = await _downloadService.getDownloadDirectory();
|
||||
|
||||
for (final file in files) {
|
||||
final fileName = file['name'] as String;
|
||||
final fileUri = file['path'] as String;
|
||||
final fileSize = file['size'] as int? ?? 0;
|
||||
|
||||
await addDownloadTask(
|
||||
fileName: fileName,
|
||||
fileUri: fileUri,
|
||||
fileSize: fileSize,
|
||||
savePath: '${dir.path}/$fileName',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理下载回调
|
||||
void _handleDownloadCallback(
|
||||
String taskId, DownloadStatus status, int progress) async {
|
||||
AppLogger.d(
|
||||
'DownloadManagerProvider._handleDownloadCallback: taskId=$taskId, status=$status, progress=$progress');
|
||||
|
||||
// 获取当前任务
|
||||
final task = _tasks[taskId];
|
||||
if (task == null) {
|
||||
AppLogger.d('任务不存在: taskId=$taskId');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算下载字节数和速度
|
||||
final downloadedBytes = (task.fileSize * progress / 100).toInt();
|
||||
int speed = 0;
|
||||
final now = DateTime.now();
|
||||
final lastTime = _lastProgressTime[taskId];
|
||||
final lastBytes = _lastProgressBytes[taskId] ?? 0;
|
||||
|
||||
if (status == DownloadStatus.downloading && lastTime != null) {
|
||||
final elapsed = now.difference(lastTime).inMilliseconds;
|
||||
if (elapsed > 0) {
|
||||
speed = ((downloadedBytes - lastBytes) * 1000 / elapsed).round();
|
||||
}
|
||||
}
|
||||
|
||||
if (status == DownloadStatus.downloading) {
|
||||
_lastProgressTime[taskId] = now;
|
||||
_lastProgressBytes[taskId] = downloadedBytes;
|
||||
} else {
|
||||
_lastProgressTime.remove(taskId);
|
||||
_lastProgressBytes.remove(taskId);
|
||||
}
|
||||
|
||||
// 判断是否在等待WiFi
|
||||
final waitingForWifi =
|
||||
status == DownloadStatus.waiting && _isWifiOnlyEnabled;
|
||||
|
||||
// 更新任务
|
||||
final updatedTask = task.copyWith(
|
||||
status: status,
|
||||
downloadedBytes: downloadedBytes,
|
||||
speed: speed,
|
||||
waitingForWifi: waitingForWifi,
|
||||
);
|
||||
|
||||
// 如果下载完成,设置完成时间
|
||||
if (status == DownloadStatus.completed) {
|
||||
_tasks[taskId] = updatedTask.copyWith(
|
||||
completedAt: DateTime.now(),
|
||||
speed: 0,
|
||||
);
|
||||
} else {
|
||||
_tasks[taskId] = updatedTask;
|
||||
}
|
||||
|
||||
AppLogger.d('任务已更新: ${_tasks[taskId]!.status}');
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 恢复下载
|
||||
Future<void> resumeDownload(String taskId) async {
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
await _downloadService.resumeDownload(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停下载
|
||||
Future<void> pauseDownload(String taskId) async {
|
||||
await _downloadService.pauseDownload(taskId);
|
||||
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
if (task.status == DownloadStatus.downloading) {
|
||||
_tasks[taskId] = task.copyWith(
|
||||
status: DownloadStatus.paused, speed: 0, waitingForWifi: false);
|
||||
_lastProgressTime.remove(taskId);
|
||||
_lastProgressBytes.remove(taskId);
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消下载
|
||||
Future<void> cancelDownload(String taskId) async {
|
||||
await _downloadService.cancelDownload(taskId);
|
||||
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
_tasks[taskId] = task.copyWith(
|
||||
status: DownloadStatus.cancelled, waitingForWifi: false);
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
|
||||
// 延迟移除任务,同时从存储中删除
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
_tasks.remove(taskId);
|
||||
_downloadService.disposeTask(taskId);
|
||||
_saveTasks();
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除下载任务(包括文件)
|
||||
Future<void> deleteDownloadTask(String taskId) async {
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
// 删除已下载的文件
|
||||
if (task.status == DownloadStatus.completed) {
|
||||
await _downloadService.deleteDownloadedFile(task.savePath);
|
||||
}
|
||||
|
||||
// 移除任务
|
||||
_tasks.remove(taskId);
|
||||
_downloadService.disposeTask(taskId);
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新下载
|
||||
Future<void> retryDownload(String taskId) async {
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
// 删除已下载的部分文件
|
||||
await _downloadService.deleteDownloadedFile(task.savePath);
|
||||
|
||||
// 重置任务状态
|
||||
_tasks[taskId] = task.copyWith(
|
||||
downloadedBytes: 0,
|
||||
speed: 0,
|
||||
status: DownloadStatus.waiting,
|
||||
errorMessage: null,
|
||||
completedAt: null,
|
||||
waitingForWifi: false,
|
||||
);
|
||||
_lastProgressTime.remove(taskId);
|
||||
_lastProgressBytes.remove(taskId);
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
|
||||
// 重新开始下载
|
||||
await _downloadService.startDownload(_tasks[taskId]!);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有已完成的任务
|
||||
Future<void> clearCompletedTasks() async {
|
||||
final completedTasks = getTasksByStatus(DownloadStatus.completed);
|
||||
for (final task in completedTasks) {
|
||||
await deleteDownloadTask(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有失败的任务
|
||||
Future<void> clearFailedTasks() async {
|
||||
final failedTasks = getTasksByStatus(DownloadStatus.failed);
|
||||
for (final task in failedTasks) {
|
||||
_tasks.remove(task.id);
|
||||
_downloadService.disposeTask(task.id);
|
||||
}
|
||||
await _saveTasks();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 获取任务
|
||||
DownloadTaskModel? getTask(String taskId) {
|
||||
return _tasks[taskId];
|
||||
}
|
||||
|
||||
/// 从本地存储加载下载任务
|
||||
Future<void> _loadTasks() async {
|
||||
try {
|
||||
final tasksJson =
|
||||
await StorageService.instance.getString(StorageKeys.downloadTasks);
|
||||
if (tasksJson == null || tasksJson.isEmpty) {
|
||||
AppLogger.d('没有保存的下载任务');
|
||||
return;
|
||||
}
|
||||
|
||||
final tasksList = jsonDecode(tasksJson) as List<dynamic>;
|
||||
final loadedTasks = <DownloadTaskModel>[];
|
||||
|
||||
final now = DateTime.now();
|
||||
for (final taskJson in tasksList) {
|
||||
try {
|
||||
final task =
|
||||
DownloadTaskModel.fromJson(taskJson as Map<String, dynamic>);
|
||||
// 过滤掉已取消的任务(修复4:已取消任务不恢复)
|
||||
if (task.status == DownloadStatus.cancelled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果任务已完成,只保留配置天数内的记录
|
||||
if (task.status == DownloadStatus.completed) {
|
||||
if (task.completedAt == null) {
|
||||
continue;
|
||||
}
|
||||
final retentionDays = await StorageService.instance
|
||||
.getInt(StorageKeys.taskRetentionDays) ??
|
||||
7;
|
||||
// retentionDays == -1 表示永不过期
|
||||
if (retentionDays > 0) {
|
||||
final daysSinceCompletion =
|
||||
now.difference(task.completedAt!).inDays;
|
||||
if (daysSinceCompletion > retentionDays) {
|
||||
AppLogger.d(
|
||||
'跳过超过$retentionDays天的已完成任务: ${task.fileName}');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadedTasks.add(task);
|
||||
} catch (e) {
|
||||
AppLogger.d('解析下载任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 将加载的任务添加到当前任务列表
|
||||
for (final task in loadedTasks) {
|
||||
_tasks[task.id] = task;
|
||||
}
|
||||
|
||||
AppLogger.d('从存储加载了 ${loadedTasks.length} 个下载任务');
|
||||
|
||||
// 通知 UI 更新
|
||||
if (loadedTasks.isNotEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 恢复未完成的任务
|
||||
for (final task in loadedTasks) {
|
||||
if (task.status == DownloadStatus.downloading ||
|
||||
task.status == DownloadStatus.waiting) {
|
||||
AppLogger.d('恢复下载任务: ${task.fileName}');
|
||||
// 使用 resumeDownloadAfterRestart 支持断点续传
|
||||
await _downloadService.resumeDownloadAfterRestart(task);
|
||||
} else if (task.status == DownloadStatus.paused) {
|
||||
// 修复5:暂停的任务需要重建 bdTasks 映射,以便继续下载
|
||||
AppLogger.d('重建暂停任务映射: ${task.fileName}');
|
||||
await _downloadService.resumeDownloadAfterRestart(task);
|
||||
// 重建映射后立即暂停,保持任务在暂停状态
|
||||
await _downloadService.pauseDownload(task.id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.d('加载下载任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存下载任务到本地存储
|
||||
Future<void> _saveTasks() async {
|
||||
try {
|
||||
final tasksList = _tasks.values.map((task) => task.toJson()).toList();
|
||||
final tasksJson = jsonEncode(tasksList);
|
||||
await StorageService.instance
|
||||
.setString(StorageKeys.downloadTasks, tasksJson);
|
||||
AppLogger.d('已保存 ${_tasks.length} 个下载任务到存储');
|
||||
} catch (e) {
|
||||
AppLogger.d('保存下载任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_downloadService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../data/models/file_model.dart';
|
||||
import '../../services/file_service.dart';
|
||||
import '../../services/thumbnail_service.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
import '../../core/utils/file_utils.dart';
|
||||
|
||||
/// 文件视图类型
|
||||
enum FileViewType { list, grid, gallery }
|
||||
|
||||
/// 刷新结果
|
||||
class RefreshResult {
|
||||
final int added;
|
||||
final int removed;
|
||||
final int updated;
|
||||
const RefreshResult({required this.added, required this.removed, required this.updated});
|
||||
bool get isUnchanged => added == 0 && removed == 0 && updated == 0;
|
||||
}
|
||||
|
||||
/// 文件管理Provider
|
||||
class FileManagerProvider extends ChangeNotifier {
|
||||
String _currentPath = '/';
|
||||
List<FileModel> _files = [];
|
||||
List<String> _selectedFiles = [];
|
||||
FileViewType _viewType = FileViewType.list;
|
||||
bool _isLoading = false;
|
||||
bool _hasMore = true;
|
||||
String? _errorMessage;
|
||||
String? _contextHint;
|
||||
String? _highlightPath;
|
||||
Timer? _highlightTimer;
|
||||
|
||||
String get currentPath => _currentPath;
|
||||
List<FileModel> get files => _files;
|
||||
List<String> get selectedFiles => _selectedFiles;
|
||||
FileViewType get viewType => _viewType;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get hasMore => _hasMore;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String? get contextHint => _contextHint;
|
||||
bool get hasSelection => _selectedFiles.isNotEmpty;
|
||||
String? get highlightPath => _highlightPath;
|
||||
|
||||
/// 加载文件列表
|
||||
Future<void> loadFiles({bool refresh = false, Duration timeout = const Duration(seconds: 5)}) async {
|
||||
if (refresh) {
|
||||
_selectedFiles.clear();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await FileService().listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
).timeout(timeout);
|
||||
|
||||
final List<dynamic> filesData = response['files'] as List<dynamic>? ?? [];
|
||||
final pagination = response['pagination'] as Map<String, dynamic>? ?? {};
|
||||
AppLogger.d("获取files列表: $filesData");
|
||||
setState(() {
|
||||
_files = filesData
|
||||
.map((f) => FileModel.fromJson(f as Map<String, dynamic>))
|
||||
.toList();
|
||||
_hasMore = pagination['next_token'] != null;
|
||||
_contextHint = response['context_hint'] as String?;
|
||||
});
|
||||
} on TimeoutException {
|
||||
setState(() {
|
||||
_errorMessage = '加载超时,请检查网络后重试';
|
||||
_hasMore = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorMessage = e.toString();
|
||||
_hasMore = false;
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 进入文件夹
|
||||
Future<void> enterFolder(String path) async {
|
||||
_currentPath = path;
|
||||
_selectedFiles.clear();
|
||||
_highlightPath = null;
|
||||
_highlightTimer?.cancel();
|
||||
ThumbnailService.instance.clearAll();
|
||||
await loadFiles();
|
||||
}
|
||||
|
||||
/// 返回上级
|
||||
Future<void> goBack() async {
|
||||
if (_currentPath == '/' || _currentPath.isEmpty) return;
|
||||
|
||||
final parts = _currentPath.split('/');
|
||||
if (parts.length > 1) {
|
||||
parts.removeLast();
|
||||
_currentPath = parts.join('/');
|
||||
} else {
|
||||
_currentPath = '/';
|
||||
}
|
||||
_selectedFiles.clear();
|
||||
_highlightPath = null;
|
||||
_highlightTimer?.cancel();
|
||||
ThumbnailService.instance.clearAll();
|
||||
notifyListeners();
|
||||
await loadFiles();
|
||||
}
|
||||
|
||||
/// 选择/取消选择文件
|
||||
void toggleSelection(String path) {
|
||||
if (_selectedFiles.contains(path)) {
|
||||
_selectedFiles.remove(path);
|
||||
} else {
|
||||
_selectedFiles.add(path);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 选择所有
|
||||
void selectAll() {
|
||||
_selectedFiles = _files.map((f) => f.path).toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 清除选择
|
||||
void clearSelection() {
|
||||
_selectedFiles.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 切换视图类型
|
||||
void setViewType(FileViewType type) {
|
||||
_viewType = type;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 设置错误信息
|
||||
void setErrorMessage(String? message) {
|
||||
_errorMessage = message;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 设置状态
|
||||
void setState(VoidCallback fn) {
|
||||
fn();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 删除选中的文件
|
||||
Future<String?> deleteSelectedFiles() async {
|
||||
if (_selectedFiles.isEmpty) return null;
|
||||
|
||||
try {
|
||||
AppLogger.d("删除文件: ${_selectedFiles.join(', ')}");
|
||||
await FileService().deleteFiles(uris: _selectedFiles);
|
||||
|
||||
setState(() {
|
||||
_files.removeWhere((file) => _selectedFiles.contains(file.path));
|
||||
});
|
||||
|
||||
clearSelection();
|
||||
return null;
|
||||
} catch (e) {
|
||||
final error = e.toString();
|
||||
setErrorMessage(error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建文件夹
|
||||
Future<String?> createFolder(String name) async {
|
||||
try {
|
||||
String uri;
|
||||
if (_currentPath == '/' || _currentPath.isEmpty) {
|
||||
uri = '/$name';
|
||||
} else {
|
||||
uri = '$_currentPath/$name';
|
||||
}
|
||||
|
||||
final response = await FileService().createFile(
|
||||
uri: uri,
|
||||
type: 'folder',
|
||||
errOnConflict: true,
|
||||
);
|
||||
|
||||
final newFolder = FileModel.fromJson(response);
|
||||
|
||||
setState(() {
|
||||
_files.insert(0, newFolder);
|
||||
});
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
final error = e.toString();
|
||||
setErrorMessage(error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除单个文件(增量移除)
|
||||
Future<String?> deleteFile(String path) async {
|
||||
try {
|
||||
await FileService().deleteFiles(uris: [path]);
|
||||
setState(() {
|
||||
_files.removeWhere((file) => file.path == path);
|
||||
_selectedFiles.remove(path);
|
||||
});
|
||||
return null;
|
||||
} catch (e) {
|
||||
final error = e.toString();
|
||||
setErrorMessage(error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/// 移动文件(增量更新)
|
||||
Future<String?> moveFiles(List<String> uris, String destination, {bool copy = false}) async {
|
||||
try {
|
||||
await FileService().moveFiles(uris: uris, dst: destination);
|
||||
clearSelection();
|
||||
|
||||
if (!copy) {
|
||||
// 移动:文件离开当前目录,直接从列表移除
|
||||
setState(() {
|
||||
_files.removeWhere((file) => uris.contains(file.path));
|
||||
});
|
||||
} else {
|
||||
// 复制:仅当目标是当前目录时需要刷新
|
||||
final normalizedDst = FileUtils.toCloudreveUri(destination);
|
||||
final normalizedCur = FileUtils.toCloudreveUri(_currentPath);
|
||||
if (normalizedDst == normalizedCur) {
|
||||
await loadFiles();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
final error = e.toString();
|
||||
setErrorMessage(error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/// 重命名文件(原地更新,不刷新列表)
|
||||
Future<String?> renameFile(String path, String newName) async {
|
||||
try {
|
||||
final response = await FileService().renameFile(uri: path, newName: newName);
|
||||
if (response.isEmpty) {
|
||||
await loadFiles();
|
||||
return null;
|
||||
}
|
||||
final updatedFile = FileModel.fromJson(response);
|
||||
final index = _files.indexWhere((f) => f.path == path);
|
||||
if (index != -1) {
|
||||
setState(() {
|
||||
_files[index] = updatedFile;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
final error = e.toString();
|
||||
setErrorMessage(error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过 URI 获取文件信息并添加到列表(用于上传完成后)
|
||||
Future<void> addFileByUri(String fileUri) async {
|
||||
try {
|
||||
final response = await FileService().getFileInfo(uri: fileUri);
|
||||
final newFile = FileModel.fromJson(response);
|
||||
final exists = _files.any((f) => f.id == newFile.id);
|
||||
if (!exists) {
|
||||
setState(() {
|
||||
_files.insert(0, newFile);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.d('获取上传文件信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 高亮指定文件路径(3 秒后自动清除)
|
||||
void setHighlightPath(String? path) {
|
||||
_highlightTimer?.cancel();
|
||||
_highlightPath = path;
|
||||
notifyListeners();
|
||||
if (path != null) {
|
||||
_highlightTimer = Timer(const Duration(seconds: 3), () {
|
||||
_highlightPath = null;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 导航到指定文件夹并高亮目标文件
|
||||
Future<void> navigateAndHighlight(String folderPath, String filePath) async {
|
||||
_currentPath = folderPath;
|
||||
_selectedFiles.clear();
|
||||
_highlightPath = null;
|
||||
_highlightTimer?.cancel();
|
||||
await loadFiles();
|
||||
setHighlightPath(filePath);
|
||||
}
|
||||
|
||||
/// 清空文件列表
|
||||
void clearFiles() {
|
||||
setState(() {
|
||||
_files = [];
|
||||
_selectedFiles = [];
|
||||
_currentPath = '/';
|
||||
_errorMessage = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// 智能刷新 - 只更新差异部分
|
||||
Future<RefreshResult> refreshFiles({Duration timeout = const Duration(seconds: 5)}) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await FileService().listFiles(
|
||||
uri: _currentPath,
|
||||
pageSize: 50,
|
||||
).timeout(timeout);
|
||||
|
||||
final List<dynamic> filesData = response['files'] as List<dynamic>? ?? [];
|
||||
final newFiles = filesData
|
||||
.map((f) => FileModel.fromJson(f as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final currentMap = <String, FileModel>{};
|
||||
for (final file in _files) {
|
||||
currentMap[file.path] = file;
|
||||
}
|
||||
|
||||
final newMap = <String, FileModel>{};
|
||||
for (final file in newFiles) {
|
||||
newMap[file.path] = file;
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
int removed = 0;
|
||||
int updated = 0;
|
||||
|
||||
final updatedFiles = <FileModel>[];
|
||||
|
||||
for (final file in newFiles) {
|
||||
final existingFile = currentMap[file.path];
|
||||
if (existingFile != null) {
|
||||
if (existingFile.updatedAt != file.updatedAt ||
|
||||
existingFile.size != file.size) {
|
||||
updatedFiles.add(file);
|
||||
updated++;
|
||||
} else {
|
||||
updatedFiles.add(existingFile);
|
||||
}
|
||||
} else {
|
||||
updatedFiles.add(file);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
for (final file in _files) {
|
||||
if (!newMap.containsKey(file.path)) {
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_files = updatedFiles;
|
||||
_hasMore = response['pagination']?['next_token'] != null;
|
||||
_contextHint = response['context_hint'] as String?;
|
||||
});
|
||||
|
||||
return RefreshResult(added: added, removed: removed, updated: updated);
|
||||
} on TimeoutException {
|
||||
setState(() {
|
||||
_errorMessage = '加载超时,请检查网络后重试';
|
||||
});
|
||||
return const RefreshResult(added: 0, removed: 0, updated: 0);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorMessage = e.toString();
|
||||
});
|
||||
return const RefreshResult(added: 0, removed: 0, updated: 0);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_highlightTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class NavigationProvider extends ChangeNotifier {
|
||||
int _currentIndex = 0; // 默认概览页
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
|
||||
void setIndex(int index) {
|
||||
if (_currentIndex != index) {
|
||||
_currentIndex = index;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../services/storage_service.dart';
|
||||
|
||||
/// 主题模式
|
||||
enum AppThemeMode {
|
||||
light,
|
||||
dark,
|
||||
system,
|
||||
}
|
||||
|
||||
/// 主题Provider - 管理主题模式和主题色
|
||||
class ThemeProvider extends ChangeNotifier {
|
||||
AppThemeMode _themeMode = AppThemeMode.system;
|
||||
Color _seedColor = const Color(0xFF3B82F6);
|
||||
|
||||
static const Color lightScaffoldBg = Color(0xFFF8FAFC);
|
||||
static const Color darkScaffoldBg = Color(0xFF0F172A);
|
||||
|
||||
AppThemeMode get themeMode => _themeMode;
|
||||
Color get seedColor => _seedColor;
|
||||
bool get isDark => _themeMode == AppThemeMode.dark;
|
||||
|
||||
/// 初始化
|
||||
Future<void> init() async {
|
||||
await Future.wait([
|
||||
loadThemeMode(),
|
||||
loadSeedColor(),
|
||||
]);
|
||||
}
|
||||
|
||||
/// 加载主题模式
|
||||
Future<void> loadThemeMode() async {
|
||||
final savedMode = await StorageService.instance.themeMode;
|
||||
if (savedMode != null) {
|
||||
switch (savedMode) {
|
||||
case 'light':
|
||||
_themeMode = AppThemeMode.light;
|
||||
case 'dark':
|
||||
_themeMode = AppThemeMode.dark;
|
||||
default:
|
||||
_themeMode = AppThemeMode.system;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 加载主题色
|
||||
Future<void> loadSeedColor() async {
|
||||
final saved = await StorageService.instance.getString('theme_seed_color');
|
||||
if (saved != null && saved.isNotEmpty) {
|
||||
final color = _colorFromHex(saved);
|
||||
if (color != null) {
|
||||
_seedColor = color;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置主题模式
|
||||
Future<void> setThemeMode(AppThemeMode mode) async {
|
||||
_themeMode = mode;
|
||||
notifyListeners();
|
||||
|
||||
String modeString;
|
||||
switch (mode) {
|
||||
case AppThemeMode.light:
|
||||
modeString = 'light';
|
||||
case AppThemeMode.dark:
|
||||
modeString = 'dark';
|
||||
case AppThemeMode.system:
|
||||
modeString = 'system';
|
||||
}
|
||||
await StorageService.instance.setThemeMode(modeString);
|
||||
}
|
||||
|
||||
/// 设置主题色
|
||||
Future<void> setSeedColor(Color color) async {
|
||||
_seedColor = color;
|
||||
notifyListeners();
|
||||
await StorageService.instance.setString('theme_seed_color', _colorToHex(color));
|
||||
}
|
||||
|
||||
/// 切换主题
|
||||
Future<void> toggleTheme() async {
|
||||
final newMode = isDark ? AppThemeMode.light : AppThemeMode.dark;
|
||||
await setThemeMode(newMode);
|
||||
}
|
||||
|
||||
/// 构建亮色主题
|
||||
ThemeData buildLightTheme() {
|
||||
return _buildTheme(Brightness.light);
|
||||
}
|
||||
|
||||
/// 构建暗色主题
|
||||
ThemeData buildDarkTheme() {
|
||||
return _buildTheme(Brightness.dark);
|
||||
}
|
||||
|
||||
ThemeData _buildTheme(Brightness brightness) {
|
||||
final isLight = brightness == Brightness.light;
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: _seedColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final bodyColor = isLight ? Colors.black87 : Colors.white;
|
||||
final displayColor = isLight ? Colors.black87 : Colors.white;
|
||||
|
||||
final baseTextTheme = ThemeData(brightness: brightness).textTheme;
|
||||
var textTheme = baseTextTheme.apply(
|
||||
bodyColor: bodyColor,
|
||||
displayColor: displayColor,
|
||||
fontFamily: _getPlatformFont(),
|
||||
);
|
||||
|
||||
if (_getPlatformFont() == 'NotoSansSC') {
|
||||
textTheme = textTheme.copyWith(
|
||||
bodyLarge: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500),
|
||||
bodyMedium: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
bodySmall: textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
titleLarge: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
titleMedium: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
titleSmall: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
labelLarge: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
);
|
||||
}
|
||||
|
||||
return ThemeData(
|
||||
textTheme: textTheme,
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: isLight ? lightScaffoldBg : darkScaffoldBg,
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
backgroundColor: isLight
|
||||
? lightScaffoldBg.withValues(alpha: 0.85)
|
||||
: darkScaffoldBg.withValues(alpha: 0.85),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
foregroundColor: isLight ? Colors.black87 : Colors.white,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 0,
|
||||
shadowColor: Colors.black12,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: isLight
|
||||
? Colors.black.withValues(alpha: 0.06)
|
||||
: Colors.white.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
color: isLight ? Colors.white : const Color(0xFF1E293B),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
dialogTheme: DialogThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
elevation: 0,
|
||||
backgroundColor: isLight
|
||||
? lightScaffoldBg.withValues(alpha: 0.9)
|
||||
: darkScaffoldBg.withValues(alpha: 0.9),
|
||||
indicatorColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
elevation: 0,
|
||||
backgroundColor: isLight
|
||||
? lightScaffoldBg
|
||||
: darkScaffoldBg,
|
||||
indicatorColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Color → hex string (不含alpha)
|
||||
static String _colorToHex(Color color) {
|
||||
return '#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2)}';
|
||||
}
|
||||
|
||||
/// hex string → Color
|
||||
static Color? _colorFromHex(String hex) {
|
||||
final clean = hex.replaceFirst('#', '');
|
||||
if (clean.length == 6) {
|
||||
return Color(int.parse('FF$clean', radix: 16));
|
||||
}
|
||||
if (clean.length == 8) {
|
||||
return Color(int.parse(clean, radix: 16));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _getPlatformFont() {
|
||||
if (Platform.isWindows || Platform.isLinux) return 'NotoSansSC';
|
||||
if (Platform.isMacOS) return 'PingFang SC';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../data/models/upload_task_model.dart';
|
||||
import '../../services/upload_service.dart';
|
||||
|
||||
/// 上传管理Provider
|
||||
class UploadManagerProvider extends ChangeNotifier {
|
||||
final UploadService _uploadService = UploadService.instance;
|
||||
bool _isInitialized = false;
|
||||
bool _shouldShowDialog = false;
|
||||
|
||||
bool get showUploadDialog => _shouldShowDialog && _uploadService.allTasks.isNotEmpty;
|
||||
List<UploadTaskModel> get allTasks => _uploadService.allTasks;
|
||||
List<UploadTaskModel> get activeTasks => _uploadService.activeTasks;
|
||||
|
||||
/// 初始化上传管理器
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
await _uploadService.initialize();
|
||||
_uploadService.addListener(_onServiceChanged);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
void _onServiceChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 标记应该显示对话框
|
||||
void markShouldShowDialog() {
|
||||
_shouldShowDialog = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 隐藏对话框
|
||||
void hideDialog() {
|
||||
_shouldShowDialog = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 开始上传
|
||||
Future<void> startUpload(
|
||||
List<File> files,
|
||||
String targetPath,
|
||||
) async {
|
||||
for (final file in files) {
|
||||
// 构建目标路径 URI
|
||||
String uri;
|
||||
if (targetPath.startsWith('cloudreve://my')) {
|
||||
uri = targetPath;
|
||||
} else {
|
||||
// 移除前导斜杠避免重复
|
||||
String pathPart = targetPath;
|
||||
if (pathPart.startsWith('/')) {
|
||||
pathPart = pathPart.substring(1);
|
||||
}
|
||||
uri = pathPart.isEmpty ? 'cloudreve://my' : 'cloudreve://my/$pathPart';
|
||||
}
|
||||
|
||||
final task = UploadTaskModel(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString() + file.path,
|
||||
file: file,
|
||||
fileName: file.uri.pathSegments.last,
|
||||
fileSize: await file.length(),
|
||||
targetPath: uri,
|
||||
);
|
||||
_uploadService.addTask(task);
|
||||
|
||||
// 开始上传任务
|
||||
_uploadService.startUpload(task);
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消上传
|
||||
void cancelUpload(String taskId) {
|
||||
_uploadService.cancelUpload(taskId);
|
||||
}
|
||||
|
||||
/// 重试上传
|
||||
void retryUpload(String taskId) {
|
||||
_uploadService.retryUpload(taskId);
|
||||
}
|
||||
|
||||
/// 删除任务
|
||||
void removeTask(String taskId) {
|
||||
_uploadService.removeTask(taskId);
|
||||
}
|
||||
|
||||
/// 清除所有已完成的任务
|
||||
void clearCompletedTasks() {
|
||||
_uploadService.clearCompletedTasks();
|
||||
}
|
||||
|
||||
/// 清除失败的任务
|
||||
void clearFailedTasks() {
|
||||
_uploadService.clearFailedTasks();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uploadService.removeListener(_onServiceChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../data/models/user_setting_model.dart';
|
||||
import '../../services/user_setting_service.dart';
|
||||
import '../../core/utils/app_logger.dart';
|
||||
|
||||
/// 用户设置状态
|
||||
enum UserSettingState { idle, loading, error }
|
||||
|
||||
/// 用户设置 Provider
|
||||
class UserSettingProvider extends ChangeNotifier {
|
||||
UserSettingState _state = UserSettingState.idle;
|
||||
UserSettingModel? _settings;
|
||||
UserCapacityModel? _capacity;
|
||||
String? _errorMessage;
|
||||
|
||||
UserSettingState get state => _state;
|
||||
UserSettingModel? get settings => _settings;
|
||||
UserCapacityModel? get capacity => _capacity;
|
||||
String? get errorMessage => _errorMessage;
|
||||
bool get isLoading => _state == UserSettingState.loading;
|
||||
|
||||
final UserSettingService _service = UserSettingService.instance;
|
||||
|
||||
/// 加载用户设置
|
||||
Future<void> loadSettings() async {
|
||||
try {
|
||||
_setState(UserSettingState.loading);
|
||||
_settings = await _service.getUserSetting();
|
||||
_setState(UserSettingState.idle);
|
||||
} catch (e) {
|
||||
AppLogger.d('加载用户设置失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
_setState(UserSettingState.error);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载存储用量
|
||||
Future<void> loadCapacity() async {
|
||||
try {
|
||||
_capacity = await _service.getUserCapacity();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
AppLogger.d('加载存储用量失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 同时加载设置和容量
|
||||
Future<void> loadAll() async {
|
||||
await Future.wait([
|
||||
loadSettings(),
|
||||
loadCapacity(),
|
||||
]);
|
||||
}
|
||||
|
||||
/// 修改昵称
|
||||
Future<bool> updateNick(String nick) async {
|
||||
try {
|
||||
await _service.updateNick(nick);
|
||||
// 成功后刷新设置
|
||||
await loadSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('修改昵称失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改主题色
|
||||
Future<bool> updatePreferredTheme(String themeColor) async {
|
||||
try {
|
||||
await _service.updatePreferredTheme(themeColor);
|
||||
if (_settings != null) {
|
||||
_settings = _settings!.copyWith(); // 本地无需维护此字段
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('修改主题色失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改语言
|
||||
Future<bool> updateLanguage(String language) async {
|
||||
try {
|
||||
await _service.updateLanguage(language);
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('修改语言失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改密码
|
||||
Future<bool> changePassword({
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
try {
|
||||
await _service.changePassword(
|
||||
currentPassword: currentPassword,
|
||||
newPassword: newPassword,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('修改密码失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 启用2FA
|
||||
Future<String?> prepare2FA() async {
|
||||
try {
|
||||
return await _service.prepare2FA();
|
||||
} catch (e) {
|
||||
AppLogger.d('准备启用2FA失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> enable2FA(String code) async {
|
||||
try {
|
||||
await _service.enable2FA(code);
|
||||
await loadSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('启用2FA失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 禁用2FA
|
||||
Future<bool> disable2FA(String code) async {
|
||||
try {
|
||||
await _service.disable2FA(code);
|
||||
await loadSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('禁用2FA失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新版本保留设置
|
||||
Future<bool> updateVersionRetention({
|
||||
bool? enabled,
|
||||
List<String>? ext,
|
||||
int? max,
|
||||
}) async {
|
||||
try {
|
||||
await _service.updateUserSetting(
|
||||
versionRetentionEnabled: enabled,
|
||||
versionRetentionExt: ext,
|
||||
versionRetentionMax: max,
|
||||
);
|
||||
await loadSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('更新版本保留设置失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新视图同步
|
||||
Future<bool> updateViewSync(bool disabled) async {
|
||||
try {
|
||||
await _service.updateUserSetting(disableViewSync: disabled);
|
||||
if (_settings != null) {
|
||||
_settings = _settings!.copyWith(disableViewSync: disabled);
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('更新视图同步设置失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新分享链接可见性
|
||||
Future<bool> updateShareLinksInProfile(String value) async {
|
||||
try {
|
||||
await _service.updateUserSetting(shareLinksInProfile: value);
|
||||
if (_settings != null) {
|
||||
_settings = _settings!.copyWith(shareLinksInProfile: value);
|
||||
}
|
||||
notifyListeners();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('更新分享链接可见性失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 撤销OAuth授权
|
||||
Future<bool> revokeOAuthGrant(String appId) async {
|
||||
try {
|
||||
await _service.revokeOAuthGrant(appId);
|
||||
await loadSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('撤销OAuth授权失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 解绑OIDC提供商
|
||||
Future<bool> unlinkOpenId(int provider) async {
|
||||
try {
|
||||
await _service.unlinkOpenId(provider);
|
||||
await loadSettings();
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.d('解绑OIDC失败: $e');
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除错误
|
||||
void clearError() {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setState(UserSettingState state) {
|
||||
_state = state;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user