diff --git a/lib/Controller/PhotosController.php b/lib/Controller/PhotosController.php index 4e21b4a83..dffc0083a 100644 --- a/lib/Controller/PhotosController.php +++ b/lib/Controller/PhotosController.php @@ -16,8 +16,10 @@ use OCA\Maps\Service\GeophotoService; use OCA\Maps\Service\PhotofilesService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\DB\Exception; +use OCP\Files\Folder; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; @@ -114,14 +116,18 @@ public function getNonLocalizedPhotos(?int $myMapId = null, ?string $timezone = public function placePhotos($paths, $lats, $lngs, bool $directory = false, $myMapId = null, bool $relative = false): DataResponse { $userFolder = $this->root->getUserFolder($this->userId); if (!is_null($myMapId) and $myMapId !== '') { - // forbid folder placement in my-maps if ($directory === 'true') { + // forbid folder placement in my-maps throw new NotPermittedException(); } - $folders = $userFolder->getById($myMapId); - $folder = array_shift($folders); + + $folder = $userFolder->getFirstNodeById($myMapId); + if (!($folder instanceof Folder)) { + return new DataResponse(statusCode: Http::STATUS_BAD_REQUEST); + } + // photo's path is relative to this map's folder => get full path, don't copy - if ($relative === 'true') { + if ($relative) { foreach ($paths as $key => $path) { $photoFile = $folder->get($path); $paths[$key] = $userFolder->getRelativePath($photoFile->getPath()); diff --git a/lib/Service/PhotofilesService.php b/lib/Service/PhotofilesService.php index fd85a24f9..a0b979db8 100644 --- a/lib/Service/PhotofilesService.php +++ b/lib/Service/PhotofilesService.php @@ -291,7 +291,11 @@ private function setFilesCoords($userId, $paths, $lats, $lngs) { if ($this->isPhoto($file) && $file->isUpdateable()) { $lat = (count($lats) > $i) ? $lats[$i] : $lats[0]; $lng = (count($lngs) > $i) ? $lngs[$i] : $lngs[0]; - $photo = $this->photoMapper->findByFileIdUserId($file->getId(), $userId); + try { + $photo = $this->photoMapper->findByFileIdUserId($file->getId(), $userId); + } catch (DoesNotExistException) { + $photo = null; + } $done[] = [ 'path' => preg_replace('/^files/', '', $file->getInternalPath()), 'lat' => $lat, diff --git a/src/network.js b/src/network.js index 1dfc6364b..df393934e 100644 --- a/src/network.js +++ b/src/network.js @@ -1,9 +1,6 @@ import axios from '@nextcloud/axios' import { default as realAxios } from 'axios' import { generateUrl } from '@nextcloud/router' -import { - showError, -} from '@nextcloud/dialogs' export function saveOptionValues(optionValues, myMapId = null, token = null) { const req = { @@ -328,6 +325,13 @@ export async function getPhotoSuggestions(myMapId = null, token = null, timezone return axios.get(url, conf) } +/** + * @param {string[]} paths + * @param {number[]} lats + * @param {number[]} lngs + * @param {boolean} directory - Is the placed path a directory + * @param {number | null} myMapId - The myMapId + */ export function placePhotos(paths, lats, lngs, directory = false, myMapId = null) { const req = { paths, diff --git a/src/utils/photoPicker.ts b/src/utils/photoPicker.ts new file mode 100644 index 000000000..db78acc00 --- /dev/null +++ b/src/utils/photoPicker.ts @@ -0,0 +1,109 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { DialogBuilder, FilePickerBuilder } from '@nextcloud/dialogs' +import { n, t } from '@nextcloud/l10n' +import { placePhotos } from '../network.js' + +interface LotLong { + lat: number + lng: number +} + +const dialogBuilder = new DialogBuilder(t('maps', 'What do you want to place')) + +/** + * Place photos or a photo folder on a given map and location. + * + * @param latLong - The geo location where to place the photos + * @param myMapId - The map to place the photos + */ +export async function placeFileOrFolder(latLong: LotLong, myMapId: number) { + const { promise, resolve } = Promise.withResolvers() + const dialog = dialogBuilder + .setButtons([ + { + label: t('maps', 'Photo folder'), + callback() { + resolve(placeFolder(latLong, myMapId)) + }, + }, + { + label: t('maps', 'Photo files'), + callback() { + resolve(placeFiles(latLong, myMapId)) + }, + variant: 'primary', + }, + ]) + .build() + + await dialog.show() + return promise +} + +/** + * Callback to select and place a folder. + * + * @param latLong - The location where to place + * @param myMapId - The map to place photos to + */ +async function placeFolder(latLong: LotLong, myMapId: number) { + const filePickerBuilder = new FilePickerBuilder(t('maps', 'Choose directory of photos to place')) + const filePicker = filePickerBuilder.allowDirectories(true) + .setMimeTypeFilter(['httpd/unix-directory']) + .setButtonFactory((nodes) => [{ + callback: () => {}, + label: nodes.length === 1 + ? t('maps', 'Select {photo}', { photo: nodes[0].displayname }, { escape: false }) + : (nodes.length === 0 + ? t('maps', 'Select folder') + : n('maps', 'Select %n folder', 'Select %n folders', nodes.length) + ), + disabled: nodes.length === 0, + variant: 'primary', + }]) + .setMultiSelect(false) + .build() + + try { + const folder = await filePicker.pick() + return placePhotos([folder], [latLong.lat], [latLong.lng], true, myMapId) + } catch { + // cancelled picking + } +} + +/** + * Callback to select and place on or multiple photo files. + * + * @param latLong - The location where to place + * @param myMapId - The map to place photos to + */ +async function placeFiles(latLong: LotLong, myMapId: number) { + const filePickerBuilder = new FilePickerBuilder(t('maps', 'Choose photos to place')) + const filePicker = filePickerBuilder + .setMimeTypeFilter(['image/jpeg', 'image/tiff']) + .setButtonFactory((nodes) => [{ + callback: () => {}, + label: nodes.length === 1 + ? t('maps', 'Select {photo}', { photo: nodes[0].displayname }, { escape: false }) + : (nodes.length === 0 + ? t('maps', 'Select photo') + : n('maps', 'Select %n photo', 'Select %n photos', nodes.length) + ), + disabled: nodes.length === 0, + variant: 'primary', + }]) + .setMultiSelect(true) + .build() + + try { + const nodes = await filePicker.pick() + return placePhotos(nodes, [latLong.lat], [latLong.lng], false, myMapId) + } catch { + // cancelled picking + } +} diff --git a/src/views/App.vue b/src/views/App.vue index 6f69993ed..c11e0cdfe 100644 --- a/src/views/App.vue +++ b/src/views/App.vue @@ -233,6 +233,7 @@ import { geoToLatLng, getFormattedADR } from '../utils/mapUtils.js' import * as network from '../network.js' import { all as axiosAll, spread as axiosSpread } from 'axios' import { generateUrl } from '@nextcloud/router' +import { placeFileOrFolder } from '../utils/photoPicker.ts' export default { name: 'App', @@ -946,50 +947,17 @@ export default { console.error(error) }) }, - placePhotoFilesOrFolder(latlng) { - OC.dialogs.confirmDestructive( - '', - t('maps', 'What do you want to place?'), - { - type: OC.dialogs.YES_NO_BUTTONS, - confirm: t('maps', 'Photo files'), - confirmClasses: '', - cancel: t('maps', 'Photo folders'), - }, - (result) => { - if (result) { - this.placePhotoFiles(latlng) - } else { - this.placePhotoFolder(latlng) - } - }, - true, - ) - }, - placePhotoFiles(latlng) { - OC.dialogs.filepicker( - t('maps', 'Choose pictures to place'), - (targetPath) => { - this.placePhotos(targetPath, [latlng.lat], [latlng.lng]) - }, - true, - ['image/jpeg', 'image/tiff'], - true, - ) - }, - placePhotoFolder(latlng) { - OC.dialogs.filepicker( - t('maps', 'Choose directory of pictures to place'), - (targetPath) => { - if (targetPath === '') { - targetPath = '/' - } - this.placePhotos([targetPath], [latlng.lat], [latlng.lng], true) - }, - false, - 'httpd/unix-directory', - true, - ) + async placePhotoFilesOrFolder(latLong) { + try { + const response = await placeFileOrFolder(latLong, this.myMapId) + this.getPhotos() + this.saveAction({ + type: 'photoMove', + content: response.data, + }) + } catch (error) { + console.error(error) + } }, placePhotos(paths, lats, lngs, directory = false, save = true, reload = true) { network.placePhotos(paths, lats, lngs, directory, this.myMapId).then((response) => { diff --git a/tsconfig.json b/tsconfig.json index 8a72d3d7a..8863c6894 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,11 @@ { "extends": "@vue/tsconfig", "compilerOptions": { + "allowJs": true, "allowImportingTsExtensions": true, "rewriteRelativeImportExtensions": true, "noEmit": false, + "outDir": "js", }, "vueCompilerOptions": { "target": 2.7