feat(web): persist browser state

Keep settings, aliases, and recent drawings across browser sessions.

Load Start-page videos from a Pages-generated snapshot.
This commit is contained in:
Hakan Seven committed 2026-07-29 00:44:00 +03:00
1 parent 25166b9508
commit d4e792b2bc
15 files changed
+546 -88

No files matched your search

+85 -8
View File
@@ -1,11 +1,8 @@
//! Start-page tutorial-video list, sourced live from the official YouTube
//! playlist at boot — nothing is baked into the binary or the installer.
//! Start-page tutorial-video list, sourced from the official YouTube playlist.
//!
//! The playlist page is scanned for its ordered video ids, each id's title
//! comes from the stable key-less oEmbed endpoint, and each thumbnail is
//! downloaded once and cached under the user config directory
//! (`video_thumbs/<id>.jpg`), so later launches show the list instantly and
//! offline launches still show everything fetched before.
//! Native builds scan the playlist at boot and cache the result. Web builds
//! read a same-origin `videos.json` and thumbnails generated by the Pages
//! workflow, avoiding YouTube's browser CORS restrictions.
/// The official tutorials playlist.
pub const PLAYLIST_URL: &str =
@@ -14,11 +11,12 @@ pub const PLAYLIST_URL: &str =
const PLAYLIST_ID: &str = "PLZq_TEkIFh9bAnoOX1HiCAunm3anZDBOl";
/// One playlist entry, ready for the Start page.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, serde::Deserialize)]
pub struct VideoEntry {
pub id: String,
pub title: String,
/// JPEG bytes of the thumbnail (from cache or the network), if available.
#[serde(default)]
pub thumb: Option<Vec<u8>>,
}
@@ -173,3 +171,82 @@ pub fn fetch_playlist() -> Result<Vec<VideoEntry>, String> {
}
Ok(out)
}
#[cfg(target_arch = "wasm32")]
pub async fn fetch_playlist_web() -> Result<Vec<VideoEntry>, String> {
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
let window = web_sys::window().ok_or("no window")?;
let response = JsFuture::from(window.fetch_with_str("videos.json"))
.await
.map_err(|_| "videos.json fetch failed")?
.dyn_into::<web_sys::Response>()
.map_err(|_| "videos.json response is invalid")?;
if !response.ok() {
return Err(format!("videos.json HTTP {}", response.status()));
}
let text = JsFuture::from(
response
.text()
.map_err(|_| "videos.json text() unavailable")?,
)
.await
.map_err(|_| "videos.json body read failed")?;
let body = text.as_string().ok_or("videos.json body is not a string")?;
let mut entries: Vec<VideoEntry> =
serde_json::from_str(&body).map_err(|error| error.to_string())?;
entries.retain(|entry| entry.id.len() == 11 && !entry.title.trim().is_empty());
if entries.is_empty() {
return Err("videos.json contains no videos".into());
}
let thumbnails = iced::futures::future::join_all(
entries
.iter()
.map(|entry| fetch_thumbnail_web(entry.id.clone())),
)
.await;
for (entry, thumbnail) in entries.iter_mut().zip(thumbnails) {
entry.thumb = thumbnail.ok();
}
Ok(entries)
}
#[cfg(target_arch = "wasm32")]
async fn fetch_thumbnail_web(id: String) -> Result<Vec<u8>, String> {
let local = format!("video_thumbs/{id}.jpg");
match fetch_thumbnail_url(&local).await {
Ok(bytes) => Ok(bytes),
Err(_) => {
// Local development uses the checked-in JSON snapshot without
// binary thumbnails. YouTube's image CDN explicitly permits CORS,
// so it is a safe fallback; deployed Pages builds use local files.
fetch_thumbnail_url(&format!("https://i.ytimg.com/vi/{id}/mqdefault.jpg")).await
}
}
}
#[cfg(target_arch = "wasm32")]
async fn fetch_thumbnail_url(url: &str) -> Result<Vec<u8>, String> {
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
let window = web_sys::window().ok_or("no window")?;
let response = JsFuture::from(window.fetch_with_str(url))
.await
.map_err(|_| "thumbnail fetch failed")?
.dyn_into::<web_sys::Response>()
.map_err(|_| "thumbnail response is invalid")?;
if !response.ok() {
return Err(format!("thumbnail HTTP {}", response.status()));
}
let buffer = JsFuture::from(
response
.array_buffer()
.map_err(|_| "thumbnail arrayBuffer() unavailable")?,
)
.await
.map_err(|_| "thumbnail body read failed")?;
Ok(js_sys::Uint8Array::new(&buffer).to_vec())
}