@@ -53,23 +58,75 @@ export default function(render) {
transition(qs($page, ".audioplayer_box"));
const $control = {
+ main: qs($page, `.audioplayer_control`),
+
play: qs($page, `.audioplayer_control [alt="play"]`),
pause: qs($page, `.audioplayer_control [alt="pause"]`),
- // loading: qs($page, `.audioplayer_control component-icon[name="loading"]`),
+ loading: qs($page, `.audioplayer_control component-icon[name="loading"]`),
};
const $volume = {
range: qs($page, `input[type="range"]`),
- // icon_mute: qs($page, `img[alt="volume_mute"]`),
- // icon_low: qs($page, `img[alt="volume_low"]`),
- // icon_normal: qs($page, `img[alt="volume"]`),
- };
- const setVolume = (volume, wavesurfer) => {
- wavesurfer.setVolume(volume / 100);
+ icon_mute: qs($page, `img[alt="volume_mute"]`),
+ icon_low: qs($page, `img[alt="volume_low"]`),
+ icon_normal: qs($page, `img[alt="volume"]`),
};
const currentTime$ = new rxjs.BehaviorSubject([
- 0, // time of lastSeek: we want to start from the start
- 0, // seek offset to align with wavesurfer currentTime in the audiocontext
+ 0, // starting time - does change when seeking to another point
+ 0, // offset to align the audio context currentTime. otherwise when seeking, the
+ // currentTime keep growing and progress bar goes haywire
]);
+ const currentTime = (wavesurfer) => {
+ return currentTime$.value[0] + (wavesurfer.backend.ac.currentTime - currentTime$.value[1]);
+ };
+ const setVolume = (volume, wavesurfer) => {
+ settings_put("volume", volume);
+ wavesurfer.setVolume(volume / 100);
+ $volume.range.value = volume;
+ if (volume === 0) {
+ $volume.icon_mute.classList.remove("hidden");
+ $volume.icon_low.classList.add("hidden");
+ $volume.icon_normal.classList.add("hidden");
+ } else if (volume < 50) {
+ $volume.icon_mute.classList.add("hidden");
+ $volume.icon_low.classList.remove("hidden");
+ $volume.icon_normal.classList.add("hidden");
+ } else {
+ $volume.icon_mute.classList.add("hidden");
+ $volume.icon_low.classList.add("hidden");
+ $volume.icon_normal.classList.remove("hidden");
+ }
+ };
+ const setStatus = (status, wavesurfer) => {
+ switch (status) {
+ case "PLAYING":
+ $control.play.classList.add("hidden");
+ $control.pause.classList.remove("hidden");
+ $control.loading.classList.add("hidden");
+ wavesurfer.backend.ac.resume();
+ break;
+ case "PAUSED":
+ $control.play.classList.remove("hidden");
+ $control.pause.classList.add("hidden");
+ $control.loading.classList.add("hidden");
+ wavesurfer.backend.ac.suspend();
+ break;
+ case "BUFFERING":
+ $control.play.classList.add("hidden");
+ $control.pause.classList.add("hidden");
+ $control.loading.classList.remove("hidden");
+ break;
+ default:
+ assert.fail(status);
+ }
+ }
+ const setSeek = (newTime, wavesurfer) => {
+ currentTime$.next([newTime, wavesurfer.backend.ac.currentTime]);
+ wavesurfer.backend.source.stop(0);
+ wavesurfer.backend.disconnectSource();
+ wavesurfer.backend.createSource();
+ wavesurfer.backend.startPosition = newTime;
+ wavesurfer.backend.source.start(0, newTime);
+ };
// feature1: setup the dom
const setup$ = rxjs.of(qs($page, "#waveform")).pipe(
@@ -85,13 +142,16 @@ export default function(render) {
barWidth: 1,
}))),
rxjs.tap((wavesurfer) => {
- window.audio = wavesurfer; // TODO: remove
wavesurfer.load(getDownloadUrl());
- wavesurfer.on("error", (err) => {
+ wavesurfer.on("error", (err) => { // TODO: one liner to check
throw new Error(err)
});
+ wavesurfer.on("ready", () => {
+ $control.main.classList.remove("hidden");
+ qs($control.main, "#totalDuration").textContent = formatTimecode(wavesurfer.getDuration());
+ });
+ onDestroy(() => wavesurfer.destroy())
}),
- rxjs.tap((wavesurfer) => onDestroy(() => wavesurfer.destroy())),
rxjs.catchError(ctrlError()),
rxjs.shareReplay(1),
);
@@ -115,7 +175,7 @@ export default function(render) {
}),
));
- // feature3: prepare the data source
+ // feature3: connect the audio
const ready$ = setup$.pipe(
rxjs.mergeMap((wavesurfer) => new rxjs.Observable((observer) => {
wavesurfer.on("ready", () => observer.next(wavesurfer));
@@ -127,65 +187,121 @@ export default function(render) {
wavesurfer.backend.startPosition = 0;
wavesurfer.backend.lastPlay = 0;
wavesurfer.backend.source.start(0, 0);
+ wavesurfer.backend.ac.suspend();
})));
- // feature: player control - volume
+ // feature4: hint of song progress
+ effect(setup$.pipe(
+ rxjs.mergeMap(() => rxjs.merge(
+ onClick($control.play).pipe(rxjs.mapTo(STATUS_PLAYING)),
+ rxjs.fromEvent(document, "keydown").pipe(rxjs.mapTo(STATUS_PLAYING)),
+ )),
+ rxjs.first(),
+ rxjs.mergeMap((status) => setup$.pipe(rxjs.tap((wavesurfer) => setStatus(status, wavesurfer)))),
+ rxjs.switchMap((wavesurfer) => rxjs.animationFrames().pipe(
+ rxjs.tap(() => {
+ const _currentTime = currentTime(wavesurfer)
+ const percent = _currentTime / wavesurfer.getDuration();
+ if (percent > 1) return;
+ wavesurfer.drawer.progress(percent);
+ qs($control.main, "#currentTime").textContent = formatTimecode(_currentTime);
+ }),
+ )),
+ ));
+
+ // feature5: player control - play / pause
+ effect(setup$.pipe(
+ rxjs.mergeMap((wavesurfer) => rxjs.merge(
+ onClick($control.play).pipe(rxjs.mapTo(STATUS_PLAYING)),
+ onClick($control.pause).pipe(rxjs.mapTo(STATUS_PAUSED)),
+ ).pipe(
+ rxjs.tap((status) => setStatus(status, wavesurfer)),
+ )),
+ ));
+
+ // feature6: player control - volume
effect(setup$.pipe(
rxjs.switchMap(() => rxjs.fromEvent($volume.range, "input").pipe(rxjs.map((e) => e.target.value))),
rxjs.startWith(settings_get("volume") === null ? 80 : settings_get("volume")),
- rxjs.mergeMap((volume) => setup$.pipe(rxjs.tap((wavesurfer) => setVolume(volume, wavesurfer)))),
+ rxjs.mergeMap((volume) => setup$.pipe(rxjs.tap((wavesurfer) => setVolume(parseInt(volume), wavesurfer)))),
));
- // feature: player control - seek
- effect(setup$.pipe(
+ // feature7: player control - seek
+ effect(ready$.pipe(
rxjs.mergeMap((wavesurfer) => rxjs.fromEvent(qs($page, "#waveform"), "click").pipe(
rxjs.map((e) => ({ e, wavesurfer })),
)),
rxjs.map(({ e, wavesurfer }) => {
- const rec = e.target.getBoundingClientRect();
+ const rec = e.target.closest("#waveform").getBoundingClientRect();
return { wavesurfer, progress: (e.clientX - rec.x) / rec.width };
}),
rxjs.tap(({ progress, wavesurfer }) => {
wavesurfer.drawer.progress(progress);
-
const newTime = wavesurfer.getDuration() * progress;
-
- currentTime$.next([newTime, wavesurfer.backend.ac.currentTime]);
- wavesurfer.backend.source.stop(0);
- wavesurfer.backend.disconnectSource();
- wavesurfer.backend.createSource();
- wavesurfer.backend.startPosition = newTime;
- wavesurfer.backend.source.start(0, newTime);
+ setSeek(newTime, wavesurfer);
}),
));
- // feature4: time progression
+ // feature8: player control - keyboard shortcut
effect(ready$.pipe(
- rxjs.mergeMap(() => rxjs.merge(
- onClick($control.play).pipe(rxjs.mapTo(STATUS_PLAYING)),
- onClick($control.pause).pipe(rxjs.mapTo(STATUS_PAUSED)),
- )),
- rxjs.mergeMap((status) => setup$.pipe(rxjs.tap((wavesurfer) => {
- switch (status) {
- case STATUS_PLAYING:
- wavesurfer.backend.ac.resume();
- $control.play.classList.add("hidden");
- $control.pause.classList.remove("hidden");
- break;
- case STATUS_PAUSED:
- wavesurfer.backend.ac.suspend();
- $control.play.classList.remove("hidden");
- $control.pause.classList.add("hidden");
- break;
- default:
- throw new Error("Unknown state");
- }
- }))),
- rxjs.switchMap((wavesurfer) => rxjs.animationFrames().pipe(
- rxjs.tap(() => {
- const percent = (currentTime$.value[0] + (wavesurfer.backend.ac.currentTime - currentTime$.value[1])) / wavesurfer.getDuration();
- if (percent > 1) return;
- wavesurfer.drawer.progress(percent);
+ rxjs.switchMap((wavesurfer) => rxjs.fromEvent(document, "keydown").pipe(
+ rxjs.map((e) => e.code),
+ rxjs.tap((code) => {
+ switch(code) {
+ case "Space":
+ case "KeyK":
+ setStatus(
+ wavesurfer.backend.ac.state === "suspended" ?
+ STATUS_PLAYING : STATUS_PAUSED,
+ wavesurfer,
+ );
+ break;
+ case "KeyM":
+ setVolume(wavesurfer.getVolume() > 0 ? 0 : settings_get("volume"), wavesurfer);
+ break;
+ case "ArrowUp":
+ setVolume(Math.min(wavesurfer.getVolume()*100 + 10, 100), wavesurfer);
+ break;
+ case "ArrowDown":
+ setVolume(Math.max(wavesurfer.getVolume()*100 - 10, 0), wavesurfer);
+ break;
+ case "KeyL":
+ setSeek(Math.min(wavesurfer.getDuration(), currentTime(wavesurfer) + 10), wavesurfer);
+ break;
+ case "KeyJ":
+ setSeek(Math.max(0, currentTime(wavesurfer) - 10), wavesurfer);
+ break;
+ case "Digit0":
+ setSeek(0, wavesurfer);
+ break;
+ case "Digit1":
+ setSeek(wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit2":
+ setSeek(2 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit3":
+ setSeek(3 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit4":
+ setSeek(4 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit5":
+ setSeek(5 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit6":
+ setSeek(6 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit7":
+ setSeek(7 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit8":
+ setSeek(8 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ case "Digit9":
+ setSeek(9 * wavesurfer.getDuration() / 10, wavesurfer);
+ break;
+ }
}),
)),
));
diff --git a/public/assets/pages/viewerpage/application_ebook.js b/public/assets/pages/viewerpage/application_ebook.js
index 751e8424..e9dd8536 100644
--- a/public/assets/pages/viewerpage/application_ebook.js
+++ b/public/assets/pages/viewerpage/application_ebook.js
@@ -28,7 +28,8 @@ export default function(render) {
replacements: "blobUrl",
});
const rendition = book.renderTo($epub, {
- height: "100%", width: "100%",
+ height: "100%",
+ width: "100%",
flow: "scrolled-doc",
method: "continuous",
allowScriptedContent: false,
diff --git a/public/assets/pages/viewerpage/application_editor.js b/public/assets/pages/viewerpage/application_editor.js
index c96046e8..3f9ce915 100644
--- a/public/assets/pages/viewerpage/application_editor.js
+++ b/public/assets/pages/viewerpage/application_editor.js
@@ -17,11 +17,13 @@ export default async function(render) {
const $page = createElement(`
`);
render($page);
+ const $editor = qs($page, ".component_editor");
+ const $menubar = qs($page, "component-menubar");
const content$ = ajax(getDownloadUrl()).pipe(
rxjs.map(({ response }) => response),
rxjs.shareReplay(),
@@ -33,7 +35,7 @@ export default async function(render) {
ajax("/about").pipe(rxjs.delay(TIME_BEFORE_ABORT_EDIT), rxjs.map(() => null)),
).pipe(
rxjs.mergeMap((content) => {
- if (content === null || has_binary(content)) {
+ // if (content === null || has_binary(content)) {
return rxjs.from(initDownloader()).pipe(
removeLoader,
rxjs.mergeMap(() => {
@@ -41,12 +43,13 @@ export default async function(render) {
return rxjs.EMPTY;
}),
);
- }
+ // }
return rxjs.of(content);
}),
removeLoader,
rxjs.tap((content) => {
- window.CodeMirror(qs($page, ".component_editor"), {
+ $editor.classList.remove("hidden");
+ window.CodeMirror($editor, {
value: content,
lineNumbers: true,
// mode: mode,
@@ -62,8 +65,8 @@ export default async function(render) {
matchTags: { bothTags: true },
autoCloseTags: true,
});
- transition(qs($page, ".component_editor"));
- qs($page, "component-menubar").classList.remove("hidden");
+ transition($editor);
+ $menubar.classList.remove("hidden");
}),
rxjs.catchError(ctrlError()),
))
diff --git a/public/assets/pages/viewerpage/application_form.js b/public/assets/pages/viewerpage/application_form.js
index 3cb1b987..0893f06b 100644
--- a/public/assets/pages/viewerpage/application_form.js
+++ b/public/assets/pages/viewerpage/application_form.js
@@ -18,21 +18,21 @@ export default function(render) {
const $page = createElement(`
-
`);
render($page);
- transition(qs($page, ".formviewer_container"));
- const file$ = new rxjs.ReplaySubject(1);
+ const $container = qs($page, ".formviewer_container");
const $fab = qs($page, `[is="component-fab"]`);
const formState = () => [...new FormData(qs($page, "form"))].reduce((acc, el) => {
acc[el[0]] = el[1];
return acc;
}, {});
+ const file$ = new rxjs.ReplaySubject(1);
// feature1: setup the dom
effect(getFile$().pipe(
@@ -55,6 +55,10 @@ export default function(render) {
`),
}))).pipe(
applyMutation(qs($page, "form"), "replaceChildren"),
+ rxjs.tap(() => {
+ $container.classList.remove("hidden");
+ transition($container);
+ }),
rxjs.mapTo(formSpec),
)),
rxjs.tap((formSpec) => file$.next(formObjToJSON(formSpec))),
diff --git a/public/assets/pages/viewerpage/application_iframe.css b/public/assets/pages/viewerpage/application_iframe.css
index 022d74c4..c739e7e2 100644
--- a/public/assets/pages/viewerpage/application_iframe.css
+++ b/public/assets/pages/viewerpage/application_iframe.css
@@ -6,7 +6,6 @@ body:not(.dark-mode) .component_appframe {
text-align: center;
width: 100%;
}
-
.component_appframe iframe {
width: 100%;
height: 100%;
diff --git a/public/assets/pages/viewerpage/application_iframe.js b/public/assets/pages/viewerpage/application_iframe.js
index 7610f361..d1ee573f 100644
--- a/public/assets/pages/viewerpage/application_iframe.js
+++ b/public/assets/pages/viewerpage/application_iframe.js
@@ -1,17 +1,42 @@
import { createElement } from "../../lib/skeleton/index.js";
+import rxjs, { effect } from "../../lib/rx.js";
+import { loadCSS } from "../../helpers/loader.js";
import assert from "../../lib/assert.js";
+import ctrlError from "../ctrl_error.js";
+
export default function(render, opts = {}) {
const { endpoint = null } = opts;
-
- assert.truthy(endpoint);
const $page = createElement(`
`);
render($page);
+
+ effect(rxjs.fromEvent(window, "message").pipe(
+ rxjs.filter((event) => event.origin === location.origin),
+ rxjs.tap((event) => { // TODO: notification
+ switch(event.data.type) {
+ case "notify::info":
+ notify.send(t(event.data.message), "info");
+ break;
+ case "notify::error":
+ notify.send(t(event.data.message), "error");
+ break
+ }
+ }),
+ ));
+
+ effect(rxjs.of(assert.truthy(endpoint)).pipe(
+ rxjs.catchError(ctrlError()),
+ ));
+}
+
+export function init() {
+ return loadCSS(import.meta.url, "./application_iframe.css");
+}
+
+function getFrameUrl(endpoint) {
+ return endpoint + "?path=/home/mickael/Downloads/office.docx" + "&share=";
}
diff --git a/public/assets/pages/viewerpage/application_image.css b/public/assets/pages/viewerpage/application_image.css
index 1c8b06a2..1f1f51da 100644
--- a/public/assets/pages/viewerpage/application_image.css
+++ b/public/assets/pages/viewerpage/application_image.css
@@ -1,8 +1,9 @@
-body:not(.dark-mode) .component_imageviewer .component_image_container, body:not(.dark-mode) .component_imageviewer .component_image_container .fullscreen .component_pager .wrapper > span {
+.component_imageviewer .component_image_container,
+body:not(.dark-mode) .component_imageviewer .component_image_container .fullscreen .component_pager .wrapper > span {
background: #525659;
}
-.component_imageviewer, .component_imageviewer .component_image_container > .images_wrapper, .component_imageviewer .component_image_container > .images_wrapper > span, .component_imageviewer .component_image_container > .images_wrapper > span > div {
+.component_imageviewer, .component_imageviewer .component_image_container > .images_wrapper {
flex: 1;
display: flex;
overflow: hidden;
@@ -27,7 +28,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
.component_imageviewer .component_image_container.fullscreen .component_pager .wrapper > span {
background: var(--dark);
}
-.component_imageviewer .component_image_container.fullscreen img[srcset] {
+.component_imageviewer .component_image_container.fullscreen img.photo {
background: var(--color);
}
@media screen and (max-height: 410px) {
@@ -162,7 +163,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
.component_imageviewer .component_image_container .images_aside .content .more_container .meta_key .value {
color: var(--bg-color);
}
-.component_imageviewer .component_image_container img[srcset] {
+.component_imageviewer .component_image_container img.photo:not(.error) {
margin: auto;
max-height: 100%;
max-width: 100%;
@@ -171,7 +172,7 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
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;
border-radius: 2px;
}
-.component_imageviewer .component_image_container img[srcset].idle {
+.component_imageviewer .component_image_container img.photo.idle {
transition: 0.2s ease transform;
}
@@ -196,7 +197,6 @@ body:not(.dark-mode) .component_imageviewer .component_image_container, body:not
margin-top: 10px;
}
-
.component_imageviewer .component_image_container img.error {
filter: contrast(0.8);
width: 160px;
diff --git a/public/assets/pages/viewerpage/application_video.js b/public/assets/pages/viewerpage/application_video.js
index de8bca2e..fd7791fd 100644
--- a/public/assets/pages/viewerpage/application_video.js
+++ b/public/assets/pages/viewerpage/application_video.js
@@ -11,6 +11,7 @@ import Hls from "../../lib/vendor/hlsjs/hls.js";
import ctrlError from "../ctrl_error.js";
import { transition, getDownloadUrl } from "./common.js";
+import { formatTimecode } from "./common_player.js";
import { ICON } from "./common_icon.js";
import { menubarDownload, buildMenubar } from "./common_menubar.js";
import { render as renderMenubar } from "../../components/menubar.js";
@@ -367,9 +368,3 @@ export function init() {
if (typeof window.overrides["video-map-sources"] !== "function") window.overrides["video-map-sources"] = (s) => (s);
});
}
-
-function formatTimecode(seconds) {
- return String(parseInt(seconds / 60)).padStart(2, "0") +
- ":"+
- String(parseInt(seconds % 60)).padStart(2, "0");
-}
diff --git a/public/assets/pages/viewerpage/common_player.js b/public/assets/pages/viewerpage/common_player.js
new file mode 100644
index 00000000..cb67aefb
--- /dev/null
+++ b/public/assets/pages/viewerpage/common_player.js
@@ -0,0 +1,7 @@
+export function formatTimecode(seconds) {
+ return String(parseInt(seconds / 60)).padStart(2, "0") +
+ ":"+
+ String(parseInt(seconds % 60)).padStart(2, "0");
+}
+
+// TODO: abstract setVolume, setSeek and setStatus