Merge branch 'Cl00e9ment-structured-shared-folder'

pull/888/head
Varun Patil 2023-10-19 22:48:13 -07:00
commit b641d08651
16 changed files with 93 additions and 41 deletions

View File

@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- **Breaking**: Nextcloud 26+ and PHP 8 are now required.
- **Feature**: Folders view in shares ([#880](https://github.com/pulsejet/memories/pull/880))
- Significant improvements to code quality and maintainability.
- Improvements to the [documentation](https://memories.gallery/install/).

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
function getWildcard($param)
{
return [
@ -34,7 +36,6 @@ return [
w(['name' => 'Page#tags', 'url' => '/tags/{name}', 'verb' => 'GET'], 'name'),
// Public folder share
['name' => 'Public#showShare', 'url' => '/s/{token}', 'verb' => 'GET'],
[
'name' => 'Public#showAuthenticate',
'url' => '/s/{token}/authenticate/{redirect}',
@ -45,6 +46,7 @@ return [
'url' => '/s/{token}/authenticate/{redirect}',
'verb' => 'POST',
],
w(['name' => 'Public#showShare', 'url' => '/s/{token}/{path}', 'verb' => 'GET'], 'path'),
// Public album share
['name' => 'PublicAlbum#showShare', 'url' => '/a/{token}', 'verb' => 'GET'],

View File

@ -15,6 +15,8 @@ class FoldersController extends GenericApiController
{
/**
* @NoAdminRequired
*
* @PublicPage
*/
public function sub(string $folder): Http\Response
{
@ -24,14 +26,22 @@ class FoldersController extends GenericApiController
throw Exceptions::BadRequest('Invalid parameter folder');
}
// Get the root folder (share root or user root)
$root = $this->fs->getShareNode() ?? Util::getUserFolder();
if (!$root instanceof Folder) {
throw Exceptions::BadRequest('Root is not a folder');
}
// Get the inner folder
try {
$node = Util::getUserFolder()->get($folder);
$node = $root->get($folder);
} catch (\OCP\Files\NotFoundException) {
throw Exceptions::NotFound("Folder not found: {$folder}");
}
// Make sure we have a folder
if (!$node instanceof Folder) {
throw Exceptions::NotFound('Path is not a folder');
throw Exceptions::BadRequest('Path is not a folder');
}
// Ugly: get the view of the folder with reflection
@ -65,7 +75,6 @@ class FoldersController extends GenericApiController
return [
'fileid' => $node->getId(),
'name' => $node->getName(),
'path' => $node->getPath(),
'previews' => $this->tq->getRootPreviews($root),
];
}, $folders);

View File

@ -81,6 +81,7 @@ class PublicAlbumController extends Controller
// Share info
$this->initialState->provideInitialState('share_title', $album['name']);
$this->initialState->provideInitialState('share_type', 'album');
// Render main template
$response = new PublicTemplateResponse(Application::APPNAME, 'main', PageController::getMainParams());

View File

@ -98,15 +98,18 @@ class PublicController extends AuthPublicShareController
// Scripts
\OCP\Util::addScript(Application::APPNAME, 'memories-main');
// Get share node
$node = $share->getNode();
// Share info
$this->initialState->provideInitialState('no_download', $share->getHideDownload());
$this->initialState->provideInitialState('share_title', $node->getName());
// Share file id only if not a folder
$node = $share->getNode();
if ($node instanceof \OCP\Files\File) {
$this->initialState->provideInitialState('single_item', $this->getSingleItemInitialState($node));
$this->initialState->provideInitialState('share_type', 'file');
} elseif ($node instanceof \OCP\Files\Folder) {
$this->initialState->provideInitialState('share_title', $node->getName());
$this->initialState->provideInitialState('share_type', 'folder');
} else {
throw new NotFoundException();
}

View File

@ -84,6 +84,22 @@ class FsManager
throw new \Exception('Share is not a folder');
}
// Folder inside shared folder
if ($path = $this->getRequestFolder()) {
$sanitized = Util::sanitizePath($path);
if (null === $sanitized) {
throw new \Exception("Invalid parameter path: {$path}");
}
// Get subnode from share
try {
$share = $share->get($sanitized);
} catch (\OCP\Files\NotFoundException $e) {
throw new \Exception("Folder not found: {$e->getMessage()}");
}
}
// This internally checks if the node is a folder
$root->addFolder($share);
return $root;
@ -322,6 +338,9 @@ class FsManager
return $share;
}
/**
* Get the share node from the request.
*/
public function getShareNode(): ?Node
{
$share = $this->getShareObject();

View File

@ -580,7 +580,7 @@ export default defineComponent({
}
// Folder
if (this.routeIsFolders) {
if (this.routeIsFolders || this.routeIsFolderShare) {
const path = utils.getFolderRoutePath(this.config.folders_path);
set(DaysFilterType.FOLDER, path);
if (this.$route.query.recursive) {

View File

@ -58,18 +58,17 @@ export default defineComponent({
computed: {
/** Open folder */
target() {
const path = this.data.path
.split('/')
.filter((x) => x)
.slice(2) as string[];
// Remove base path if present
const basePath = this.config.folders_path.split('/').filter((x) => x);
if (path.length >= basePath.length && path.slice(0, basePath.length).every((x, i) => x === basePath[i])) {
path.splice(0, basePath.length);
let path: string[] | string = this.$route.params.path || [];
if (typeof path === 'string') {
path = path.split('/');
}
return { name: 'folders', params: { path } };
return {
name: this.$route.name,
params: {
path: [...path, this.data.name],
},
};
},
},
@ -89,12 +88,6 @@ export default defineComponent({
// Reset state
this.error = false;
// Check if valid path present
if (!this.data.path) {
this.error = true;
return;
}
// Get preview infos
const previews = this.data.previews;
if (previews) {

View File

@ -15,9 +15,11 @@ import FolderDynamicTopMatter from './FolderDynamicTopMatter.vue';
import PlacesDynamicTopMatterVue from './PlacesDynamicTopMatter.vue';
import OnThisDay from './OnThisDay.vue';
import * as PublicShareHeader from './PublicShareHeader';
import * as strings from '../../services/strings';
// Auto-hide top header on public shares if redundant
import './PublicShareHeader';
export default defineComponent({
name: 'DynamicTopMatter',
@ -35,7 +37,7 @@ export default defineComponent({
},
currentmatter(): Component | null {
if (this.routeIsFolders) {
if (this.routeIsFolders || this.routeIsFolderShare) {
return FolderDynamicTopMatter;
} else if (this.routeIsPlaces) {
return PlacesDynamicTopMatterVue;
@ -50,17 +52,18 @@ export default defineComponent({
viewName(): string {
// Show album name for album view
if (this.routeIsAlbums) {
return this.$route.params.name || '';
return this.$route.params.name || String();
}
// Show share name for public shares
if (this.routeIsPublic) {
return PublicShareHeader.title;
// Show share name for public shares, except for folder share,
// because the name is already present in the breadcrumbs
if (this.routeIsPublic && !this.routeIsFolderShare) {
return this.initstate.shareTitle;
}
// Only static top matter for these routes
if (this.routeIsTags || this.routeIsPeople || this.routeIsPlaces) {
return '';
return String();
}
return strings.viewName(this.$route.name!);

View File

@ -1,5 +1,5 @@
<template>
<FolderGrid v-if="folders.length" :items="folders" />
<FolderGrid v-if="folders.length && !$route.query.recursive" :items="folders" />
</template>
<script lang="ts">

View File

@ -1,16 +1,22 @@
<template>
<div class="top-matter">
<NcBreadcrumbs>
<NcBreadcrumb title="Home" :to="{ name: 'folders' }">
<NcBreadcrumb :title="rootFolderName" :to="{ name: $route.name }">
<template #icon>
<HomeIcon :size="20" />
<template v-if="routeIsPublic">
<ShareIcon :size="20" />
<span class="share-name">{{ rootFolderName }}</span>
</template>
<template v-else>
<HomeIcon :size="20" />
</template>
</template>
</NcBreadcrumb>
<NcBreadcrumb
v-for="folder in list"
:key="folder.idx"
:title="folder.text"
:to="{ name: 'folders', params: { path: folder.path } }"
:to="{ name: $route.name, params: { path: folder.path } }"
/>
</NcBreadcrumbs>
@ -23,7 +29,12 @@
<TimelineIcon v-else :size="20" />
</template>
</NcActionButton>
<NcActionButton :aria-label="t('memories', 'Share folder')" @click="share()" close-after-click>
<NcActionButton
v-if="!routeIsPublic"
:aria-label="t('memories', 'Share folder')"
@click="share()"
close-after-click
>
{{ t('memories', 'Share folder') }}
<template #icon> <ShareIcon :size="20" /> </template>
</NcActionButton>
@ -86,6 +97,10 @@ export default defineComponent({
recursive(): boolean {
return !!this.$route.query.recursive;
},
rootFolderName(): string {
return this.routeIsPublic ? this.initstate.shareTitle : this.t('memories', 'Home');
},
},
methods: {
@ -110,6 +125,9 @@ export default defineComponent({
.breadcrumb {
min-width: 0;
height: unset;
.share-name {
margin-left: 0.75em;
}
}
}
</style>

View File

@ -1,10 +1,13 @@
import * as utils from '../../services/utils';
// Shown in dynamic top matter (Timeline::viewName)
export const title = utils.initstate.shareTitle;
const title = utils.initstate.shareTitle;
// Hide on album shares only
const hide = utils.initstate.shareType === 'album';
// Set up hook to monitor recycler scroll to show/hide header
if (title) {
if (title && hide) {
const header = document.querySelector('header#header .header-appname') as HTMLElement;
let isHidden = false; // cache state to avoid unnecessary DOM updates

View File

@ -45,6 +45,7 @@ export default defineComponent({
currentmatter() {
switch (this.$route.name) {
case _m.routes.Folders.name:
case _m.routes.FolderShare.name:
return FolderTopMatter;
case _m.routes.Albums.name:
return AlbumTopMatter;

View File

@ -110,7 +110,7 @@ export const routes: { [key in RouteId]: RouteConfig } = {
},
FolderShare: {
path: '/s/:token',
path: '/s/:token/:path*',
component: Timeline,
name: 'folder-share',
props: (route: Route) => ({ rootTitle: t('memories', 'Shared Folder') }),

View File

@ -22,6 +22,7 @@ export const constants = Object.freeze({
export const initstate = Object.freeze({
noDownload: loadState('memories', 'no_download', false) !== false,
shareTitle: loadState('memories', 'share_title', '') as string,
shareType: loadState('memories', 'share_type', null) as 'file' | 'folder' | 'album' | null,
singleItem: loadState('memories', 'single_item', null) as IPhoto | null,
});

View File

@ -156,8 +156,6 @@ export interface IExif {
}
export interface IFolder extends IPhoto {
/** Path to folder */
path: string;
/** Photos for preview images */
previews?: IPhoto[];
/** Name of folder */