import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:smtias_capture/services/raw_capture_service.dart';
/// 画像一覧の取得・キャッシュ・削除操作を管理する.
class GalleryProvider extends ChangeNotifier {
/// 画像一覧の取得元ディレクトリは省略可能(省略時は本番の共有ストレージパス).
///
/// MediaStore 通知に使うサービスもコンストラクタで差し替え可能(省略時は既定実装).
/// テストでは一時ディレクトリを渡して実メソッドを直接検証できる.
GalleryProvider({String? basePath, RawCaptureService? rawCaptureService})
: _basePath = basePath ?? _defaultBasePath,
_rawCaptureService = rawCaptureService ?? RawCaptureService();
static const _defaultBasePath = '/storage/emulated/0/Pictures/MiniTIAS';
final String _basePath;
final RawCaptureService _rawCaptureService;
List<File> _images = [];
bool _isLoading = false;
List<File> get images => _images;
bool get isLoading => _isLoading;
/// Pictures/MiniTIAS/ 内の PNG ファイルと QM プレビュー JPEG を取得し,新しい順に並べる.
Future<void> loadImages() async {
_isLoading = true;
notifyListeners();
try {
final directory = Directory(_basePath);
if (!await directory.exists()) {
_images = [];
} else {
final files = await directory
.list()
.where((entity) {
final lower = entity.path.toLowerCase();
return entity is File &&
(lower.endsWith('.png') || lower.endsWith('.preview.jpg'));
})
.cast<File>()
.toList();
// 更新日時の新しい順にソート
files.sort((a, b) {
final aStat = a.statSync();
final bStat = b.statSync();
return bStat.modified.compareTo(aStat.modified);
});
_images = files;
}
} catch (e) {
debugPrint('画像一覧の取得に失敗: $e');
_images = [];
}
_isLoading = false;
notifyListeners();
}
/// 指定したファイルを削除し,一覧から除去する.
///
/// QM プレビュー (*.preview.jpg) を削除する場合,同じ baseName の
/// .dng ファイルと .meta.json ファイルも連動して削除する.
/// .png ファイルは単独削除(後方互換).
Future<bool> deleteImage(File file) async {
final path = file.path;
final lower = path.toLowerCase();
// 削除対象ファイル一覧を確定
final filesToDelete = <File>[file];
if (lower.endsWith('.preview.jpg')) {
final base = path.substring(0, path.length - '.preview.jpg'.length);
filesToDelete.add(File('$base.dng'));
filesToDelete.add(File('$base.meta.json'));
}
try {
final deletedPaths = <String>[];
for (final f in filesToDelete) {
if (await f.exists()) {
await f.delete();
deletedPaths.add(f.path);
}
}
// 元ファイルが消えたら成功とみなす
if (!await file.exists()) {
_images.removeWhere((f) => f.path == file.path);
notifyListeners();
// 物理削除したファイルを MediaStore に再スキャンさせ,削除済みエントリ
// (MTP や他アプリから見える幽霊エントリ)を索引から除去する.
await _notifyMediaStoreDeleted(deletedPaths);
return true;
}
return false;
} catch (e) {
debugPrint('画像の削除に失敗: $e');
return false;
}
}
/// 物理削除済みのパスを MediaStore に再スキャンさせ,索引から除去する.
///
/// 存在しなくなったパスへの `scanFile` は,MediaStore の該当エントリを削除する
/// (Android の挙動).通知の失敗は削除自体の成否に影響させない(握りつぶす).
Future<void> _notifyMediaStoreDeleted(List<String> deletedPaths) async {
for (final path in deletedPaths) {
try {
await _rawCaptureService.scanFile(path);
} catch (e) {
debugPrint('MediaStore 更新の通知に失敗 ($path): $e');
}
}
}
}