mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-30 01:26:43 +08:00
feature (tag): support for tagging
This commit is contained in:
@ -32,7 +32,7 @@ export default function(ctrl) {
|
||||
const $main = qs($page, `[data-bind="filemanager-children"]`);
|
||||
$main.classList.remove("hidden");
|
||||
ctrl(createRender($main));
|
||||
ctrlSidebar(createRender(qs($page, `[data-bind="sidebar"]`)));
|
||||
ctrlSidebar(createRender(qs($page, `[data-bind="sidebar"]`)), {});
|
||||
onDestroy(async() => {
|
||||
if ((history.state.previous || "").startsWith("/view/") && location.pathname.startsWith("/files/")) {
|
||||
await animate($main, { time: 100, keyframes: slideYOut(20), fill: "none" });
|
||||
|
||||
@ -17,6 +17,21 @@
|
||||
direction: rtl;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.component_filemanager_shell .component_sidebar .component_skeleton {
|
||||
margin-bottom: 5px;
|
||||
width: calc(100% - 5px);
|
||||
margin-left: 5px;
|
||||
opacity: 0;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 1s;
|
||||
animation-name: skeleton-appear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
@keyframes skeleton-appear {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.component_filemanager_shell .component_sidebar > div { direction: ltr; }
|
||||
.component_filemanager_shell .component_sidebar h3 {
|
||||
display: flex;
|
||||
@ -79,7 +94,8 @@ body.touch-no .component_filemanager_shell .component_sidebar h3 img:hover {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.component_filemanager_shell .component_sidebar a:hover, .component_filemanager_shell .component_sidebar a[aria-selected="true"] {
|
||||
.component_filemanager_shell .component_sidebar [data-bind="your-files"] a:hover,
|
||||
.component_filemanager_shell .component_sidebar a[aria-selected="true"] {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
@ -107,3 +123,26 @@ body.touch-no .component_filemanager_shell .component_sidebar h3 img:hover {
|
||||
.component_filemanager_shell li.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a {
|
||||
margin-bottom: 2px;
|
||||
justify-content: space-between;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a[aria-selected="true"] {
|
||||
background: var(--light);
|
||||
color: #f2f2f2;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a[aria-selected="true"] svg {
|
||||
display: block;
|
||||
}
|
||||
.component_filemanager_shell .component_sidebar [data-bind="taglist"] a svg {
|
||||
display: none;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
align-self: center;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
import { createElement, createRender, onDestroy } from "../lib/skeleton/index.js";
|
||||
import rxjs, { effect, onClick } from "../lib/rx.js";
|
||||
import ajax from "../lib/ajax.js";
|
||||
import assert from "../lib/assert.js";
|
||||
import { fromHref, toHref } from "../lib/skeleton/router.js";
|
||||
import { qs, qsa, safe } from "../lib/dom.js";
|
||||
import { forwardURLParams } from "../lib/path.js";
|
||||
import { settingsGet, settingsSave } from "../lib/store.js";
|
||||
import { get as getConfig } from "../model/config.js";
|
||||
import { loadCSS } from "../helpers/loader.js";
|
||||
import t from "../locales/index.js";
|
||||
import cache from "../pages/filespage/cache.js";
|
||||
import { hooks, mv as mv$ } from "../pages/filespage/model_files.js";
|
||||
import { extractPath, isDir, isNativeFileUpload } from "../pages/filespage/helper.js";
|
||||
import { mv as mvVL, withVirtualLayer } from "../pages/filespage/model_virtual_layer.js";
|
||||
import { getCurrentPath } from "../pages/viewerpage/common.js";
|
||||
import { generateSkeleton } from "./skeleton.js";
|
||||
|
||||
const state = { scrollTop: 0, $cache: null };
|
||||
const mv = (from, to) => withVirtualLayer(
|
||||
@ -18,7 +22,7 @@ const mv = (from, to) => withVirtualLayer(
|
||||
mvVL(from, to),
|
||||
);
|
||||
|
||||
export default async function ctrlSidebar(render, nRestart = 0) {
|
||||
export default async function ctrlSidebar(render, { nRestart = 0 }) {
|
||||
if (!shouldDisplay()) return;
|
||||
|
||||
const $sidebar = render(createElement(`
|
||||
@ -28,11 +32,6 @@ export default async function ctrlSidebar(render, nRestart = 0) {
|
||||
<input type="text" placeholder="${t("Your Files")}" />
|
||||
</h3>
|
||||
<div data-bind="your-files"></div>
|
||||
|
||||
<h3>
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIHN0eWxlPSJmaWxsOiAjNTc1OTVhOyIgZD0iTTEgNy43NzVWMi43NUMxIDEuNzg0IDEuNzg0IDEgMi43NSAxaDUuMDI1Yy40NjQgMCAuOTEuMTg0IDEuMjM4LjUxM2w2LjI1IDYuMjVhMS43NSAxLjc1IDAgMCAxIDAgMi40NzRsLTUuMDI2IDUuMDI2YTEuNzUgMS43NSAwIDAgMS0yLjQ3NCAwbC02LjI1LTYuMjVBMS43NTIgMS43NTIgMCAwIDEgMSA3Ljc3NVptMS41IDBjMCAuMDY2LjAyNi4xMy4wNzMuMTc3bDYuMjUgNi4yNWEuMjUuMjUgMCAwIDAgLjM1NCAwbDUuMDI1LTUuMDI1YS4yNS4yNSAwIDAgMCAwLS4zNTRsLTYuMjUtNi4yNWEuMjUuMjUgMCAwIDAtLjE3Ny0uMDczSDIuNzVhLjI1LjI1IDAgMCAwLS4yNS4yNVpNNiA1YTEgMSAwIDEgMSAwIDIgMSAxIDAgMCAxIDAtMloiPjwvcGF0aD4NCjwvc3ZnPg0K" alt="tag">
|
||||
${t("Tags")}
|
||||
</h3>
|
||||
<div data-bind="your-tags"></div>
|
||||
</div>
|
||||
`));
|
||||
@ -76,7 +75,12 @@ export default async function ctrlSidebar(render, nRestart = 0) {
|
||||
ctrlNavigationPane(render, { $sidebar, nRestart });
|
||||
|
||||
// feature: tag viewer
|
||||
effect(rxjs.merge(
|
||||
rxjs.of(null),
|
||||
rxjs.fromEvent(window, "filestash::tag"),
|
||||
).pipe(rxjs.tap(() => {
|
||||
ctrlTagPane(createRender(qs($sidebar, `[data-bind="your-tags"]`)));
|
||||
})));
|
||||
}
|
||||
|
||||
const withResize = (function() {
|
||||
@ -254,37 +258,63 @@ async function _createListOfFiles(path, currentName, dirpath) {
|
||||
}
|
||||
|
||||
async function ctrlTagPane(render) {
|
||||
if (!getConfig("enable_tags", false)) return;
|
||||
render(createElement(`<div>${generateSkeleton(2)}</div>`));
|
||||
|
||||
const $page = createElement(`
|
||||
<div>
|
||||
<h3>
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIHN0eWxlPSJmaWxsOiAjNTc1OTVhOyIgZD0iTTEgNy43NzVWMi43NUMxIDEuNzg0IDEuNzg0IDEgMi43NSAxaDUuMDI1Yy40NjQgMCAuOTEuMTg0IDEuMjM4LjUxM2w2LjI1IDYuMjVhMS43NSAxLjc1IDAgMCAxIDAgMi40NzRsLTUuMDI2IDUuMDI2YTEuNzUgMS43NSAwIDAgMS0yLjQ3NCAwbC02LjI1LTYuMjVBMS43NTIgMS43NTIgMCAwIDEgMSA3Ljc3NVptMS41IDBjMCAuMDY2LjAyNi4xMy4wNzMuMTc3bDYuMjUgNi4yNWEuMjUuMjUgMCAwIDAgLjM1NCAwbDUuMDI1LTUuMDI1YS4yNS4yNSAwIDAgMCAwLS4zNTRsLTYuMjUtNi4yNWEuMjUuMjUgMCAwIDAtLjE3Ny0uMDczSDIuNzVhLjI1LjI1IDAgMCAwLS4yNS4yNVpNNiA1YTEgMSAwIDEgMSAwIDIgMSAxIDAgMCAxIDAtMloiPjwvcGF0aD4NCjwvc3ZnPg0K" alt="tag">
|
||||
${t("Tags")}
|
||||
</h3>
|
||||
<ul>
|
||||
<li data-bind="taglist"></li>
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
|
||||
// only enable this pane in canary mode until it's actually ready
|
||||
if (new URLSearchParams(location.search).get("canary") !== "true") {
|
||||
$page.classList.add("hidden");
|
||||
const orFail = (something) => assert.type(something, HTMLElement);
|
||||
orFail(orFail($page.parentElement).previousElementSibling).classList.add("hidden");
|
||||
const tags = await ajax({
|
||||
url: forwardURLParams(`api/metadata/search`, ["share"]),
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
body: JSON.stringify({
|
||||
"tags": [],
|
||||
"path": getCurrentPath("(/view/|/files/)"),
|
||||
}),
|
||||
}).pipe(
|
||||
rxjs.map(({ responseJSON }) =>
|
||||
responseJSON.results
|
||||
.filter(({type}) => type === "folder")
|
||||
.map(({ name }) => name)
|
||||
.sort()
|
||||
),
|
||||
rxjs.catchError(() => rxjs.of([])),
|
||||
).toPromise();
|
||||
if (tags.length === 0) {
|
||||
render(createElement("<div></div>"));
|
||||
return;
|
||||
}
|
||||
render($page);
|
||||
|
||||
const tags = [
|
||||
{ name: t("Bookmark"), color: "green" },
|
||||
{ name: "important", color: "red" },
|
||||
{ name: "foobar", color: "saddlebrown" },
|
||||
];
|
||||
const $tmpl = (name, color) => createElement(`
|
||||
<a data-link href="/tags/${name}/?canary=true" draggable="false">
|
||||
<svg class="component_icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="50" style="opacity: 0.25; fill: ${color};" />
|
||||
</svg>
|
||||
const $fragment = document.createDocumentFragment();
|
||||
tags.forEach((name) => {
|
||||
const $tag = createElement(`
|
||||
<a data-link draggable="false" class="no-select">
|
||||
<div class="ellipsis">${name}</div>
|
||||
<svg class="component_icon" draggable="false" alt="close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</a>
|
||||
`);
|
||||
const $fragment = document.createDocumentFragment();
|
||||
tags.forEach(({ name, color }) => {
|
||||
$fragment.appendChild($tmpl(name, color));
|
||||
const url = new URL(location.href);
|
||||
if (url.searchParams.getAll("tag").indexOf(name) === -1) {
|
||||
$tag.setAttribute("href", forwardURLParams(getCurrentPath() + "?tag=" + name, ["share", "tag"]));
|
||||
} else {
|
||||
url.searchParams.delete("tag", name);
|
||||
$tag.setAttribute("href", url.toString());
|
||||
$tag.setAttribute("aria-selected", "true");
|
||||
}
|
||||
$fragment.appendChild($tag);
|
||||
});
|
||||
qs($page, `[data-bind="taglist"]`).appendChild($fragment);
|
||||
}
|
||||
|
||||
@ -12,10 +12,11 @@ export function join(baseURL, segment) {
|
||||
}
|
||||
|
||||
export function forwardURLParams(url, allowed = []) {
|
||||
const _url = new URL(window.location.origin + "/" + url);
|
||||
const link = new URL(window.location.origin + "/" + url);
|
||||
for (const [key, value] of new URLSearchParams(location.search)) {
|
||||
if (allowed.indexOf(key) < 0) continue;
|
||||
_url.searchParams.set(key, value);
|
||||
else if (link.searchParams.getAll(key).indexOf(value) !== -1) continue;
|
||||
link.searchParams.append(key, value);
|
||||
}
|
||||
return _url.pathname.substring(1) + _url.search;
|
||||
return link.pathname.substring(1) + link.search;
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ function componentLeft(render, { $scroll, getSelectionLength$ }) {
|
||||
<button data-action="share" title="${t("Share")}" class="${(getConfig("enable_share") && !new URLSearchParams(location.search).has("share")) ? "" : "hidden"}">
|
||||
${t("Share")}
|
||||
</button>
|
||||
<button data-action="tag" title="${t("Tag")}" class="${new URLSearchParams(location.search).get("canary") === "true" ? "" : "hidden"}" tabindex="-1">
|
||||
<button data-action="tag" title="${t("Tag")}" class="${getConfig("enable_tags", false) ? "" : "hidden"}">
|
||||
${t("Tag")}
|
||||
</button>
|
||||
`))),
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
.component_tag {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.component_tag [data-bind="taglist"] {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
margin: -10px;
|
||||
padding: 20px 10px 10px 10px;
|
||||
.component_tag form {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.component_tag input {
|
||||
.component_tag form input {
|
||||
font-size: 1rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 5px;
|
||||
@ -18,9 +14,20 @@
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.component_tag input::placeholder {
|
||||
.component_tag form input::placeholder {
|
||||
color: rgba(0,0,0,0.2);
|
||||
}
|
||||
.component_tag [data-bind="taglist"] {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
margin: -3px 0 0px 0px;
|
||||
padding: 0;
|
||||
}
|
||||
.component_tag [data-bind="taglist"] .component_skeleton {
|
||||
margin: 0;
|
||||
height: 29px;
|
||||
}
|
||||
.component_tag .tag {
|
||||
display: flex;
|
||||
background: #f2f2f2;
|
||||
@ -31,6 +38,7 @@
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
letter-spacing: -0.5px;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.component_tag .tag.active {
|
||||
background: var(--dark);
|
||||
|
||||
@ -1,40 +1,147 @@
|
||||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { effect, onClick } from "../../lib/rx.js";
|
||||
import { forwardURLParams } from "../../lib/path.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
import { qs } from "../../lib/dom.js";
|
||||
import assert from "../../lib/assert.js";
|
||||
import { generateSkeleton } from "../../components/skeleton.js";
|
||||
import t from "../../locales/index.js";
|
||||
|
||||
|
||||
const shareID = new URLSearchParams(location.search).get("share");
|
||||
|
||||
const $tmpl = createElement(`
|
||||
<div class="tag no-select">
|
||||
<div class="ellipsis">Projects</div>
|
||||
<svg class="component_icon" draggable="false" alt="close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg style="${shareID ? "opacity:0.2" : ""}" class="component_icon" draggable="false" alt="close" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
`);
|
||||
|
||||
export default function(render, { path }) {
|
||||
const tags = [
|
||||
{ name: "Bookmark", active: true },
|
||||
{ name: "Projects" },
|
||||
{ name: "Important" },
|
||||
];
|
||||
|
||||
export default async function(render, { path }) {
|
||||
const $modal = createElement(`
|
||||
<div class="component_tag">
|
||||
<form>
|
||||
<input type="text" placeholder="${t("Create a Tag")}" value="">
|
||||
<form class="${shareID ? "hidden" : ""}">
|
||||
<input name="tag" type="text" placeholder="${t("Add a Tag")}" value="">
|
||||
</form>
|
||||
<div class="scroll-y" data-bind="taglist"></div>
|
||||
<div class="scroll-y" data-bind="taglist">
|
||||
${generateSkeleton(1)}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const $fragment = document.createDocumentFragment();
|
||||
tags.forEach(({ name, active }) => {
|
||||
render($modal);
|
||||
|
||||
const tags$ = new rxjs.BehaviorSubject(await rxjs.zip(
|
||||
ajax({ url: forwardURLParams(`api/metadata?path=${path}`, ["share"]), method: "GET", responseType: "json" }).pipe(
|
||||
rxjs.map(({ responseJSON }) =>
|
||||
responseJSON.results
|
||||
.reduce((acc, { id, value }) => {
|
||||
if (id !== "tags") return acc;
|
||||
acc = acc.concat(value.split(", ").map(
|
||||
(name) => ({ name, active: true })
|
||||
));
|
||||
return acc;
|
||||
}, [])
|
||||
),
|
||||
),
|
||||
ajax({ url: forwardURLParams("api/metadata/search", ["share"]), method: "POST", responseType: "json", body: { path: "/" }}).pipe(
|
||||
rxjs.map(({ responseJSON }) =>
|
||||
responseJSON.results
|
||||
.filter(({ type, name }) => type === "folder")
|
||||
.map(({ name }) => ({ name, active: false }))
|
||||
),
|
||||
),
|
||||
).pipe(rxjs.map(([currentTags, allTags]) => {
|
||||
for (let i=0; i<allTags.length; i++) {
|
||||
for (let j=0; j<currentTags.length; j++) {
|
||||
if (currentTags[j].name === allTags[i].name) {
|
||||
allTags[i].active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!shareID && allTags.length === 0) {
|
||||
return [{ name: t("bookmark"), active: false }];
|
||||
}
|
||||
if (shareID) return allTags.filter(({ active }) => active);
|
||||
return allTags;
|
||||
})).toPromise());
|
||||
const save = (tags) => ajax({
|
||||
url: forwardURLParams(`api/metadata?path=${path}`, ["share"]),
|
||||
method: "POST",
|
||||
body: tags.length === 0 ? [] : [{
|
||||
id: "tags",
|
||||
type: "hidden",
|
||||
value: tags.join(", "),
|
||||
}],
|
||||
}).pipe(rxjs.tap(() => window.dispatchEvent(new Event("filestash::tag"))));
|
||||
|
||||
// feature: create DOM
|
||||
const dom$ = tags$.pipe(
|
||||
rxjs.map((tags) => tags.sort((a, b) => a.name > b.name ? 1 : -1)),
|
||||
rxjs.map((tags) => tags.map(({ name, active }) => {
|
||||
const $el = assert.type($tmpl, HTMLElement).cloneNode(true);
|
||||
$el.firstElementChild.innerText = name;
|
||||
if (active) $el.classList.add("active");
|
||||
$fragment.appendChild($el);
|
||||
});
|
||||
qs($modal, `[data-bind="taglist"]`).appendChild($fragment);
|
||||
render($modal, ({ id }) => { console.log(`QUIT id=${id} path=${path}`); });
|
||||
qs($el, "svg").onclick = (e) => {
|
||||
if (shareID) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$el.classList.remove("active");
|
||||
tags$.next(tags$.value.filter((tag) => {
|
||||
return tag.name !== $el.innerText.trim();
|
||||
}));
|
||||
save(tags$.value
|
||||
.filter(({ active }) => active)
|
||||
.map(({ name }) => name)).toPromise();
|
||||
}
|
||||
return $el;
|
||||
})),
|
||||
rxjs.tap(($nodes) => {
|
||||
const $container = qs($modal, `[data-bind="taglist"]`);
|
||||
if ($nodes.length === 0) $container.replaceChildren(createElement(`<div class="center full-width">∅</div>`));
|
||||
else $container.replaceChildren(...$nodes);
|
||||
}),
|
||||
);
|
||||
|
||||
// feature: tag creation
|
||||
effect(rxjs.fromEvent(qs($modal, "form"), "submit").pipe(
|
||||
rxjs.filter(() => !shareID),
|
||||
rxjs.tap((e) => {
|
||||
e.preventDefault();
|
||||
const tagname = new FormData(e.target).get("tag").toLowerCase().trim();
|
||||
if (!tagname) return;
|
||||
else if (tags$.value.find(({ name }) => name === tagname)) return;
|
||||
qs($modal, `input[name="tag"]`).value = "";
|
||||
tags$.next(tags$.value.concat({
|
||||
name: tagname,
|
||||
active: true,
|
||||
}));
|
||||
}),
|
||||
rxjs.mergeMap(() => save(
|
||||
tags$.value
|
||||
.filter(({ active }) => !!active)
|
||||
.map(({ name }) => name)
|
||||
)),
|
||||
));
|
||||
|
||||
// feature: toggle tags
|
||||
effect(dom$.pipe(
|
||||
rxjs.mergeMap(($nodes) => rxjs.merge(
|
||||
...$nodes.map(($node) => onClick($node)),
|
||||
).pipe(
|
||||
rxjs.filter(() => !shareID),
|
||||
rxjs.tap(($node) => $node.classList.toggle("active")),
|
||||
rxjs.debounceTime(800),
|
||||
rxjs.map(() =>
|
||||
$nodes
|
||||
.filter(($node) => $node.classList.contains("active"))
|
||||
.map(($node) => $node.innerText.trim())
|
||||
.filter((text) => !!text)
|
||||
),
|
||||
)),
|
||||
rxjs.mergeMap((tags) => save(tags)),
|
||||
));
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ export function getDownloadUrl() {
|
||||
return forwardURLParams("api/files/cat?path=" + encodeURIComponent(getCurrentPath()), ["share"]);
|
||||
}
|
||||
|
||||
export function getCurrentPath() {
|
||||
export function getCurrentPath(start = "/view/") {
|
||||
const fullpath = fromHref(location.pathname + location.hash);
|
||||
return decodeURIComponent(fullpath.replace(new RegExp("^/view"), ""));
|
||||
return decodeURIComponent(fullpath.replace(new RegExp("^" + start), "/"));
|
||||
}
|
||||
|
||||
@ -358,7 +358,6 @@ func (this *Configuration) Export() interface{} {
|
||||
Name string `json:"name"`
|
||||
UploadButton bool `json:"upload_button"`
|
||||
Connections interface{} `json:"connections"`
|
||||
EnableShare bool `json:"enable_share"`
|
||||
SharedLinkDefaultAccess string `json:"share_default_access"`
|
||||
SharedLinkRedirect string `json:"share_redirect"`
|
||||
Logout string `json:"logout"`
|
||||
@ -370,9 +369,11 @@ func (this *Configuration) Export() interface{} {
|
||||
FilePageDefaultView string `json:"default_view"`
|
||||
AuthMiddleware []string `json:"auth"`
|
||||
Thumbnailer []string `json:"thumbnailer"`
|
||||
EnableChromecast bool `json:"enable_chromecast"`
|
||||
Origin string `json:"origin"`
|
||||
Version string `json:"version"`
|
||||
EnableChromecast bool `json:"enable_chromecast"`
|
||||
EnableShare bool `json:"enable_share"`
|
||||
EnableTags bool `json:"enable_tags"`
|
||||
}{
|
||||
Editor: this.Get("general.editor").String(),
|
||||
ForkButton: this.Get("general.fork_button").Bool(),
|
||||
@ -380,7 +381,6 @@ func (this *Configuration) Export() interface{} {
|
||||
Name: this.Get("general.name").String(),
|
||||
UploadButton: this.Get("general.upload_button").Bool(),
|
||||
Connections: this.Conn,
|
||||
EnableShare: this.Get("features.share.enable").Bool(),
|
||||
SharedLinkDefaultAccess: this.Get("features.share.default_access").String(),
|
||||
SharedLinkRedirect: this.Get("features.share.redirect").String(),
|
||||
Logout: this.Get("general.logout").String(),
|
||||
@ -408,7 +408,6 @@ func (this *Configuration) Export() interface{} {
|
||||
}
|
||||
return tArray
|
||||
}(),
|
||||
EnableChromecast: this.Get("features.protection.enable_chromecast").Bool(),
|
||||
Origin: func() string {
|
||||
host := this.Get("general.host").String()
|
||||
if host == "" {
|
||||
@ -421,6 +420,11 @@ func (this *Configuration) Export() interface{} {
|
||||
return scheme + host
|
||||
}(),
|
||||
Version: BUILD_REF,
|
||||
EnableChromecast: this.Get("features.protection.enable_chromecast").Bool(),
|
||||
EnableShare: this.Get("features.share.enable").Bool(),
|
||||
EnableTags: func() bool {
|
||||
return Hooks.Get.Metadata() != nil
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -254,6 +254,16 @@ func (this Get) StaticPatch() [][]byte {
|
||||
return staticOverrides
|
||||
}
|
||||
|
||||
var meta IMetadata
|
||||
|
||||
func (this Register) Metadata(m IMetadata) {
|
||||
meta = m
|
||||
}
|
||||
|
||||
func (this Get) Metadata() IMetadata {
|
||||
return meta
|
||||
}
|
||||
|
||||
func init() {
|
||||
Hooks.Register.FrontendOverrides(OverrideVideoSourceMapper)
|
||||
}
|
||||
|
||||
@ -66,6 +66,18 @@ type AuditQueryResult struct {
|
||||
RenderHTML string `json:"render"`
|
||||
}
|
||||
|
||||
const (
|
||||
MetaModeTag = 1 << iota
|
||||
MetaModeBookmark
|
||||
MetaModeForm
|
||||
)
|
||||
|
||||
type IMetadata interface {
|
||||
Get(ctx *App, path string) ([]FormElement, error)
|
||||
Set(ctx *App, path string, value []FormElement) error
|
||||
Search(ctx *App, basePath string, facets map[string]any) ([]IFile, error)
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FName string `json:"name"`
|
||||
FType string `json:"type"`
|
||||
|
||||
76
server/ctrl/metadata.go
Normal file
76
server/ctrl/metadata.go
Normal file
@ -0,0 +1,76 @@
|
||||
package ctrl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
. "github.com/mickael-kerjean/filestash/server/common"
|
||||
)
|
||||
|
||||
func MetaGet(ctx *App, w http.ResponseWriter, r *http.Request) {
|
||||
m := Hooks.Get.Metadata()
|
||||
if m == nil {
|
||||
SendErrorResult(w, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
path, err := PathBuilder(ctx, r.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
SendErrorResult(w, err)
|
||||
return
|
||||
}
|
||||
out, err := m.Get(ctx, path)
|
||||
if err != nil {
|
||||
SendErrorResult(w, err)
|
||||
return
|
||||
}
|
||||
SendSuccessResults(w, out)
|
||||
}
|
||||
|
||||
func MetaUpsert(ctx *App, w http.ResponseWriter, r *http.Request) {
|
||||
m := Hooks.Get.Metadata()
|
||||
if m == nil {
|
||||
SendErrorResult(w, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
path, err := PathBuilder(ctx, r.URL.Query().Get("path"))
|
||||
if err != nil {
|
||||
SendErrorResult(w, err)
|
||||
return
|
||||
}
|
||||
forms := []FormElement{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&forms); err != nil {
|
||||
SendErrorResult(w, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
if err := m.Set(ctx, path, forms); err != nil {
|
||||
SendErrorResult(w, err)
|
||||
return
|
||||
}
|
||||
SendSuccessResult(w, nil)
|
||||
}
|
||||
|
||||
func MetaSearch(ctx *App, w http.ResponseWriter, r *http.Request) {
|
||||
m := Hooks.Get.Metadata()
|
||||
if m == nil {
|
||||
SendErrorResult(w, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
facets := map[string]any{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&facets); err != nil {
|
||||
SendErrorResult(w, ErrNotImplemented)
|
||||
return
|
||||
}
|
||||
path, err := PathBuilder(ctx, fmt.Sprintf("%s", facets["path"]))
|
||||
if err != nil {
|
||||
SendErrorResult(w, err)
|
||||
return
|
||||
}
|
||||
delete(facets, "path")
|
||||
out, err := m.Search(ctx, path, facets)
|
||||
if err != nil {
|
||||
SendErrorResult(w, err)
|
||||
return
|
||||
}
|
||||
SendSuccessResults(w, out)
|
||||
}
|
||||
@ -73,6 +73,12 @@ func Build(r *mux.Router, a App) {
|
||||
middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, BodyParser, CanManageShare, PluginInjector}
|
||||
share.HandleFunc("/{share}", NewMiddlewareChain(ShareUpsert, middlewares, a)).Methods("POST")
|
||||
|
||||
meta := r.PathPrefix(WithBase("/api/metadata")).Subrouter()
|
||||
middlewares = []Middleware{ApiHeaders, SecureHeaders, SecureOrigin, WithPublicAPI, SessionStart, LoggedInOnly, PluginInjector}
|
||||
meta.HandleFunc("", NewMiddlewareChain(MetaGet, middlewares, a)).Methods("GET")
|
||||
meta.HandleFunc("", NewMiddlewareChain(MetaUpsert, middlewares, a)).Methods("POST")
|
||||
meta.HandleFunc("/search", NewMiddlewareChain(MetaSearch, middlewares, a)).Methods("POST")
|
||||
|
||||
// Webdav server / Shared Link
|
||||
middlewares = []Middleware{IndexHeaders, SecureHeaders, PluginInjector}
|
||||
r.HandleFunc(WithBase("/s/{share}"), NewMiddlewareChain(ServeFrontofficeHandler, middlewares, a)).Methods("GET")
|
||||
|
||||
Reference in New Issue
Block a user