feat: Takeout walkthrough, design-system restyle, replacing import

Adds a seven-step in-app guide to exporting subscriptions from Google
Takeout, with the real URLs opened in the system browser.

Restyles the app onto the supplied design system: slate/sky palette,
9-15px type ladder, outline-first controls, tiered radii, borders for
separation and shadows only for elevation. Light and dark are both
designed, with a System/Light/Dark control and a pre-paint script so
the window does not flash light on a dark machine.

Importing now replaces the subscription list rather than merging, per
request. Because that can delete downloaded files, a confirmation
dialog names exactly what will go first.
This commit is contained in:
vincent
2026-08-29 03:00:54 +02:00
parent 11331671c5
commit cac6727072
18 changed files with 1202 additions and 306 deletions
+40 -8
View File
@@ -3,7 +3,9 @@
use crate::db::Db;
use crate::downloader::{self, Progress};
use crate::feed;
use crate::models::{ChannelWithCount, DownloadState, FeedFilter, FeedItem, Prereqs};
use crate::models::{
Channel, ChannelWithCount, DownloadState, FeedFilter, FeedItem, ImportPreview, Prereqs,
};
use crate::net;
use crate::takeout;
use crate::thumbs;
@@ -113,12 +115,8 @@ pub async fn check_prereqs(state: State<'_, AppState>) -> Result<Prereqs, String
})
}
#[tauri::command]
pub async fn import_takeout_csv(
path: String,
state: State<'_, AppState>,
) -> Result<usize, String> {
let raw = tokio::fs::read(&path)
async fn read_channels(path: &str) -> Result<Vec<Channel>, String> {
let raw = tokio::fs::read(path)
.await
.map_err(|e| format!("Cannot read {path}: {e}"))?;
// Takeout exports are UTF-8, sometimes with a BOM.
@@ -129,8 +127,42 @@ pub async fn import_takeout_csv(
if channels.is_empty() {
return Err("No channels found in that file.".into());
}
Ok(channels)
}
/// Reports what a replacing import would add and destroy, so the UI can name
/// the consequences before the user commits to them.
#[tauri::command]
pub async fn preview_takeout_import(
path: String,
state: State<'_, AppState>,
) -> Result<ImportPreview, String> {
let channels = read_channels(&path).await?;
state.db.lock().await.preview_replace(&channels)
}
/// The imported CSV becomes the entire subscription list. Channels that are no
/// longer in it are removed along with their videos, download records, and the
/// downloaded files themselves — leaving those on disk would orphan gigabytes
/// the app can no longer show or delete.
#[tauri::command]
pub async fn import_takeout_csv(
path: String,
state: State<'_, AppState>,
) -> Result<usize, String> {
let channels = read_channels(&path).await?;
let doomed = state
.db
.lock()
.await
.paths_dropped_by_replace(&channels)?;
for p in doomed {
let _ = tokio::fs::remove_file(&p).await;
}
let mut db = state.db.lock().await;
db.upsert_channels(&channels)
db.replace_channels(&channels)
}
#[tauri::command]