diff --git a/.gitignore b/.gitignore index 2c98bc2..fbd8f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ gitea-runner/nohup.out # Rust build artifacts **/target/ +# Generated WASM output +vue/src/wasm/ + # Logs logs *.log diff --git a/docker-compose.yml b/docker-compose.yml index f0187c2..3834d61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,7 @@ services: - backend - icecast2 - gitea + - hasura - quartz - searxng networks: @@ -112,8 +113,6 @@ services: image: hasura/graphql-engine:v2.44.0 container_name: "${HASURA_HOST}" restart: always - profiles: - - disabled depends_on: - db networks: diff --git a/vue/Dockerfile b/vue/Dockerfile index 2d0ba90..86e9e37 100644 --- a/vue/Dockerfile +++ b/vue/Dockerfile @@ -1,7 +1,17 @@ +# Stage 1: Build WASM from Rust +FROM rust:slim AS wasm-builder +RUN rustup target add wasm32-unknown-unknown \ + && cargo install wasm-pack +WORKDIR /wasm +COPY crates/stp_wasm/ crates/stp_wasm/ +RUN wasm-pack build crates/stp_wasm --target web --out-dir ../../src/wasm + +# Stage 2: Build Vue frontend FROM node:22-slim RUN apt-get update && apt-get install -y make git && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . -CMD ["sh", "-c", "npm run build -- --outDir /output --emptyOutDir"] +COPY --from=wasm-builder /wasm/src/wasm/ src/wasm/ +CMD ["sh", "-c", "npx vite build --outDir /output --emptyOutDir"] diff --git a/vue/crates/stp_wasm/Cargo.toml b/vue/crates/stp_wasm/Cargo.toml index ec356bd..63a25fb 100644 --- a/vue/crates/stp_wasm/Cargo.toml +++ b/vue/crates/stp_wasm/Cargo.toml @@ -3,6 +3,9 @@ name = "stp_wasm" version = "0.1.0" edition = "2024" +[lib] +crate-type = ["cdylib"] + [dependencies] js-sys = "0.3.85" wasm-bindgen = "0.2.108" @@ -10,6 +13,13 @@ web-sys = { version = "0.3.85", features = [ "console", "Document", "Element", + "HtmlElement", "Window", "Animation", + "CssStyleDeclaration", + "ResizeObserver", + "ResizeObserverEntry", + "ResizeObserverSize", + "EventTarget", + "MouseEvent", ] } diff --git a/vue/crates/stp_wasm/src/auto_scroll.rs b/vue/crates/stp_wasm/src/auto_scroll.rs new file mode 100644 index 0000000..85bb98d --- /dev/null +++ b/vue/crates/stp_wasm/src/auto_scroll.rs @@ -0,0 +1,259 @@ +use std::cell::RefCell; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use web_sys::HtmlElement; + +const SPEED: f64 = 0.0005; // % per frame +const PAUSE: i32 = 2000; // ms at top/bottom + +struct Inner { + el: HtmlElement, + pos: f64, + direction: f64, // 1.0 = down, -1.0 = up + hovered: bool, + cached_scroll_height: f64, + raf_id: Option, + pause_timeout_id: Option, + resize_observer: Option, + // closures kept alive + tick_closure: Option>, + resize_closure: Option>, + mouseenter_closure: Option>, + mouseleave_closure: Option>, + start_after_pause_closure: Option>, +} + +#[wasm_bindgen] +pub struct AutoScroller { + inner: Rc>, +} + +impl Inner { + fn measure_scroll_height(&mut self) { + self.cached_scroll_height = self.el.scroll_height() as f64; + } + + fn stop_loop(&mut self) { + let window = web_sys::window().unwrap(); + if let Some(id) = self.raf_id.take() { + window.cancel_animation_frame(id).ok(); + } + if let Some(id) = self.pause_timeout_id.take() { + window.clear_timeout_with_handle(id); + } + } +} + +fn start_loop(inner: &Rc>) { + { + let mut s = inner.borrow_mut(); + s.stop_loop(); + } + schedule_tick(inner); +} + +fn schedule_tick(inner: &Rc>) { + let inner_clone = Rc::clone(inner); + + // Create a fresh tick closure each frame + let closure = Closure::once(move || { + tick(&inner_clone); + }); + + let mut s = inner.borrow_mut(); + let window = web_sys::window().unwrap(); + let id = window + .request_animation_frame(closure.as_ref().unchecked_ref()) + .unwrap(); + s.raf_id = Some(id); + // Store the closure to keep it alive until the frame fires + s.tick_closure = Some(closure); +} + +fn tick(inner: &Rc>) { + let should_continue; + { + let mut s = inner.borrow_mut(); + s.raf_id = None; + + if s.hovered { + return; + } + + if s.cached_scroll_height == 0.0 { + drop(s); + schedule_tick(inner); + return; + } + + let reached_bottom = s.pos >= 1.0; + let reached_top = s.pos <= 0.0; + + if reached_bottom { + s.pos = 0.999; + s.direction = -1.0; + drop(s); + schedule_pause(inner); + return; + } else if reached_top && s.direction == -1.0 { + s.pos = 0.001; + s.direction = 1.0; + drop(s); + schedule_pause(inner); + return; + } + + s.pos += s.direction * SPEED; + s.el.set_scroll_top((s.pos * s.cached_scroll_height) as i32); + should_continue = true; + } + + if should_continue { + schedule_tick(inner); + } +} + +fn schedule_pause(inner: &Rc>) { + { + let mut s = inner.borrow_mut(); + s.stop_loop(); + } + + let inner_clone = Rc::clone(inner); + let closure = Closure::once(move || { + start_loop(&inner_clone); + }); + + let mut s = inner.borrow_mut(); + let window = web_sys::window().unwrap(); + let id = window + .set_timeout_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + PAUSE, + ) + .unwrap(); + s.pause_timeout_id = Some(id); + s.start_after_pause_closure = Some(closure); +} + +#[wasm_bindgen] +impl AutoScroller { + #[wasm_bindgen(constructor)] + pub fn new(el: HtmlElement) -> AutoScroller { + let inner = Rc::new(RefCell::new(Inner { + el, + pos: 0.0, + direction: 1.0, + hovered: false, + cached_scroll_height: 0.0, + raf_id: None, + pause_timeout_id: None, + resize_observer: None, + tick_closure: None, + resize_closure: None, + mouseenter_closure: None, + mouseleave_closure: None, + start_after_pause_closure: None, + })); + + // Set up mouseenter listener + { + let inner_clone = Rc::clone(&inner); + let closure = Closure::wrap(Box::new(move || { + let mut s = inner_clone.borrow_mut(); + s.hovered = true; + s.stop_loop(); + }) as Box); + let s = inner.borrow(); + s.el + .add_event_listener_with_callback("mouseenter", closure.as_ref().unchecked_ref()) + .unwrap(); + drop(s); + inner.borrow_mut().mouseenter_closure = Some(closure); + } + + // Set up mouseleave listener + { + let inner_clone = Rc::clone(&inner); + let closure = Closure::wrap(Box::new(move || { + { + let mut s = inner_clone.borrow_mut(); + s.hovered = false; + if s.cached_scroll_height > 0.0 { + s.pos = s.el.scroll_top() as f64 / s.cached_scroll_height; + } + } + start_loop(&inner_clone); + }) as Box); + let s = inner.borrow(); + s.el + .add_event_listener_with_callback("mouseleave", closure.as_ref().unchecked_ref()) + .unwrap(); + drop(s); + inner.borrow_mut().mouseleave_closure = Some(closure); + } + + AutoScroller { inner } + } + + pub fn start(&self) { + // Measure initial scroll height + self.inner.borrow_mut().measure_scroll_height(); + + // Set up resize observer + let inner_clone = Rc::clone(&self.inner); + let resize_closure = Closure::wrap(Box::new(move |_entries: js_sys::Array| { + inner_clone.borrow_mut().measure_scroll_height(); + }) as Box); + + let observer = + web_sys::ResizeObserver::new(resize_closure.as_ref().unchecked_ref()).unwrap(); + + // Clone the element ref and drop the borrow before calling observe(), + // because observe() can fire the resize callback synchronously, + // which would conflict with an active borrow. + let el_clone = self.inner.borrow().el.clone(); + observer.observe(&el_clone); + + { + let mut s = self.inner.borrow_mut(); + s.resize_observer = Some(observer); + s.resize_closure = Some(resize_closure); + } + + // Start with a pause then begin scrolling + schedule_pause(&self.inner); + } + + pub fn destroy(&self) { + let mut s = self.inner.borrow_mut(); + s.stop_loop(); + + if let Some(observer) = s.resize_observer.take() { + observer.disconnect(); + } + + if let Some(ref closure) = s.mouseenter_closure { + s.el + .remove_event_listener_with_callback( + "mouseenter", + closure.as_ref().unchecked_ref(), + ) + .ok(); + } + if let Some(ref closure) = s.mouseleave_closure { + s.el + .remove_event_listener_with_callback( + "mouseleave", + closure.as_ref().unchecked_ref(), + ) + .ok(); + } + + s.mouseenter_closure = None; + s.mouseleave_closure = None; + s.resize_closure = None; + s.tick_closure = None; + s.start_after_pause_closure = None; + } +} diff --git a/vue/crates/stp_wasm/src/headline.rs b/vue/crates/stp_wasm/src/headline.rs new file mode 100644 index 0000000..b43aedf --- /dev/null +++ b/vue/crates/stp_wasm/src/headline.rs @@ -0,0 +1,135 @@ +use std::cell::RefCell; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use web_sys::HtmlElement; + +const SPEED: f64 = 0.5; // pixels per frame + +struct Inner { + container: HtmlElement, + item1: HtmlElement, + offset: f64, + cached_width: f64, + raf_id: Option, + resize_observer: Option, + // closures kept alive + animate_closure: Option>, + resize_closure: Option>, +} + +#[wasm_bindgen] +pub struct HeadlineScroller { + inner: Rc>, +} + +impl Inner { + fn measure_width(&mut self) { + let container_width = self.container.offset_width() as f64; + let item_width = self.item1.scroll_width() as f64; + self.cached_width = container_width.max(item_width); + } +} + +fn schedule_frame(inner: &Rc>) { + let inner_clone = Rc::clone(inner); + + let closure = Closure::once(move || { + animate(&inner_clone); + }); + + let mut s = inner.borrow_mut(); + let window = web_sys::window().unwrap(); + let id = window + .request_animation_frame(closure.as_ref().unchecked_ref()) + .unwrap(); + s.raf_id = Some(id); + s.animate_closure = Some(closure); +} + +fn animate(inner: &Rc>) { + { + let mut s = inner.borrow_mut(); + s.raf_id = None; + + if s.cached_width == 0.0 { + drop(s); + schedule_frame(inner); + return; + } + + s.offset -= SPEED; + + if s.offset <= -s.cached_width { + s.offset += s.cached_width; + } + + let transform = format!("translateX({}px)", s.offset); + s.container.style().set_property("transform", &transform).ok(); + } + + schedule_frame(inner); +} + +#[wasm_bindgen] +impl HeadlineScroller { + #[wasm_bindgen(constructor)] + pub fn new(container: HtmlElement, item1: HtmlElement) -> HeadlineScroller { + let inner = Rc::new(RefCell::new(Inner { + container, + item1, + offset: 0.0, + cached_width: 0.0, + raf_id: None, + resize_observer: None, + animate_closure: None, + resize_closure: None, + })); + + HeadlineScroller { inner } + } + + pub fn start(&self) { + // Measure initial width + self.inner.borrow_mut().measure_width(); + + // Set up resize observer + let inner_clone = Rc::clone(&self.inner); + let resize_closure = Closure::wrap(Box::new(move |_entries: js_sys::Array| { + inner_clone.borrow_mut().measure_width(); + }) as Box); + + let observer = + web_sys::ResizeObserver::new(resize_closure.as_ref().unchecked_ref()).unwrap(); + + // Clone the element ref and drop the borrow before calling observe(), + // because observe() can fire the resize callback synchronously, + // which would conflict with an active borrow. + let container_clone = self.inner.borrow().container.clone(); + observer.observe(&container_clone); + + { + let mut s = self.inner.borrow_mut(); + s.resize_observer = Some(observer); + s.resize_closure = Some(resize_closure); + } + + // Start animation loop + schedule_frame(&self.inner); + } + + pub fn destroy(&self) { + let mut s = self.inner.borrow_mut(); + + let window = web_sys::window().unwrap(); + if let Some(id) = s.raf_id.take() { + window.cancel_animation_frame(id).ok(); + } + + if let Some(observer) = s.resize_observer.take() { + observer.disconnect(); + } + + s.animate_closure = None; + s.resize_closure = None; + } +} diff --git a/vue/crates/stp_wasm/src/lib.rs b/vue/crates/stp_wasm/src/lib.rs index 957c5ac..f968869 100644 --- a/vue/crates/stp_wasm/src/lib.rs +++ b/vue/crates/stp_wasm/src/lib.rs @@ -1,6 +1,5 @@ -use wasm_bindgen::prelude::*; +mod auto_scroll; +mod headline; -#[wasm_bindgen] -pub struct BadApplePlayer { - is_playing: bool, -} +pub use auto_scroll::AutoScroller; +pub use headline::HeadlineScroller; diff --git a/vue/package-lock.json b/vue/package-lock.json index e7a79d6..f493576 100644 --- a/vue/package-lock.json +++ b/vue/package-lock.json @@ -25,7 +25,9 @@ "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", "vite": "^7.1.11", - "vite-plugin-vue-devtools": "^8.0.3" + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-vue-devtools": "^8.0.3", + "vite-plugin-wasm": "^3.6.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1024,6 +1026,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -1349,6 +1369,293 @@ "win32" ] }, + "node_modules/@swc/core": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz", + "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.24", + "@swc/core-darwin-x64": "1.15.24", + "@swc/core-linux-arm-gnueabihf": "1.15.24", + "@swc/core-linux-arm64-gnu": "1.15.24", + "@swc/core-linux-arm64-musl": "1.15.24", + "@swc/core-linux-ppc64-gnu": "1.15.24", + "@swc/core-linux-s390x-gnu": "1.15.24", + "@swc/core-linux-x64-gnu": "1.15.24", + "@swc/core-linux-x64-musl": "1.15.24", + "@swc/core-win32-arm64-msvc": "1.15.24", + "@swc/core-win32-ia32-msvc": "1.15.24", + "@swc/core-win32-x64-msvc": "1.15.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz", + "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz", + "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz", + "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz", + "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz", + "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz", + "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz", + "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz", + "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz", + "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz", + "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz", + "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz", + "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@swc/wasm": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.15.24.tgz", + "integrity": "sha512-vFjzOE8dhJcfeTbM4+HO9Qy58IINV0ysqStAgw81uds+KqCeUDM9huN+SZ5lWZ6U+5nf8VcZoEw5N81xMtAidg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@tailwindcss/node": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", @@ -3518,6 +3825,20 @@ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "license": "(WTFPL OR MIT)" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3661,6 +3982,22 @@ "dev": true, "license": "MIT" }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz", + "integrity": "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.12.14", + "@swc/wasm": "^1.12.14", + "uuid": "10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, "node_modules/vite-plugin-vue-devtools": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.5.tgz", @@ -3736,6 +4073,16 @@ "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, "node_modules/vue": { "version": "3.5.26", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", diff --git a/vue/package.json b/vue/package.json index 7699a2d..3e58b28 100644 --- a/vue/package.json +++ b/vue/package.json @@ -7,8 +7,9 @@ "node": "^20.19.0 || >=22.12.0" }, "scripts": { + "build:wasm": "wasm-pack build crates/stp_wasm --target web --out-dir ../../src/wasm", "dev": "vite", - "build": "vite build", + "build": "npm run build:wasm && vite build", "preview": "vite preview" }, "dependencies": { @@ -29,6 +30,8 @@ "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", "vite": "^7.1.11", - "vite-plugin-vue-devtools": "^8.0.3" + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-vue-devtools": "^8.0.3", + "vite-plugin-wasm": "^3.6.0" } } diff --git a/vue/src/components/text/Headline.vue b/vue/src/components/text/Headline.vue index 1325378..eb4c4be 100644 --- a/vue/src/components/text/Headline.vue +++ b/vue/src/components/text/Headline.vue @@ -1,55 +1,22 @@ diff --git a/vue/src/components/text/ToggleHeader.vue b/vue/src/components/text/ToggleHeader.vue index 5922854..223455a 100644 --- a/vue/src/components/text/ToggleHeader.vue +++ b/vue/src/components/text/ToggleHeader.vue @@ -31,6 +31,7 @@ const handleClick = () => { class="pointer-events-none" :model-value="props.modelValue" @update:model-value="updateValue" + @click.stop ref="toggleButtonRef" /> diff --git a/vue/src/components/util/AutoScroll.vue b/vue/src/components/util/AutoScroll.vue index bfceea4..ea9fd0c 100644 --- a/vue/src/components/util/AutoScroll.vue +++ b/vue/src/components/util/AutoScroll.vue @@ -1,108 +1,26 @@ diff --git a/vue/src/main.js b/vue/src/main.js index b8fb667..0944289 100644 --- a/vue/src/main.js +++ b/vue/src/main.js @@ -3,6 +3,9 @@ import { createPinia } from "pinia"; import App from "./App.vue"; import router from "./router"; import "./assets/styles.css"; +import init from "@/wasm/stp_wasm.js"; + +await init(); const app = createApp(App); diff --git a/vue/src/router/index.js b/vue/src/router/index.js index 6fa3ce8..684b5dc 100644 --- a/vue/src/router/index.js +++ b/vue/src/router/index.js @@ -34,11 +34,6 @@ const router = createRouter({ component: () => import("@/views/admin/Admin.vue"), meta: { requiresAdmin: true }, }, - { - path: "bookmarks", - name: "bookmarks", - component: () => import("@/views/home/bookmarks/Bookmarks.vue"), - }, { path: "shrines", name: "shrine links", diff --git a/vue/src/views/home/Home.vue b/vue/src/views/home/Home.vue index 8f562ef..52deccb 100644 --- a/vue/src/views/home/Home.vue +++ b/vue/src/views/home/Home.vue @@ -21,6 +21,7 @@ import Favorites from "./Favorites.vue"; import Gym2 from "./Gym2.vue"; import Consumption from "./Consumption.vue"; import Steam from "./Steam.vue"; +import Bookmarks from "./bookmarks/Bookmarks.vue";