From 220342f9bc2e4a582297de3ba72a37394e73c684 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 30 May 2022 18:39:08 +0900 Subject: [PATCH] merge ambiguously cased directories --- lib/model/entry.dart | 8 +++-- lib/model/entry_dirs.dart | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 lib/model/entry_dirs.dart diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 7e9d89805..8071699f1 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:aves/geo/countries.dart'; import 'package:aves/model/entry_cache.dart'; +import 'package:aves/model/entry_dirs.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/geotiff.dart'; import 'package:aves/model/metadata/address.dart'; @@ -31,7 +32,8 @@ class AvesEntry { // `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode int id; String uri; - String? _path, _directory, _filename, _extension, _sourceTitle; + String? _path, _filename, _extension, _sourceTitle; + EntryDir? _directory; int? pageId, contentId; final String sourceMimeType; int width, height, sourceRotationDegrees; @@ -175,8 +177,8 @@ class AvesEntry { // directory path, without the trailing separator String? get directory { - _directory ??= path != null ? pContext.dirname(path!) : null; - return _directory; + _directory ??= entryDirRepo.getOrCreate(path != null ? pContext.dirname(path!) : null); + return _directory!.resolved; } String? get filenameWithoutExtension { diff --git a/lib/model/entry_dirs.dart b/lib/model/entry_dirs.dart new file mode 100644 index 000000000..d18ff2dd8 --- /dev/null +++ b/lib/model/entry_dirs.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:aves/services/common/services.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:collection/collection.dart'; + +final entryDirRepo = EntryDirRepo._private(); + +class EntryDirRepo { + EntryDirRepo._private(); + + // mapping between the raw entry directory path to a resolvable directory + final Map _dirs = {}; + final StreamController _ambiguousDirStreamController = StreamController.broadcast(); + + Stream get ambiguousDirStream => _ambiguousDirStreamController.stream; + + // get a resolvable directory for a raw entry directory path + EntryDir getOrCreate(String? asIs) { + var entryDir = _dirs[asIs]; + if (entryDir != null) return entryDir; + + final asIsLower = asIs?.toLowerCase(); + entryDir = _dirs.values.firstWhereOrNull((dir) => dir.asIsLower == asIsLower); + if (entryDir != null && !entryDir.ambiguous) { + entryDir.ambiguous = true; + _ambiguousDirStreamController.add(entryDir); + } + + return _dirs.putIfAbsent(asIs, () => entryDir ?? EntryDir(asIs)); + } +} + +// Some directories are ambiguous because they use different cases, +// but the OS merge and present them as one directory. +// This class resolves ambiguous directories to get the directory path +// with the right case, as presented by the OS. +class EntryDir { + final String? asIs, asIsLower; + bool ambiguous = false; + String? _resolved; + + EntryDir(this.asIs) : asIsLower = asIs?.toLowerCase(); + + String? get resolved { + if (!ambiguous) return asIs; + if (asIs == null) return null; + + _resolved ??= _resolve(); + return _resolved; + } + + String? _resolve() { + final vrl = VolumeRelativeDirectory.fromPath(asIs!); + if (vrl == null || vrl.relativeDir.isEmpty) return asIs; + + var resolved = vrl.volumePath; + final parts = pContext.split(vrl.relativeDir); + for (final part in parts) { + final partLower = part.toLowerCase(); + final childrenDirs = Directory(resolved).listSync().where((v) => v.absolute is Directory).toSet(); + final found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower); + resolved = found?.path ?? '$resolved${pContext.separator}$part'; + } + return resolved; + } +}