Skip to content

Commit

Permalink
Add location grouping views
Browse files Browse the repository at this point in the history
Signed-off-by: Louis Chemineau <louis@chmn.me>
  • Loading branch information
artonge committed Oct 27, 2022
1 parent d652092 commit c18d8bb
Show file tree
Hide file tree
Showing 14 changed files with 1,208 additions and 12 deletions.
8 changes: 8 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@
'path' => '',
]
],
['name' => 'page#index', 'url' => '/locations/{path}', 'verb' => 'GET', 'postfix' => 'locations',
'requirements' => [
'path' => '.*',
],
'defaults' => [
'path' => '',
]
],
[ 'name' => 'publicAlbum#get', 'url' => '/public/{token}', 'verb' => 'GET',
'requirements' => [
'token' => '.*',
Expand Down
58 changes: 58 additions & 0 deletions cypress/e2e/locations.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { randHash } from '../utils'

const alice = `alice_${randHash()}`

const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
})

describe('Manage locations', () => {
before(function () {
cy.logout()
cy.nextcloudCreateUser(alice, 'password')

cy.login(alice, 'password')
cy.uploadTestMedia()

// wait a bit for things to be settled
cy.wait(1000)
})

beforeEach(() => {
cy.visit(`${Cypress.env('baseUrl')}/index.php/apps/photos/locations`)
})

it('Check that we detect some locations out of the existing files', () => {
cy.get('ul.collections__list li').should('have.length', 4)
})

it('Navigate to location and check that it contains some files', () => {
cy.navigateToLocation('Lauris')
cy.get('.collection li a.file').should('have.length', 1)
})
})
36 changes: 26 additions & 10 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,23 @@ Cypress.Commands.add('uploadFile', (fixtureFileName, mimeType, path = '', upload
.join("/")

const url = `${Cypress.env('baseUrl')}/remote.php/webdav${encodedPath}/${encodeURIComponent(uploadedFileName)}`
return cy.request({
method: 'PUT',
url,
body: file,
encoding: 'binary',
headers: {
'Content-Type': mimeType,
requesttoken: window.OC.requestToken,
},
})
return cy
.request({
method: 'PUT',
url,
body: file,
encoding: 'binary',
headers: {
'Content-Type': mimeType,
requesttoken: window.OC.requestToken,
},
})
// Call cron.php multiple times to trigger location's background job.
.request(`${Cypress.env('baseUrl')}/cron.php`)
.request(`${Cypress.env('baseUrl')}/cron.php`)
.request(`${Cypress.env('baseUrl')}/cron.php`)
.request(`${Cypress.env('baseUrl')}/cron.php`)
.request(`${Cypress.env('baseUrl')}/cron.php`)
})
})

Expand Down Expand Up @@ -245,3 +252,12 @@ Cypress.Commands.add('removeSharedAlbums', () => {
cy.get('[aria-label="Open actions menu"]').click()
cy.contains("Delete album").click()
})

Cypress.Commands.add('navigateToCollection', (collectionType, collectionName) => {
cy.get('.app-navigation__list').contains(collectionType).click()
cy.get('ul.collections__list').contains(collectionName).click()
})

Cypress.Commands.add('navigateToLocation', locationName => {
cy.navigateToCollection('Locations', locationName)
})
5 changes: 4 additions & 1 deletion src/Photos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@
:title="t('photos', 'Tags')">
<Tag slot="icon" :size="20" />
</NcAppNavigationItem>
<NcAppNavigationItem :to="{name: 'locations'}" :title="t('photos', 'Locations')">
<MapMarker slot="icon" :size="20" />
</NcAppNavigationItem>
<NcAppNavigationItem v-if="showLocationMenuEntry"
:to="{name: 'maps'}"
:title="t('photos', 'Locations')">
:title="t('photos', 'Maps')">
<MapMarker slot="icon" :size="20" />
</NcAppNavigationItem>
</template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Collection/CollectionCover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default {
&__image {
width: 350px;
height: 350px;
object-fit: none;
object-fit: cover;
border-radius: var(--border-radius-large);
@media only screen and (max-width: 1200px) {
Expand Down
126 changes: 126 additions & 0 deletions src/mixins/FetchCollectionsContentMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
*
* @author Louis Chemineau <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { mapActions } from 'vuex'

import { showError } from '@nextcloud/dialogs'

import AbortControllerMixin from './AbortControllerMixin.js'
import { fetchCollection, fetchCollectionFiles } from '../services/collectionFetcher.js'
import logger from '../services/logger.js'
import SemaphoreWithPriority from '../utils/semaphoreWithPriority.js'

export default {
name: 'FetchCollectionsContentMixin',

data() {
return {
semaphore: new SemaphoreWithPriority(30),
fetchSemaphore: new SemaphoreWithPriority(1),
semaphoreSymbol: null,
loadingCollection: false,
loadingCollectionFiles: false,
errorFetchingCollection: null,
errorFetchingCollectionFiles: null,
}
},

mixins: [
AbortControllerMixin,
],

methods: {
...mapActions([
'appendFiles',
'addCollections',
'setCollectionFiles',
]),

async fetchCollection(collectionFileName) {
if (this.loadingCollection) {
return
}

try {
this.loadingCollection = true
this.errorFetchingCollection = null

const collection = await fetchCollection(collectionFileName, { signal: this.abortController.signal })
this.addCollections({ collections: [collection] })
return collection
} catch (error) {
if (error.response?.status === 404) {
this.errorFetchingCollection = 404
return
}

this.errorFetchingCollection = error
logger.error('[PublicLocationContent] Error fetching location', { error })
showError(this.t('photos', 'Failed to fetch location.'))
} finally {
this.loadingCollection = false
}
},

async fetchCollectionFiles(collectionFileName) {
if (this.loadingCollectionFiles) {
return []
}

const semaphoreSymbol = await this.semaphore.acquire(() => 0, 'fetchFiles')
const fetchSemaphoreSymbol = await this.fetchSemaphore.acquire()

try {
this.errorFetchingCollectionFiles = null
this.loadingCollectionFiles = true
this.semaphoreSymbol = semaphoreSymbol

const fetchedFiles = await fetchCollectionFiles(collectionFileName, { signal: this.abortController.signal })
const fileIds = fetchedFiles.map(file => file.fileid.toString())

this.appendFiles(fetchedFiles)

if (fetchedFiles.length > 0) {
await this.$store.commit('setCollectionFiles', { collectionFileName, fileIds })
}

return fetchedFiles
} catch (error) {
if (error.response?.status === 404) {
this.errorFetchingCollectionFiles = 404
return []
}

this.errorFetchingCollectionFiles = error

showError(this.t('photos', 'Failed to fetch locations list.'))
logger.error('[PublicLocationContent] Error fetching location files', { error })
} finally {
this.loadingCollectionFiles = false
this.semaphore.release(semaphoreSymbol)
this.fetchSemaphore.release(fetchSemaphoreSymbol)
}

return []
},
},
}
74 changes: 74 additions & 0 deletions src/mixins/FetchCollectionsMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
*
* @author Louis Chemineau <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { mapActions } from 'vuex'

import AbortControllerMixin from './AbortControllerMixin.js'
import { fetchCollections } from '../services/collectionFetcher.js'

export default {
name: 'FetchCollectionsMixin',

data() {
return {
errorFetchingCollections: null,
loadingCollections: false,
}
},

mixins: [
AbortControllerMixin,
],

methods: {
...mapActions([
'addCollections',
]),

async fetchCollections(collectionHome) {
if (this.loadingCollections) {
return []
}

try {
this.loadingCollections = true
this.errorFetchingCollections = null

const collections = await fetchCollections(collectionHome, { signal: this.abortController.signal })

this.addCollections({ collections })

return collections
} catch (error) {
if (error.response?.status === 404) {
this.errorFetchingCollections = 404
} else {
this.errorFetchingCollections = error
}
} finally {
this.loadingCollections = false
}

return []
},
},
}
15 changes: 15 additions & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const AlbumContent = () => import('../views/AlbumContent')
const SharedAlbums = () => import('../views/SharedAlbums')
const SharedAlbumContent = () => import('../views/SharedAlbumContent')
const PublicAlbumContent = () => import('../views/PublicAlbumContent')
const Locations = () => import('../views/Locations')
const LocationContent = () => import('../views/LocationContent')
const Tags = () => import('../views/Tags')
const TagContent = () => import('../views/TagContent')
const Timeline = () => import('../views/Timeline')
Expand Down Expand Up @@ -127,6 +129,19 @@ const router = new Router({
token: route.params.token,
}),
},
{
path: '/locations',
component: Locations,
name: 'locations',
},
{
path: '/locations/:locationName*',
component: LocationContent,
name: 'locations',
props: route => ({
locationName: route.params.locationName,
}),
},
{
path: '/folders/:path*',
component: Folders,
Expand Down
Loading

0 comments on commit c18d8bb

Please sign in to comment.