Skip to content

Commit 817b57c

Browse files
authored
Merge pull request #1464 from nextcloud/fix/add-files
fix: use filepicker API for placing photos on the map
2 parents 2b43e1e + ab25f54 commit 817b57c

File tree

6 files changed

+145
-52
lines changed

6 files changed

+145
-52
lines changed

lib/Controller/PhotosController.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use OCA\Maps\Service\GeophotoService;
1717
use OCA\Maps\Service\PhotofilesService;
1818
use OCP\AppFramework\Controller;
19+
use OCP\AppFramework\Http;
1920
use OCP\AppFramework\Http\DataResponse;
2021
use OCP\DB\Exception;
22+
use OCP\Files\Folder;
2123
use OCP\Files\InvalidPathException;
2224
use OCP\Files\IRootFolder;
2325
use OCP\Files\NotFoundException;
@@ -114,14 +116,18 @@ public function getNonLocalizedPhotos(?int $myMapId = null, ?string $timezone =
114116
public function placePhotos($paths, $lats, $lngs, bool $directory = false, $myMapId = null, bool $relative = false): DataResponse {
115117
$userFolder = $this->root->getUserFolder($this->userId);
116118
if (!is_null($myMapId) and $myMapId !== '') {
117-
// forbid folder placement in my-maps
118119
if ($directory === 'true') {
120+
// forbid folder placement in my-maps
119121
throw new NotPermittedException();
120122
}
121-
$folders = $userFolder->getById($myMapId);
122-
$folder = array_shift($folders);
123+
124+
$folder = $userFolder->getFirstNodeById($myMapId);
125+
if (!($folder instanceof Folder)) {
126+
return new DataResponse(statusCode: Http::STATUS_BAD_REQUEST);
127+
}
128+
123129
// photo's path is relative to this map's folder => get full path, don't copy
124-
if ($relative === 'true') {
130+
if ($relative) {
125131
foreach ($paths as $key => $path) {
126132
$photoFile = $folder->get($path);
127133
$paths[$key] = $userFolder->getRelativePath($photoFile->getPath());

lib/Service/PhotofilesService.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,11 @@ private function setFilesCoords($userId, $paths, $lats, $lngs) {
291291
if ($this->isPhoto($file) && $file->isUpdateable()) {
292292
$lat = (count($lats) > $i) ? $lats[$i] : $lats[0];
293293
$lng = (count($lngs) > $i) ? $lngs[$i] : $lngs[0];
294-
$photo = $this->photoMapper->findByFileIdUserId($file->getId(), $userId);
294+
try {
295+
$photo = $this->photoMapper->findByFileIdUserId($file->getId(), $userId);
296+
} catch (DoesNotExistException) {
297+
$photo = null;
298+
}
295299
$done[] = [
296300
'path' => preg_replace('/^files/', '', $file->getInternalPath()),
297301
'lat' => $lat,

src/network.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import axios from '@nextcloud/axios'
22
import { default as realAxios } from 'axios'
33
import { generateUrl } from '@nextcloud/router'
4-
import {
5-
showError,
6-
} from '@nextcloud/dialogs'
74

85
export function saveOptionValues(optionValues, myMapId = null, token = null) {
96
const req = {
@@ -328,6 +325,13 @@ export async function getPhotoSuggestions(myMapId = null, token = null, timezone
328325
return axios.get(url, conf)
329326
}
330327

328+
/**
329+
* @param {string[]} paths
330+
* @param {number[]} lats
331+
* @param {number[]} lngs
332+
* @param {boolean} directory - Is the placed path a directory
333+
* @param {number | null} myMapId - The myMapId
334+
*/
331335
export function placePhotos(paths, lats, lngs, directory = false, myMapId = null) {
332336
const req = {
333337
paths,

src/utils/photoPicker.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { DialogBuilder, FilePickerBuilder } from '@nextcloud/dialogs'
7+
import { n, t } from '@nextcloud/l10n'
8+
import { placePhotos } from '../network.js'
9+
10+
interface LotLong {
11+
lat: number
12+
lng: number
13+
}
14+
15+
const dialogBuilder = new DialogBuilder(t('maps', 'What do you want to place'))
16+
17+
/**
18+
* Place photos or a photo folder on a given map and location.
19+
*
20+
* @param latLong - The geo location where to place the photos
21+
* @param myMapId - The map to place the photos
22+
*/
23+
export async function placeFileOrFolder(latLong: LotLong, myMapId: number) {
24+
const { promise, resolve } = Promise.withResolvers<unknown>()
25+
const dialog = dialogBuilder
26+
.setButtons([
27+
{
28+
label: t('maps', 'Photo folder'),
29+
callback() {
30+
resolve(placeFolder(latLong, myMapId))
31+
},
32+
},
33+
{
34+
label: t('maps', 'Photo files'),
35+
callback() {
36+
resolve(placeFiles(latLong, myMapId))
37+
},
38+
variant: 'primary',
39+
},
40+
])
41+
.build()
42+
43+
await dialog.show()
44+
return promise
45+
}
46+
47+
/**
48+
* Callback to select and place a folder.
49+
*
50+
* @param latLong - The location where to place
51+
* @param myMapId - The map to place photos to
52+
*/
53+
async function placeFolder(latLong: LotLong, myMapId: number) {
54+
const filePickerBuilder = new FilePickerBuilder(t('maps', 'Choose directory of photos to place'))
55+
const filePicker = filePickerBuilder.allowDirectories(true)
56+
.setMimeTypeFilter(['httpd/unix-directory'])
57+
.setButtonFactory((nodes) => [{
58+
callback: () => {},
59+
label: nodes.length === 1
60+
? t('maps', 'Select {photo}', { photo: nodes[0].displayname }, { escape: false })
61+
: (nodes.length === 0
62+
? t('maps', 'Select folder')
63+
: n('maps', 'Select %n folder', 'Select %n folders', nodes.length)
64+
),
65+
disabled: nodes.length === 0,
66+
variant: 'primary',
67+
}])
68+
.setMultiSelect(false)
69+
.build()
70+
71+
try {
72+
const folder = await filePicker.pick()
73+
return placePhotos([folder], [latLong.lat], [latLong.lng], true, myMapId)
74+
} catch {
75+
// cancelled picking
76+
}
77+
}
78+
79+
/**
80+
* Callback to select and place on or multiple photo files.
81+
*
82+
* @param latLong - The location where to place
83+
* @param myMapId - The map to place photos to
84+
*/
85+
async function placeFiles(latLong: LotLong, myMapId: number) {
86+
const filePickerBuilder = new FilePickerBuilder(t('maps', 'Choose photos to place'))
87+
const filePicker = filePickerBuilder
88+
.setMimeTypeFilter(['image/jpeg', 'image/tiff'])
89+
.setButtonFactory((nodes) => [{
90+
callback: () => {},
91+
label: nodes.length === 1
92+
? t('maps', 'Select {photo}', { photo: nodes[0].displayname }, { escape: false })
93+
: (nodes.length === 0
94+
? t('maps', 'Select photo')
95+
: n('maps', 'Select %n photo', 'Select %n photos', nodes.length)
96+
),
97+
disabled: nodes.length === 0,
98+
variant: 'primary',
99+
}])
100+
.setMultiSelect(true)
101+
.build()
102+
103+
try {
104+
const nodes = await filePicker.pick()
105+
return placePhotos(nodes, [latLong.lat], [latLong.lng], false, myMapId)
106+
} catch {
107+
// cancelled picking
108+
}
109+
}

src/views/App.vue

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ import { geoToLatLng, getFormattedADR } from '../utils/mapUtils.js'
233233
import * as network from '../network.js'
234234
import { all as axiosAll, spread as axiosSpread } from 'axios'
235235
import { generateUrl } from '@nextcloud/router'
236+
import { placeFileOrFolder } from '../utils/photoPicker.ts'
236237

237238
export default {
238239
name: 'App',
@@ -946,50 +947,17 @@ export default {
946947
console.error(error)
947948
})
948949
},
949-
placePhotoFilesOrFolder(latlng) {
950-
OC.dialogs.confirmDestructive(
951-
'',
952-
t('maps', 'What do you want to place?'),
953-
{
954-
type: OC.dialogs.YES_NO_BUTTONS,
955-
confirm: t('maps', 'Photo files'),
956-
confirmClasses: '',
957-
cancel: t('maps', 'Photo folders'),
958-
},
959-
(result) => {
960-
if (result) {
961-
this.placePhotoFiles(latlng)
962-
} else {
963-
this.placePhotoFolder(latlng)
964-
}
965-
},
966-
true,
967-
)
968-
},
969-
placePhotoFiles(latlng) {
970-
OC.dialogs.filepicker(
971-
t('maps', 'Choose pictures to place'),
972-
(targetPath) => {
973-
this.placePhotos(targetPath, [latlng.lat], [latlng.lng])
974-
},
975-
true,
976-
['image/jpeg', 'image/tiff'],
977-
true,
978-
)
979-
},
980-
placePhotoFolder(latlng) {
981-
OC.dialogs.filepicker(
982-
t('maps', 'Choose directory of pictures to place'),
983-
(targetPath) => {
984-
if (targetPath === '') {
985-
targetPath = '/'
986-
}
987-
this.placePhotos([targetPath], [latlng.lat], [latlng.lng], true)
988-
},
989-
false,
990-
'httpd/unix-directory',
991-
true,
992-
)
950+
async placePhotoFilesOrFolder(latLong) {
951+
try {
952+
const response = await placeFileOrFolder(latLong, this.myMapId)
953+
this.getPhotos()
954+
this.saveAction({
955+
type: 'photoMove',
956+
content: response.data,
957+
})
958+
} catch (error) {
959+
console.error(error)
960+
}
993961
},
994962
placePhotos(paths, lats, lngs, directory = false, save = true, reload = true) {
995963
network.placePhotos(paths, lats, lngs, directory, this.myMapId).then((response) => {

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"extends": "@vue/tsconfig",
33
"compilerOptions": {
4+
"allowJs": true,
45
"allowImportingTsExtensions": true,
56
"rewriteRelativeImportExtensions": true,
67
"noEmit": false,
8+
"outDir": "js",
79
},
810
"vueCompilerOptions": {
911
"target": 2.7

0 commit comments

Comments
 (0)