Added navigation, albums, init tags

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2019-11-07 19:45:14 +01:00
parent 9c94a3e10f
commit ae28cc9b2d
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
26 changed files with 659 additions and 127 deletions

24
appinfo/app.php Normal file
View File

@ -0,0 +1,24 @@
<?php
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
use OCA\Photos\AppInfo\Application;
\OC::$server->query(Application::class);

27
css/icons.scss Normal file
View File

@ -0,0 +1,27 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
.icon-folder.icon-dark {
@include icon-color('folder', 'filetypes', $color-black, 1, true);
}
@include icon-black-white('photos', 'photos', 1);

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0"><path fill="#fff" d="M2.69 4a.9.9 0 00-.69.88v22.25c0 .46.42.87.88.87h26.25c.45 0 .87-.42.87-.87V5.22C30 4.55 29.47 4 28.97 4zM4 6h24v10l-2-2-6 8-6-6-8 8H4zm5 2a3 3 0 100 6 3 3 0 000-6z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><path d="M2.8 4a1.3 1.3 0 00-1.3 1.3v22.4c0 .6.7 1.3 1.3 1.3h26.4c.6 0 1.3-.7 1.3-1.3V5.3c0-.6-.7-1.3-1.3-1.3zm.7 2h25v19h-25z" fill="#fff"/><circle cx="8.5" cy="11.2" r="3" fill="#fff"/><path d="M26.4 14.5l-4.7 6.2L20 23l-1.6-1.8-4.5-4.6-6 5.7-4.7 4.3h26.2v-8.7z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 359 B

1
img/photos.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><g transform="translate(-11.5 2.5)"><path d="M20.5 7.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h20c.5 0 1-.5 1-1v-17c0-.4-.5-1-1-1zM21 9h19v14.5H21z"/><circle cx="24.8" cy="13" r="2.3"/><path d="M38.4 15.5L35 20.2 33.6 22l-1.2-1.4-3.5-3.5-4.5 4.3-3.6 3.3h19.9v-6.6zM14.5 2.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h6v-3H15V4h19v3.5h1.5v-4c0-.4-.5-1-1-1h-20z"/></g></svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@ -28,9 +28,9 @@ use OCP\AppFramework\App;
class Application extends App {
const appID = 'photos';
const APP_ID = 'photos';
public function __construct() {
parent::__construct(self::appID);
parent::__construct(self::APP_ID);
}
}

View File

@ -30,6 +30,7 @@ use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\Util;
class PageController extends Controller {
@ -58,6 +59,10 @@ class PageController extends Controller {
$this->eventDispatcher->dispatch(LoadSidebar::class, new LoadSidebar());
$this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer());
Util::addScript('photos', 'photos');
Util::addStyle('photos', 'icons');
$response = new TemplateResponse($this->appName, 'main');
return $response;
}

26
package-lock.json generated
View File

@ -1,5 +1,5 @@
{
"name": "gallery",
"name": "photos",
"version": "19.0.0",
"lockfileVersion": 1,
"requires": true,
@ -872,18 +872,18 @@
}
},
"@nextcloud/vue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-1.0.0.tgz",
"integrity": "sha512-jYggwGf9so7g9uWP59cLspSo62uN7qX0+T096T/QBxyiEhoa+yspf9+Py/RIDylLacceKPDLTU4AdxELj63ZYQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-1.1.0.tgz",
"integrity": "sha512-SGWrNTalT/59vElPnPJZ0xQb9sui1yq5fKRiIA2w81NlAK9JQGq+ADeqdVb8sciVnLGobQJ1qERhpo3pIyOhaQ==",
"requires": {
"@babel/polyfill": "^7.4.4",
"@nextcloud/axios": "^0.4.0",
"escape-html": "^1.0.3",
"hammerjs": "^2.0.8",
"md5": "^2.2.1",
"v-click-outside": "^2.1.4",
"v-tooltip": "^2.0.0-rc.33",
"vue": "^2.6.7",
"vue-click-outside": "^1.0.7",
"vue-color": "^2.7.0",
"vue-multiselect": "^2.1.3",
"vue-visible": "^1.0.2",
@ -9307,6 +9307,11 @@
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
"dev": true
},
"v-click-outside": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-2.1.5.tgz",
"integrity": "sha512-VPNCOTZK6WZy73lcWc+R7IW1uaBFEO3/Csrs5CzWVOdvE30V8Y1+BE/BtTlcEmeDGx0eqdE7bSCg55Jj37PMJg=="
},
"v-tooltip": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.0.2.tgz",
@ -9382,11 +9387,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
},
"vue-click-outside": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.0.7.tgz",
"integrity": "sha1-zdKxYF48SUR4TheU6uShKg9wC9Y="
},
"vue-color": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.7.0.tgz",
@ -9506,9 +9506,9 @@
"integrity": "sha512-yaX2its9XAJKGuQqf7LsiZHHSkxsIK8rmCOQOvEGEoF41blKRK8qr9my4qYoD6ikdLss4n8tKqYBecmaY0+WJg=="
},
"vue2-datepicker": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.13.2.tgz",
"integrity": "sha512-bgtCdSTpFJogL37A5n2HnNPkyKVi0WTiM2+H+fYTHVYbRpSyNaPQ1Kj86A6tx3T14cv6qq4Oo8MrCxXiarDx2w==",
"version": "2.13.3",
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.13.3.tgz",
"integrity": "sha512-kAiTpCLlDC88mMTW5OqhlME0ZSB1fJKlHbKSEryPIi3lRJWHn4BlRSvGUTnSmBUr/5Qidma7Pxei9vih9Luicw==",
"requires": {
"fecha": "^2.3.3"
}

View File

@ -35,7 +35,7 @@
"@nextcloud/axios": "^0.5.0",
"@nextcloud/l10n": "^0.2.1",
"@nextcloud/router": "^0.1.0",
"@nextcloud/vue": "^1.0.0",
"@nextcloud/vue": "^1.1.0",
"cdav-library": "git+https://github.com/nextcloud/cdav-library.git",
"path-posix": "^1.0.0",
"qs": "^6.9.0",

View File

@ -22,6 +22,17 @@
<template>
<Content app-name="photos">
<AppNavigation>
<AppNavigationItem :to="{name: 'root'}"
class="app-navigation__photos"
:title="t('photos', 'Your photos')"
icon="icon-photos" />
<AppNavigationItem to="/favorites" :title="t('photos', 'Favorites')" icon="icon-favorite" />
<AppNavigationItem :to="{name: 'albums'}" :title="t('photos', 'Your albums')" icon="icon-files-dark" />
<AppNavigationItem :to="{name: 'shared'}" :title="t('photos', 'Shared albums')" icon="icon-share" />
<AppNavigationItem :to="{name: 'tags'}" :title="t('photos', 'Tags')" icon="icon-tag" />
<AppNavigationItem :to="{name: 'maps'}" :title="t('photos', 'Locations')" icon="icon-address" />
</AppNavigation>
<AppContent :class="{ 'icon-loading': loading }">
<router-view v-show="!loading" :loading.sync="loading" />
@ -34,6 +45,8 @@
<script>
import Content from '@nextcloud/vue/dist/Components/Content'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import svgplaceholder from './assets/img-placeholder.svg'
export default {
@ -41,6 +54,8 @@ export default {
components: {
Content,
AppContent,
AppNavigation,
AppNavigationItem,
},
data: function() {
return {
@ -50,3 +65,8 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.app-navigation__photos::v-deep .app-navigation-entry-icon.icon-photos {
background-size: 20px;
}
</style>

View File

@ -23,7 +23,7 @@
<template>
<router-link :class="{'folder--clear': isEmpty}"
class="folder"
:to="folder.filename"
:to="to"
:aria-label="ariaLabel">
<transition name="fade">
<div v-show="loaded"
@ -39,8 +39,8 @@
</transition>
<div
class="folder-name">
<span :class="{'icon-white': !isEmpty}"
class="folder-name__icon icon-folder"
<span :class="[!isEmpty ? 'icon-white' : 'icon-dark', icon]"
class="folder-name__icon"
role="img" />
<p :id="ariaUuid" class="folder-name__name">
{{ folder.basename }}
@ -66,6 +66,10 @@ export default {
type: Object,
required: true,
},
icon: {
type: String,
default: 'icon-folder',
},
},
data() {
@ -106,6 +110,20 @@ export default {
ariaLabel() {
return t('photos', 'Open the "{name}" sub-directory', { name: this.folder.basename })
},
/**
* We do not want encoded slashes when browsing by folder
* so we generate a new valid route object, get the final url back
* decode it and use it as a direct string, which vue-router
* does not encode afterwards
*/
to() {
const route = Object.assign({}, this.$route, {
// always remove first slash
params: { path: this.folder.filename.substr(1) }
});
return decodeURIComponent(this.$router.resolve(route).resolved.path)
},
},
async created() {
@ -215,6 +233,9 @@ $name-height: 1.2rem;
.folder {
// if no img, let's display the folder icon as default black
&--clear {
.folder-name__icon {
opacity: .3;
}
.folder-name__name {
color: var(--color-main-text);
text-shadow: 0 0 8px var(--color-main-background);

87
src/components/Grid.vue Normal file
View File

@ -0,0 +1,87 @@
<!--
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- 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/>.
-
-->
<template>
<!-- Folder content -->
<transition-group
class="photos-grid"
role="grid"
name="list"
tag="div">
<slot />
<div key="footer" role="none" class="photos-grid__footer-spacer" />
</transition-group>
</template>
<script>
export default {
name: 'Grid',
}
</script>
<style scoped lang="scss">
.photos-grid {
display: grid;
align-items: center;
justify-content: center;
gap: 8px;
grid-template-columns: repeat(10, 1fr);
position: relative;
// always put one more row of grid for the spacer
&__footer-spacer {
// always add one row, so placing it on the first
// column will always add one more
grid-column: 1;
// same height as the width
padding-bottom: 100%;
}
}
.list-move {
transition: transform var(--animation-quick);
}
// TODO: use mixins/GridSizes as soon as node-sass supports it
// needs node-sass 5.0 (with libsass 3.6)
// https://github.com/sass/node-sass/pull/2312
$previous: 0;
@each $size, $config in get('sizes') {
$count: map-get($config, 'count');
$marginTop: map-get($config, 'marginTop');
$marginW: map-get($config, 'marginW');
// if this is the last entry, only use min-width
$rule: '(min-width: #{$previous}px) and (max-width: #{$size}px)';
@if $size == 'max' {
$rule: '(min-width: #{$previous}px)';
}
@media #{$rule} {
.photos-grid {
padding: #{$marginTop}px #{$marginW}px #{$marginW}px #{$marginW}px;
grid-template-columns: repeat($count, 1fr);
}
}
$previous: $size;
}
</style>

View File

@ -89,11 +89,25 @@ export default {
}
return t('photos', 'Back to {folder}', { folder: this.parentName })
},
/**
* We do not want encoded slashes when browsing by folder
* so we generate a new valid route object, get the final url back
* decode it and use it as a direct string, which vue-router
* does not encode afterwards
*/
to() {
const route = Object.assign({}, this.$route, {
// always remove first slash
params: { path: this.parentPath.substr(1) }
});
return decodeURIComponent(this.$router.resolve(route).resolved.path)
},
},
methods: {
folderUp() {
this.$router.push(this.parentPath)
this.$router.push(this.to)
},
},
}

View File

@ -26,7 +26,7 @@ import { sync } from 'vuex-router-sync'
import { translate, translatePlural } from '@nextcloud/l10n'
import Vue from 'vue'
import Gallery from './Gallery'
import Photos from './Photos'
import router from './router'
import store from './store'
@ -49,8 +49,8 @@ Vue.prototype.n = translatePlural
export default new Vue({
el: '#content',
// eslint-disable-next-line vue/match-component-file-name
name: 'GalleryRoot',
name: 'PhotosRoot',
router,
store,
render: h => h(Gallery),
render: h => h(Photos),
})

View File

@ -24,10 +24,17 @@ import { generateUrl } from '@nextcloud/router'
import Router from 'vue-router'
import Vue from 'vue'
import Grid from '../views/Grid'
import Albums from '../views/Albums'
import Tags from '../views/Tags'
Vue.use(Router)
// shortcut to properly format the path prop
const props = route => ({
// always lead current path with a slash
path: `/${route.params.path ? route.params.path : ''}`,
})
export default new Router({
mode: 'history',
// if index.php is in the url AND we got this far, then it's working:
@ -37,20 +44,65 @@ export default new Router({
routes: [
{
path: '/',
component: Grid,
props: route => ({
// always lead current path with a slash
path: `/${route.params.path ? route.params.path : ''}`,
}),
component: Albums,
name: 'root',
},
{
path: '/albums',
component: Albums,
name: 'albums',
props,
children: [
{
path: ':path*',
name: 'path',
component: Grid,
component: Albums,
},
],
},
{ path: '*', redirect: { name: 'root' } },
{
path: '/shared',
component: Albums,
name: 'shared',
props,
children: [
{
path: ':path*',
name: 'path',
component: Albums,
},
],
},
{
path: '/favorites',
component: Tags,
name: 'favorites',
props,
children: [
{
path: ':path*',
name: 'path',
component: Tags,
},
],
},
{
path: '/tags',
component: Tags,
name: 'tags',
props,
children: [
{
path: ':path*',
name: 'path',
component: Tags,
},
],
},
{
path: '/maps',
name: 'maps',
redirect: '',
},
],
})

View File

@ -24,14 +24,13 @@ import webdav from 'webdav'
import axios from '@nextcloud/axios'
import parseUrl from 'url-parse'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
// force our axios
const patcher = webdav.getPatcher()
patcher.patch('request', axios)
// init webdav client
const remote = generateRemoteUrl(`dav/files/${getCurrentUser().uid}`)
const remote = generateRemoteUrl(`dav`)
const client = webdav.createClient(remote)
export const remotePath = parseUrl(remote).pathname

View File

@ -20,12 +20,14 @@
*
*/
import { getCurrentUser } from '@nextcloud/auth'
import { getSingleValue, getValueForKey, parseXML, propsToStat } from 'webdav/dist/interface/dav'
import { handleResponseCode, processResponsePayload } from 'webdav/dist/response'
import { normaliseHREF, normalisePath } from 'webdav/dist/url'
import client, { remotePath } from './DavClient'
import pathPosix from 'path-posix'
import request from './DavRequest'
import parseFile from '../utils/ParseFile'
/**
* List files from a folder and filter out unwanted mimes
@ -46,6 +48,8 @@ export default async function(path, options) {
details: true,
}, options)
const prefixPath = `/files/${getCurrentUser().uid}`
/**
* Fetch listing
*
@ -54,7 +58,7 @@ export default async function(path, options) {
* see https://github.com/perry-mitchell/webdav-client/blob/baf858a4856d44ae19ac12cb10c469b3e6c41ae4/source/interface/directoryContents.js#L11
*/
let response = null
const { data } = await client.customRequest(path, options)
const { data } = await client.customRequest(prefixPath + path, options)
.then(handleResponseCode)
.then(res => {
response = res
@ -64,14 +68,7 @@ export default async function(path, options) {
.then(result => getDirectoryFiles(result, remotePath, options.details))
.then(files => processResponsePayload(response, files, options.details))
const list = data
.map(entry => {
return Object.assign({
id: parseInt(entry.props.fileid),
isFavorite: entry.props.favorite !== '0',
hasPreview: entry.props['has-preview'] !== 'false',
}, entry)
})
const list = data.map(data => parseFile(data, prefixPath))
// filter all the files and folders
let folder = {}
@ -91,6 +88,15 @@ export default async function(path, options) {
return { folder, folders, files }
}
/**
* Modified function to include the root requested folder
* Into the returned data
*
* @param {Object} result the request result
* @param {string} serverBasePath server base path
* @param {boolean} isDetailed detailed request
* @returns {Array}
*/
function getDirectoryFiles(result, serverBasePath, isDetailed = false) {
const serverBase = pathPosix.join(serverBasePath, '/')
// Extract the response items (directory contents)

View File

@ -20,8 +20,10 @@
*
*/
import { getCurrentUser } from '@nextcloud/auth'
import client from './DavClient'
import request from './DavRequest'
import parseFile from '../utils/ParseFile'
/**
* List files from a folder and filter out unwanted mimes
@ -33,17 +35,13 @@ export default async function(path) {
// getDirectoryContents doesn't accept / for root
const fixedPath = path === '/' ? '' : path
const prefixPath = `/files/${getCurrentUser().uid}`
// fetch listing
const response = await client.stat(fixedPath, {
const response = await client.stat(prefixPath + fixedPath, {
data: request,
details: true,
})
const entry = response.data
return Object.assign({
id: parseInt(entry.props.fileid),
isFavorite: entry.props.favorite !== '0',
hasPreview: entry.props['has-preview'] !== 'false',
}, entry)
return parseFile(response.data, prefixPath)
}

View File

@ -20,8 +20,10 @@
*
*/
import client from './DavClient'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import client from './DavClient'
import parseFile from '../utils/ParseFile'
/**
* List files from a folder and filter out unwanted mimes
@ -35,7 +37,7 @@ export default async function() {
headers: {
'content-Type': 'text/xml',
},
url: '/remote.php/dav/',
url: generateRemoteUrl(`dav`),
data: `<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns"

View File

@ -0,0 +1,56 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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 client from './DavClient'
import { generateRemoteUrl } from '@nextcloud/router'
/**
* List files from a folder and filter out unwanted mimes
*
* @returns {Array} the file list
*/
export default async function() {
const response = await client.getDirectoryContents('/systemtags/', {
data: `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:id />
<oc:display-name />
<oc:user-visible />
<oc:user-assignable />
<oc:can-assign />
</d:prop>
</d:propfind>`,
details: true,
})
console.info(response)
const entry = response.data
return Object.assign({
id: parseInt(entry.props.fileid),
isFavorite: entry.props.favorite !== '0',
hasPreview: entry.props['has-preview'] !== 'false',
}, entry)
}

View File

@ -25,12 +25,14 @@ import Vuex, { Store } from 'vuex'
import files from './files'
import folders from './folders'
import systemtags from './systemtags'
Vue.use(Vuex)
export default new Store({
modules: {
files,
folders,
systemtags,
},
strict: process.env.NODE_ENV !== 'production',

99
src/store/systemtags.js Normal file
View File

@ -0,0 +1,99 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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 Vue from 'vue'
const state = {
paths: {},
tags: {},
}
const mutations = {
/**
* Index folders paths and ids
*
* @param {Object} state vuex state
* @param {Object} data destructuring object
* @param {number} data.id current folder id
* @param {Array} data.files list of files
*/
updateTags(state, { id, files }) {
if (files.length > 0) {
// sort by last modified
const list = files.sort((a, b) => {
return new Date(b.lastmod).getTime() - new Date(a.lastmod).getTime()
})
// Set folder list
Vue.set(state.tags, id, list.map(file => file.id))
}
},
/**
* Index folders paths and ids
*
* @param {Object} state vuex state
* @param {Object} data destructuring object
* @param {string} data.path path of this folder
* @param {number} data.id id of this folder
*/
addPath(state, { path, id }) {
Vue.set(state.paths, path, id)
},
}
const getters = {
tags: state => state.tags,
tag: state => id => state.tags[id],
tagId: state => path => state.paths[path],
}
const actions = {
/**
* Update files and folders
*
* @param {Object} context vuex context
* @param {Object} data destructuring object
* @param {number} data.id current folder id
* @param {Array} data.files list of files
* @param {Array} data.folders list of folders
*/
updateTags(context, { id, files, folders }) {
context.commit('updateTags', { id, files })
// then add each folders path indexes
folders.forEach(folder => context.commit('addPath', { path: folder.filename, id: folder.id }))
},
/**
* Index folders paths and ids
*
* @param {Object} context vuex context
* @param {Object} data destructuring object
* @param {string} data.path path of this folder
* @param {number} data.id id of this folder
*/
addPath(context, { path, id }) {
context.commit('addPath', { path, id })
},
}
export default { state, mutations, getters, actions }

37
src/utils/ParseFile.js Normal file
View File

@ -0,0 +1,37 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
/**
* Format a file into a usable fileinfo object
*
* @param {Object} fileData file data returned by the webdav lib
* @param {String} prefixPath path to substract from the files
* @returns {Object}
*/
export default function(fileData, prefixPath = '') {
const filename = fileData.filename.replace(prefixPath, '/').replace(/^\/\//, '/')
return Object.assign({
id: parseInt(fileData.props.fileid),
isFavorite: fileData.props.favorite !== '0',
hasPreview: fileData.props['has-preview'] !== 'false',
}, fileData, { filename })
}

View File

@ -33,16 +33,11 @@
</EmptyContent>
<!-- Folder content -->
<transition-group v-else
class="photos-grid"
role="grid"
name="list"
tag="div">
<Grid v-else>
<Navigation v-if="folder" key="navigation" v-bind="folder" />
<Folder v-for="dir in folderList" :key="dir.id" :folder="dir" />
<File v-for="file in fileList" :key="file.id" v-bind="file" />
<div key="footer" role="none" class="photos-grid__footer-spacer" />
</transition-group>
</Grid>
</template>
<script>
@ -55,16 +50,18 @@ import getPictures from '../services/FileList'
import EmptyContent from './EmptyContent'
import Folder from '../components/Folder'
import File from '../components/File'
import Grid from '../components/Grid'
import Navigation from '../components/Navigation'
import cancelableRequest from '../utils/CancelableRequest'
export default {
name: 'Grid',
name: 'Albums',
components: {
EmptyContent,
File,
Folder,
Grid,
Navigation,
},
props: {
@ -205,50 +202,3 @@ export default {
}
</script>
<style lang="scss">
.photos-grid {
display: grid;
align-items: center;
justify-content: center;
gap: 8px;
grid-template-columns: repeat(10, 1fr);
position: relative;
// always put one more row of grid for the spacer
&__footer-spacer {
// always add one row, so placing it on the first
// column will always add one more
grid-column: 1;
// same height as the width
padding-bottom: 100%;
}
}
.list-move {
transition: transform var(--animation-quick);
}
// TODO: use mixins/GridSizes as soon as node-sass supports it
// needs node-sass 5.0 (with libsass 3.6)
// https://github.com/sass/node-sass/pull/2312
$previous: 0;
@each $size, $config in get('sizes') {
$count: map-get($config, 'count');
$marginTop: map-get($config, 'marginTop');
$marginW: map-get($config, 'marginW');
// if this is the last entry, only use min-width
$rule: '(min-width: #{$previous}px) and (max-width: #{$size}px)';
@if $size == 'max' {
$rule: '(min-width: #{$previous}px)';
}
@media #{$rule} {
.photos-grid {
padding: #{$marginTop}px #{$marginW}px #{$marginW}px #{$marginW}px;
grid-template-columns: repeat($count, 1fr);
}
}
$previous: $size;
}
</style>

111
src/views/Tags.vue Normal file
View File

@ -0,0 +1,111 @@
<!--
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- 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/>.
-
-->
<template>
<!-- Errors handlers-->
<!-- <EmptyContent v-if="error === 404" illustration-name="folder">
{{ t('photos', 'This folder does not exists') }}
</EmptyContent>
<EmptyContent v-else-if="error">
{{ t('photos', 'An error occurred') }}
</EmptyContent>
<EmptyContent v-else-if="!loading && isEmpty" illustration-name="empty">
{{ t('photos', 'This folder does not contain pictures') }}
</EmptyContent> -->
<!-- Folder content -->
<!-- <Grid v-else>
<Navigation v-if="folder" key="navigation" v-bind="folder" />
<Folder v-for="dir in folderList" :key="dir.id" :folder="dir" />
<File v-for="file in fileList" :key="file.id" v-bind="file" />
</Grid> -->
<span>Test</span>
</template>
<script>
import { mapGetters } from 'vuex'
import getSystemTags from '../services/SystemTags'
import EmptyContent from './EmptyContent'
import Folder from '../components/Folder'
import File from '../components/File'
import Grid from '../components/Grid'
import Navigation from '../components/Navigation'
import cancelableRequest from '../utils/CancelableRequest'
export default {
name: 'Tags',
components: {
EmptyContent,
File,
Folder,
Grid,
Navigation,
},
props: {
path: {
type: String,
default: '/',
},
loading: {
type: Boolean,
required: true,
},
},
data() {
return {
error: null,
cancelRequest: () => {},
}
},
computed: {
// global lists
...mapGetters([
'files',
'tags',
]),
},
watch: {
path(path) {
console.debug('changed:', path)
this.fetchFolderContent()
},
},
async beforeMount() {
console.debug('beforemount: GRID')
this.fetchFolderContent()
},
methods: {
async fetchFolderContent() {
await getSystemTags()
},
},
}
</script>

View File

@ -1,4 +1,25 @@
<?php
script('photos', 'photos');
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
?>
<div id="content"></div>

View File

@ -14,13 +14,13 @@ module.exports = {
path: path.resolve(__dirname, './js'),
publicPath: '/js/',
filename: `${appName}.js`,
chunkFilename: 'chunks/[name]-[hash].js'
chunkFilename: 'chunks/[name]-[hash].js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader', 'postcss-loader']
use: ['vue-style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.scss$/,
@ -32,34 +32,34 @@ module.exports = {
loader: 'sass-loader',
options: {
functions: {
'get($keys)': SassGetGridConfig
}
}
}
]
'get($keys)': SassGetGridConfig,
},
},
},
],
},
{
test: /\.(js|vue)$/,
use: 'eslint-loader',
exclude: /node_modules/,
enforce: 'pre'
enforce: 'pre',
},
{
test: /\.vue$/,
loader: 'vue-loader',
exclude: /node_modules/
exclude: /node_modules/,
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules(?!(\/|\\)(hot-patcher|webdav)(\/|\\))/
exclude: /node_modules(?!(\/|\\)(hot-patcher|webdav)(\/|\\))/,
},
{
test: /\.svg$/,
// illustrations
loader: 'svg-inline-loader'
}
]
loader: 'svg-inline-loader',
},
],
},
plugins: [
new VueLoaderPlugin(),
@ -68,12 +68,12 @@ module.exports = {
new ModuleReplaceWebpackPlugin({
modules: [{
test: /request.js/,
replace: './src/patchedRequest.js'
}]
})
replace: './src/patchedRequest.js',
}],
}),
],
resolve: {
extensions: ['*', '.js', '.vue'],
symlinks: false
}
symlinks: false,
},
}