feature (image): add page change & UX improvements

This commit is contained in:
MickaelK 2025-05-13 17:02:40 +10:00
parent 2adad52308
commit 67f3ccc2cb
8 changed files with 147 additions and 89 deletions

View file

@ -50,6 +50,16 @@ function loadModule(appName) {
throw new ApplicationError("Internal Error", `Unknown opener app "${appName}" at "${getCurrentPath()}"`);
}
};
const loadModuleWithMemory = (function() {
const memory = {};
return function(appName) {
if (memory[appName]) return Promise.resolve(memory[appName]);
return loadModule(appName).then((module) => {
memory[appName] = module;
return module;
});
};
})();
export default WithShell(async function(render) {
const $page = createElement(`<div class="component_page_viewerpage"></div>`);
@ -58,7 +68,7 @@ export default WithShell(async function(render) {
// feature: render viewer application
effect(rxjs.of(getConfig("mime", {})).pipe(
rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)),
rxjs.mergeMap(([opener, opts]) => rxjs.from(loadModule(opener)).pipe(rxjs.switchMap(async(module) => {
rxjs.mergeMap(([opener, opts]) => rxjs.from(loadModuleWithMemory(opener)).pipe(rxjs.switchMap(async(module) => {
module.default(createRender($page), { ...opts, acl$: options(), getFilename, getDownloadUrl });
}))),
rxjs.catchError(ctrlError()),

View file

@ -76,11 +76,8 @@ body:not(.dark-mode) .component_imageviewer .component_image_container .fullscre
z-index: 1;
min-width: 0px;
transition: 0.15s ease min-width;
background: #949290;
color: var(--dark);
}
.dark-mode .component_imageviewer .images_aside {
background: #f2f2f2;
color: var(--dark);
}
.component_imageviewer .images_aside.open {
min-width: 300px;
@ -129,6 +126,12 @@ body:not(.dark-mode) .component_imageviewer .component_image_container .fullscre
height: 18px;
float: right;
cursor: pointer;
padding: 5px;
margin: -5px -5px 0 0;
}
.component_imageviewer .images_aside .header .component_icon:hover {
background: #0000000a;
border-radius: 50%;
}
.component_imageviewer .images_aside [data-bind="body"] {
padding: 10px 20px 0px 20px;
@ -154,7 +157,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container .fullscre
display: flex;
justify-content: space-between;
margin: 5px 0;
border-top: 1px solid #ffffff15;
border-top: 1px solid #0000000a;
padding-top: 5px;
text-align: right;
font-size: 0.85em;
@ -163,5 +166,5 @@ body:not(.dark-mode) .component_imageviewer .component_image_container .fullscre
margin-right: 5px;
}
.component_imageviewer .images_aside .meta_key .value {
color: var(--bg-color);
color: var(--light);
}

View file

@ -14,11 +14,9 @@ import notification from "../../components/notification.js";
import t from "../../locales/index.js";
import ctrlError from "../ctrl_error.js";
import { transition } from "./common.js";
import componentMetadata, { init as initMetadata } from "./application_image_metadata.js";
import componentToolbox, { init as initToolbox } from "./application_image_toolbox.js";
import ctrlDownloader, { init as initDownloader } from "./application_downloader.js";
import componentPager, { init as initPager } from "./component_pager.js";
import { renderMenubar, buttonDownload, buttonFullscreen } from "./component_menubar.js";
@ -33,14 +31,13 @@ export default function(render, { getFilename, getDownloadUrl, mime, hasMenubar
<div class="component_image_container">
<div class="images_wrapper">
<img class="photo idle hidden" draggable="false" />
<div data-bind="component_navigation"></div>
</div>
<div class="images_aside scroll-y"></div>
<div class="component_pager hidden"></div>
</div>
</div>
`);
render($page);
transition(qs($page, ".component_image_container"));
const $imgContainer = qs($page, ".images_wrapper");
const $photo = qs($page, "img.photo");
@ -97,14 +94,14 @@ export default function(render, { getFilename, getDownloadUrl, mime, hasMenubar
return ctrlError()(err);
}),
));
componentPager(createRender(qs($page, ".component_pager")));
componentToolbox(createRender(qs($page, "[data-bind=\"component_navigation\"]")), { $img: $photo });
}
export function init() {
return Promise.all([
loadCSS(import.meta.url, "./application_image.css"),
loadCSS(import.meta.url, "./component_menubar.css"),
initPager(), initMetadata(),
initToolbox(), initMetadata(),
]);
}

View file

@ -0,0 +1,31 @@
/* PAGINATION */
.component_pager {
position: absolute;
top: 0;
bottom: 0;
display: flex;
opacity: 0.2;
margin: auto;
padding: 15px;
color: #f2f2f2;
z-index: 1;
}
.component_pager.left { left: 0; padding-right: 150px; }
.component_pager.right { right: 0; padding-left: 150px; }
.component_pager:hover {
opacity: 0.9;
transition: opacity 0.5s ease;
}
.component_pager a { margin: auto; }
.component_pager a svg {
transition: opacity 0.2s ease;
width: 37px;
padding: 5px;
background: var(--dark);
border-radius: 50%;
margin: auto;
opacity: 0.75;
}
.component_pager a svg:hover {
opacity: 1;
}

View file

@ -0,0 +1,91 @@
import { createFragment } from "../../lib/skeleton/index.js";
import rxjs from "../../lib/rx.js";
import { qs } from "../../lib/dom.js";
import { join } from "../../lib/path.js";
import { animate, slideXOut } from "../../lib/animate.js";
import { loadCSS } from "../../helpers/loader.js";
import { get as getConfig } from "../../model/config.js";
import { getCurrentPath, getFilename } from "./common.js";
import { getMimeType } from "./mimetype.js";
import fscache from "../filespage/cache.js";
import { sort } from "../filespage/helper.js";
import { getState$ as getParams$, init as initParams } from "../filespage/state_config.js";
export default async function(render, { $img }) {
const lsCache = await fscache().get(join(location, getCurrentPath() + "/../"));
if (!lsCache) return;
const params = await getParams$().pipe(rxjs.first()).toPromise();
const files = sort(lsCache.files, params["sort"], params["order"]);
const currentFilename = getFilename();
const mimeTypes = getConfig("mime", {});
const state = {
prev: null,
curr: null,
next: null,
length: 0,
};
for (let i=0; i<files.length; i++) {
const filename = files[i].name;
if (!getMimeType(filename, mimeTypes).startsWith("image/")) {
continue;
}
state.length += 1;
if (currentFilename === filename) {
state.curr = i;
} else if (state.curr === null) {
state.prev = i;
} else {
state.next = i;
break;
}
}
if (state.length <= 1) return;
const $page = createFragment(`
<div class="component_pager left hidden">
<a data-link>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
</a>
</div>
<div class="component_pager right hidden">
<a>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
`);
if (state.prev !== null) updateDOM({
$el: $page.children[0],
name: files[state.prev].name,
$img,
});
if (state.next !== null) updateDOM({
$el: $page.children[1],
name: files[state.next].name,
$img,
});
render($page);
}
function updateDOM({ $el, name, $img }) {
const $link = qs($el, "a");
$link.onclick = async(e) => {
if (e.target.hasAttribute("data-link")) return;
e.preventDefault(); e.stopPropagation();
await animate($img, { keyframes: slideXOut(-10), time: 200 });
$link.setAttribute("data-link", "true");
$link.click();
};
$link.setAttribute("href", "/view" + join(location, getCurrentPath() + "/../" + name));
$el.classList.remove("hidden");
}
export function init() {
return Promise.all([
loadCSS(import.meta.url, "./application_image_toolbox.css"),
initParams(),
]);
}

View file

@ -1,46 +0,0 @@
.component_pager {
position: fixed;
bottom: 0px;
left: 0;
right: 0;
z-index: 2;
}
.component_pager .wrapper {
text-align: center;
color: var(--super-light);
margin: 0 auto;
padding: 15px 0;
text-shadow: 0px 0px 2px var(--dark);
}
.component_pager .wrapper > span {
display: inline-block;
background: var(--dark);
padding: 5px 10px;
border-radius: 5px;
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
}
.component_pager .wrapper > span .pager {
line-height: 22px;
}
.component_pager .wrapper > span .pager .separator {
padding: 0 5px;
}
.component_pager .wrapper > span .component_icon {
width: 24px;
}
.component_pager .wrapper > span form {
display: inline-block;
}
.component_pager .wrapper > span form input[type="number"] {
padding: 0;
text-align: right;
background: inherit;
border: none;
color: inherit;
font-size: inherit;
-moz-appearance: textfield;
}
.component_pager .wrapper > span form input[type="number"]::-webkit-inner-spin-button, .component_pager .wrapper > span form input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View file

@ -1,28 +0,0 @@
import { createElement } from "../../lib/skeleton/index.js";
import { loadCSS } from "../../helpers/loader.js";
export default function(render) {
render(createElement(`
<div class="wrapper no-select">
<span>
<a href="/view/Documents/assets/background.png">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6I2YyZjJmMjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MS41MTE4MTEwMjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZSIgZD0ibSAxNiw3LjE2IC00LjU4LDQuNTkgNC41OCw0LjU5IC0xLjQxLDEuNDEgLTYsLTYgNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" alt="arrow_left_white">
</a>
<label class="pager">
<form>
<input class="prevent" type="number" value="2" style="width: 12px;">
</form>
<span class="separator">/</span>
<span>2</span>
</label>
<a href="/view/Documents/assets/background.png">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6I2YyZjJmMjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MS41MTE4MTEwMjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZSIgZD0iTTguNTkgMTYuMzRsNC41OC00LjU5LTQuNTgtNC41OUwxMCA1Ljc1bDYgNi02IDZ6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" alt="arrow_right_white">
</a>
</span>
</div>
`));
}
export function init() {
return loadCSS(import.meta.url, "./component_pager.css");
}

View file

@ -41,6 +41,6 @@ export function opener(file = "", mimes) {
return ["editor", { mime }];
}
function getMimeType(file, mimes = {}) {
export function getMimeType(file, mimes = {}) {
return mimes[file.split(".").slice(-1)[0].toLowerCase()] || "text/plain";
}