Add albums
parent
0ce5224148
commit
4936a2fdf8
|
@ -2,6 +2,14 @@
|
||||||
return [
|
return [
|
||||||
'routes' => [
|
'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
|
// API
|
||||||
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||||
|
|
|
@ -31,6 +31,7 @@ use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\AppFramework\Http\StreamResponse;
|
use OCP\AppFramework\Http\StreamResponse;
|
||||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||||
|
use OCP\Files\IRootFolder;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
|
@ -41,12 +42,14 @@ class ApiController extends Controller {
|
||||||
private IUserSession $userSession;
|
private IUserSession $userSession;
|
||||||
private IDBConnection $connection;
|
private IDBConnection $connection;
|
||||||
private \OCA\Polaroid\Db\Util $util;
|
private \OCA\Polaroid\Db\Util $util;
|
||||||
|
private IRootFolder $rootFolder;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
IConfig $config,
|
IConfig $config,
|
||||||
IUserSession $userSession,
|
IUserSession $userSession,
|
||||||
IDBConnection $connection
|
IDBConnection $connection,
|
||||||
|
IRootFolder $rootFolder,
|
||||||
) {
|
) {
|
||||||
parent::__construct(Application::APPNAME, $request);
|
parent::__construct(Application::APPNAME, $request);
|
||||||
|
|
||||||
|
@ -54,6 +57,7 @@ class ApiController extends Controller {
|
||||||
$this->userSession = $userSession;
|
$this->userSession = $userSession;
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
$this->util = new \OCA\Polaroid\Db\Util($this->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);
|
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
|
* @NoAdminRequired
|
||||||
* @NoCSRFRequired
|
* @NoCSRFRequired
|
||||||
|
@ -100,7 +129,33 @@ class ApiController extends Controller {
|
||||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
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);
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +171,12 @@ class ApiController extends Controller {
|
||||||
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
|
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);
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,14 @@
|
||||||
<AppNavigation>
|
<AppNavigation>
|
||||||
<template id="app-polaroid-navigation" #list>
|
<template id="app-polaroid-navigation" #list>
|
||||||
<AppNavigationItem :to="{name: 'timeline'}"
|
<AppNavigationItem :to="{name: 'timeline'}"
|
||||||
class="app-navigation__photos"
|
|
||||||
:title="t('timeline', 'Timeline')"
|
:title="t('timeline', 'Timeline')"
|
||||||
icon="icon-yourphotos"
|
icon="icon-yourphotos"
|
||||||
exact>
|
exact>
|
||||||
</AppNavigationItem>
|
</AppNavigationItem>
|
||||||
|
<AppNavigationItem :to="{name: 'albums'}"
|
||||||
|
:title="t('albums', 'Albums')"
|
||||||
|
icon="icon-files-dark">
|
||||||
|
</AppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
</AppNavigation>
|
</AppNavigation>
|
||||||
|
|
||||||
|
@ -32,7 +35,6 @@ import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
||||||
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
|
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
|
||||||
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
||||||
import Timeline from './components/Timeline.vue'
|
import Timeline from './components/Timeline.vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
|
|
@ -19,10 +19,22 @@
|
||||||
v-bind:style="{ height: rowHeight + 'px' }">
|
v-bind:style="{ height: rowHeight + 'px' }">
|
||||||
|
|
||||||
<div class="photo" v-for="img of item.photos">
|
<div class="photo" v-for="img of item.photos">
|
||||||
|
<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>
|
<div v-if="img.is_video" class="icon-video-white"></div>
|
||||||
<img
|
<img
|
||||||
@click="openFile(img, item)"
|
@click="openFile(img, item)"
|
||||||
:src="img.src" :key="img.file_id"
|
: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()"
|
@load = "img.l = Math.random()"
|
||||||
@error="(e)=>e.target.src='img/error.svg'"
|
@error="(e)=>e.target.src='img/error.svg'"
|
||||||
v-bind:style="{
|
v-bind:style="{
|
||||||
|
@ -31,6 +43,7 @@
|
||||||
}"/>
|
}"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</RecycleScroller>
|
</RecycleScroller>
|
||||||
|
|
||||||
<div ref="timelineScroll" class="timeline-scroll"
|
<div ref="timelineScroll" class="timeline-scroll"
|
||||||
|
@ -88,6 +101,9 @@ export default {
|
||||||
currentStart: 0,
|
currentStart: 0,
|
||||||
/** Current end index */
|
/** Current end index */
|
||||||
currentEnd: 0,
|
currentEnd: 0,
|
||||||
|
|
||||||
|
/** State for request cancellations */
|
||||||
|
state: Math.random(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -101,7 +117,32 @@ export default {
|
||||||
}, false);
|
}, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
$route(from, to) {
|
||||||
|
console.log('route changed', from, to)
|
||||||
|
this.resetState();
|
||||||
|
this.fetchDays();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.resetState();
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
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 */
|
/** Handle window resize and initialization */
|
||||||
handleResize() {
|
handleResize() {
|
||||||
let height = this.$refs.container.clientHeight;
|
let height = this.$refs.container.clientHeight;
|
||||||
|
@ -163,8 +204,18 @@ export default {
|
||||||
|
|
||||||
/** Fetch timeline main call */
|
/** Fetch timeline main call */
|
||||||
async fetchDays() {
|
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();
|
const data = await res.json();
|
||||||
|
if (this.state !== startState) return;
|
||||||
|
|
||||||
this.days = data;
|
this.days = data;
|
||||||
|
|
||||||
// Ticks
|
// Ticks
|
||||||
|
@ -191,14 +242,19 @@ export default {
|
||||||
// Create tick if month changed
|
// Create tick if month changed
|
||||||
const dtYear = dateTaken.getUTCFullYear();
|
const dtYear = dateTaken.getUTCFullYear();
|
||||||
const dtMonth = dateTaken.getUTCMonth()
|
const dtMonth = dateTaken.getUTCMonth()
|
||||||
if (dtMonth !== prevMonth || dtYear !== prevYear) {
|
if (Number.isInteger(day.day_id) && (dtMonth !== prevMonth || dtYear !== prevYear)) {
|
||||||
this.timelineTicks.push({
|
this.timelineTicks.push({
|
||||||
dayId: day.id,
|
dayId: day.id,
|
||||||
top: currTop,
|
top: currTop,
|
||||||
text: dtYear === prevYear ? undefined : dtYear,
|
text: dtYear === prevYear ? undefined : dtYear,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
prevMonth = dtMonth;
|
prevMonth = dtMonth;
|
||||||
prevYear = dtYear;
|
prevYear = dtYear;
|
||||||
|
|
||||||
|
// Special headers
|
||||||
|
if (day.day_id === -0.1) {
|
||||||
|
dateStr = "Folders";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add header to list
|
// 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
|
// Fix view height variable
|
||||||
this.handleViewSizeChange();
|
this.handleViewSizeChange();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
@ -230,18 +293,36 @@ export default {
|
||||||
|
|
||||||
/** Fetch image data for one dayId */
|
/** Fetch image data for one dayId */
|
||||||
async fetchDay(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];
|
const head = this.heads[dayId];
|
||||||
head.loadedImages = true;
|
head.loadedImages = true;
|
||||||
|
|
||||||
let data = [];
|
let data = [];
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/apps/polaroid/api/days/${dayId}`);
|
const startState = this.state;
|
||||||
|
const res = await fetch(url);
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
|
if (this.state !== startState) return;
|
||||||
|
|
||||||
this.days.find(d => d.day_id === dayId).detail = data;
|
this.days.find(d => d.day_id === dayId).detail = data;
|
||||||
|
this.processDay(dayId, data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
head.loadedImages = false;
|
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)
|
// Get index of header O(n)
|
||||||
const headIdx = this.list.findIndex(item => item.id === head.id);
|
const headIdx = this.list.findIndex(item => item.id === head.id);
|
||||||
|
@ -260,11 +341,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the photo to the row
|
// Add the photo to the row
|
||||||
this.list[rowIdx].photos.push({
|
this.list[rowIdx].photos.push(p);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of any extra rows
|
// Get rid of any extra rows
|
||||||
|
@ -321,6 +398,11 @@ export default {
|
||||||
this.$refs.scroller.scrollToPosition(1000);
|
this.$refs.scroller.scrollToPosition(1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Open album folder */
|
||||||
|
openFolder(id) {
|
||||||
|
this.$router.push({ name: 'albums', params: { id } });
|
||||||
|
},
|
||||||
|
|
||||||
/** Open viewer */
|
/** Open viewer */
|
||||||
async openFile(img, row) {
|
async openFile(img, row) {
|
||||||
const day = this.days.find(d => d.day_id === row.dayId);
|
const day = this.days.find(d => d.day_id === row.dayId);
|
||||||
|
@ -415,6 +497,22 @@ export default {
|
||||||
top: 8px; right: 8px;
|
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 {
|
.head-row {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding-top: 13px;
|
padding-top: 13px;
|
||||||
|
|
|
@ -54,5 +54,14 @@
|
||||||
rootTitle: t('timeline', 'Timeline'),
|
rootTitle: t('timeline', 'Timeline'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/albums/:id*',
|
||||||
|
component: Timeline,
|
||||||
|
name: 'albums',
|
||||||
|
props: route => ({
|
||||||
|
rootTitle: t('albums', 'Albums'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
Loading…
Reference in New Issue