aboutsummaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2025-11-17 09:57:09 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2025-11-17 09:57:09 +0100
commite95cf5c7b6a08eb560763d5167fbddc1c2117bcc (patch)
tree2f7815c9f39328fcaced2113de727f63e4837fa3 /static
parent0016aaa197697ec5ff38dfb3f63ac8b6f74b48e0 (diff)
add file uploading and multi-threading
Diffstat (limited to 'static')
-rw-r--r--static/api/images.js35
-rw-r--r--static/index.js48
-rw-r--r--static/pages/image-viewer/index.js4
-rw-r--r--static/widgets/upload-bar/index.css41
-rw-r--r--static/widgets/upload-bar/index.js42
5 files changed, 145 insertions, 25 deletions
diff --git a/static/api/images.js b/static/api/images.js
index 4a41a3c..4bdac1f 100644
--- a/static/api/images.js
+++ b/static/api/images.js
@@ -1,31 +1,29 @@
import * as sfw from 'sfw';
const { Input } = sfw.element.native;
+import * as rest from './rest.js';
class FileUploader {
- constructor(url) {
+ constructor(url, files) {
this.onprogress = () => {}
this.ondone = () => {}
this.url = url;
this.sessions = [];
+ this.files = files;
}
- send(...files) {
+ send() {
let count = 0;
- this.sessions = this.sessions.concat(files.map(
- file => new Promise((resolve) => {
+ this.sessions = this.sessions.concat(this.files.map(
+ (file) => new Promise((resolve) => {
const xhr = new XMLHttpRequest();
- xhr.upload.addEventListener("progress", (event) => {
- if (event.lengthComputable) {
- this.onprogress(file, event.loaded, event.total)
- }
- });
-
- xhr.addEventListener("loadend", () => {
+ xhr.addEventListener("loadend", (event) => {
count += 1;
+ this.onprogress(count, this.files.length)
+
resolve(xhr.readyState === 4 && xhr.status === 200);
- if (count == files.length) {
+ if (count == this.files.length) {
this.ondone();
}
});
@@ -46,11 +44,12 @@ export async function upload_to_timeline() {
})
input.click();
- const uploader = new FileUploader('/api/image/upload');
- input.onchange = async () => {
- uploader.send(...input.files);
- }
+ return new Promise((resolve) => {
+ input.onchange = () => {
+ resolve(new FileUploader('/api/image/upload', [...input.files]));
+ }
+ })
}
export async function upload_to_profile() {
@@ -61,3 +60,7 @@ export async function upload_to_profile() {
})
input.click();
}
+
+export function list() {
+ return rest.get('/api/image/list').then(r => r.images);
+}
diff --git a/static/index.js b/static/index.js
index 90b5ffd..2fcca88 100644
--- a/static/index.js
+++ b/static/index.js
@@ -11,14 +11,25 @@ import Search from './widgets/search/index.js';
import MonthSelect from './widgets/month-select/index.js';
import SettingsView from './pages/settings/index.js';
import ShuffleView from './pages/shuffle/index.js';
+import UploadBar from './widgets/upload-bar/index.js';
sfw.theme.add_css(await sfw.css(import.meta.url, './index.css'));
const image_viewer = ImageViewer.new();
+const reload = () => {
+ image_viewer.clear();
+ api.images.list().then(images => {
+ for (const image of images) {
+ image_viewer.add(`/api/image/load/${image.id}`);
+ }
+ });
+}
+
const login = LoginView.new({
onlogin: async (user, password) => {
if (await api.auth.login(user, password)) {
+ reload();
login.hide();
} else {
login.comment = 'Incorrect username or password.';
@@ -38,10 +49,13 @@ const search = Search.new({
onsubmit: (content) => console.log(content),
onhide: () => main.show(),
});
+
const month_select = MonthSelect.new({
months: m`2019-08`.to(m`2025-11`),
});
+const upload_bar = UploadBar.new();
+
const settings = SettingsView.new({
onlogout: () => {
login.show();
@@ -49,8 +63,7 @@ const settings = SettingsView.new({
},
});
-const shuffle = ShuffleView.new({
-});
+const shuffle = ShuffleView.new({ });
const main = MainView.new({
active_view: image_viewer,
@@ -66,7 +79,23 @@ const main = MainView.new({
main.active_view = image_viewer;
month_select.show();
},
- onupload: () => api.images.upload_to_timeline(),
+ onupload: async () => {
+ const uploader = await api.images.upload_to_timeline();
+
+ upload_bar.progress = 0;
+ upload_bar.show();
+
+ uploader.onprogress = (count, total) => {
+ upload_bar.progress = count / total;
+ }
+
+ uploader.ondone = () => {
+ upload_bar.hide();
+ reload();
+ }
+
+ uploader.send();
+ },
onshuffle: () => {
main.active_kind = MainView.Kind.home;
main.active_view = shuffle;
@@ -86,12 +115,13 @@ document.body.append(
main,
search,
month_select,
+ upload_bar,
);
-login.hide();
-//if (await api.session.is_valid()) {
-// login.hide();
-//} else {
-// login.focus();
-//}
+if (await api.session.is_valid()) {
+ login.hide();
+ reload();
+} else {
+ login.focus();
+}
diff --git a/static/pages/image-viewer/index.js b/static/pages/image-viewer/index.js
index 76d720f..f65c12b 100644
--- a/static/pages/image-viewer/index.js
+++ b/static/pages/image-viewer/index.js
@@ -17,4 +17,8 @@ export default class ImageViewer extends sfw.element.Container {
add(url) {
this.#container.append(Img.new({ src: url }));
}
+
+ clear() {
+ this.#container.innerHTML = '';
+ }
}
diff --git a/static/widgets/upload-bar/index.css b/static/widgets/upload-bar/index.css
new file mode 100644
index 0000000..8ec1697
--- /dev/null
+++ b/static/widgets/upload-bar/index.css
@@ -0,0 +1,41 @@
+
+#container {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: calc(100% - 10px);
+ height: 100px;
+ background: #efefef99;
+ backdrop-filter: blur(10px);
+ border-radius: var(--border-radius);
+ max-width: 500px;
+ padding: 10px 20px;
+ display: grid;
+}
+
+#files {
+ background: #efefef;
+ padding: 10px;
+ border-radius: 20px;
+ margin: auto;
+ width: min-content;
+ white-space: nowrap;
+}
+
+#inner-progress {
+ width: 100%;
+ height: 5px;
+ border-radius: 5px;
+ background: #efefef;
+ overflow: hidden;
+ margin: auto;
+}
+
+
+#progress {
+ background: var(--primary);
+ height: 100%;
+ border-radius: 5px;
+ transition: width 0.2s ease;
+}
diff --git a/static/widgets/upload-bar/index.js b/static/widgets/upload-bar/index.js
new file mode 100644
index 0000000..a9e0220
--- /dev/null
+++ b/static/widgets/upload-bar/index.js
@@ -0,0 +1,42 @@
+import * as sfw from 'sfw';
+const { Div } = sfw.element.native;
+
+const css = await sfw.css(import.meta.url, './index.css')
+
+export default class UploadBar extends sfw.element.Container {
+ #files_progress
+ #inner_progress
+ #container
+
+ constructor() {
+ super({ css });
+
+ this.body.append(
+ this.#container = Div.new({
+ id: 'container',
+ style: { display: 'none' },
+ children: [
+ this.#files_progress = Div.new({ id: 'files', innerText: 'Uploading...' }),
+ Div.new({
+ id: 'inner-progress',
+ children: [
+ this.#inner_progress = Div.new({ id: 'progress' }),
+ ]
+ })
+ ]
+ })
+ );
+ }
+
+ set progress(progress) {
+ this.#inner_progress.style.width = `${progress * 100}%`
+ }
+
+ hide() {
+ this.#container.style.display = 'none';
+ }
+
+ show() {
+ this.#container.style.display = '';
+ }
+}