Files
arcx-app/lib/features/profile/application/profile_controller.dart
T

117 lines
3.8 KiB
Dart

import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../../core/constants/app_defaults.dart';
import '../../../core/providers/app_providers.dart';
import '../../../data/local/models/models.dart';
final profileProvider =
AsyncNotifierProvider<ProfileNotifier, UserProfile>(ProfileNotifier.new);
class ProfileNotifier extends AsyncNotifier<UserProfile> {
@override
Future<UserProfile> build() async {
final isar = await ref.watch(isarProvider.future);
final profile = await isar.userProfiles.get(AppDefaults.singletonId);
if (profile == null) {
// Bootstrap should have created one; create defensively.
final fresh = UserProfile.createDefault();
await isar.writeTxn(() => isar.userProfiles.put(fresh));
return fresh;
}
if (_isLocalAvatarMissing(profile)) {
return _clearMissingLocalAvatar(profile);
}
return profile;
}
bool _isLocalAvatarMissing(UserProfile profile) {
if (profile.avatarSource != AvatarSource.localFile) return false;
final path = profile.avatarLocalPath?.trim();
if (path == null || path.isEmpty) return true;
return !File(path).existsSync();
}
Future<UserProfile> _clearMissingLocalAvatar(UserProfile current) async {
final updated = current.copyWith(
avatarAssetPath: AppDefaults.defaultAvatarAssetPath,
avatarLocalPath: null,
avatarSource: AvatarSource.asset,
);
final isar = await ref.read(isarProvider.future);
await isar.writeTxn(() => isar.userProfiles.put(updated));
return updated;
}
Future<void> updateNickname(String nickname) async {
final trimmed = nickname.trim();
if (trimmed.isEmpty) return;
final current = state.value;
if (current == null) return;
final updated = current.copyWith(nickname: trimmed);
final isar = await ref.read(isarProvider.future);
await isar.writeTxn(() => isar.userProfiles.put(updated));
state = AsyncData(updated);
}
/// Picks an image from gallery or camera, copies it into the app sandbox
/// and stores the local file path on the profile.
Future<bool> updateAvatar({required bool fromCamera}) async {
final current = state.value;
if (current == null) return false;
final picker = ImagePicker();
final xfile = await picker.pickImage(
source: fromCamera ? ImageSource.camera : ImageSource.gallery,
maxWidth: 512,
maxHeight: 512,
imageQuality: 85,
);
if (xfile == null) return false;
final docs = await getApplicationDocumentsDirectory();
final destPath = p.join(docs.path, AppDefaults.avatarFileName);
final sourceFile = File(xfile.path);
await sourceFile.copy(destPath);
final updated = current.copyWith(
avatarLocalPath: destPath,
avatarSource: AvatarSource.localFile,
);
final isar = await ref.read(isarProvider.future);
await isar.writeTxn(() => isar.userProfiles.put(updated));
state = AsyncData(updated);
return true;
}
Future<void> resetAvatar() async {
final current = state.value;
if (current == null) return;
final path = current.avatarLocalPath;
if (path != null && path.isNotEmpty) {
final file = File(path);
if (await file.exists()) {
try {
await file.delete();
} catch (_) {
// Best-effort cleanup; profile reset still proceeds.
}
}
}
final updated = current.copyWith(
avatarAssetPath: AppDefaults.defaultAvatarAssetPath,
avatarLocalPath: null,
avatarSource: AvatarSource.asset,
);
final isar = await ref.read(isarProvider.future);
await isar.writeTxn(() => isar.userProfiles.put(updated));
state = AsyncData(updated);
}
}