202 lines
7.3 KiB
Dart
202 lines
7.3 KiB
Dart
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|