ponshu-room-lite/lib/services/image_batch_compression_ser...

202 lines
7.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import '../models/sake_item.dart';
import 'image_compression_service.dart';
// ⚠️ Critical Fix (Day 5.5): cleanupTempFiles() を修正
// 問題: getApplicationDocumentsDirectory() をスキャンして _compressed, _gallery を削除
// 結果: 本物の画像を誤削除
// 修正: getTemporaryDirectory() のみをスキャン
/// 既存画像の一括圧縮サービス
///
/// 用途: アプリ更新後、既存の未圧縮画像を圧縮してストレージを削減
class ImageBatchCompressionService {
/// 既存の画像を一括圧縮
///
/// 処理内容:
/// 1. すべての SakeItem から画像パスを取得
/// 2. 各画像を圧縮1024px, JPEG 85%
/// 3. 元画像を削除
/// 4. SakeItem の imagePaths を更新
///
/// 戻り値: (圧縮成功数, 圧縮失敗数, 削減したバイト数)
static Future<(int, int, int)> compressAllImages({
required Function(int current, int total, String fileName) onProgress,
}) async {
final box = Hive.box<SakeItem>('sake_items');
final allItems = box.values.toList();
int successCount = 0;
int failedCount = 0;
int savedBytes = 0;
int totalImages = 0;
// 全画像数をカウント
for (final item in allItems) {
totalImages += item.displayData.imagePaths.length;
}
int processedCount = 0;
for (final item in allItems) {
final originalPaths = List<String>.from(item.displayData.imagePaths);
final newPaths = <String>[];
for (final originalPath in originalPaths) {
processedCount++;
final file = File(originalPath);
try {
// ファイルが存在するか確認
if (!await file.exists()) {
debugPrint('⚠️ File not found: $originalPath');
newPaths.add(originalPath); // パスをそのまま保持
failedCount++;
continue;
}
// 元のファイルサイズを取得
final originalSize = await file.length();
// ファイル名から拡張子を取得
final fileName = originalPath.split('/').last;
onProgress(processedCount, totalImages, fileName);
// 既に圧縮済みか確認(ファイルサイズで判断)
if (originalSize < 500 * 1024) { // 500KB以下なら既に圧縮済み
debugPrint('✅ Already compressed: $fileName (${(originalSize / 1024).toStringAsFixed(1)}KB)');
newPaths.add(originalPath);
successCount++;
continue;
}
// Day 5: 安全な圧縮(一時ファイル経由)
debugPrint('🗜️ Compressing: $fileName (${(originalSize / 1024 / 1024).toStringAsFixed(1)}MB)');
// 1. 一時ファイルに圧縮targetPathを指定しない
final tempCompressedPath = await ImageCompressionService.compressForGemini(originalPath);
// 2. 圧縮後のサイズを取得
final compressedSize = await File(tempCompressedPath).length();
final saved = originalSize - compressedSize;
savedBytes += saved;
debugPrint('✅ Compressed: $fileName - ${(originalSize / 1024).toStringAsFixed(1)}KB → ${(compressedSize / 1024).toStringAsFixed(1)}KB (${(saved / 1024).toStringAsFixed(1)}KB saved)');
// 3. 圧縮成功後に元ファイルを削除
try {
await file.delete();
debugPrint('🗑️ Deleted original: $originalPath');
} catch (e) {
debugPrint('⚠️ Failed to delete original: $e');
// エラー時は一時ファイルを削除して元のパスを保持
await File(tempCompressedPath).delete();
newPaths.add(originalPath);
failedCount++;
continue;
}
// 4. 一時ファイルを元の場所に移動
try {
await File(tempCompressedPath).rename(originalPath);
debugPrint('📦 Moved compressed file to: $originalPath');
} catch (e) {
debugPrint('⚠️ Failed to rename file: $e');
// エラー時は一時ファイルをそのまま使用
newPaths.add(tempCompressedPath);
failedCount++;
continue;
}
newPaths.add(originalPath);
successCount++;
} catch (e) {
debugPrint('❌ Failed to compress: $originalPath - $e');
newPaths.add(originalPath); // エラー時は元のパスを保持
failedCount++;
}
}
// SakeItem の imagePaths を更新
if (newPaths.isNotEmpty) {
final updatedItem = item.copyWith(
imagePaths: newPaths,
);
await box.put(item.key, updatedItem);
}
}
return (successCount, failedCount, savedBytes);
}
/// ストレージ使用量を取得
///
/// 戻り値: (総ファイル数, 総バイト数)
static Future<(int, int)> getStorageUsage() async {
final box = Hive.box<SakeItem>('sake_items');
final allItems = box.values.toList();
int totalFiles = 0;
int totalBytes = 0;
for (final item in allItems) {
for (final path in item.displayData.imagePaths) {
final file = File(path);
if (await file.exists()) {
totalFiles++;
totalBytes += await file.length();
}
}
}
return (totalFiles, totalBytes);
}
/// 一時ファイルをクリーンアップ
///
/// 🔒 Safe: getTemporaryDirectory() のみをスキャン(永続ファイルは削除しない)
///
/// 戻り値: (削除したファイル数, 削減したバイト数)
static Future<(int, int)> cleanupTempFiles() async {
try {
// ⚠️ 重要: getTemporaryDirectory() を使用getApplicationDocumentsDirectory() ではない)
final directory = await getTemporaryDirectory();
final dir = Directory(directory.path);
int deletedCount = 0;
int deletedBytes = 0;
// ディレクトリ内のすべてのファイルをスキャン
await for (final entity in dir.list()) {
if (entity is File) {
final fileName = entity.path.split('/').last;
// 一時ファイルを検出(画像ファイルのみ)
if (fileName.endsWith('.jpg') || fileName.endsWith('.jpeg') || fileName.endsWith('.png')) {
try {
final fileSize = await entity.length();
await entity.delete();
deletedCount++;
deletedBytes += fileSize;
debugPrint('🗑️ Deleted temp file: $fileName (${(fileSize / 1024).toStringAsFixed(1)}KB)');
} catch (e) {
debugPrint('⚠️ Failed to delete temp file: $fileName - $e');
}
}
}
}
debugPrint('✅ Cleanup complete: $deletedCount files, ${(deletedBytes / 1024 / 1024).toStringAsFixed(1)}MB');
return (deletedCount, deletedBytes);
} catch (e) {
debugPrint('❌ Cleanup error: $e');
return (0, 0);
}
}
}