import 'dart:convert'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; /// Stores short, searchable image descriptions as a JSON sidecar in the image's /// own directory. File name: .ocideck_descriptions.json, keyed by base name. /// /// Kept separate from captions (which are source/credit lines): a description /// is free-text used to find images by the words in it. class DescriptionService { static const _sidecar = '.ocideck_descriptions.json'; Future getDescription(String imagePath) async { if (imagePath.isEmpty) return null; final file = _sidecarFile(imagePath); if (!file.existsSync()) return null; try { final data = jsonDecode(await file.readAsString()) as Map; final value = data[p.basename(imagePath)]; return value is String ? value : null; } catch (_) { return null; } } Future saveDescription(String imagePath, String description) async { if (imagePath.isEmpty) return; final file = _sidecarFile(imagePath); Map data = {}; if (file.existsSync()) { try { data = Map.from( jsonDecode(await file.readAsString()) as Map, ); } catch (_) {} } final key = p.basename(imagePath); if (description.trim().isEmpty) { data.remove(key); } else { data[key] = description.trim(); } if (data.isEmpty) { if (file.existsSync()) await file.delete(); } else { await file.writeAsString( const JsonEncoder.withIndent(' ').convert(data), ); } } /// Remove the description entry for [imagePath] (used when an image is /// deleted). Safe to call when no entry exists. Future removeDescription(String imagePath) => saveDescription(imagePath, ''); /// Load every description stored in the directories that contain [imagePaths]. /// Returns a map of absolute image path → description. Each sidecar is read /// once, so this stays cheap even for thousands of images. Future> loadFor(Iterable imagePaths) async { final dirs = {for (final path in imagePaths) p.dirname(path)}; final result = {}; for (final dir in dirs) { final file = File(p.join(dir, _sidecar)); if (!file.existsSync()) continue; try { final data = jsonDecode(await file.readAsString()) as Map; for (final entry in data.entries) { if (entry.value is String) { result[p.join(dir, entry.key as String)] = entry.value as String; } } } catch (_) {} } return result; } File _sidecarFile(String imagePath) { return File(p.join(p.dirname(imagePath), _sidecar)); } } final descriptionServiceProvider = Provider( (_) => DescriptionService(), );