feat(start): tutorial-rail implementation for the playlist fetch

Source half of b672023 (which captured only the removed bundled
thumbnails — the add aborted on the already-staged deletions): the
videos module (playlist scan, oEmbed titles, config-dir thumbnail
cache), the boot fetch on its own thread with the loading state, the
overlay rail + Videos tab on the Start page, and the earlier tabbed-
layout handover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-21 10:07:21 +03:00
commit 7699f79424
6 changed files with 388 additions and 133 deletions

View file

@ -214,6 +214,7 @@ struct AddSelectedRestore {
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub enum StartSection {
Recent,
Videos,
#[default]
Welcome,
Supporters,
@ -241,6 +242,14 @@ pub(super) struct OpenCADStudio {
/// Paying Patreon supporters shown on the Start page (name, pledge cents),
/// fetched once at boot, highest pledge first.
patrons: Vec<(String, i64)>,
/// Tutorial-playlist videos for the Start page: seeded from the on-disk
/// cache at boot, refreshed by a live playlist fetch.
videos: Vec<crate::videos::VideoEntry>,
/// Decoded thumbnail handles keyed by video id — built once per video when
/// the list arrives, so the view never re-decodes JPEG bytes per frame.
video_thumbs: std::collections::HashMap<String, iced::widget::image::Handle>,
/// True while the boot-time playlist fetch is still in flight.
videos_loading: bool,
/// Which Start-page section is shown when the page is too narrow for all
/// three side by side and falls back to a tab bar.
start_section: StartSection,
@ -1778,6 +1787,12 @@ pub enum Message {
PluginRegistryFetched(Result<Vec<crate::plugin::external::RegistryEntry>, String>),
/// Patreon supporters fetched at boot for the Start page (name, pledge cents).
PatronsFetched(Result<Vec<(String, i64)>, String>),
/// Tutorial-playlist videos fetched at boot for the Start page.
VideosFetched(Result<Vec<crate::videos::VideoEntry>, String>),
/// Recent-file DWG preview thumbnails decoded on a background thread.
RecentThumbsLoaded(
Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>,
),
/// Installable release tags fetched for `owner/repo`.
PluginReleasesFetched(String, Result<Vec<String>, String>),
/// Choose a release tag for a repo (`repo`, `tag`).
@ -2145,6 +2160,26 @@ pub enum Message {
}
impl OpenCADStudio {
/// Install the Start-page video list, decoding each thumbnail's JPEG into
/// an image Handle exactly once (a fresh Handle per view frame would
/// re-upload the texture every frame).
fn set_videos(&mut self, videos: Vec<crate::videos::VideoEntry>) {
if videos.is_empty() {
return;
}
self.video_thumbs = videos
.iter()
.filter_map(|v| {
let bytes = v.thumb.clone()?;
Some((
v.id.clone(),
iced::widget::image::Handle::from_bytes(bytes),
))
})
.collect();
self.videos = videos;
}
fn new() -> Self {
// Boot with only the Welcome/Start tab. The user creates drawings
// explicitly (File → New); we never auto-spawn Drawing1.
@ -2163,6 +2198,9 @@ impl OpenCADStudio {
recent_limit_input: recent::RECENT_DEFAULT.to_string(),
command_line: CommandLine::new(),
patrons: Vec::new(),
videos: Vec::new(),
video_thumbs: std::collections::HashMap::new(),
videos_loading: false,
start_section: StartSection::default(),
props_expanded: false,
history_content: iced::widget::text_editor::Content::new(),
@ -2582,6 +2620,32 @@ impl OpenCADStudio {
);
#[cfg(target_arch = "wasm32")]
let patrons_fetch = Task::none();
// Tutorial videos: show the on-disk cache instantly, refresh from the
// live playlist in the background. Nothing ships in the binary. The
// fetch runs on its own OS thread — its several sequential HTTP
// requests would otherwise sit on the async executor and hold up the
// rest of the boot tasks (the Start page waited on it).
#[cfg(not(target_arch = "wasm32"))]
let videos_fetch = {
s.set_videos(crate::videos::load_cached());
s.videos_loading = true;
let (tx, rx) = iced::futures::channel::oneshot::channel();
std::thread::spawn(move || {
let _ = tx.send(crate::videos::fetch_playlist());
});
Task::perform(
async move {
rx.await
.unwrap_or_else(|_| Err("video fetch thread died".into()))
},
Message::VideosFetched,
)
};
#[cfg(target_arch = "wasm32")]
let videos_fetch = Task::none();
// Recent-file thumbnails: decoded off-thread — parsing every recent
// DWG's preview on the boot path held the first frame back.
let thumbs_fetch = s.refresh_recent_thumbs();
(
s,
Task::batch([
@ -2592,6 +2656,8 @@ impl OpenCADStudio {
script,
assoc_prompt,
patrons_fetch,
videos_fetch,
thumbs_fetch,
]),
)
}