Skip to content

Commit c18d8bb

Browse files
committed
Add location grouping views
Signed-off-by: Louis Chemineau <[email protected]>
1 parent d652092 commit c18d8bb

File tree

14 files changed

+1208
-12
lines changed

14 files changed

+1208
-12
lines changed

appinfo/routes.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@
4646
'path' => '',
4747
]
4848
],
49+
['name' => 'page#index', 'url' => '/locations/{path}', 'verb' => 'GET', 'postfix' => 'locations',
50+
'requirements' => [
51+
'path' => '.*',
52+
],
53+
'defaults' => [
54+
'path' => '',
55+
]
56+
],
4957
[ 'name' => 'publicAlbum#get', 'url' => '/public/{token}', 'verb' => 'GET',
5058
'requirements' => [
5159
'token' => '.*',

cypress/e2e/locations.cy.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @copyright Copyright (c) 2022 Louis Chmn <[email protected]>
3+
*
4+
* @author Louis Chmn <[email protected]>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
import { randHash } from '../utils'
23+
24+
const alice = `alice_${randHash()}`
25+
26+
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
27+
Cypress.on('uncaught:exception', (err) => {
28+
/* returning false here prevents Cypress from failing the test */
29+
if (resizeObserverLoopErrRe.test(err.message)) {
30+
return false
31+
}
32+
})
33+
34+
describe('Manage locations', () => {
35+
before(function () {
36+
cy.logout()
37+
cy.nextcloudCreateUser(alice, 'password')
38+
39+
cy.login(alice, 'password')
40+
cy.uploadTestMedia()
41+
42+
// wait a bit for things to be settled
43+
cy.wait(1000)
44+
})
45+
46+
beforeEach(() => {
47+
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/locations`)
48+
})
49+
50+
it('Check that we detect some locations out of the existing files', () => {
51+
cy.get('ul.collections__list li').should('have.length', 4)
52+
})
53+
54+
it('Navigate to location and check that it contains some files', () => {
55+
cy.navigateToLocation('Lauris')
56+
cy.get('.collection li a.file').should('have.length', 1)
57+
})
58+
})

cypress/support/commands.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,23 @@ Cypress.Commands.add('uploadFile', (fixtureFileName, mimeType, path = '', upload
115115
.join("/")
116116

117117
const url = `${Cypress.env('baseUrl')}/remote.php/webdav${encodedPath}/${encodeURIComponent(uploadedFileName)}`
118-
return cy.request({
119-
method: 'PUT',
120-
url,
121-
body: file,
122-
encoding: 'binary',
123-
headers: {
124-
'Content-Type': mimeType,
125-
requesttoken: window.OC.requestToken,
126-
},
127-
})
118+
return cy
119+
.request({
120+
method: 'PUT',
121+
url,
122+
body: file,
123+
encoding: 'binary',
124+
headers: {
125+
'Content-Type': mimeType,
126+
requesttoken: window.OC.requestToken,
127+
},
128+
})
129+
// Call cron.php multiple times to trigger location's background job.
130+
.request(`${Cypress.env('baseUrl')}/cron.php`)
131+
.request(`${Cypress.env('baseUrl')}/cron.php`)
132+
.request(`${Cypress.env('baseUrl')}/cron.php`)
133+
.request(`${Cypress.env('baseUrl')}/cron.php`)
134+
.request(`${Cypress.env('baseUrl')}/cron.php`)
128135
})
129136
})
130137

@@ -245,3 +252,12 @@ Cypress.Commands.add('removeSharedAlbums', () => {
245252
cy.get('[aria-label="Open actions menu"]').click()
246253
cy.contains("Delete album").click()
247254
})
255+
256+
Cypress.Commands.add('navigateToCollection', (collectionType, collectionName) => {
257+
cy.get('.app-navigation__list').contains(collectionType).click()
258+
cy.get('ul.collections__list').contains(collectionName).click()
259+
})
260+
261+
Cypress.Commands.add('navigateToLocation', locationName => {
262+
cy.navigateToCollection('Locations', locationName)
263+
})

src/Photos.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,12 @@
6464
:title="t('photos', 'Tags')">
6565
<Tag slot="icon" :size="20" />
6666
</NcAppNavigationItem>
67+
<NcAppNavigationItem :to="{name: 'locations'}" :title="t('photos', 'Locations')">
68+
<MapMarker slot="icon" :size="20" />
69+
</NcAppNavigationItem>
6770
<NcAppNavigationItem v-if="showLocationMenuEntry"
6871
:to="{name: 'maps'}"
69-
:title="t('photos', 'Locations')">
72+
:title="t('photos', 'Maps')">
7073
<MapMarker slot="icon" :size="20" />
7174
</NcAppNavigationItem>
7275
</template>

src/components/Collection/CollectionCover.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export default {
9595
&__image {
9696
width: 350px;
9797
height: 350px;
98-
object-fit: none;
98+
object-fit: cover;
9999
border-radius: var(--border-radius-large);
100100
101101
@media only screen and (max-width: 1200px) {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @copyright Copyright (c) 2022 Louis Chemineau <[email protected]>
3+
*
4+
* @author Louis Chemineau <[email protected]>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import { mapActions } from 'vuex'
24+
25+
import { showError } from '@nextcloud/dialogs'
26+
27+
import AbortControllerMixin from './AbortControllerMixin.js'
28+
import { fetchCollection, fetchCollectionFiles } from '../services/collectionFetcher.js'
29+
import logger from '../services/logger.js'
30+
import SemaphoreWithPriority from '../utils/semaphoreWithPriority.js'
31+
32+
export default {
33+
name: 'FetchCollectionsContentMixin',
34+
35+
data() {
36+
return {
37+
semaphore: new SemaphoreWithPriority(30),
38+
fetchSemaphore: new SemaphoreWithPriority(1),
39+
semaphoreSymbol: null,
40+
loadingCollection: false,
41+
loadingCollectionFiles: false,
42+
errorFetchingCollection: null,
43+
errorFetchingCollectionFiles: null,
44+
}
45+
},
46+
47+
mixins: [
48+
AbortControllerMixin,
49+
],
50+
51+
methods: {
52+
...mapActions([
53+
'appendFiles',
54+
'addCollections',
55+
'setCollectionFiles',
56+
]),
57+
58+
async fetchCollection(collectionFileName) {
59+
if (this.loadingCollection) {
60+
return
61+
}
62+
63+
try {
64+
this.loadingCollection = true
65+
this.errorFetchingCollection = null
66+
67+
const collection = await fetchCollection(collectionFileName, { signal: this.abortController.signal })
68+
this.addCollections({ collections: [collection] })
69+
return collection
70+
} catch (error) {
71+
if (error.response?.status === 404) {
72+
this.errorFetchingCollection = 404
73+
return
74+
}
75+
76+
this.errorFetchingCollection = error
77+
logger.error('[PublicLocationContent] Error fetching location', { error })
78+
showError(this.t('photos', 'Failed to fetch location.'))
79+
} finally {
80+
this.loadingCollection = false
81+
}
82+
},
83+
84+
async fetchCollectionFiles(collectionFileName) {
85+
if (this.loadingCollectionFiles) {
86+
return []
87+
}
88+
89+
const semaphoreSymbol = await this.semaphore.acquire(() => 0, 'fetchFiles')
90+
const fetchSemaphoreSymbol = await this.fetchSemaphore.acquire()
91+
92+
try {
93+
this.errorFetchingCollectionFiles = null
94+
this.loadingCollectionFiles = true
95+
this.semaphoreSymbol = semaphoreSymbol
96+
97+
const fetchedFiles = await fetchCollectionFiles(collectionFileName, { signal: this.abortController.signal })
98+
const fileIds = fetchedFiles.map(file => file.fileid.toString())
99+
100+
this.appendFiles(fetchedFiles)
101+
102+
if (fetchedFiles.length > 0) {
103+
await this.$store.commit('setCollectionFiles', { collectionFileName, fileIds })
104+
}
105+
106+
return fetchedFiles
107+
} catch (error) {
108+
if (error.response?.status === 404) {
109+
this.errorFetchingCollectionFiles = 404
110+
return []
111+
}
112+
113+
this.errorFetchingCollectionFiles = error
114+
115+
showError(this.t('photos', 'Failed to fetch locations list.'))
116+
logger.error('[PublicLocationContent] Error fetching location files', { error })
117+
} finally {
118+
this.loadingCollectionFiles = false
119+
this.semaphore.release(semaphoreSymbol)
120+
this.fetchSemaphore.release(fetchSemaphoreSymbol)
121+
}
122+
123+
return []
124+
},
125+
},
126+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @copyright Copyright (c) 2022 Louis Chemineau <[email protected]>
3+
*
4+
* @author Louis Chemineau <[email protected]>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import { mapActions } from 'vuex'
24+
25+
import AbortControllerMixin from './AbortControllerMixin.js'
26+
import { fetchCollections } from '../services/collectionFetcher.js'
27+
28+
export default {
29+
name: 'FetchCollectionsMixin',
30+
31+
data() {
32+
return {
33+
errorFetchingCollections: null,
34+
loadingCollections: false,
35+
}
36+
},
37+
38+
mixins: [
39+
AbortControllerMixin,
40+
],
41+
42+
methods: {
43+
...mapActions([
44+
'addCollections',
45+
]),
46+
47+
async fetchCollections(collectionHome) {
48+
if (this.loadingCollections) {
49+
return []
50+
}
51+
52+
try {
53+
this.loadingCollections = true
54+
this.errorFetchingCollections = null
55+
56+
const collections = await fetchCollections(collectionHome, { signal: this.abortController.signal })
57+
58+
this.addCollections({ collections })
59+
60+
return collections
61+
} catch (error) {
62+
if (error.response?.status === 404) {
63+
this.errorFetchingCollections = 404
64+
} else {
65+
this.errorFetchingCollections = error
66+
}
67+
} finally {
68+
this.loadingCollections = false
69+
}
70+
71+
return []
72+
},
73+
},
74+
}

src/router/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const AlbumContent = () => import('../views/AlbumContent')
3636
const SharedAlbums = () => import('../views/SharedAlbums')
3737
const SharedAlbumContent = () => import('../views/SharedAlbumContent')
3838
const PublicAlbumContent = () => import('../views/PublicAlbumContent')
39+
const Locations = () => import('../views/Locations')
40+
const LocationContent = () => import('../views/LocationContent')
3941
const Tags = () => import('../views/Tags')
4042
const TagContent = () => import('../views/TagContent')
4143
const Timeline = () => import('../views/Timeline')
@@ -127,6 +129,19 @@ const router = new Router({
127129
token: route.params.token,
128130
}),
129131
},
132+
{
133+
path: '/locations',
134+
component: Locations,
135+
name: 'locations',
136+
},
137+
{
138+
path: '/locations/:locationName*',
139+
component: LocationContent,
140+
name: 'locations',
141+
props: route => ({
142+
locationName: route.params.locationName,
143+
}),
144+
},
130145
{
131146
path: '/folders/:path*',
132147
component: Folders,

0 commit comments

Comments
 (0)