117 lines
3.8 KiB
Dart
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);
|
|
}
|
|
}
|