From fc7791cde1444e1be0935f1fbc32d956fa6eb756 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 26 Sep 2025 11:27:43 +0800 Subject: [PATCH] feat(media meta): reverse geocoding from mapbox (#2922) --- public/locales/de-DE/application.json | 7 ++ public/locales/de-DE/dashboard.json | 5 + public/locales/en-US/application.json | 7 ++ public/locales/en-US/dashboard.json | 5 + public/locales/es-ES/application.json | 7 ++ public/locales/es-ES/dashboard.json | 5 + public/locales/fr-FR/application.json | 7 ++ public/locales/fr-FR/dashboard.json | 5 + public/locales/it-IT/application.json | 7 ++ public/locales/it-IT/dashboard.json | 5 + public/locales/ja-JP/application.json | 7 ++ public/locales/ja-JP/dashboard.json | 5 + public/locales/ko-KR/application.json | 7 ++ public/locales/ko-KR/dashboard.json | 5 + public/locales/pt-BR/application.json | 7 ++ public/locales/pt-BR/dashboard.json | 5 + public/locales/ru-RU/application.json | 7 ++ public/locales/ru-RU/dashboard.json | 5 + public/locales/zh-CN/application.json | 7 ++ public/locales/zh-CN/dashboard.json | 5 + public/locales/zh-TW/application.json | 7 ++ public/locales/zh-TW/dashboard.json | 5 + src/api/explorer.ts | 8 ++ .../Admin/Settings/Media/Extractors.tsx | 24 +++- src/component/Admin/Settings/Settings.tsx | 2 + .../Explorer/ListView/AddColumn.tsx | 7 ++ .../FileManager/Explorer/ListView/Cell.tsx | 18 +++ .../FileManager/Explorer/ListView/Column.tsx | 30 +++++ .../Search/AdvanceSearch/AddCondition.tsx | 86 +++++++++++-- .../FileManager/Sidebar/MediaInfo.tsx | 115 +++++++++++++++++- src/component/Icons/LocationFilled.tsx | 9 ++ 31 files changed, 416 insertions(+), 15 deletions(-) create mode 100644 src/component/Icons/LocationFilled.tsx diff --git a/public/locales/de-DE/application.json b/public/locales/de-DE/application.json index 1e96ba2..67b9c76 100644 --- a/public/locales/de-DE/application.json +++ b/public/locales/de-DE/application.json @@ -287,6 +287,13 @@ "exposureValue": "{{num}} Sekunden", "exposure": "Belichtung", "aperture": "Blende", + "address": "Adresse", + "street": "Straße", + "locality": "Ort", + "place": "Stadt", + "district": "Bezirk", + "region": "Provinz", + "country": "Land", "mediaInfo": "Medieninformationen", "details": "Details", "activity": "Aktivität", diff --git a/public/locales/de-DE/dashboard.json b/public/locales/de-DE/dashboard.json index c39d52d..150e6ad 100644 --- a/public/locales/de-DE/dashboard.json +++ b/public/locales/de-DE/dashboard.json @@ -167,6 +167,11 @@ "exifBruteForceDes": "Wenn aktiviert, wird die gesamte Datei gescannt, um EXIF-Daten zu finden, falls sie nicht am Standard-Header-Standort gefunden werden können. Dies kann die Verarbeitungszeit erhöhen, aber EXIF-Daten an nicht-standardmäßigen Standorten finden.", "musicCover": "Musik-Cover", "musicCoverDes": "Album-Cover aus Musikdateien extrahieren, unterstützt ID3 (v1, 2.2, 2.3 und 2.4) Container. Dieser Generator hängt von einem anderen Bild-Miniaturansichten-Generator ab (Cloudreve eingebaut oder VIPS).", + "geocoding": "Geokodierung", + "geocodingDes": "Adressinformationen mit dem Mapbox-Dienst basierend auf den in den Medien-EXIF aufgezeichneten Koordinateninformationen abrufen.", + "mapboxAK": "Mapbox API-Schlüssel", + "mapboxAKDes": "API-Schlüssel, der in der Mapbox-Konsole erstellt wurde.", + "geocodingDependencyWarning": "Der Geokodierungs-Generator hängt vom EXIF-Generator ab, bitte aktivieren Sie den EXIF-Generator.", "notAppliedToNativeGenerator": "{{prefix}}Nicht anwendbar auf nativen Generator von Speicherrichtlinien.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}Nicht anwendbar auf nativen Generator von OneDrive oder SharePoint Speicherrichtlinien.", "fileBlobMargin": "Datei-Blob-URL-Cache-Marge (Sekunden)", diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index 7e0ad98..594220c 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -253,6 +253,13 @@ "exposureValue": "{{num}} s", "exposure": "Exposure", "aperture": "Aperture", + "address": "Address", + "street": "Street", + "locality": "Locality", + "place": "City", + "district": "District", + "region": "Province", + "country": "Country", "mediaInfo": "Media info", "details": "Details", "activity": "Activity", diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index ed4d04f..b88c734 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -166,6 +166,11 @@ "exifBruteForceDes": "When enabled, the entire file will be scanned to find EXIF data if it cannot be found in the standard header location. This may increase processing time but can find EXIF data in non-standard locations.", "musicCover": "Music cover", "musicCoverDes": "Extract album cover from music files, supports ID3 (v1, 2.2, 2.3 and 2.4) container. This generator depends on any other image thumbnail generator (Cloudreve built-in or VIPS).", + "geocoding": "Geocoding", + "geocodingDes": "Get address information using Mapbox service based on coordinate information recorded in media EXIF.", + "mapboxAK": "Mapbox API Key", + "mapboxAKDes": "API key created in Mapbox console.", + "geocodingDependencyWarning": "Geocoding generator depends on EXIF generator, please enable EXIF generator.", "notAppliedToNativeGenerator": "{{prefix}}Not applicable to native generator of storage policies.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}Not applicable to native generator of OneDrive or SharePoint storage policies.", "fileBlobMargin": "File Blob URL Cache Margin (seconds)", diff --git a/public/locales/es-ES/application.json b/public/locales/es-ES/application.json index 44dadf3..b3508a3 100644 --- a/public/locales/es-ES/application.json +++ b/public/locales/es-ES/application.json @@ -287,6 +287,13 @@ "exposureValue": "{{num}} s", "exposure": "Exposición", "aperture": "Apertura", + "address": "Dirección", + "street": "Calle", + "locality": "Localidad", + "place": "Ciudad", + "district": "Distrito", + "region": "Provincia", + "country": "País", "mediaInfo": "Información multimedia", "details": "Detalles", "activity": "Actividad", diff --git a/public/locales/es-ES/dashboard.json b/public/locales/es-ES/dashboard.json index 79c0225..8e10fc0 100644 --- a/public/locales/es-ES/dashboard.json +++ b/public/locales/es-ES/dashboard.json @@ -167,6 +167,11 @@ "exifBruteForceDes": "Cuando está habilitado, todo el archivo será escaneado para encontrar datos EXIF si no puede ser encontrado en la ubicación estándar del encabezado. Esto puede aumentar el tiempo de procesamiento pero puede encontrar datos EXIF en ubicaciones no estándar.", "musicCover": "Portada de música", "musicCoverDes": "Extraer portada de álbum de archivos de música, soporta contenedor ID3 (v1, 2.2, 2.3 y 2.4). Este generador depende de cualquier otro generador de miniaturas de imagen (Cloudreve integrado o VIPS).", + "geocoding": "Geocodificación", + "geocodingDes": "Obtener información de dirección usando el servicio Mapbox basado en la información de coordenadas registrada en EXIF de medios.", + "mapboxAK": "Clave API de Mapbox", + "mapboxAKDes": "Clave API creada en la consola de Mapbox.", + "geocodingDependencyWarning": "El generador de geocodificación depende del generador EXIF, por favor habilite el generador EXIF.", "notAppliedToNativeGenerator": "{{prefix}}No aplicable al generador nativo de políticas de almacenamiento.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}No aplicable al generador nativo de políticas de almacenamiento OneDrive o SharePoint.", "fileBlobMargin": "Margen de Cache de URL de Blob de Archivo (segundos)", diff --git a/public/locales/fr-FR/application.json b/public/locales/fr-FR/application.json index dc08870..ab63dad 100644 --- a/public/locales/fr-FR/application.json +++ b/public/locales/fr-FR/application.json @@ -290,6 +290,13 @@ "exposureValue": "{{num}} s", "exposure": "Exposition", "aperture": "Ouverture", + "address": "Adresse", + "street": "Rue", + "locality": "Localité", + "place": "Ville", + "district": "District", + "region": "Province", + "country": "Pays", "mediaInfo": "Informations média", "details": "Détails", "activity": "Activité", diff --git a/public/locales/fr-FR/dashboard.json b/public/locales/fr-FR/dashboard.json index 238cca2..dea7afb 100644 --- a/public/locales/fr-FR/dashboard.json +++ b/public/locales/fr-FR/dashboard.json @@ -167,6 +167,11 @@ "exifBruteForceDes": "Lorsqu'activé, l'ensemble du fichier sera scanné pour trouver les données EXIF si elles ne peuvent pas être trouvées dans l'emplacement d'en-tête standard. Cela peut augmenter le temps de traitement mais peut trouver des données EXIF dans des emplacements non standard.", "musicCover": "Pochette musicale", "musicCoverDes": "Extraire la pochette d'album des fichiers musicaux, prend en charge le conteneur ID3 (v1, 2.2, 2.3 et 2.4). Ce générateur dépend de tout autre générateur de miniatures d'images (intégré à Cloudreve ou VIPS).", + "geocoding": "Géocodage", + "geocodingDes": "Obtenez des informations d'adresse en utilisant le service Mapbox basé sur les informations de coordonnées enregistrées dans l'EXIF des médias.", + "mapboxAK": "Clé API Mapbox", + "mapboxAKDes": "Clé API créée dans la console Mapbox.", + "geocodingDependencyWarning": "Le générateur de géocodage dépend du générateur EXIF, veuillez activer le générateur EXIF.", "notAppliedToNativeGenerator": "{{prefix}}Non applicable au générateur natif des politiques de stockage.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}Non applicable au générateur natif des politiques de stockage OneDrive ou SharePoint.", "fileBlobMargin": "Marge du cache d'URL Blob de fichier (secondes)", diff --git a/public/locales/it-IT/application.json b/public/locales/it-IT/application.json index 472d8d7..8a62e92 100644 --- a/public/locales/it-IT/application.json +++ b/public/locales/it-IT/application.json @@ -290,6 +290,13 @@ "exposureValue": "{{num}} s", "exposure": "Esposizione", "aperture": "Apertura", + "address": "Indirizzo", + "street": "Via", + "locality": "Località", + "place": "Città", + "district": "Distretto", + "region": "Provincia", + "country": "Paese", "mediaInfo": "Info media", "details": "Dettagli", "activity": "Attività", diff --git a/public/locales/it-IT/dashboard.json b/public/locales/it-IT/dashboard.json index 3cbeff4..067c39a 100644 --- a/public/locales/it-IT/dashboard.json +++ b/public/locales/it-IT/dashboard.json @@ -167,6 +167,11 @@ "exifBruteForceDes": "Quando abilitato, l'intero file verrà scansionato per trovare dati EXIF se non possono essere trovati nella posizione header standard. Questo potrebbe aumentare il tempo di elaborazione ma può trovare dati EXIF in posizioni non standard.", "musicCover": "Copertina musicale", "musicCoverDes": "Estrai copertina album dai file musicali, supporta container ID3 (v1, 2.2, 2.3 e 2.4). Questo generatore dipende da qualsiasi altro generatore di miniature immagini (Cloudreve integrato o VIPS).", + "geocoding": "Geocodifica", + "geocodingDes": "Ottieni informazioni sull'indirizzo utilizzando il servizio Mapbox basato sulle informazioni delle coordinate registrate nell'EXIF dei media.", + "mapboxAK": "Chiave API Mapbox", + "mapboxAKDes": "Chiave API creata nella console Mapbox.", + "geocodingDependencyWarning": "Il generatore di geocodifica dipende dal generatore EXIF, si prega di abilitare il generatore EXIF.", "notAppliedToNativeGenerator": "{{prefix}}Non applicabile al generatore nativo delle policy di archiviazione.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}Non applicabile al generatore nativo delle policy di archiviazione OneDrive o SharePoint.", "fileBlobMargin": "Margine Cache URL Blob File (secondi)", diff --git a/public/locales/ja-JP/application.json b/public/locales/ja-JP/application.json index ae5e7a1..16e4785 100644 --- a/public/locales/ja-JP/application.json +++ b/public/locales/ja-JP/application.json @@ -256,6 +256,13 @@ "exposureValue": "{{num}} 秒", "exposure": "露出", "aperture": "絞り", + "address": "住所", + "street": "通り", + "locality": "地区", + "place": "都市", + "district": "区", + "region": "省", + "country": "国", "mediaInfo": "メディア情報", "details": "詳細", "activity": "アクティビティ", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index 85263a0..00c72bb 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -166,6 +166,11 @@ "exifBruteForceDes": "有効にすると、標準ヘッダーの位置でEXIFデータが見つからない場合、EXIFデータを見つけるためにファイル全体をスキャンします。処理時間が長くなる可能性がありますが、非標準の位置にあるEXIFデータを見つけることができます。", "musicCover": "曲のジャケット画像", "musicCoverDes": "オーディオファイルからアルバムジャケット画像を抽出します。ID3(v1、2.2、2.3、2.4)メタデータコンテナをサポートします。このジェネレーターは、他の画像ジェネレーター(Cloudreve組み込みまたはVIPS)に依存します。", + "geocoding": "ジオコーディング", + "geocodingDes": "メディアのEXIFに記録された座標情報に基づいて、Mapboxサービスを使用して住所情報を取得します。", + "mapboxAK": "Mapbox APIキー", + "mapboxAKDes": "Mapboxコンソールで作成されたAPIキー。", + "geocodingDependencyWarning": "ジオコーディングジェネレーターはEXIFジェネレーターに依存しています。EXIFジェネレーターを有効にしてください。", "notAppliedToNativeGenerator": "{{prefix}}はストレージポリシーネイティブジェネレーターには適用されません。", "notAppliedToOneDriveNativeGenerator": "{{prefix}}はOneDriveまたはSharePointストレージポリシーネイティブジェネレーターには適用されません。", "fileBlobMargin": "ファイルBlob一時URLキャッシュ冗長性(秒)", diff --git a/public/locales/ko-KR/application.json b/public/locales/ko-KR/application.json index 72fe7e9..72de585 100644 --- a/public/locales/ko-KR/application.json +++ b/public/locales/ko-KR/application.json @@ -290,6 +290,13 @@ "exposureValue": "{{num}}초", "exposure": "노출", "aperture": "조리개", + "address": "주소", + "street": "거리", + "locality": "지역", + "place": "도시", + "district": "구", + "region": "성", + "country": "국가", "mediaInfo": "미디어 정보", "details": "상세정보", "activity": "활동", diff --git a/public/locales/ko-KR/dashboard.json b/public/locales/ko-KR/dashboard.json index 121a4c5..96722dd 100644 --- a/public/locales/ko-KR/dashboard.json +++ b/public/locales/ko-KR/dashboard.json @@ -166,6 +166,11 @@ "exifBruteForceDes": "활성화하면 표준 헤더 위치에서 EXIF 데이터를 찾을 수 없을 때 전체 파일을 스캔하여 EXIF 데이터를 찾습니다. 처리 시간이 증가할 수 있지만 비표준 위치의 EXIF 데이터를 찾을 수 있습니다.", "musicCover": "앨범 커버", "musicCoverDes": "오디오 파일에서 앨범 커버를 추출하며, ID3 (v1, 2.2, 2.3, 2.4) 메타데이터 컨테이너를 지원합니다. 이 생성기는 다른 이미지 생성기(Cloudreve 내장 또는 VIPS) 중 하나에 의존합니다.", + "geocoding": "지오코딩", + "geocodingDes": "미디어 EXIF에 기록된 좌표 정보를 기반으로 Mapbox 서비스를 사용하여 주소 정보를 가져옵니다.", + "mapboxAK": "Mapbox API 키", + "mapboxAKDes": "Mapbox 콘솔에서 생성된 API 키입니다.", + "geocodingDependencyWarning": "지오코딩 생성기는 EXIF 생성기에 의존합니다. EXIF 생성기를 활성화하십시오.", "notAppliedToNativeGenerator": "{{prefix}}저장소 정책 네이티브 생성기에는 적용되지 않습니다.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}OneDrive 또는 SharePoint 저장소 정책 네이티브 생성기에는 적용되지 않습니다.", "fileBlobMargin": "파일 Blob 임시 URL 캐시 여유분(초)", diff --git a/public/locales/pt-BR/application.json b/public/locales/pt-BR/application.json index bc912c9..d826853 100644 --- a/public/locales/pt-BR/application.json +++ b/public/locales/pt-BR/application.json @@ -290,6 +290,13 @@ "exposureValue": "{{num}} s", "exposure": "Exposição", "aperture": "Abertura", + "address": "Endereço", + "street": "Rua", + "locality": "Localidade", + "place": "Cidade", + "district": "Distrito", + "region": "Província", + "country": "País", "mediaInfo": "Informações de mídia", "details": "Detalhes", "activity": "Atividade", diff --git a/public/locales/pt-BR/dashboard.json b/public/locales/pt-BR/dashboard.json index 64ff78e..d62a9f6 100644 --- a/public/locales/pt-BR/dashboard.json +++ b/public/locales/pt-BR/dashboard.json @@ -167,6 +167,11 @@ "exifBruteForceDes": "Quando habilitado, o arquivo inteiro será escaneado para encontrar dados EXIF se não puder ser encontrado no local padrão do cabeçalho. Isso pode aumentar o tempo de processamento, mas pode encontrar dados EXIF em locais não padrão.", "musicCover": "Capa da música", "musicCoverDes": "Extrair capa do álbum de arquivos de música, suporta contêiner ID3 (v1, 2.2, 2.3 e 2.4). Este gerador depende de qualquer outro gerador de miniatura de imagem (Cloudreve integrado ou VIPS).", + "geocoding": "Geocodificação", + "geocodingDes": "Obter informações de endereço usando o serviço Mapbox baseado nas informações de coordenadas registradas no EXIF da mídia.", + "mapboxAK": "Chave API do Mapbox", + "mapboxAKDes": "Chave API criada no console do Mapbox.", + "geocodingDependencyWarning": "O gerador de geocodificação depende do gerador EXIF, por favor habilite o gerador EXIF.", "notAppliedToNativeGenerator": "{{prefix}}Não aplicável ao gerador nativo de políticas de armazenamento.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}Não aplicável ao gerador nativo de políticas de armazenamento OneDrive ou SharePoint.", "fileBlobMargin": "Margem do Cache de URL do Blob de Arquivo (segundos)", diff --git a/public/locales/ru-RU/application.json b/public/locales/ru-RU/application.json index d323821..787c00b 100644 --- a/public/locales/ru-RU/application.json +++ b/public/locales/ru-RU/application.json @@ -290,6 +290,13 @@ "exposureValue": "{{num}} сек", "exposure": "Экспозиция", "aperture": "Диафрагма", + "address": "Адрес", + "street": "Улица", + "locality": "Местность", + "place": "Город", + "district": "Район", + "region": "Провинция", + "country": "Страна", "mediaInfo": "Информация о медиа", "details": "Подробности", "activity": "Активность", diff --git a/public/locales/ru-RU/dashboard.json b/public/locales/ru-RU/dashboard.json index fd2b51b..6c8aa47 100644 --- a/public/locales/ru-RU/dashboard.json +++ b/public/locales/ru-RU/dashboard.json @@ -167,6 +167,11 @@ "exifBruteForceDes": "При включении весь файл будет просканирован для поиска данных EXIF, если они не могут быть найдены в стандартном расположении заголовка. Это может увеличить время обработки, но может найти данные EXIF в нестандартных местах.", "musicCover": "Обложка музыки", "musicCoverDes": "Извлекать обложку альбома из музыкальных файлов, поддерживает контейнер ID3 (v1, 2.2, 2.3 и 2.4). Этот генератор зависит от любого другого генератора миниатюр изображений (встроенного Cloudreve или VIPS).", + "geocoding": "Геокодирование", + "geocodingDes": "Получить информацию об адресе с помощью сервиса Mapbox на основе информации о координатах, записанной в EXIF медиафайлов.", + "mapboxAK": "API-ключ Mapbox", + "mapboxAKDes": "API-ключ, созданный в консоли Mapbox.", + "geocodingDependencyWarning": "Генератор геокодирования зависит от генератора EXIF, пожалуйста, включите генератор EXIF.", "notAppliedToNativeGenerator": "{{prefix}}Не применимо к нативному генератору политик хранения.", "notAppliedToOneDriveNativeGenerator": "{{prefix}}Не применимо к нативному генератору политик хранения OneDrive или SharePoint.", "fileBlobMargin": "Запас кэша URL файлового блоба (секунды)", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index 220ac3d..b11521f 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -253,6 +253,13 @@ "exposureValue": "{{num}} 秒", "exposure": "曝光", "aperture": "光圈", + "address": "地址", + "street": "街道", + "locality": "城市区", + "place": "城市", + "district": "区", + "region": "省", + "country": "国家", "mediaInfo": "媒体信息", "details": "详情", "activity": "活动", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index de74cf9..2875e94 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -166,6 +166,11 @@ "exifBruteForceDes": "启用后,如果在标准头部位置找不到 EXIF 数据,将扫描整个文件以查找 EXIF 数据。这可能会增加处理时间,但可以找到非标准位置的 EXIF 数据。", "musicCover": "歌曲封面", "musicCoverDes": "提取音频文件中的专辑封面, 支持 ID3 (v1, 2.2, 2.3, 2.4) 元数据容器。这一生成器依赖于任一其他图像生成器(Cloudreve 内置 或 VIPS)。", + "geocoding": "地理编码", + "geocodingDes": "根据媒体 EXIF 中记录的坐标信息,使用 Mapbox 服务获取地址信息。", + "mapboxAK": "Mapbox 密钥", + "mapboxAKDes": "在 Mapbox 控制台创建的密钥。", + "geocodingDependencyWarning": "地理编码生成器依赖于 EXIF 生成器,请开启 EXIF 生成器。", "notAppliedToNativeGenerator": "{{prefix}}不适用于存储策略原生生成器。", "notAppliedToOneDriveNativeGenerator": "{{prefix}}不适用于 OneDrive 或 SharePoint 存储策略原生生成器。", "fileBlobMargin": "文件 Blob 临时 URL 缓存冗余(秒)", diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 7208852..1aac3d0 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -256,6 +256,13 @@ "exposureValue": "{{num}} 秒", "exposure": "曝光", "aperture": "光圈", + "address": "地址", + "street": "街道", + "locality": "城市區", + "place": "城市", + "district": "區", + "region": "省", + "country": "國家", "mediaInfo": "媒體資訊", "details": "詳情", "activity": "活動", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index 523c9bd..bb53f10 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -166,6 +166,11 @@ "exifBruteForceDes": "啟用後,如果在標準頭部位置找不到 EXIF 資料,將掃描整個檔案以查詢 EXIF 資料。這可能會增加處理時間,但可以找到非標準位置的 EXIF 資料。", "musicCover": "歌曲封面", "musicCoverDes": "提取音訊檔案中的專輯封面, 支援 ID3 (v1, 2.2, 2.3, 2.4) 元資料容器。這一生成器依賴於任一其他影象生成器(Cloudreve 內建 或 VIPS)。", + "geocoding": "地理編碼", + "geocodingDes": "根據媒體 EXIF 中記錄的座標資訊,使用 Mapbox 服務獲取地址資訊。", + "mapboxAK": "Mapbox 密鑰", + "mapboxAKDes": "在 Mapbox 控制台建立的密鑰。", + "geocodingDependencyWarning": "地理編碼產生器依賴於 EXIF 產生器,請開啟 EXIF 產生器。", "notAppliedToNativeGenerator": "{{prefix}}不適用於儲存策略原生生成器。", "notAppliedToOneDriveNativeGenerator": "{{prefix}}不適用於 OneDrive 或 SharePoint 儲存策略原生生成器。", "fileBlobMargin": "檔案 Blob 臨時 URL 快取冗餘(秒)", diff --git a/src/api/explorer.ts b/src/api/explorer.ts index 9da39aa..531c4ba 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -211,6 +211,14 @@ export const Metadata = { stream_indexed_bitrate: "bitrate", stream_indexed_width: "width", stream_indexed_height: "height", + + // Geocoding + street: "geocoding:street", + locality: "geocoding:locality", + place: "geocoding:place", + district: "geocoding:district", + region: "geocoding:region", + country: "geocoding:country", }; export interface FileThumbResponse { diff --git a/src/component/Admin/Settings/Media/Extractors.tsx b/src/component/Admin/Settings/Media/Extractors.tsx index 1c644bc..6a07332 100644 --- a/src/component/Admin/Settings/Media/Extractors.tsx +++ b/src/component/Admin/Settings/Media/Extractors.tsx @@ -76,6 +76,18 @@ const extractors: ExtractorRenderProps[] = [ maxSizeLocalSetting: "media_meta_ffprobe_size_local", maxSizeRemoteSetting: "media_meta_ffprobe_size_remote", }, + { + name: "geocoding", + des: "geocodingDes", + enableFlag: "media_meta_geocoding", + additionalSettings: [ + { + name: "media_meta_geocoding_mapbox_ak", + label: "mapboxAK", + des: "mapboxAKDes", + }, + ], + }, ]; const Extractors = ({ values, setSetting }: ExtractorsProps) => { @@ -88,6 +100,15 @@ const Extractors = ({ values, setSetting }: ExtractorsProps) => { setSetting({ [name]: e.target.checked ? "1" : "0", }); + + const newValues = { ...values, [name]: e.target.checked ? "1" : "0" }; + if (isTrueVal(newValues["media_meta_geocoding"]) && !isTrueVal(newValues["media_meta_exif"])) { + enqueueSnackbar({ + message: t("settings.geocodingDependencyWarning"), + variant: "warning", + action: DefaultCloseAction, + }); + } }; const doTest = (name: string, executable: string) => { @@ -215,7 +236,8 @@ const Extractors = ({ values, setSetting }: ExtractorsProps) => { /> ) : ( setSetting({ diff --git a/src/component/Admin/Settings/Settings.tsx b/src/component/Admin/Settings/Settings.tsx index 25b1d6a..9629300 100644 --- a/src/component/Admin/Settings/Settings.tsx +++ b/src/component/Admin/Settings/Settings.tsx @@ -265,6 +265,8 @@ const Settings = () => { "media_meta_ffprobe_path", "media_meta_ffprobe_size_local", "media_meta_ffprobe_size_remote", + "media_meta_geocoding", + "media_meta_geocoding_mapbox_ak", ]} > diff --git a/src/component/FileManager/Explorer/ListView/AddColumn.tsx b/src/component/FileManager/Explorer/ListView/AddColumn.tsx index b7590d5..defbf77 100644 --- a/src/component/FileManager/Explorer/ListView/AddColumn.tsx +++ b/src/component/FileManager/Explorer/ListView/AddColumn.tsx @@ -46,6 +46,13 @@ const mediaInfoOptions: (ColumType | null)[] = [ ColumType.artist, ColumType.album, ColumType.duration, + null, + ColumType.street, + ColumType.locality, + ColumType.place, + ColumType.district, + ColumType.region, + ColumType.country, ]; const AddColumn = (props: AddColumnProps) => { diff --git a/src/component/FileManager/Explorer/ListView/Cell.tsx b/src/component/FileManager/Explorer/ListView/Cell.tsx index 7785cfb..cc60350 100644 --- a/src/component/FileManager/Explorer/ListView/Cell.tsx +++ b/src/component/FileManager/Explorer/ListView/Cell.tsx @@ -30,6 +30,8 @@ import { getArtist, getCameraMake, getCameraModel, + getCountry, + getDistrict, getDuration, getExposure, getExposureBias, @@ -39,8 +41,12 @@ import { getIso, getLensMake, getLensModel, + getLocality, getMediaTitle, + getPlace, + getRegion, getSoftware, + getStreet, takenAt, } from "../../Sidebar/MediaInfo.tsx"; import { MediaMetaElements } from "../../Sidebar/MediaMetaCard.tsx"; @@ -352,6 +358,18 @@ const Cell = memo((props: CellProps) => { return ; case ColumType.duration: return ; + case ColumType.street: + return ; + case ColumType.locality: + return ; + case ColumType.place: + return ; + case ColumType.district: + return ; + case ColumType.region: + return ; + case ColumType.country: + return ; case ColumType.custom_props: if (customProp) { return getPropsContent(customProp, () => {}, false, true); diff --git a/src/component/FileManager/Explorer/ListView/Column.tsx b/src/component/FileManager/Explorer/ListView/Column.tsx index 5d91fd4..a453261 100644 --- a/src/component/FileManager/Explorer/ListView/Column.tsx +++ b/src/component/FileManager/Explorer/ListView/Column.tsx @@ -54,6 +54,12 @@ export enum ColumType { artist = 23, album = 24, duration = 25, + street = 27, + locality = 28, + place = 29, + district = 30, + region = 31, + country = 32, // Custom props custom_props = 26, @@ -179,6 +185,30 @@ export const ColumnTypeDefaults: { [key: number]: ColumTypeDefaults } = { title: "application:fileManager.duration", width: 100, }, + [ColumType.street]: { + title: "application:fileManager.street", + width: 100, + }, + [ColumType.locality]: { + title: "application:fileManager.locality", + width: 100, + }, + [ColumType.place]: { + title: "application:fileManager.place", + width: 100, + }, + [ColumType.district]: { + title: "application:fileManager.district", + width: 100, + }, + [ColumType.region]: { + title: "application:fileManager.region", + width: 100, + }, + [ColumType.country]: { + title: "application:fileManager.country", + width: 100, + }, }; export const getColumnTypeDefaults = ( diff --git a/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx b/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx index 0abe525..8b8e7f8 100644 --- a/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx +++ b/src/component/FileManager/Search/AdvanceSearch/AddCondition.tsx @@ -17,7 +17,7 @@ import Tag from "../../../Icons/Tag.tsx"; import TextBulletListSquareEdit from "../../../Icons/TextBulletListSquareEdit.tsx"; import TextCaseTitle from "../../../Icons/TextCaseTitle.tsx"; import { CascadingSubmenu } from "../../ContextMenu/CascadingMenu.tsx"; -import { SquareMenuItem } from "../../ContextMenu/ContextMenu.tsx"; +import { DenseDivider, SquareMenuItem } from "../../ContextMenu/ContextMenu.tsx"; import { customPropsMetadataPrefix } from "../../Sidebar/CustomProps/CustomProps.tsx"; import { Condition, ConditionType } from "./ConditionBox.tsx"; @@ -77,7 +77,7 @@ const options: ConditionOption[] = [ }, ]; -const mediaMetaOptions: ConditionOption[] = [ +const mediaMetaOptions: (ConditionOption | null)[] = [ { name: "application:fileManager.title", condition: { @@ -105,6 +105,7 @@ const mediaMetaOptions: ConditionOption[] = [ id: Metadata.music_album, }, }, + null, // divider { name: "application:fileManager.cameraMake", condition: { @@ -141,6 +142,61 @@ const mediaMetaOptions: ConditionOption[] = [ id: Metadata.lens_model, }, }, + null, // divider + { + name: "application:fileManager.street", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.street, + id: Metadata.street, + }, + }, + { + name: "application:fileManager.locality", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.locality, + id: Metadata.locality, + }, + }, + { + name: "application:fileManager.place", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.place, + id: Metadata.place, + }, + }, + { + name: "application:fileManager.district", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.district, + id: Metadata.district, + }, + }, + { + name: "application:fileManager.region", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.region, + id: Metadata.region, + }, + }, + { + name: "application:fileManager.country", + condition: { + type: ConditionType.metadata, + metadata_key_readonly: true, + metadata_key: Metadata.country, + id: Metadata.country, + }, + }, ]; const AddCondition = (props: AddConditionProps) => { @@ -186,17 +242,21 @@ const AddCondition = (props: AddConditionProps) => { popupId={"mediaInfo"} title={t("application:fileManager.mediaInfo")} > - {mediaMetaOptions.map((option, index) => ( - onConditionAdd(option.condition)}> - - {t(option.name)} - - - ))} + {mediaMetaOptions.map((option, index) => + option ? ( + onConditionAdd(option.condition)}> + + {t(option.name)} + + + ) : ( + + ), + )} {customPropsOptions && customPropsOptions.length > 0 && ( { + if (target.metadata?.[Metadata.street]) { + return { + display: target.metadata[Metadata.street], + searchKey: Metadata.street, + searchValue: target.metadata[Metadata.street], + }; + } +}; + +export const getLocality = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.locality]) { + return { + display: target.metadata[Metadata.locality], + searchKey: Metadata.locality, + searchValue: target.metadata[Metadata.locality], + }; + } +}; + +export const getPlace = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.place]) { + return { + display: target.metadata[Metadata.place], + searchKey: Metadata.place, + searchValue: target.metadata[Metadata.place], + }; + } +}; + +export const getDistrict = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.district]) { + return { + display: target.metadata[Metadata.district], + searchKey: Metadata.district, + searchValue: target.metadata[Metadata.district], + }; + } +}; + +export const getRegion = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.region]) { + return { + display: target.metadata[Metadata.region], + searchKey: Metadata.region, + searchValue: target.metadata[Metadata.region], + }; + } +}; + +export const getCountry = (target: FileResponse): MediaMetaElements | undefined => { + if (target.metadata?.[Metadata.country]) { + return { + display: target.metadata[Metadata.country], + searchKey: Metadata.country, + searchValue: target.metadata[Metadata.country], + }; + } +}; + const MediaInfo = ({ target }: MediaInfoProps) => { if (!target.metadata) { return undefined; @@ -556,6 +617,56 @@ const MediaInfo = ({ target }: MediaInfoProps) => { return undefined; }, [target, t]); + const gpsAddressContent = useMemo(() => { + let content: (MediaMetaElements | string)[] = []; + + // Build address components in hierarchical order (most specific to least specific) + const addressComponents: (MediaMetaElements | undefined)[] = []; + + const street = getStreet(target); + if (street) { + addressComponents.push(street); + } + const locality = getLocality(target); + if (locality) { + addressComponents.push(locality); + } + const place = getPlace(target); + if (place) { + addressComponents.push(place); + } + const district = getDistrict(target); + if (district) { + addressComponents.push(district); + } + const region = getRegion(target); + if (region) { + addressComponents.push(region); + } + const country = getCountry(target); + if (country) { + addressComponents.push(country); + } + + // Filter out undefined components and join with commas + const validComponents = addressComponents.filter(Boolean) as MediaMetaElements[]; + + if (validComponents.length > 0) { + // Add the first component + content.push(validComponents[0]); + + // Add remaining components with comma separators + for (let i = 1; i < validComponents.length; i++) { + content.push(", "); + content.push(validComponents[i]); + } + + return { title: [t("fileManager.address")], content }; + } + + return undefined; + }, [target, t]); + const showExifBasic = exifContents.length > 0; const showLightInfo = lightInfoContent.length > 0; const showCopyRight = !!copyRightContent; @@ -573,7 +684,8 @@ const MediaInfo = ({ target }: MediaInfoProps) => { durationContent || streamFormatContent || singleStreamContents || - mapBoxGps; + mapBoxGps || + gpsAddressContent; if (!showMediaInfo) { return undefined; @@ -592,6 +704,7 @@ const MediaInfo = ({ target }: MediaInfoProps) => { {showCopyRight && } {softwareContent && } {mapBoxGps && } + {gpsAddressContent && } {mediaTitleContent && } {musicArtistContent && } {albumContent && } diff --git a/src/component/Icons/LocationFilled.tsx b/src/component/Icons/LocationFilled.tsx new file mode 100644 index 0000000..c19bb54 --- /dev/null +++ b/src/component/Icons/LocationFilled.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function LocationFilled(props: SvgIconProps) { + return ( + + + + ); +}