Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fc5c27f
docs: add Yoinkit Pro feature gating, gallery, legal & payment spec
Mar 22, 2026
75f2740
docs: address spec review — fix critical and major issues
Mar 22, 2026
49d1392
docs: add Pro implementation plan with review fixes
Mar 22, 2026
545b9b0
feat(db): add migrate_v4 — gallery, collections, legal consent tables
Mar 22, 2026
8c25494
feat: add ProBadge, ProOverlay, LegalConsent, and ConfettiCelebration…
Mar 22, 2026
94bf3c0
feat(db): add gallery CRUD, collection CRUD, and legal consent methods
Mar 22, 2026
bb702f9
feat: add license key validation module and extend settings
Mar 22, 2026
d019d52
feat: add gallery, license, legal consent commands and Pro gating
Mar 22, 2026
c0c0728
feat: add gallery/license/consent API bindings and usePro/useGallery …
Mar 22, 2026
8d997dc
feat: rename Downloads to Yoinks, add Gallery nav, add legal consent …
Mar 22, 2026
fc580b5
fix: add error handling to consent accept, document fail-open behavior
Mar 22, 2026
f757680
feat: add Gallery page with grid/list view and free tier limit
Mar 22, 2026
3f6922e
fix: remove dead api import, fix hardcoded count threshold in gallery
Mar 22, 2026
a6b45ef
feat: add Pro gating to video quality and audio format/quality selectors
Mar 22, 2026
31366ba
feat: add license key management and legal links to Settings
Mar 22, 2026
076af93
feat: add legal info touchpoints to Clipper and Archive pages
Mar 22, 2026
476e217
feat: rewrite Pro page — shop window for free, dashboard for Pro
Mar 22, 2026
b476aac
fix: default video quality to 720p for free users
Mar 22, 2026
48a681f
chore: bump version to v0.3.0 for Pro tier release
Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ scraper = "0.21"
reqwest = { version = "0.12", features = ["json", "native-tls-vendored"] }
sha2 = "0.10"
tantivy = "0.22"
hostname = "0.4"

[build-dependencies]
tauri-build = { version = "2", features = [] }
116 changes: 114 additions & 2 deletions apps/desktop/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ pub fn list_downloads(state: State<'_, AppState>) -> Result<Vec<crate::db::Downl

#[tauri::command]
pub fn delete_download(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.download_manager.delete_download(&id)
state.download_manager.delete_download(&id)?;
let _ = state.db.delete_gallery_meta(&id, "download");
Ok(())
}

#[tauri::command]
Expand Down Expand Up @@ -173,6 +175,16 @@ pub async fn start_video_download(
sub_lang: Option<String>,
sub_format: Option<String>,
) -> Result<String, String> {
// Pro gating for high-quality video
let app_settings = crate::settings::get_settings(&state.db)?;
if !app_settings.pro_unlocked {
if let Some(ref q) = quality {
if q == "4k" || q == "1080p" {
return Err("Pro required for 1080p and 4K quality".to_string());
}
}
}

let save_dir = save_path.unwrap_or_else(|| {
state.db.get_setting("default_save_path")
.ok()
Expand Down Expand Up @@ -276,6 +288,21 @@ pub async fn start_audio_download(
sub_lang: Option<String>,
sub_format: Option<String>,
) -> Result<String, String> {
// Pro gating for premium audio formats/quality
let app_settings = crate::settings::get_settings(&state.db)?;
if !app_settings.pro_unlocked {
if let Some(ref f) = format {
if f != "mp3" {
return Err("Pro required for FLAC, WAV, AAC, and Opus formats".to_string());
}
}
if let Some(ref q) = quality {
if q == "0" {
return Err("Pro required for 320kbps quality".to_string());
}
}
}

start_video_download(state, url, format, quality, true, save_path, write_subs, sub_lang, sub_format).await
}

Expand Down Expand Up @@ -498,7 +525,9 @@ pub fn get_clip(id: String, state: State<'_, AppState>) -> Result<Option<Clip>,

#[tauri::command]
pub fn delete_clip(id: String, state: State<'_, AppState>) -> Result<(), String> {
state.db.delete_clip(&id).map_err(|e| format!("DB error: {}", e))
state.db.delete_clip(&id).map_err(|e| format!("DB error: {}", e))?;
let _ = state.db.delete_gallery_meta(&id, "clip");
Ok(())
}

#[tauri::command]
Expand Down Expand Up @@ -738,6 +767,11 @@ pub fn export_batch_notebooklm(ids: Vec<String>, export_dir: String, batch_name:

#[tauri::command]
pub fn create_monitor(url: String, state: State<'_, AppState>) -> Result<String, String> {
let app_settings = crate::settings::get_settings(&state.db)?;
if !app_settings.pro_unlocked {
return Err("Pro required for this feature".to_string());
}

let monitor = crate::db::Monitor {
id: Uuid::new_v4().to_string(),
url,
Expand Down Expand Up @@ -800,6 +834,11 @@ pub async fn generate_digest(state: State<'_, AppState>) -> Result<Clip, String>

#[tauri::command]
pub fn create_schedule(url: String, job_type: String, cron: String, flags: Option<String>, state: State<'_, AppState>) -> Result<String, String> {
let app_settings = crate::settings::get_settings(&state.db)?;
if !app_settings.pro_unlocked {
return Err("Pro required for this feature".to_string());
}

let schedule = crate::db::Schedule {
id: Uuid::new_v4().to_string(),
url,
Expand Down Expand Up @@ -835,3 +874,76 @@ pub fn toggle_schedule(id: String, enabled: bool, state: State<'_, AppState>) ->
schedule.enabled = if enabled { 1 } else { 0 };
state.db.update_schedule(&schedule).map_err(|e| format!("DB error: {}", e))
}

// Gallery commands

#[tauri::command]
pub fn list_gallery(limit: Option<i64>, offset: Option<i64>, state: State<'_, AppState>) -> Result<Vec<crate::db::GalleryItem>, String> {
state.db.list_gallery_items(limit.unwrap_or(50), offset.unwrap_or(0))
.map_err(|e| format!("DB error: {}", e))
}

#[tauri::command]
pub fn gallery_count(state: State<'_, AppState>) -> Result<i64, String> {
state.db.count_gallery_items().map_err(|e| format!("DB error: {}", e))
}

#[tauri::command]
pub fn update_gallery_item(item_id: String, item_type: String, collection_id: Option<String>, tags: String, flag: String, state: State<'_, AppState>) -> Result<(), String> {
state.db.update_gallery_meta(&item_id, &item_type, collection_id.as_deref(), &tags, &flag)
.map_err(|e| format!("DB error: {}", e))
}

// Collection commands

#[tauri::command]
pub fn create_collection(name: String, color: Option<String>, state: State<'_, AppState>) -> Result<crate::db::Collection, String> {
let collection = crate::db::Collection {
id: uuid::Uuid::new_v4().to_string(),
name,
color,
position: 0,
created_at: chrono::Utc::now().to_rfc3339(),
};
state.db.insert_collection(&collection).map_err(|e| format!("DB error: {}", e))?;
Ok(collection)
}

#[tauri::command]
pub fn list_collections_cmd(state: State<'_, AppState>) -> Result<Vec<crate::db::Collection>, String> {
state.db.list_collections().map_err(|e| format!("DB error: {}", e))
}

#[tauri::command]
pub fn delete_collection_cmd(id: String, state: State<'_, AppState>) -> Result<(), String> {
state.db.delete_collection(&id).map_err(|e| format!("DB error: {}", e))
}

// License activation

#[tauri::command]
pub async fn activate_license(license_key: String, state: State<'_, AppState>) -> Result<crate::license::ActivationResult, String> {
let result = crate::license::activate_license(&license_key).await?;
if result.success {
let mut settings = crate::settings::get_settings(&state.db)?;
settings.pro_unlocked = true;
settings.license_key = license_key;
settings.pro_since = chrono::Utc::now().to_rfc3339();
crate::settings::update_settings(&state.db, &settings)?;
}
Ok(result)
}

// Legal consent

const TOS_VERSION: &str = "1.0";

#[tauri::command]
pub fn check_consent(state: State<'_, AppState>) -> Result<bool, String> {
state.db.has_valid_consent(TOS_VERSION).map_err(|e| format!("DB error: {}", e))
}

#[tauri::command]
pub fn accept_consent(state: State<'_, AppState>) -> Result<(), String> {
state.db.record_consent(TOS_VERSION).map_err(|e| format!("DB error: {}", e))
}
Loading
Loading