Add albums

pull/37/head
Varun Patil 2022-08-17 20:39:48 +00:00
parent 0ce5224148
commit 4936a2fdf8
6 changed files with 213 additions and 36 deletions

View File

@ -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'],

View File

@ -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);
} }

View File

@ -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',

View File

@ -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;

View File

@ -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'),
}),
},
], ],
}) })