Display archived content in full-page overlay

next to sidebar; only display if available

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
This commit is contained in:
Marcel Klehr 2021-04-12 16:17:44 +02:00
parent 2e2533f2ab
commit a228f07b20
7 changed files with 222 additions and 132 deletions

View File

@ -43,6 +43,7 @@ use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use OCP\IURLGenerator;
@ -114,9 +115,13 @@ class BookmarkController extends ApiController {
* @var FolderService
*/
private $folders;
/**
* @var IRootFolder
*/
private $rootFolder;
public function __construct(
$appName, $request, IL10N $l10n, BookmarkMapper $bookmarkMapper, TagMapper $tagMapper, FolderMapper $folderMapper, TreeMapper $treeMapper, PublicFolderMapper $publicFolderMapper, ITimeFactory $timeFactory, LoggerInterface $logger, IURLGenerator $url, HtmlExporter $htmlExporter, Authorizer $authorizer, BookmarkService $bookmarks, FolderService $folders
$appName, $request, IL10N $l10n, BookmarkMapper $bookmarkMapper, TagMapper $tagMapper, FolderMapper $folderMapper, TreeMapper $treeMapper, PublicFolderMapper $publicFolderMapper, ITimeFactory $timeFactory, LoggerInterface $logger, IURLGenerator $url, HtmlExporter $htmlExporter, Authorizer $authorizer, BookmarkService $bookmarks, FolderService $folders, IRootFolder $rootFolder
) {
parent::__construct($appName, $request);
$this->request = $request;
@ -133,6 +138,7 @@ class BookmarkController extends ApiController {
$this->authorizer = $authorizer;
$this->bookmarks = $bookmarks;
$this->folders = $folders;
$this->rootFolder = $rootFolder;
}
/**
@ -156,6 +162,13 @@ class BookmarkController extends ApiController {
if (!isset($array['tags'])) {
$array['tags'] = $this->tagMapper->findByBookmark($bookmark->getId());
}
if ($array['archivedFile'] !== 0) {
$results = $this->rootFolder->getById($array['archivedFile']);
if (count($results)) {
$array['archivedFilePath'] = $results[0]->getPath();
$array['archivedFileType'] = $results[0]->getMimePart();
}
}
return $array;
}

View File

@ -103,6 +103,7 @@ class WebViewController extends Controller {
$policy->addAllowedWorkerSrcDomain("'self'");
$policy->addAllowedScriptDomain("'self'");
$policy->addAllowedConnectDomain("'self'");
$policy->addAllowedFrameDomain("'self'");
$res->setContentSecurityPolicy($policy);
// Provide complete folder hierarchy

View File

@ -0,0 +1,110 @@
<!--
- Copyright (c) 2021. The Nextcloud Bookmarks contributors.
-
- This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
-->
<template>
<div v-if="isActive && hasMinLength" class="bookmark-content">
<template v-if="archivedFile">
<div class="content iframe">
<iframe :src="archivedFileUrl" />
</div>
</template>
<div v-else-if="bookmark.textContent" class="content" v-html="content" />
<div v-else>
<EmptyContent icon="icon-download">
{{ t('bookmarks', 'Content pending') }}
<template #desc>
{{ t('bookmarks', ' This content is being downloaded for offline use. Please check back later.') }}
</template>
</EmptyContent>
</div>
</div>
</template>
<script>
import sanitizeHtml from 'sanitize-html'
import { generateUrl, generateRemoteUrl } from '@nextcloud/router'
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
const MIN_TEXT_LENGTH = 350
export default {
name: 'BookmarkContent',
components: { EmptyContent },
computed: {
isActive() {
if (!this.$store.state.sidebar) return false
return this.$store.state.sidebar.type === 'bookmark'
},
bookmark() {
if (!this.isActive) return
return this.$store.getters.getBookmark(this.$store.state.sidebar.id)
},
hasMinLength() {
return !this.bookmark.textContent || this.bookmark.textContent.length >= MIN_TEXT_LENGTH
},
content() {
return sanitizeHtml(this.bookmark.htmlContent, {
allowProtocolRelative: false,
})
},
archivedFileUrl() {
// remove `/username/files/`
const barePath = this.bookmark.archivedFilePath.split('/').slice(3).join('/')
return generateRemoteUrl(`webdav/${barePath}`)
},
archivedFile() {
if (this.bookmark.archivedFile) {
return generateUrl(`/apps/files/?fileid=${this.bookmark.archivedFile}`)
}
return null
},
},
}
</script>
<style>
.bookmark-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--color-main-background);
z-index: 110;
display: flex;
padding-top: 50px;
}
.bookmark-content .content {
padding: 30px 30px;
font-size: 15px;
text-align: justify;
position: relative;
flex-grow: 1;
overflow: scroll;
}
.bookmark-content .content.iframe {
margin: 0 -30px;
margin-bottom: -30px;
padding: 0;
position: relative;
}
.bookmark-content .content iframe {
height: 100%;
width: 100%;
}
.bookmark-content h1, .bookmark-content h2, .bookmark-content h3, .bookmark-content h4, .bookmark-content h5, .bookmark-content p {
margin-top: 10px !important;
}
.bookmark-content a:link,
.bookmark-content a[href] {
text-decoration: underline !important;
}
</style>

View File

@ -1,66 +0,0 @@
<!--
- Copyright (c) 2020. The Nextcloud Bookmarks contributors.
-
- This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
-->
<template>
<Modal :title="title" @close="$emit('close', $event)">
<div class="content-modal">
<h1 class="title">
{{ bookmark.title }}
</h1>
<div v-html="content" />
</div>
</Modal>
</template>
<script>
import Modal from '@nextcloud/vue/dist/Components/Modal'
import sanitizeHtml from 'sanitize-html'
export default {
name: 'ContentModal',
components: {
Modal,
},
props: {
bookmark: {
type: Object,
required: true,
},
},
computed: {
title() {
return this.bookmark.title
},
content() {
return sanitizeHtml(this.bookmark.htmlContent, {
allowProtocolRelative: false,
})
},
},
}
</script>
<style>
.content-modal {
overflow-y: scroll;
padding: 44px;
text-wrap: normal;
height: 700px;
max-width: 700px;
}
.content-modal .title {
font-size: 2em;
margin: 22px 0;
}
.content-modal h1, .content-modal h2, .content-modal h3, .content-modal h4, .content-modal h5, .content-modal p {
margin-top: 10px !important;
}
.content-modal a:link,
.content-modal a[href] {
text-decoration: underline !important;
}
</style>

View File

@ -30,56 +30,57 @@
icon="icon-info"
:order="0">
<div>
<div v-if="!editingUrl" class="bookmark-details__line">
<span class="bookmark-details__url">{{ bookmark.url }}</span>
<Actions v-if="isEditable" class="bookmark-details__action">
<div v-if="!editingUrl" class="details__line">
<span class="icon-external" :aria-label="t('bookmarks', 'Link')" :title="t('bookmarks', 'Link')" />
<span class="details__url">{{ bookmark.url }}</span>
<Actions v-if="isEditable" class="details__action">
<ActionButton icon="icon-rename" @click="onEditUrl" />
</Actions>
</div>
<div v-else class="bookmark-details__line">
<input v-model="url" class="bookmark-details__url">
<Actions class="bookmark-details__action">
<div v-else class="details__line">
<span class="icon-external" :aria-label="t('bookmarks', 'Link')" :title="t('bookmarks', 'Link')" />
<input v-model="url" class="details__url">
<Actions class="details__action">
<ActionButton icon="icon-confirm" @click="onEditUrlSubmit" />
</Actions>
<Actions class="bookmark-details__action">
<Actions class="details__action">
<ActionButton icon="icon-close" @click="onEditUrlCancel" />
</Actions>
</div>
<div class="details__line">
<span class="icon-tag" :aria-label="t('bookmarks', 'Tags')" :title="t('bookmarks', 'Tags')" />
<Multiselect
class="tags"
:value="tags"
:auto-limit="false"
:limit="7"
:options="allTags"
:multiple="true"
:taggable="true"
:placeholder="t('bookmarks', 'Select tags and create new ones')"
:disabled="!isEditable"
@input="onTagsChange"
@tag="onAddTag" />
</div>
<div class="details__line">
<span class="icon-edit"
role="figure"
:aria-label="t('bookmarks', 'Notes')"
:title="t('bookmarks', 'Notes')" />
<RichContenteditable
:value.sync="bookmark.description"
:contenteditable="isEditable"
:auto-complete="() => {}"
:placeholder="t('bookmarks', 'Notes for this bookmark …')"
:multiline="true"
class="notes"
@update:value="onNotesChange" />
</div>
</div>
<div v-if="archivedFile">
<h3><ArchiveArrowDownIcon slot="icon" :size="18" /> {{ t('bookmarks', 'Archived file') }}</h3>
<a :href="archivedFile" class="button">{{ t('bookmarks', 'Open archived file') }}</a>
</div>
<div v-else-if="bookmark.textContent">
<h3><ArchiveArrowDownIcon slot="icon" :size="18" /> {{ t('bookmarks', 'Archived content') }}</h3>
<blockquote v-text="bookmark.textContent.substr(0, 250)+'...'" />
<a href="javascript:void(0)" class="button" @click="showContentModal = true">{{ t('bookmarks', 'Read more') }}</a>
<ContentModal v-if="showContentModal" :bookmark="bookmark" @close="showContentModal = false" />
</div>
<div>
<h3><span class="icon-tag" /> {{ t('bookmarks', 'Tags') }}</h3>
<Multiselect
class="sidebar__tags"
:value="tags"
:auto-limit="false"
:limit="7"
:options="allTags"
:multiple="true"
:taggable="true"
:placeholder="t('bookmarks', 'Select tags and create new ones')"
:disabled="!isEditable"
@input="onTagsChange"
@tag="onAddTag" />
</div>
<div>
<h3><span class="icon-edit" /> {{ t('bookmarks', 'Notes') }}</h3>
<RichContenteditable
:value.sync="bookmark.description"
:contenteditable="isEditable"
:auto-complete="() => {}"
:placeholder="t('bookmarks', 'Notes for this bookmark …')"
:multiline="true"
@update:value="onNotesChange" />
<h3><FileDocumentIcon slot="icon" :size="18" /> {{ t('bookmarks', 'Archived file') }}</h3>
<a class="button" :href="archivedFileUrl" target="_blank"><FileDocumentIcon :size="18" :fill-color="colorMainText" /> {{ t('bookmarks', 'Open File') }}</a>
<a class="button" :href="archivedFile" target="_blank"><span class="icon-files-dark" /> {{ t('bookmarks', 'Open File location') }}</a>
</div>
</AppSidebarTab>
</AppSidebar>
@ -91,19 +92,18 @@ import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
import ArchiveArrowDownIcon from 'vue-material-design-icons/ArchiveArrowDown'
import FileDocumentIcon from 'vue-material-design-icons/FileDocument'
import { getCurrentUser } from '@nextcloud/auth'
import { generateUrl } from '@nextcloud/router'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
import humanizeDuration from 'humanize-duration'
import { actions, mutations } from '../store/'
import ContentModal from './ContentModal'
const MAX_RELATIVE_DATE = 1000 * 60 * 60 * 24 * 7 // one week
export default {
name: 'SidebarBookmark',
components: { ContentModal, AppSidebar, AppSidebarTab, Multiselect, Actions, ActionButton, RichContenteditable, ArchiveArrowDownIcon },
components: { AppSidebar, AppSidebarTab, Multiselect, Actions, ActionButton, RichContenteditable, FileDocumentIcon },
data() {
return {
title: '',
@ -163,6 +163,11 @@ export default {
}
return null
},
archivedFileUrl() {
// remove `/username/files/`
const barePath = this.bookmark.archivedFilePath.split('/').slice(3).join('/')
return generateRemoteUrl(`webdav/${barePath}`)
},
},
created() {
},
@ -229,36 +234,56 @@ export default {
opacity: 0.5;
}
.sidebar .details__line > span[class^='icon-'],
.sidebar .details__line > .material-design-icon {
display: inline-block;
position: relative;
top: 11px;
opacity: 0.5;
margin-right: 10px;
}
.sidebar h3 {
margin-top: 20px;
}
.sidebar__tags {
.sidebar .tags {
width: 100%;
}
.sidebar__notes {
min-height: 200px !important;
width: auto !important;
}
.bookmark-details__line {
display: flex;
}
.bookmark-details__url {
.sidebar .notes {
flex-grow: 1;
padding: 8px 0;
min-height: 80px;
}
.bookmark-details__action {
.sidebar .details__line {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}
.sidebar .details__line > * {
flex-grow: 0;
}
.sidebar blockquote {
border-left: var(--color-placeholder-dark) 3px solid;
padding-left: 10px;
color: var(--color-text-lighter);
margin: 10px 0;
.sidebar .details__line > :nth-child(2) {
flex-grow: 1;
}
.sidebar .details__line .notes {
flex-grow: 1;
}
.sidebar .details__url {
flex-grow: 1;
padding: 8px 0;
text-overflow: ellipsis;
height: 2em;
display: inline-block;
overflow: hidden;
}
.sidebar .details__action {
flex-grow: 0;
}
</style>

View File

@ -10,6 +10,7 @@
<AppContent>
<Controls />
<BookmarksList :bookmarks="bookmarks" />
<BookmarkContent />
</AppContent>
<SidebarBookmark />
<SidebarFolder />
@ -30,11 +31,13 @@ import MoveDialog from './MoveDialog'
import { privateRoutes } from '../router'
import { actions, mutations } from '../store/'
import LoadingModal from './LoadingModal'
import BookmarkContent from './BookmarkContent'
import { getCurrentUser } from '@nextcloud/auth'
export default {
name: 'ViewPrivate',
components: {
BookmarkContent,
LoadingModal,
Navigation,
Content,

View File

@ -29,6 +29,7 @@ use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
@ -188,10 +189,13 @@ class BookmarkControllerTest extends TestCase {
$this->authorizer = OC::$server->get(Authorizer::class);
$this->folders = OC::$server->get(FolderService::class);
$this->controller = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders);
$this->otherController = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders);
/** @var IRootFolder $rootFolder */
$rootFolder = OC::$server->get(IRootFolder::class);
$this->publicController = new BookmarkController('bookmarks', $this->publicRequest, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders);
$this->controller = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder);
$this->otherController = new BookmarkController('bookmarks', $this->request, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder);
$this->publicController = new BookmarkController('bookmarks', $this->publicRequest, $l, $this->bookmarkMapper, $this->tagMapper, $this->folderMapper, $this->treeMapper, $this->publicFolderMapper, $timeFactory, $logger, $urlGenerator, $htmlExporter, $this->authorizer, $this->bookmarks, $this->folders, $rootFolder);
}
/**