Add albums
parent
0ce5224148
commit
4936a2fdf8
|
@ -1,12 +1,20 @@
|
|||
<?php
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#index', 'url' => '/albums/{path}', 'verb' => 'GET', 'postfix' => 'albums',
|
||||
'requirements' => [
|
||||
'path' => '.*',
|
||||
],
|
||||
'defaults' => [
|
||||
'path' => '',
|
||||
]
|
||||
],
|
||||
|
||||
// API
|
||||
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
||||
['name' => 'api#folder', 'url' => '/api/folder/{folder}', 'verb' => 'GET'],
|
||||
['name' => 'api#folderDay', 'url' => '/api/folder/{folder}/{dayId}', 'verb' => 'GET'],
|
||||
// API
|
||||
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
||||
['name' => 'api#folder', 'url' => '/api/folder/{folder}', 'verb' => 'GET'],
|
||||
['name' => 'api#folderDay', 'url' => '/api/folder/{folder}/{dayId}', 'verb' => 'GET'],
|
||||
]
|
||||
];
|
||||
|
|
|
@ -31,6 +31,7 @@ use OCP\AppFramework\Http;
|
|||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\StreamResponse;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IRequest;
|
||||
|
@ -41,12 +42,14 @@ class ApiController extends Controller {
|
|||
private IUserSession $userSession;
|
||||
private IDBConnection $connection;
|
||||
private \OCA\Polaroid\Db\Util $util;
|
||||
private IRootFolder $rootFolder;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
IConfig $config,
|
||||
IUserSession $userSession,
|
||||
IDBConnection $connection
|
||||
IDBConnection $connection,
|
||||
IRootFolder $rootFolder,
|
||||
) {
|
||||
parent::__construct(Application::APPNAME, $request);
|
||||
|
||||
|
@ -54,6 +57,7 @@ class ApiController extends Controller {
|
|||
$this->userSession = $userSession;
|
||||
$this->connection = $connection;
|
||||
$this->util = new \OCA\Polaroid\Db\Util($this->connection);
|
||||
$this->rootFolder = $rootFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +92,31 @@ class ApiController extends Controller {
|
|||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if folder is allowed and get it if yes
|
||||
*/
|
||||
private function getAllowedFolder(int $folder, $user) {
|
||||
// Get root if folder not specified
|
||||
$root = $this->rootFolder->getUserFolder($user->getUID());
|
||||
if ($folder === 0) {
|
||||
$folder = $root->getId();
|
||||
}
|
||||
|
||||
// Check access to folder
|
||||
$nodes = $root->getById($folder);
|
||||
if (empty($nodes)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check it is a folder
|
||||
$node = $nodes[0];
|
||||
if (!$node instanceof \OCP\Files\Folder) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
|
@ -100,7 +129,33 @@ class ApiController extends Controller {
|
|||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
$list = $this->util->getDaysFolder(intval($folder));
|
||||
// Check permissions
|
||||
$node = $this->getAllowedFolder(intval($folder), $user);
|
||||
if (is_null($node)) {
|
||||
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
// Get response from db
|
||||
$list = $this->util->getDaysFolder($node->getId());
|
||||
|
||||
// Get subdirectories
|
||||
$sub = array_filter($node->getDirectoryListing(), function ($item) use ($node) {
|
||||
return $item instanceof \OCP\Files\Folder;
|
||||
});
|
||||
// map sub to array of id
|
||||
$subdir = [
|
||||
"day_id" => -0.1,
|
||||
"detail" => array_map(function ($item) {
|
||||
return [
|
||||
"file_id" => $item->getId(),
|
||||
"name" => $item->getName(),
|
||||
"is_folder" => 1,
|
||||
];
|
||||
}, $sub, []),
|
||||
];
|
||||
$subdir["count"] = count($subdir["detail"]);
|
||||
array_unshift($list, $subdir);
|
||||
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
}
|
||||
|
||||
|
@ -116,7 +171,12 @@ class ApiController extends Controller {
|
|||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
$list = $this->util->getDayFolder(intval($folder), intval($dayId));
|
||||
$node = $this->getAllowedFolder(intval($folder), $user);
|
||||
if ($node === NULL) {
|
||||
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$list = $this->util->getDayFolder($node->getId(), intval($dayId));
|
||||
return new JSONResponse($list, Http::STATUS_OK);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
<AppNavigation>
|
||||
<template id="app-polaroid-navigation" #list>
|
||||
<AppNavigationItem :to="{name: 'timeline'}"
|
||||
class="app-navigation__photos"
|
||||
:title="t('timeline', 'Timeline')"
|
||||
icon="icon-yourphotos"
|
||||
exact>
|
||||
</AppNavigationItem>
|
||||
<AppNavigationItem :to="{name: 'albums'}"
|
||||
:title="t('albums', 'Albums')"
|
||||
icon="icon-files-dark">
|
||||
</AppNavigationItem>
|
||||
</template>
|
||||
</AppNavigation>
|
||||
|
||||
|
@ -32,7 +35,6 @@ 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 Timeline from './components/Timeline.vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
|
|
@ -19,16 +19,29 @@
|
|||
v-bind:style="{ height: rowHeight + 'px' }">
|
||||
|
||||
<div class="photo" v-for="img of item.photos">
|
||||
<div v-if="img.is_video" class="icon-video-white"></div>
|
||||
<img
|
||||
@click="openFile(img, item)"
|
||||
:src="img.src" :key="img.file_id"
|
||||
@load = "img.l = Math.random()"
|
||||
@error="(e)=>e.target.src='img/error.svg'"
|
||||
<div v-if="img.is_folder" class="folder"
|
||||
@click="openFolder(img.file_id)"
|
||||
v-bind:style="{
|
||||
width: rowHeight + 'px',
|
||||
height: rowHeight + 'px',
|
||||
}"/>
|
||||
}">
|
||||
<div class="icon-folder icon-dark"></div>
|
||||
<div class="name">{{ img.name }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="img.is_video" class="icon-video-white"></div>
|
||||
<img
|
||||
@click="openFile(img, item)"
|
||||
:src="`/core/preview?fileId=${img.file_id}&c=${img.etag}&x=250&y=250&forceIcon=0&a=0`"
|
||||
:key="img.file_id"
|
||||
@load = "img.l = Math.random()"
|
||||
@error="(e)=>e.target.src='img/error.svg'"
|
||||
v-bind:style="{
|
||||
width: rowHeight + 'px',
|
||||
height: rowHeight + 'px',
|
||||
}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RecycleScroller>
|
||||
|
@ -88,6 +101,9 @@ export default {
|
|||
currentStart: 0,
|
||||
/** Current end index */
|
||||
currentEnd: 0,
|
||||
|
||||
/** State for request cancellations */
|
||||
state: Math.random(),
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -101,7 +117,32 @@ export default {
|
|||
}, false);
|
||||
},
|
||||
|
||||
watch: {
|
||||
$route(from, to) {
|
||||
console.log('route changed', from, to)
|
||||
this.resetState();
|
||||
this.fetchDays();
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.resetState();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** Reset all state */
|
||||
resetState() {
|
||||
this.loading = true;
|
||||
this.list = [];
|
||||
this.numRows = 0;
|
||||
this.heads = {};
|
||||
this.days = [];
|
||||
this.currentStart = 0;
|
||||
this.currentEnd = 0;
|
||||
this.timelineTicks = [];
|
||||
this.state = Math.random();
|
||||
},
|
||||
|
||||
/** Handle window resize and initialization */
|
||||
handleResize() {
|
||||
let height = this.$refs.container.clientHeight;
|
||||
|
@ -163,8 +204,18 @@ export default {
|
|||
|
||||
/** Fetch timeline main call */
|
||||
async fetchDays() {
|
||||
const res = await fetch('/apps/polaroid/api/days');
|
||||
let url = '/apps/polaroid/api/days';
|
||||
|
||||
if (this.$route.name === 'albums') {
|
||||
const id = this.$route.params.id || 0;
|
||||
url = `/apps/polaroid/api/folder/${id}`;
|
||||
}
|
||||
|
||||
const startState = this.state;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (this.state !== startState) return;
|
||||
|
||||
this.days = data;
|
||||
|
||||
// Ticks
|
||||
|
@ -191,14 +242,19 @@ export default {
|
|||
// Create tick if month changed
|
||||
const dtYear = dateTaken.getUTCFullYear();
|
||||
const dtMonth = dateTaken.getUTCMonth()
|
||||
if (dtMonth !== prevMonth || dtYear !== prevYear) {
|
||||
if (Number.isInteger(day.day_id) && (dtMonth !== prevMonth || dtYear !== prevYear)) {
|
||||
this.timelineTicks.push({
|
||||
dayId: day.id,
|
||||
top: currTop,
|
||||
text: dtYear === prevYear ? undefined : dtYear,
|
||||
});
|
||||
prevMonth = dtMonth;
|
||||
prevYear = dtYear;
|
||||
}
|
||||
prevMonth = dtMonth;
|
||||
prevYear = dtYear;
|
||||
|
||||
// Special headers
|
||||
if (day.day_id === -0.1) {
|
||||
dateStr = "Folders";
|
||||
}
|
||||
|
||||
// Add header to list
|
||||
|
@ -223,6 +279,13 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
// Check preloads
|
||||
for (const day of data) {
|
||||
if (day.count && day.detail) {
|
||||
this.processDay(day.day_id, day.detail);
|
||||
}
|
||||
}
|
||||
|
||||
// Fix view height variable
|
||||
this.handleViewSizeChange();
|
||||
this.loading = false;
|
||||
|
@ -230,18 +293,36 @@ export default {
|
|||
|
||||
/** Fetch image data for one dayId */
|
||||
async fetchDay(dayId) {
|
||||
let url = `/apps/polaroid/api/days/${dayId}`;
|
||||
|
||||
if (this.$route.name === 'albums') {
|
||||
const id = this.$route.params.id || 0;
|
||||
url = `/apps/polaroid/api/folder/${id}/${dayId}`;
|
||||
}
|
||||
|
||||
// Do this in advance to prevent duplicate requests
|
||||
const head = this.heads[dayId];
|
||||
head.loadedImages = true;
|
||||
|
||||
let data = [];
|
||||
try {
|
||||
const res = await fetch(`/apps/polaroid/api/days/${dayId}`);
|
||||
const startState = this.state;
|
||||
const res = await fetch(url);
|
||||
data = await res.json();
|
||||
if (this.state !== startState) return;
|
||||
|
||||
this.days.find(d => d.day_id === dayId).detail = data;
|
||||
this.processDay(dayId, data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
head.loadedImages = false;
|
||||
}
|
||||
},
|
||||
|
||||
/** Process items from day response */
|
||||
processDay(dayId, data) {
|
||||
const head = this.heads[dayId];
|
||||
head.loadedImages = true;
|
||||
|
||||
// Get index of header O(n)
|
||||
const headIdx = this.list.findIndex(item => item.id === head.id);
|
||||
|
@ -260,11 +341,7 @@ export default {
|
|||
}
|
||||
|
||||
// Add the photo to the row
|
||||
this.list[rowIdx].photos.push({
|
||||
id: p.file_id,
|
||||
src: `/core/preview?fileId=${p.file_id}&c=${p.etag}&x=250&y=250&forceIcon=0&a=0`,
|
||||
is_video: p.is_video || undefined,
|
||||
});
|
||||
this.list[rowIdx].photos.push(p);
|
||||
}
|
||||
|
||||
// Get rid of any extra rows
|
||||
|
@ -321,6 +398,11 @@ export default {
|
|||
this.$refs.scroller.scrollToPosition(1000);
|
||||
},
|
||||
|
||||
/** Open album folder */
|
||||
openFolder(id) {
|
||||
this.$router.push({ name: 'albums', params: { id } });
|
||||
},
|
||||
|
||||
/** Open viewer */
|
||||
async openFile(img, row) {
|
||||
const day = this.days.find(d => d.day_id === row.dayId);
|
||||
|
@ -415,6 +497,22 @@ export default {
|
|||
top: 8px; right: 8px;
|
||||
}
|
||||
|
||||
.photo-row .photo .folder {
|
||||
cursor: pointer;
|
||||
}
|
||||
.photo-row .photo .folder .name {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.photo-row .photo .icon-folder {
|
||||
cursor: pointer;
|
||||
background-size: 40%;
|
||||
height: 60%; width: 100%;
|
||||
background-position: bottom;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.head-row {
|
||||
height: 40px;
|
||||
padding-top: 13px;
|
||||
|
|
|
@ -54,5 +54,14 @@
|
|||
rootTitle: t('timeline', 'Timeline'),
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
path: '/albums/:id*',
|
||||
component: Timeline,
|
||||
name: 'albums',
|
||||
props: route => ({
|
||||
rootTitle: t('albums', 'Albums'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
|
@ -3,14 +3,14 @@ import { genFileInfo } from './FileUtils'
|
|||
import client from './DavClient';
|
||||
|
||||
const props = `
|
||||
<oc:fileid />
|
||||
<d:getlastmodified />
|
||||
<d:getetag />
|
||||
<d:getcontenttype />
|
||||
<d:getcontentlength />
|
||||
<nc:has-preview />
|
||||
<oc:favorite />
|
||||
<d:resourcetype />`;
|
||||
<oc:fileid />
|
||||
<d:getlastmodified />
|
||||
<d:getetag />
|
||||
<d:getcontenttype />
|
||||
<d:getcontentlength />
|
||||
<nc:has-preview />
|
||||
<oc:favorite />
|
||||
<d:resourcetype />`;
|
||||
|
||||
export async function getFiles(fileIds) {
|
||||
const prefixPath = `/files/${getCurrentUser().uid}`;
|
||||
|
|
Loading…
Reference in New Issue