From 1da96b4fbb6ec68939c291713dba3e00ee0e136b Mon Sep 17 00:00:00 2001 From: javad Date: Sun, 6 Sep 2026 18:39:00 +0330 Subject: [PATCH 1/2] fix(gui): send the captured Referer, and name captured media after the page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CDN with hotlink protection refuses an object to a client that does not say which page linked it. The extension has always captured the referer and sent it over the extbus; the app logged it and dropped it, so every request went out without one and the download failed where the browser succeeded — reported against a 4K MP4 that plays fine in the tab. The referer now travels with the item, like `StreamInfo::referer` already does for a manifest's segments: `ExtDownload` -> `DownloadItem::referer` -> `StartSpec` -> `request_headers`, which the transfer and every probe share. `probe_link` takes those headers too, so the File Info dialog asks the same question the transfer will and reports a real size instead of `?`. Verified against the reporter's own signed URL: without the referer the probe gets nothing, with it 109566922 bytes. Naming: `download-stream` already titled a captured stream after the page, `download-url` sent no name at all, so a direct file landed under whatever the CDN stored it as — a uuid, renamed by hand afterwards. A sniffed media file whose basename is opaque now takes the page title, as IDM does; a file that names itself, and any link the sniffer never saw, keep their own name. Also surfaces the background-download switch in Options > Downloads. It was reachable only from inside the File Info dialog, which is why the same report asked for a confirm-before-downloading button that already existed. --- crates/hydra-gui/src/app.rs | 69 ++++++++++++++++++----- crates/hydra-gui/src/engine.rs | 50 +++++++++++++--- crates/hydra-gui/src/extbus.rs | 7 ++- crates/hydra-gui/src/model.rs | 8 +++ crates/hydra-gui/src/windows/options.rs | 8 +++ extensions/chrome/background.js | 44 ++++++++++++++- extensions/chrome/manifest.json | 2 +- extensions/firefox/manifest.json | 2 +- extensions/safari/Resources/background.js | 44 ++++++++++++++- extensions/safari/manifest.json | 2 +- extensions/tests/capture.test.mjs | 50 +++++++++++++++- 11 files changed, 255 insertions(+), 31 deletions(-) diff --git a/crates/hydra-gui/src/app.rs b/crates/hydra-gui/src/app.rs index e42365d..fc437fd 100644 --- a/crates/hydra-gui/src/app.rs +++ b/crates/hydra-gui/src/app.rs @@ -272,6 +272,9 @@ pub struct AddUrlState { /// Filename the browser had already resolved (Content-Disposition et /// al.) — better than what the URL path implies. pub capture_name: Option, + /// Page the browser was on when it captured this file. A CDN with + /// hotlink protection answers `403` without it. + pub capture_referer: Option, /// What the address turned out to be, when it is a manifest. A stream /// has to be asked which rendition BEFORE it starts — there is no /// changing your mind halfway through a hundred segments. @@ -338,6 +341,7 @@ pub struct PendingAdd { pub auth: Option<(String, String)>, pub cookies: Option, pub name: Option, + pub referer: Option, } #[derive(Clone, Debug, Default)] @@ -1076,6 +1080,7 @@ pub enum OptField { ServerDate(bool), NoCatDirs(bool), ShowFileInfo(bool), + BgDownload(bool), StartMinimized(bool), SpeedTab(bool), CompletionTab(bool), @@ -1577,12 +1582,20 @@ impl App { } /// Attach what the browser knew that `add_item` could not: session - /// cookies and the already-resolved filename. Must run before any start - /// so the first request carries the Cookie header; renaming re-runs - /// categorization because the URL-derived name may have had no - /// extension at all. - fn apply_capture_extras(&mut self, id: DlId, cookies: Option, name: Option) { - if cookies.is_none() && name.is_none() { + /// cookies, the page the file was linked from, and the already-resolved + /// filename. Must run before any start so the first request carries the + /// `Cookie` and `Referer` headers — a hotlink-protected CDN answers + /// `403` without the referer, whatever the cookies say. Renaming re-runs + /// categorization because the URL-derived name may have had no extension + /// at all. + fn apply_capture_extras( + &mut self, + id: DlId, + cookies: Option, + name: Option, + referer: Option, + ) { + if cookies.is_none() && name.is_none() && referer.is_none() { return; } // Resolved before the item is borrowed mutably, and through @@ -1597,6 +1610,9 @@ impl App { if let Some(c) = cookies { d.cookies = Some(c); } + if let Some(r) = referer { + d.referer = Some(r); + } if let Some(n) = name { d.file_name = n; if let Some(c) = cat { @@ -2118,6 +2134,7 @@ impl App { held: d.held.clone(), expected_size: d.size, cookies: d.cookies.clone(), + referer: d.referer.clone(), limit: None, adaptive, remote_time, @@ -2312,6 +2329,7 @@ impl App { q_order, auth, cookies: None, + referer: None, speed_limit: None, limit_paused: false, held: vec![], @@ -3995,6 +4013,11 @@ impl App { .take() .filter(|s| !s.is_empty()); let cap_name = self.add_url.capture_name.take().filter(|s| !s.is_empty()); + let cap_referer = self + .add_url + .capture_referer + .take() + .filter(|s| !s.is_empty()); // A manifest that was inspected becomes a STREAM item: the // chosen rendition and container decide the filename, which // the URL path cannot. @@ -4052,6 +4075,7 @@ impl App { auth, cookies: cap_cookies, name: cap_name, + referer: cap_referer, }); self.confirm = Some(ConfirmKind::Duplicate { existing, file }); self.add_url = AddUrlState::default(); @@ -4059,7 +4083,7 @@ impl App { return Task::batch([close, self.open_window(WinKind::Confirm)]); } let id = self.add_item(url, auth, None); - self.apply_capture_extras(id, cap_cookies, cap_name); + self.apply_capture_extras(id, cap_cookies, cap_name, cap_referer); if let Some(si) = stream { let cat = crate::model::categorize(&name, &self.cfg.categories); let dir = self @@ -4125,6 +4149,7 @@ impl App { address: dl.url, capture_cookies: dl.cookies, capture_name: dl.filename, + capture_referer: dl.referer, ..AddUrlState::default() }; self.update(Message::AddUrlOk) @@ -4434,7 +4459,8 @@ impl App { // or cookie must reach the peek before it reaches the item. let auth = (!fi.login.is_empty()).then_some((fi.login.as_str(), fi.password.as_str())); - let headers = engine::request_headers(auth, Some(&fi.cookies)); + let referer = self.item(dl).and_then(|d| d.referer.clone()); + let headers = engine::request_headers(auth, Some(&fi.cookies), referer.as_deref()); let url = fi.url.clone(); let ua = self.cfg.settings.user_agent.clone(); // The size the probe (or the transfer running behind the @@ -5074,9 +5100,10 @@ impl App { Message::BatchMetalinkProbed(url.clone(), Box::new(r.ok())) }) } else { - Task::perform(engine::probe_link(url.clone(), ua), move |meta| { - Message::BatchProbed(url.clone(), meta) - }) + Task::perform( + engine::probe_link(url.clone(), ua, vec![]), + move |meta| Message::BatchProbed(url.clone(), meta), + ) } }) .collect(); @@ -5434,7 +5461,7 @@ impl App { return close; }; let id = self.add_item(pending.url, pending.auth, None); - self.apply_capture_extras(id, pending.cookies, pending.name); + self.apply_capture_extras(id, pending.cookies, pending.name, pending.referer); // `name_1.ext`, `name_2.ext`, ... until it collides with // neither the disk nor another list entry. Locked, because // this copy exists precisely so the file already on disk is @@ -5529,8 +5556,20 @@ impl App { return self.start_download(id, false); } let ua = self.cfg.settings.user_agent.clone(); - let url = self.item(id).map(|d| d.url.clone()).unwrap_or_default(); - Task::perform(engine::probe_link(url, ua), move |meta| { + let (url, headers) = self + .item(id) + .map(|d| { + ( + d.url.clone(), + engine::request_headers( + d.auth.as_ref().map(|(u, p)| (u.as_str(), p.as_str())), + d.cookies.as_deref(), + d.referer.as_deref(), + ), + ) + }) + .unwrap_or_default(); + Task::perform(engine::probe_link(url, ua, headers), move |meta| { Message::InfoProbed(id, meta) }) } @@ -5970,6 +6009,7 @@ impl App { OptField::ServerDate(b) => s.server_file_date = b, OptField::NoCatDirs(b) => s.no_category_dirs = b, OptField::ShowFileInfo(b) => s.show_file_info_dialog = b, + OptField::BgDownload(b) => s.bg_download = b, OptField::StartMinimized(b) => s.start_minimized = b, OptField::SpeedTab(b) => s.show_speed_tab = b, OptField::CompletionTab(b) => s.show_completion_tab = b, @@ -6897,6 +6937,7 @@ mod tests { q_order: 0, auth: None, cookies: None, + referer: None, speed_limit: None, limit_paused: false, held: vec![], diff --git a/crates/hydra-gui/src/engine.rs b/crates/hydra-gui/src/engine.rs index 8f0ac85..1a740a0 100644 --- a/crates/hydra-gui/src/engine.rs +++ b/crates/hydra-gui/src/engine.rs @@ -43,6 +43,10 @@ pub struct StartSpec { pub expected_size: Option, /// `Cookie:` header value, verbatim. pub cookies: Option, + /// `Referer:` header value, verbatim: the page the file was linked from. + /// A CDN with hotlink protection refuses the object without it, whatever + /// the cookies say. + pub referer: Option, /// Aggregate cap in bytes/sec; `None` = unlimited (can be changed live). pub limit: Option, /// Start at one connection and let the in-band ramp admit more while @@ -101,6 +105,7 @@ impl StartSpec { held: Vec::new(), expected_size: None, cookies: None, + referer: None, limit: None, adaptive: false, remote_time: false, @@ -512,14 +517,14 @@ pub struct LinkMeta { /// Bounded: a pasted 500-link batch must not open 500 concurrent /// handshakes (fd limits, origin rate limiting). A small global gate keeps /// the fan-out polite; the shared connector reuses connections per host. -pub async fn probe_link(url: String, user_agent: String) -> Option { +pub async fn probe_link(url: String, user_agent: String, headers: Vec) -> Option { static GATE: OnceLock> = OnceLock::new(); let gate = GATE .get_or_init(|| Arc::new(tokio::sync::Semaphore::new(6))) .clone(); let _permit = gate.acquire_owned().await.ok()?; let connector = shared_connector().ok()?; - let (url, p) = resolve_link(connector.as_ref(), url, &user_agent, &[]).await?; + let (url, p) = resolve_link(connector.as_ref(), url, &user_agent, &headers).await?; if p.status >= 300 { return None; } @@ -1070,9 +1075,14 @@ fn base64(data: &[u8]) -> String { } /// The request headers a download's credentials turn into: HTTP Basic for a -/// login, `Cookie:` for a cookie string. Shared by the transfer and by the -/// probes that must see the same object it will. -pub fn request_headers(auth: Option<(&str, &str)>, cookies: Option<&str>) -> Vec { +/// login, `Cookie:` for a cookie string, `Referer:` for the page the file was +/// linked from. Shared by the transfer and by the probes that must see the +/// same object it will. +pub fn request_headers( + auth: Option<(&str, &str)>, + cookies: Option<&str>, + referer: Option<&str>, +) -> Vec { let mut headers = Vec::new(); if let Some((user, pass)) = auth { headers.push(format!( @@ -1083,6 +1093,9 @@ pub fn request_headers(auth: Option<(&str, &str)>, cookies: Option<&str>) -> Vec if let Some(c) = cookies.map(str::trim).filter(|c| !c.is_empty()) { headers.push(format!("Cookie: {c}")); } + if let Some(r) = referer.map(str::trim).filter(|r| !r.is_empty()) { + headers.push(format!("Referer: {r}")); + } headers } @@ -1090,6 +1103,7 @@ fn target_for(u: &ParsedUrl, spec: &StartSpec) -> Target { let headers = request_headers( spec.auth.as_ref().map(|(u, p)| (u.as_str(), p.as_str())), spec.cookies.as_deref(), + spec.referer.as_deref(), ); target_of(u, headers, &spec.user_agent) } @@ -3046,6 +3060,7 @@ mod tests { held: vec![], expected_size: None, cookies: None, + referer: None, limit: None, adaptive: std::env::var_os("HYDRA_AB_ADAPTIVE").is_some(), remote_time: false, @@ -3272,6 +3287,7 @@ mod tests { held: vec![], expected_size: None, cookies: None, + referer: None, limit: None, adaptive: false, // On, so the harness exercises the stamping path too: it is part @@ -6335,8 +6351,28 @@ mod peek_zip_tests { #[test] fn credentials_become_headers() { - let h = request_headers(Some(("me", "pw")), Some(" sid=1 ")); + let h = request_headers(Some(("me", "pw")), Some(" sid=1 "), None); assert_eq!(h, ["Authorization: Basic bWU6cHc=", "Cookie: sid=1"]); - assert!(request_headers(None, Some(" ")).is_empty()); + assert!(request_headers(None, Some(" "), Some(" ")).is_empty()); + } + + /// A hotlink-protected CDN answers `403` to a request without the page + /// it was linked from, however good the cookies are: the captured + /// referer has to reach the wire, not just the log. + #[test] + fn a_captured_referer_becomes_a_header() { + let spec = StartSpec { + cookies: Some("sid=1".into()), + referer: Some("https://www.example.com/watch".into()), + user_agent: "hydra-test".into(), + ..StartSpec::plain() + }; + let u = parse_url("https://cdn.example.com/a.mp4?e=1&s=2").unwrap(); + let t = format!("{:?}", target_for(&u, &spec)); + assert!( + t.contains("Referer: https://www.example.com/watch"), + "no referer on the target: {t}" + ); + assert!(t.contains("Cookie: sid=1"), "no cookie on the target: {t}"); } } diff --git a/crates/hydra-gui/src/extbus.rs b/crates/hydra-gui/src/extbus.rs index ededea8..f56c432 100644 --- a/crates/hydra-gui/src/extbus.rs +++ b/crates/hydra-gui/src/extbus.rs @@ -41,6 +41,8 @@ pub struct ExtDownload { /// Verbatim `Cookie:` header assembled by the extension for this URL. #[serde(default)] pub cookies: Option, + /// The page the file was linked from, replayed as `Referer:` on every + /// request: a CDN with hotlink protection refuses the object without it. #[serde(default)] pub referer: Option, #[serde(default)] @@ -453,8 +455,9 @@ fn dispatch(req: &serde_json::Value, trusted: bool) -> serde_json::Value { } Some("download") => match serde_json::from_value::(req.clone()) { Ok(dl) if !dl.url.is_empty() => { - // referer/user_agent/size arrive for the log today; the - // engine's StartSpec grows per-download headers later. + // The referer travels with the item and onto every request + // (`DownloadItem::referer`); user_agent and size are logged + // for support, the transfer using the configured agent. crate::log::info(&format!( "extbus: capture {} (mime={} size={} referer={} ua={} from={})", dl.url, diff --git a/crates/hydra-gui/src/model.rs b/crates/hydra-gui/src/model.rs index 8f555bc..1385cc5 100644 --- a/crates/hydra-gui/src/model.rs +++ b/crates/hydra-gui/src/model.rs @@ -65,6 +65,14 @@ pub struct DownloadItem { /// (filled manually or, later, by the browser extension). #[serde(default)] pub cookies: Option, + /// `Referer:` header value for a hotlink-protected origin: the page the + /// browser was on when the extension captured this file. Sites that gate + /// their CDN on it answer `403` to a request without it, however good the + /// cookies are, so it travels with the item and is replayed on every + /// start — the same way `StreamInfo::referer` already works for a + /// manifest's segments. + #[serde(default)] + pub referer: Option, /// Per-download cap, bytes/sec, when the Speed Limiter tab enables one. pub speed_limit: Option, /// Parked by the Connection tab's download limit, not by the user. The diff --git a/crates/hydra-gui/src/windows/options.rs b/crates/hydra-gui/src/windows/options.rs index 20f3a5c..ccb0374 100644 --- a/crates/hydra-gui/src/windows/options.rs +++ b/crates/hydra-gui/src/windows/options.rs @@ -347,6 +347,14 @@ fn downloads(app: &App) -> El<'_> { .style(theme::check), tr("Adding a link first shows name/category/folder while the transfer already runs in the background; off = downloads start immediately."), ), + hinted( + checkbox(s.bg_download).label(tr("Download in background while the dialog is open")) + .on_toggle(|b| o(OptField::BgDownload(b))) + .size(15.0) + .text_size(theme::FONT_SIZE) + .style(theme::check), + tr("Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it."), + ), hinted( checkbox(s.show_speed_tab).label(tr("Show \"Speed Limiter\" tab")) .on_toggle(|b| o(OptField::SpeedTab(b))) diff --git a/extensions/chrome/background.js b/extensions/chrome/background.js index d42320f..4b40d67 100644 --- a/extensions/chrome/background.js +++ b/extensions/chrome/background.js @@ -586,6 +586,31 @@ function titleName(title) { ); } +/// What to call a captured media file. +/// +/// Normally nothing: the URL's own basename is the better name, and Hydra +/// reads it off the URL by itself — `big_buck_bunny_1080p.mp4` beats any page +/// title, and on a page of samples every row would otherwise take the same +/// one. But an object stored as `fc1eced1-6d50-4375-a125-ef65c887d7d5.mp4` was +/// named by the CDN for the CDN, and lands as a row of gibberish the user then +/// renames by hand. There the page title is the answer, which is what IDM +/// gives them. +function mediaName(url, title, kind) { + const base = decodeURIComponent(url.split(/[?#]/)[0].split("/").pop() || ""); + const stem = base.replace(/\.[^.]+$/, ""); + const opaque = + !stem || + /^[0-9a-f]{8,}$/i.test(stem) || + /^[0-9a-f][0-9a-f-]{15,}$/i.test(stem) || + /^\d{6,}$/.test(stem); + if (!opaque) return null; + const named = titleName(title); + if (!named) return null; + // The extension the file really has, else what the server called its type. + const ext = (/\.([a-z0-9]{2,5})$/i.exec(base)?.[1] || kind || "").toLowerCase(); + return ext ? `${named}.${ext}` : named; +} + /// Stable identity for one variant, so the popup can name the row's choice /// back to the service worker. Duplicated verbatim in popup.js — the two run /// in different contexts and must agree. @@ -1162,9 +1187,24 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case "open-hydra": sendResponse(await request({ type: "open" })); break; - case "download-url": - sendResponse(await sendToHydra(msg.url, { referer: msg.referer || null })); + case "download-url": { + // Only a file the media sniffer saw is named after the page: an + // ordinary link — a `.zip` on a downloads page — means its own name, + // not the article's. + const id = await whichTab(); + const bare = msg.url.split(/[?#]/)[0]; + const hit = (id != null ? await tabMedia(id) : []).find( + (m) => m.url.split(/[?#]/)[0] === bare + ); + const tab = id != null ? await chrome.tabs.get(id).catch(() => null) : null; + sendResponse( + await sendToHydra(msg.url, { + referer: msg.referer || null, + filename: hit ? mediaName(msg.url, tab?.title, hit.kind) : null, + }) + ); break; + } case "download-stream": { const id = await whichTab(); const entry = (id != null ? await tabStreams(id) : []).find((s) => s.key === msg.key); diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json index edfafba..7996e49 100644 --- a/extensions/chrome/manifest.json +++ b/extensions/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Hydra Download Manager Integration", - "version": "0.3.1", + "version": "0.3.2", "description": "A browser integration for Hydra: automatic download capture, right-click downloads, and media/HLS/DASH stream sniffing.", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt7NVgqBOTaCTKcon/gTK0M4VxY4ZKcyh0pWTVQoWfjJrJlTvRHaWFxE4ZbIj+88zNKJBxVa6uMdQKtK2V+NaPYR1NTYyTDh75UErRI+mx5RXa5c2kiwudl2jwHPsYcUU9NPMI4FwqVFaxEetvIS9J91/3ze5l+FbsZDF0Yvc9uR1dYNnnc//nGg1R+uL0/wus4INeccd9COj10+Aka2SeBkvrVlLs63AMFMkPpN9iIBwyhgs01bulVRFbvHK9m/TizLfthMHAyMCkUdCkZr6gEZbYbwNwGwKCUIswomoiVRgZx/FBFYrh6cRlfoLQZUbS7kFTShpfjm+zEGBkWQ3yQIDAQAB", "minimum_chrome_version": "116", diff --git a/extensions/firefox/manifest.json b/extensions/firefox/manifest.json index 85ac69d..b560174 100644 --- a/extensions/firefox/manifest.json +++ b/extensions/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Hydra Download Manager Integration", - "version": "0.3.1", + "version": "0.3.2", "description": "A browser integration for Hydra: automatic download capture, right-click downloads, and media/HLS/DASH stream sniffing.", "browser_specific_settings": { "gecko": { diff --git a/extensions/safari/Resources/background.js b/extensions/safari/Resources/background.js index d42320f..4b40d67 100644 --- a/extensions/safari/Resources/background.js +++ b/extensions/safari/Resources/background.js @@ -586,6 +586,31 @@ function titleName(title) { ); } +/// What to call a captured media file. +/// +/// Normally nothing: the URL's own basename is the better name, and Hydra +/// reads it off the URL by itself — `big_buck_bunny_1080p.mp4` beats any page +/// title, and on a page of samples every row would otherwise take the same +/// one. But an object stored as `fc1eced1-6d50-4375-a125-ef65c887d7d5.mp4` was +/// named by the CDN for the CDN, and lands as a row of gibberish the user then +/// renames by hand. There the page title is the answer, which is what IDM +/// gives them. +function mediaName(url, title, kind) { + const base = decodeURIComponent(url.split(/[?#]/)[0].split("/").pop() || ""); + const stem = base.replace(/\.[^.]+$/, ""); + const opaque = + !stem || + /^[0-9a-f]{8,}$/i.test(stem) || + /^[0-9a-f][0-9a-f-]{15,}$/i.test(stem) || + /^\d{6,}$/.test(stem); + if (!opaque) return null; + const named = titleName(title); + if (!named) return null; + // The extension the file really has, else what the server called its type. + const ext = (/\.([a-z0-9]{2,5})$/i.exec(base)?.[1] || kind || "").toLowerCase(); + return ext ? `${named}.${ext}` : named; +} + /// Stable identity for one variant, so the popup can name the row's choice /// back to the service worker. Duplicated verbatim in popup.js — the two run /// in different contexts and must agree. @@ -1162,9 +1187,24 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case "open-hydra": sendResponse(await request({ type: "open" })); break; - case "download-url": - sendResponse(await sendToHydra(msg.url, { referer: msg.referer || null })); + case "download-url": { + // Only a file the media sniffer saw is named after the page: an + // ordinary link — a `.zip` on a downloads page — means its own name, + // not the article's. + const id = await whichTab(); + const bare = msg.url.split(/[?#]/)[0]; + const hit = (id != null ? await tabMedia(id) : []).find( + (m) => m.url.split(/[?#]/)[0] === bare + ); + const tab = id != null ? await chrome.tabs.get(id).catch(() => null) : null; + sendResponse( + await sendToHydra(msg.url, { + referer: msg.referer || null, + filename: hit ? mediaName(msg.url, tab?.title, hit.kind) : null, + }) + ); break; + } case "download-stream": { const id = await whichTab(); const entry = (id != null ? await tabStreams(id) : []).find((s) => s.key === msg.key); diff --git a/extensions/safari/manifest.json b/extensions/safari/manifest.json index c743992..9ea6fb7 100644 --- a/extensions/safari/manifest.json +++ b/extensions/safari/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Hydra Download Manager Integration", - "version": "0.3.1", + "version": "0.3.2", "description": "A browser integration for Hydra: right-click downloads, link selection, and media/HLS/DASH stream sniffing.", "browser_specific_settings": { "safari": { diff --git a/extensions/tests/capture.test.mjs b/extensions/tests/capture.test.mjs index 070a809..39bfea4 100644 --- a/extensions/tests/capture.test.mjs +++ b/extensions/tests/capture.test.mjs @@ -59,7 +59,7 @@ function build({ hydraReply = { ok: true }, store = {} } = {}) { contextMenus: { removeAll: async () => {}, create: () => {}, onClicked: onMenu }, tabs: { query: async () => [{ id: 7, url: "https://page.example/watch" }], - get: async () => ({ id: 7, url: "https://page.example/watch" }), + get: async () => ({ id: 7, url: "https://page.example/watch", title: "Sunset Timelapse 4K" }), sendMessage: async () => ({ urls: ["https://a.example/1.zip", "https://b.example/2.zip"] }), create: () => {}, onRemoved: ev(), onUpdated: ev(), }, @@ -313,5 +313,53 @@ function build({ hydraReply = { ok: true }, store = {} } = {}) { } } +// ------------------------------------- 9. naming a captured media file +// +// A CDN that stores its objects under a uuid hands the user a row of +// gibberish to rename by hand; the page title is what IDM gives them and +// what they asked for. +{ + const uuid = "https://cdn.example/1b364999/fc1eced1-6d50-4375-a125-ef65c887d7d5.mp4"; + const h = build(); + await tick(4); + h.respond({ url: uuid, mime: "video/mp4", size: 109_566_922 }); + await tick(6); + await h.send({ type: "download-url", url: `${uuid}?e=1&s=abc`, referer: "https://page.example/watch" }); + await tick(2); + const dl = h.sent.find((m) => m.type === "download"); + check( + "naming: an opaque CDN name becomes the page title", + dl?.filename === "Sunset Timelapse 4K.mp4", + JSON.stringify(dl) + ); + check("naming: the referer travels with it", dl?.referer === "https://page.example/watch"); + + // A file that names itself keeps its name: a page of samples would + // otherwise put the same title on every one of them. + const h2 = build(); + await tick(4); + h2.respond({ url: "https://cdn.example/big_buck_bunny_1080p.mp4", mime: "video/mp4" }); + await tick(6); + await h2.send({ type: "download-url", url: "https://cdn.example/big_buck_bunny_1080p.mp4" }); + await tick(2); + check( + "naming: a file that names itself is left alone", + !h2.sent.find((m) => m.type === "download")?.filename, + JSON.stringify(h2.sent) + ); + + // An ordinary link is not media the page played, so the article's title + // has nothing to do with it. + const h3 = build(); + await tick(4); + await h3.send({ type: "download-url", url: "https://files.example/8675309.zip" }); + await tick(2); + check( + "naming: an unsniffed link keeps its own name", + !h3.sent.find((m) => m.type === "download")?.filename, + JSON.stringify(h3.sent) + ); +} + console.log(fails ? `\n${fails} FAILED` : "\nall passed"); process.exit(fails ? 1 : 0); From a08a667a0e26cdee744e223d19b6dc2c507298ae Mon Sep 17 00:00:00 2001 From: javad Date: Sun, 6 Sep 2026 18:49:02 +0330 Subject: [PATCH 2/2] i18n: translate the background-download hint; reorder the Dock/taskbar tick non-english translated using AI llm --- crates/hydra-gui/assets/locale/ar.json | 1 + crates/hydra-gui/assets/locale/cs.json | 1 + crates/hydra-gui/assets/locale/da.json | 1 + crates/hydra-gui/assets/locale/de.json | 1 + crates/hydra-gui/assets/locale/el.json | 1 + crates/hydra-gui/assets/locale/en.json | 1 + crates/hydra-gui/assets/locale/es.json | 1 + crates/hydra-gui/assets/locale/fa.json | 1 + crates/hydra-gui/assets/locale/fi.json | 1 + crates/hydra-gui/assets/locale/fr.json | 1 + crates/hydra-gui/assets/locale/he.json | 1 + crates/hydra-gui/assets/locale/hi.json | 1 + crates/hydra-gui/assets/locale/hu.json | 1 + crates/hydra-gui/assets/locale/id.json | 1 + crates/hydra-gui/assets/locale/it.json | 1 + crates/hydra-gui/assets/locale/ja.json | 1 + crates/hydra-gui/assets/locale/ko.json | 1 + crates/hydra-gui/assets/locale/nl.json | 1 + crates/hydra-gui/assets/locale/pl.json | 1 + crates/hydra-gui/assets/locale/pt-BR.json | 1 + crates/hydra-gui/assets/locale/pt.json | 1 + crates/hydra-gui/assets/locale/ro.json | 1 + crates/hydra-gui/assets/locale/ru.json | 1 + crates/hydra-gui/assets/locale/sv.json | 1 + crates/hydra-gui/assets/locale/th.json | 1 + crates/hydra-gui/assets/locale/tr.json | 1 + crates/hydra-gui/assets/locale/uk.json | 1 + crates/hydra-gui/assets/locale/vi.json | 1 + crates/hydra-gui/assets/locale/zh-hant.json | 1 + crates/hydra-gui/assets/locale/zh.json | 1 + crates/hydra-gui/src/windows/options.rs | 14 ++++++++------ 31 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/hydra-gui/assets/locale/ar.json b/crates/hydra-gui/assets/locale/ar.json index 0dd357f..94c0d89 100644 --- a/crates/hydra-gui/assets/locale/ar.json +++ b/crates/hydra-gui/assets/locale/ar.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "عدد محاولات الإعادة لكل ملف في حال فشل التنزيل :", "OK": "موافق", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "إيقاف (افتراضي): يُحفظ التنزيل في مجلد فئته، مثل Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "إيقاف = لا يتم جلب أي شيء حتى الضغط على \"بدء التنزيل\"، لذا تتم إعادة التسمية أو تغيير المجلد قبل النقل لا أثناءه.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "تفعيل: يتم تجاهل المجلدات أعلاه وتُحفظ التنزيلات الجديدة مباشرة في مجلد فئة عام. تحتفظ التنزيلات الموجودة بالفعل في القائمة بمجلدها.", "Once": "مرة واحدة", "One-time downloading": "تنزيل لمرة واحدة", diff --git a/crates/hydra-gui/assets/locale/cs.json b/crates/hydra-gui/assets/locale/cs.json index 4ba740c..10eb260 100644 --- a/crates/hydra-gui/assets/locale/cs.json +++ b/crates/hydra-gui/assets/locale/cs.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Počet opakování pro každý soubor při selhání stahování:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Vypnuto (výchozí): stahování se uloží do složky své kategorie, např. Stahování/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Vypnuto = nic se nestahuje, dokud nestisknete „Spustit stahování“, takže přejmenování nebo změna složky proběhne před přenosem, ne během něj.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Zapnuto: složky uvedené výše se ignorují a nová stahování se ukládají přímo do složky kategorie Obecné. Stahování již v seznamu si svou složku ponechají.", "Once": "Jednou", "One-time downloading": "Jednorázové stahování", diff --git a/crates/hydra-gui/assets/locale/da.json b/crates/hydra-gui/assets/locale/da.json index d436222..a946816 100644 --- a/crates/hydra-gui/assets/locale/da.json +++ b/crates/hydra-gui/assets/locale/da.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Antal genforsøg pr. fil, hvis download mislykkedes:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Fra (standard): et download arkiveres i sin kategorimappe, f.eks. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Fra = der hentes intet, før der trykkes på \"Start download\", så omdøbning eller skift af mappe sker før overførslen, ikke under den.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Til: mapperne ovenfor ignoreres, og nye downloads gemmes direkte i kategorimappen Generelt. Downloads, der allerede er på listen, beholder deres mappe.", "Once": "Én gang", "One-time downloading": "Engangsdownload", diff --git a/crates/hydra-gui/assets/locale/de.json b/crates/hydra-gui/assets/locale/de.json index 8bfbf45..d3677c5 100644 --- a/crates/hydra-gui/assets/locale/de.json +++ b/crates/hydra-gui/assets/locale/de.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Anzahl der Wiederholungsversuche für jede Datei, wenn der Download fehlschlägt :", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Aus (Standard): Ein Download wird im Unterordner der Kategorie abgelegt, z. B. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Aus = es wird nichts geladen, bis „Download starten“ gedrückt wird; Umbenennen oder ein Ordnerwechsel geschieht so vor der Übertragung, nicht währenddessen.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Ein: Die Ordner der einzelnen Kategorien werden ignoriert und neue Downloads werden direkt im Ordner der Kategorie \"Allgemein\" gespeichert. Downloads, die sich bereits in der Liste befinden, behalten ihren Ordner.", "Once": "Einmal", "One-time downloading": "Einmaliger Download", diff --git a/crates/hydra-gui/assets/locale/el.json b/crates/hydra-gui/assets/locale/el.json index 32101be..3f494cf 100644 --- a/crates/hydra-gui/assets/locale/el.json +++ b/crates/hydra-gui/assets/locale/el.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Αριθμός επαναλήψεων ανά αρχείο σε περίπτωση αποτυχίας λήψης:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Ανενεργό (προεπιλογή): κάθε λήψη καταχωρίζεται στον φάκελο της κατηγορίας της, π.χ. Λήψεις/Βίντεο.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Ανενεργό = δεν λαμβάνεται τίποτα μέχρι να πατηθεί «Έναρξη λήψης», ώστε η μετονομασία ή η αλλαγή φακέλου να γίνεται πριν από τη μεταφορά και όχι κατά τη διάρκειά της.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Ενεργό: οι παραπάνω φάκελοι αγνοούνται και οι νέες λήψεις αποθηκεύονται απευθείας στον φάκελο της κατηγορίας Γενικά. Οι λήψεις που βρίσκονται ήδη στη λίστα διατηρούν τον φάκελό τους.", "Once": "Μία φορά", "One-time downloading": "Εφάπαξ λήψη", diff --git a/crates/hydra-gui/assets/locale/en.json b/crates/hydra-gui/assets/locale/en.json index 9473d02..7801d60 100644 --- a/crates/hydra-gui/assets/locale/en.json +++ b/crates/hydra-gui/assets/locale/en.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Number of retries for each file if downloading failed :", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Off (default): a download is filed in its category folder, e.g. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.", "Once": "Once", "One-time downloading": "One-time downloading", diff --git a/crates/hydra-gui/assets/locale/es.json b/crates/hydra-gui/assets/locale/es.json index c8e2099..b4c56c0 100644 --- a/crates/hydra-gui/assets/locale/es.json +++ b/crates/hydra-gui/assets/locale/es.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Número de reintentos por fichero si la descarga falla :", "OK": "Aceptar", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Apagado (por defecto): las descargas se guardan en la carpeta correspondiente a su categoría, p. ej., Descargas/Videos.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Desactivado = no se descarga nada hasta pulsar «Iniciar descarga», por lo que cambiar el nombre o la carpeta ocurre antes de la transferencia, no durante ella.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Activado: las carpetas anteriores se ignoran y las nuevas descargas se guardan directamente en la carpeta de la categoría 'General'. Las descargas que ya figuran en la lista conservan su carpeta.", "Once": "Una vez", "One-time downloading": "Descarga única", diff --git a/crates/hydra-gui/assets/locale/fa.json b/crates/hydra-gui/assets/locale/fa.json index 5db0ebc..de773c6 100644 --- a/crates/hydra-gui/assets/locale/fa.json +++ b/crates/hydra-gui/assets/locale/fa.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "تعداد تلاش مجدد برای هر فایل در صورت شکست دانلود:", "OK": "تأیید", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "خاموش (پیش‌فرض): هر دانلود در پوشه دسته خودش ذخیره می‌شود، مثلاً Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "خاموش = تا زمانی که «شروع دانلود» زده نشود چیزی دریافت نمی‌شود، بنابراین تغییر نام یا تغییر پوشه پیش از انتقال انجام می‌شود نه در حین آن.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "روشن: پوشه‌های بالا نادیده گرفته می‌شوند و دانلودهای جدید مستقیماً در پوشه دسته General ذخیره می‌شوند. دانلودهایی که از قبل در فهرست هستند پوشه خود را حفظ می‌کنند.", "Once": "یک‌بار", "One-time downloading": "دانلود یک‌باره", diff --git a/crates/hydra-gui/assets/locale/fi.json b/crates/hydra-gui/assets/locale/fi.json index fcfee5f..9cbff20 100644 --- a/crates/hydra-gui/assets/locale/fi.json +++ b/crates/hydra-gui/assets/locale/fi.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Yritysten määrä tiedostoa kohden, jos lataus epäonnistuu:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Pois päältä (oletus): lataus tallennetaan sen kategoriakansioon, esim. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Pois = mitään ei ladata ennen kuin painetaan ”Aloita lataus”, joten nimen tai kansion vaihto tapahtuu ennen siirtoa, ei sen aikana.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Päällä: yllä olevat kansiot ohitetaan, ja uudet lataukset tallennetaan suoraan Yleiset-kategoriakansioon. Luettelossa jo olevat lataukset säilyttävät kansionsa.", "Once": "Kerran", "One-time downloading": "Kertalataus", diff --git a/crates/hydra-gui/assets/locale/fr.json b/crates/hydra-gui/assets/locale/fr.json index 1147044..a244950 100644 --- a/crates/hydra-gui/assets/locale/fr.json +++ b/crates/hydra-gui/assets/locale/fr.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Nombre de tentatives pour chaque fichier en cas d'échec :", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Désactivé (par défaut) : un téléchargement est classé dans son dossier de catégorie, par ex. Téléchargements/Vidéo.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Désactivé = rien n’est téléchargé tant que « Démarrer le téléchargement » n’est pas cliqué ; renommer ou changer de dossier se fait donc avant le transfert, pas pendant.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Activé : les dossiers ci-dessus sont ignorés et les nouveaux téléchargements sont enregistrés directement dans le dossier de catégorie Général. Les téléchargements déjà présents dans la liste conservent leur dossier.", "Once": "Une fois", "One-time downloading": "Téléchargement unique", diff --git a/crates/hydra-gui/assets/locale/he.json b/crates/hydra-gui/assets/locale/he.json index 8a0b8af..dd4ef0f 100644 --- a/crates/hydra-gui/assets/locale/he.json +++ b/crates/hydra-gui/assets/locale/he.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "מספר ניסיונות חוזרים לכל קובץ אם ההורדה נכשלה:", "OK": "אישור", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "כבוי (ברירת מחדל): הורדה נשמרת בתיקיית הקטגוריה שלה, לדוגמה הורדות/וידאו (Downloads/Video).", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "כבוי = לא מורד דבר עד שנלחץ \"התחל הורדה\", כך ששינוי שם או החלפת תיקייה מתבצעים לפני ההעברה ולא במהלכה.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "מופעל: המערכת מתעלמת מהתיקיות לעיל והורדות חדשות נשמרות ישירות בתיקיית הקטגוריה 'כללי'. הורדות שכבר קיימות ברשימה שומרות על התיקייה שלהן.", "Once": "פעם אחת", "One-time downloading": "הורדה חד-פעמית", diff --git a/crates/hydra-gui/assets/locale/hi.json b/crates/hydra-gui/assets/locale/hi.json index 23a6930..a210dfa 100644 --- a/crates/hydra-gui/assets/locale/hi.json +++ b/crates/hydra-gui/assets/locale/hi.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "डाउनलोड विफल होने पर प्रत्येक फ़ाइल के लिए पुनः प्रयासों की संख्या:", "OK": "ठीक है", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "बंद (डिफ़ॉल्ट): डाउनलोड को उसके श्रेणी फ़ोल्डर में फ़ाइल किया जाता है, जैसे Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "बंद = \"डाउनलोड शुरू करें\" दबाने तक कुछ भी डाउनलोड नहीं होता, इसलिए नाम बदलना या फ़ोल्डर बदलना स्थानांतरण से पहले होता है, उसके दौरान नहीं।", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "चालू: ऊपर दिए गए फ़ोल्डर अनदेखे कर दिए जाते हैं और नए डाउनलोड सीधे General श्रेणी फ़ोल्डर में सहेजे जाते हैं। सूची में पहले से मौजूद डाउनलोड अपना फ़ोल्डर बनाए रखते हैं।", "Once": "एक बार", "One-time downloading": "एक बार का डाउनलोड", diff --git a/crates/hydra-gui/assets/locale/hu.json b/crates/hydra-gui/assets/locale/hu.json index f64b27e..c8ce2f6 100644 --- a/crates/hydra-gui/assets/locale/hu.json +++ b/crates/hydra-gui/assets/locale/hu.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Az újrapróbálkozások száma fájlonként sikertelen letöltés esetén:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Kikapcsolva (alapértelmezett): a letöltés a kategóriája szerinti mappába kerül, pl. Letöltések/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Kikapcsolva = semmi sem töltődik le, amíg meg nem nyomja a „Letöltés indítása” gombot, így az átnevezés vagy a mappaváltás az átvitel előtt történik, nem közben.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Bekapcsolva: a fenti mappák figyelmen kívül maradnak, és az új letöltések közvetlenül az Általános kategóriamappába kerülnek mentésre. A listán már szereplő letöltések megtartják mappájukat.", "Once": "Egyszer", "One-time downloading": "Egyszeri letöltés", diff --git a/crates/hydra-gui/assets/locale/id.json b/crates/hydra-gui/assets/locale/id.json index bc19610..3738c58 100644 --- a/crates/hydra-gui/assets/locale/id.json +++ b/crates/hydra-gui/assets/locale/id.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Jumlah percobaan ulang untuk setiap berkas jika pengunduhan gagal:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Nonaktif (default): unduhan disimpan di folder kategorinya, mis. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Mati = tidak ada yang diunduh sampai \"Mulai Unduhan\" ditekan, jadi mengganti nama atau folder terjadi sebelum transfer, bukan saat berlangsung.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Aktif: folder di atas diabaikan dan unduhan baru disimpan langsung di folder kategori General. Unduhan yang sudah ada di daftar tetap menggunakan folder masing-masing.", "Once": "Sekali", "One-time downloading": "Pengunduhan sekali", diff --git a/crates/hydra-gui/assets/locale/it.json b/crates/hydra-gui/assets/locale/it.json index 1016d58..fd9f740 100644 --- a/crates/hydra-gui/assets/locale/it.json +++ b/crates/hydra-gui/assets/locale/it.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Numero di tentativi per ciascun file in caso di download non riuscito:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Disattivato (predefinito): un download viene archiviato nella sua cartella di categoria, ad es. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Disattivato = non viene scaricato nulla finché non si preme \"Avvia download\", così rinominare o cambiare cartella avviene prima del trasferimento, non durante.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Attivato: le cartelle sopra vengono ignorate e i nuovi download vengono salvati direttamente nella cartella della categoria Generale. I download già nell'elenco mantengono la propria cartella.", "Once": "Una volta", "One-time downloading": "Download singolo", diff --git a/crates/hydra-gui/assets/locale/ja.json b/crates/hydra-gui/assets/locale/ja.json index 7553210..eedabfb 100644 --- a/crates/hydra-gui/assets/locale/ja.json +++ b/crates/hydra-gui/assets/locale/ja.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "ダウンロード失敗時の再試行回数:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "オフ(デフォルト): ダウンロードはカテゴリフォルダに保存されます(例: Downloads/Video)。", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "オフ = 「ダウンロード開始」を押すまで何も取得しません。名前やフォルダーの変更は転送中ではなく転送前に行えます。", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "オン: 上記のフォルダは無視され、新しいダウンロードは「一般」カテゴリフォルダに直接保存されます。リストに既にあるダウンロードは元のフォルダのままです。", "Once": "1回のみ", "One-time downloading": "1回限りのダウンロード", diff --git a/crates/hydra-gui/assets/locale/ko.json b/crates/hydra-gui/assets/locale/ko.json index fb4a7ff..a64edb4 100644 --- a/crates/hydra-gui/assets/locale/ko.json +++ b/crates/hydra-gui/assets/locale/ko.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "다운로드 실패 시 파일당 재시도 횟수:", "OK": "확인", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "끔 (기본값): 다운로드가 해당 카테고리 폴더에 저장됩니다. 예: Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "끄면 \"다운로드 시작\"을 누를 때까지 아무것도 내려받지 않으므로, 이름이나 폴더 변경이 전송 중이 아니라 전송 전에 이루어집니다.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "켬: 위의 폴더는 무시되며, 새로 다운로드한 항목은 General 카테고리 폴더에 바로 저장됩니다. 목록에 이미 있는 다운로드는 폴더를 유지합니다.", "Once": "한 번", "One-time downloading": "1회 다운로드", diff --git a/crates/hydra-gui/assets/locale/nl.json b/crates/hydra-gui/assets/locale/nl.json index 205db32..0dc67de 100644 --- a/crates/hydra-gui/assets/locale/nl.json +++ b/crates/hydra-gui/assets/locale/nl.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Aantal pogingen per bestand bij mislukte download:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Uit (standaard): een download wordt opgeslagen in de bijbehorende categoriemap, bijv. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Uit = er wordt niets opgehaald totdat op \"Download starten\" is geklikt, zodat hernoemen of een andere map kiezen vóór de overdracht gebeurt, niet tijdens.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Aan: de mappen hierboven worden genegeerd en nieuwe downloads worden direct opgeslagen in de categoriemap Algemeen. Downloads die al in de lijst staan, behouden hun map.", "Once": "Eenmalig", "One-time downloading": "Eenmalig downloaden", diff --git a/crates/hydra-gui/assets/locale/pl.json b/crates/hydra-gui/assets/locale/pl.json index 8d48fc3..547405b 100644 --- a/crates/hydra-gui/assets/locale/pl.json +++ b/crates/hydra-gui/assets/locale/pl.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Liczba ponownych prób dla każdego pliku w przypadku niepowodzenia pobierania:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Wyłączone (domyślnie): pobieranie trafia do folderu swojej kategorii, np. Pobrane/Wideo.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Wyłączone = nic nie jest pobierane, dopóki nie zostanie naciśnięte „Rozpocznij pobieranie”, więc zmiana nazwy lub folderu następuje przed transferem, a nie w jego trakcie.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Włączone: powyższe foldery są ignorowane, a nowe pobierania są zapisywane bezpośrednio w folderze kategorii Ogólne. Pobierania już znajdujące się na liście zachowują swój folder.", "Once": "Raz", "One-time downloading": "Jednorazowe pobieranie", diff --git a/crates/hydra-gui/assets/locale/pt-BR.json b/crates/hydra-gui/assets/locale/pt-BR.json index 8dda574..78ba83e 100644 --- a/crates/hydra-gui/assets/locale/pt-BR.json +++ b/crates/hydra-gui/assets/locale/pt-BR.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Número de tentativas para cada arquivo em caso de falha no download:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Desativado (padrão): um download é arquivado em sua pasta de categoria, por exemplo, Downloads/Vídeo.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Desativado = nada é baixado até \"Iniciar Download\" ser pressionado, então renomear ou mudar a pasta acontece antes da transferência, não durante.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Ativado: as pastas acima são ignoradas e os novos downloads são salvos diretamente na pasta da categoria Geral. Downloads já na lista mantêm sua pasta.", "Once": "Uma vez", "One-time downloading": "Download único", diff --git a/crates/hydra-gui/assets/locale/pt.json b/crates/hydra-gui/assets/locale/pt.json index 49addc1..0dac3e3 100644 --- a/crates/hydra-gui/assets/locale/pt.json +++ b/crates/hydra-gui/assets/locale/pt.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Número de tentativas para cada ficheiro caso a transferência falhe:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Desativado (predefinição): cada transferência é arquivada na sua pasta de categoria, por exemplo Transferências/Vídeo.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Desativado = nada é transferido até premir \"Iniciar Transferência\", pelo que mudar o nome ou a pasta acontece antes da transferência e não durante.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Ativado: as pastas acima são ignoradas e as novas transferências são guardadas diretamente na pasta de categoria Geral. As transferências já existentes na lista mantêm a sua pasta.", "Once": "Uma vez", "One-time downloading": "Transferência única", diff --git a/crates/hydra-gui/assets/locale/ro.json b/crates/hydra-gui/assets/locale/ro.json index 3e390bb..a879b78 100644 --- a/crates/hydra-gui/assets/locale/ro.json +++ b/crates/hydra-gui/assets/locale/ro.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Numărul de reîncercări pentru fiecare fișier dacă descărcarea eșuează:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Dezactivat (implicit): o descărcare este plasată în folderul categoriei sale, de ex. Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Dezactivat = nu se descarcă nimic până la apăsarea „Pornește descărcarea”, astfel redenumirea sau schimbarea folderului are loc înainte de transfer, nu în timpul lui.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Activat: folderele de mai sus sunt ignorate, iar descărcările noi sunt salvate direct în folderul categoriei General. Descărcările deja din listă își păstrează folderul.", "Once": "O dată", "One-time downloading": "Descărcare unică", diff --git a/crates/hydra-gui/assets/locale/ru.json b/crates/hydra-gui/assets/locale/ru.json index 1778884..e35bfc6 100644 --- a/crates/hydra-gui/assets/locale/ru.json +++ b/crates/hydra-gui/assets/locale/ru.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Количество повторных попыток при ошибке скачивания :", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Выкл (по умолчанию): загрузка сохраняется в папку своей категории, например Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Выключено = ничего не загружается, пока не нажата \"Начать закачку\", поэтому переименование или смена папки происходят до передачи, а не во время неё.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Вкл: папки выше игнорируются, и новые загрузки сохраняются прямо в папку категории \"Основные\". Загрузки, уже находящиеся в списке, сохраняют свою папку.", "Once": "Один раз", "One-time downloading": "Однократная загрузка", diff --git a/crates/hydra-gui/assets/locale/sv.json b/crates/hydra-gui/assets/locale/sv.json index e87f3f0..642cf88 100644 --- a/crates/hydra-gui/assets/locale/sv.json +++ b/crates/hydra-gui/assets/locale/sv.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Antal nya försök per fil om nedladdningen misslyckas:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Av (standard): en nedladdning sparas i sin kategorimapp, t.ex. Nedladdningar/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Av = inget hämtas förrän \"Starta nedladdning\" trycks, så byte av namn eller mapp sker före överföringen, inte under den.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "På: mapparna ovan ignoreras och nya nedladdningar sparas direkt i kategorimappen Allmänt. Nedladdningar som redan finns i listan behåller sin mapp.", "Once": "En gång", "One-time downloading": "Engångsnedladdning", diff --git a/crates/hydra-gui/assets/locale/th.json b/crates/hydra-gui/assets/locale/th.json index 59970dc..19c3fb1 100644 --- a/crates/hydra-gui/assets/locale/th.json +++ b/crates/hydra-gui/assets/locale/th.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "จำนวนครั้งที่ลองใหม่สำหรับแต่ละไฟล์หากดาวน์โหลดล้มเหลว :", "OK": "ตกลง", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "ปิด (ค่าเริ่มต้น): ไฟล์ดาวน์โหลดจะถูกจัดเก็บในโฟลเดอร์ตามหมวดหมู่ เช่น Downloads/Video", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "ปิด = จะไม่ดาวน์โหลดสิ่งใดจนกว่าจะกด \"เริ่มดาวน์โหลด\" ดังนั้นการเปลี่ยนชื่อหรือเปลี่ยนโฟลเดอร์จะเกิดขึ้นก่อนการถ่ายโอน ไม่ใช่ระหว่างนั้น", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "เปิด: โฟลเดอร์ด้านบนจะถูกละเว้น และการดาวน์โหลดใหม่จะถูกบันทึกลงในโฟลเดอร์หมวดหมู่ทั่วไปโดยตรง ส่วนไฟล์ที่อยู่ในรายการอยู่แล้วจะยังคงอยู่ในโฟลเดอร์เดิม", "Once": "ครั้งเดียว", "One-time downloading": "การดาวน์โหลดครั้งเดียว", diff --git a/crates/hydra-gui/assets/locale/tr.json b/crates/hydra-gui/assets/locale/tr.json index 28cbcb8..3bf9010 100644 --- a/crates/hydra-gui/assets/locale/tr.json +++ b/crates/hydra-gui/assets/locale/tr.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "İndirme başarısız olursa her dosya için yeniden deneme sayısı:", "OK": "Tamam", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Kapalı (varsayılan): bir indirme kendi kategori klasörüne dosyalanır, ör. İndirilenler/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Kapalı = \"İndirmeyi Başlat\" düğmesine basılana kadar hiçbir şey indirilmez; böylece yeniden adlandırma veya klasör değişikliği aktarım sırasında değil öncesinde yapılır.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Açık: yukarıdaki klasörler yoksayılır ve yeni indirmeler doğrudan Genel kategori klasörüne kaydedilir. Zaten listede olan indirmeler kendi klasörlerinde kalır.", "Once": "Bir Kez", "One-time downloading": "Bir kerelik indirme", diff --git a/crates/hydra-gui/assets/locale/uk.json b/crates/hydra-gui/assets/locale/uk.json index 1e35df6..c5fec54 100644 --- a/crates/hydra-gui/assets/locale/uk.json +++ b/crates/hydra-gui/assets/locale/uk.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Кількість повторних спроб для кожного файлу в разі помилки завантаження :", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Вимкнено (типово): завантаження розміщується в папці своєї категорії, наприклад Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Вимкнено = нічого не завантажується, доки не натиснуто \"Почати завантаження\", тож перейменування чи зміна теки відбувається до передавання, а не під час нього.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Увімкнено: папки вище ігноруються, і нові завантаження зберігаються безпосередньо в папці категорії «Загальні». Завантаження, які вже є в списку, зберігають свою папку.", "Once": "Один раз", "One-time downloading": "Одноразове завантаження", diff --git a/crates/hydra-gui/assets/locale/vi.json b/crates/hydra-gui/assets/locale/vi.json index 3733d0e..c0f5f12 100644 --- a/crates/hydra-gui/assets/locale/vi.json +++ b/crates/hydra-gui/assets/locale/vi.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "Số lần thử lại cho mỗi tệp nếu tải xuống thất bại:", "OK": "OK", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "Tắt (mặc định): mỗi lượt tải được lưu vào thư mục danh mục của nó, ví dụ Downloads/Video.", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "Tắt = không tải gì cho đến khi nhấn \"Bắt đầu tải xuống\", nên việc đổi tên hay đổi thư mục diễn ra trước khi truyền, không phải trong lúc truyền.", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "Bật: các thư mục ở trên bị bỏ qua và các lượt tải mới được lưu trực tiếp vào thư mục danh mục Chung. Các lượt tải đã có trong danh sách vẫn giữ nguyên thư mục của chúng.", "Once": "Một lần", "One-time downloading": "Tải xuống một lần", diff --git a/crates/hydra-gui/assets/locale/zh-hant.json b/crates/hydra-gui/assets/locale/zh-hant.json index 206b1dc..a1674bb 100644 --- a/crates/hydra-gui/assets/locale/zh-hant.json +++ b/crates/hydra-gui/assets/locale/zh-hant.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "下載失敗時,每個檔案的重試次數:", "OK": "確定", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "關閉(預設):下載內容將歸檔到其分類檔案夾中,例如 Downloads/Video。", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "關閉 = 在按下“開始下載”之前不會下載任何內容,因此重新命名或變更資料夾是在傳輸前完成,而非傳輸過程中。", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "開啟後:以上檔案夾將被忽略,新下載的檔案將直接儲存到「一般」分類檔案夾中。清單中已有的下載檔案將保留在其原始檔案夾中。", "Once": "一次", "One-time downloading": "一次下載", diff --git a/crates/hydra-gui/assets/locale/zh.json b/crates/hydra-gui/assets/locale/zh.json index 883902f..8fe048f 100644 --- a/crates/hydra-gui/assets/locale/zh.json +++ b/crates/hydra-gui/assets/locale/zh.json @@ -213,6 +213,7 @@ "Number of retries for each file if downloading failed :": "下载失败时每个文件的重试次数:", "OK": "确定", "Off (default): a download is filed in its category folder, e.g. Downloads/Video.": "关闭(默认):下载会归入其分类文件夹,例如 Downloads/Video。", + "Off = nothing is fetched until \"Start Download\" is pressed, so a rename or a change of folder happens before the transfer, not during it.": "关闭 = 在按下“开始下载”之前不会下载任何内容,因此重命名或更改文件夹是在传输前完成,而不是传输过程中。", "On: the folders above are ignored and new downloads are saved directly in the General category folder. Downloads already on the list keep their folder.": "开启:忽略以上文件夹,新下载会直接保存到“常规”分类文件夹。列表中已有的下载保留原有文件夹。", "Once": "一次", "One-time downloading": "一次性下载", diff --git a/crates/hydra-gui/src/windows/options.rs b/crates/hydra-gui/src/windows/options.rs index ccb0374..ccf09a1 100644 --- a/crates/hydra-gui/src/windows/options.rs +++ b/crates/hydra-gui/src/windows/options.rs @@ -153,6 +153,13 @@ fn general(app: &App) -> El<'_> { .style(theme::check), tr("Closing the main window leaves Hydra running in the tray, where queues and transfers carry on; open it again from the tray icon. Off: closing the window exits Hydra."), ), + ]; + // Straight after "Close to system tray": both decide what the app looks + // like once its window is gone. + if let Some(el) = hide_taskbar { + col = col.push(el); + } + col.extend([ hinted( checkbox(s.check_updates_on_startup).label(tr("Check for updates on startup")) .on_toggle(|b| o(OptField::CheckUpdates(b))) @@ -169,11 +176,6 @@ fn general(app: &App) -> El<'_> { .style(theme::check), tr("Update checks also offer release candidates (-rc tags) when one is ahead of the stable release; otherwise the stable release is used. Beta builds may be less stable."), ), - ]; - if let Some(el) = hide_taskbar { - col = col.push(el); - } - col.extend([ hinted( checkbox(s.power_save).label(tr("Power save mode")) .on_toggle(|b| o(OptField::PowerSave(b))) @@ -348,7 +350,7 @@ fn downloads(app: &App) -> El<'_> { tr("Adding a link first shows name/category/folder while the transfer already runs in the background; off = downloads start immediately."), ), hinted( - checkbox(s.bg_download).label(tr("Download in background while the dialog is open")) + checkbox(s.bg_download).label(tr("Download in background while choosing options")) .on_toggle(|b| o(OptField::BgDownload(b))) .size(15.0) .text_size(theme::FONT_SIZE)