Newer
Older
SmTIAS-Capture / lib / services / file_service.dart
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

/// 撮影画像の保存・命名・削除を行うサービス.
class FileService {
  static const _basePath = '/storage/emulated/0/Pictures/MiniTIAS';

  /// 保存先ディレクトリのパスを返す.
  String get directoryPath => _basePath;

  /// [directoryPath] が存在しない場合,再帰的に作成する.
  Future<void> _ensureDirectory() async {
    final directory = Directory(directoryPath);
    if (!await directory.exists()) {
      await directory.create(recursive: true);
    }
  }

  /// `YYYYMMDD_HHmmss` 形式のタイムスタンプ文字列を生成する.
  static String _buildTimestamp(DateTime dt) =>
      '${dt.year}'
      '${dt.month.toString().padLeft(2, '0')}'
      '${dt.day.toString().padLeft(2, '0')}'
      '_'
      '${dt.hour.toString().padLeft(2, '0')}'
      '${dt.minute.toString().padLeft(2, '0')}'
      '${dt.second.toString().padLeft(2, '0')}';

  /// PNG バイト列をファイルに書き込んで保存する.
  ///
  /// Android ネイティブ側で YUV→RGB→PNG 変換済みのバイト列を受け取り,
  /// そのまま書き込むため高速に動作する.
  Future<String> saveImage(Uint8List pngBytes) async {
    await _ensureDirectory();
    final fileName = await generateFileName();
    final filePath = '$directoryPath/$fileName';
    await File(filePath).writeAsBytes(pngBytes);
    return filePath;
  }

  /// ファイル名を生成する.
  ///
  /// 形式: MiniTIAS_YYYYMMDD_HHmmss.png
  /// 同秒の重複がある場合は _1, _2, ... と連番を付与する.
  Future<String> generateFileName() async {
    final timestamp = _buildTimestamp(DateTime.now());
    final baseName = 'MiniTIAS_$timestamp';
    final candidate = '$baseName.png';

    if (!await File('$directoryPath/$candidate').exists()) {
      return candidate;
    }

    var suffix = 1;
    while (await File('$directoryPath/${baseName}_$suffix.png').exists()) {
      suffix++;
    }
    return '${baseName}_$suffix.png';
  }

  /// カメラ診断情報を JSON ファイルとして保存する.
  ///
  /// ファイル名: `camera_diagnostics_YYYYMMDD_HHmmss.json`
  /// 保存先: `/storage/emulated/0/Pictures/MiniTIAS`
  /// 戻り値は保存したファイルのフルパス.
  Future<String> saveDiagnosticsJson(Map<String, dynamic> diagnostics) async {
    await _ensureDirectory();

    final fileName =
        'camera_diagnostics_${_buildTimestamp(DateTime.now())}.json';
    final filePath = '$directoryPath/$fileName';

    const encoder = JsonEncoder.withIndent('  ');
    await File(filePath).writeAsString(encoder.convert(diagnostics));

    return filePath;
  }

  /// テスト用: タイムスタンプとファイル名一覧からファイル名を生成する.
  static String generateFileNameSync(
    String timestamp,
    List<String> existingFiles,
  ) {
    final baseName = 'MiniTIAS_$timestamp';
    final candidate = '$baseName.png';

    if (!existingFiles.contains(candidate)) {
      return candidate;
    }

    var suffix = 1;
    while (existingFiles.contains('${baseName}_$suffix.png')) {
      suffix++;
    }
    return '${baseName}_$suffix.png';
  }

  /// 定量モード撮影用のファイル名 prefix を生成する.
  ///
  /// 形式: `MiniTIAS_QM_YYYYMMDD_HHmmss`
  /// 同秒の重複がある場合は `_1`, `_2` ...と連番を付与する.
  /// 重複判定は `<baseName>.dng` の存在チェックで行う.
  Future<String> generateQuantitativeBaseName() async {
    final candidate = 'MiniTIAS_QM_${_buildTimestamp(DateTime.now())}';

    if (!await File('$directoryPath/$candidate.dng').exists()) {
      return candidate;
    }

    var suffix = 1;
    while (await File('$directoryPath/${candidate}_$suffix.dng').exists()) {
      suffix++;
    }
    return '${candidate}_$suffix';
  }

  /// テスト用同期版.既存ファイル名のリストとタイムスタンプを受け取って baseName を返す.
  ///
  /// 候補 `MiniTIAS_QM_{timestamp}` に対して `.dng` の存在チェックを行う.
  static String generateQuantitativeBaseNameSync(
    String timestamp,
    List<String> existingFiles,
  ) {
    final candidate = 'MiniTIAS_QM_$timestamp';

    if (!existingFiles.contains('$candidate.dng')) {
      return candidate;
    }

    var suffix = 1;
    while (existingFiles.contains('${candidate}_$suffix.dng')) {
      suffix++;
    }
    return '${candidate}_$suffix';
  }

  /// ライブプレビューのスナップショット JPEG を `<baseName>.preview.jpg` として保存する.
  ///
  /// [previewBytes] は JPEG エンコード済みのバイト列.
  /// 戻り値は保存したファイルのフルパス.
  Future<String> savePreviewJpeg(
    String baseName,
    Uint8List previewBytes,
  ) async {
    await _ensureDirectory();
    final filePath = '$directoryPath/$baseName.preview.jpg';
    await File(filePath).writeAsBytes(previewBytes);
    return filePath;
  }

  /// メタデータと LSC マップを `<baseName>.meta.json` として保存する.
  ///
  /// [lscMap] が null の場合,`lscMap`/`lscMapRowCount`/`lscMapColumnCount` キーは含めない.
  Future<String> saveQuantitativeMetadata(
    String baseName,
    Map<String, dynamic> metadata, {
    List<List<double>>? lscMap,
    int? lscMapRowCount,
    int? lscMapColumnCount,
  }) async {
    await _ensureDirectory();

    final Map<String, dynamic> jsonMap = Map<String, dynamic>.from(metadata);
    if (lscMap != null) {
      jsonMap['lscMap'] = lscMap;
      jsonMap['lscMapRowCount'] = lscMapRowCount;
      jsonMap['lscMapColumnCount'] = lscMapColumnCount;
    }

    const encoder = JsonEncoder.withIndent('  ');
    final filePath = '$directoryPath/$baseName.meta.json';
    await File(filePath).writeAsString(encoder.convert(jsonMap));
    return filePath;
  }
}