Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to DealDrop are documented in this file.

## [Unreleased]
- Fixed: Settings → About → **Check for updates** says GitHub couldn't be reached when the check fails, instead of "You're on the latest version." A failed check also no longer hides an update that was already offered.
- Linux: DealDrop is now a **Flatpak** (`DealDrop_<version>_x86_64.flatpak`) instead of an AppImage. Install it once by hand; settings, sign-ins and "Start when I log in" carry over from the AppImage (same `~/.config/dealdrop/`). The AppImage gets no more updates.
- **Update to vX.Y.Z** in the sidebar when a newer release is out (checked at start and every 6 hours). The Flatpak downloads and installs it and restarts; Windows and macOS open the release page.
- Settings → About has a **Check for updates** button that says whether you're on the latest version (or that GitHub couldn't be reached).
Expand Down
19 changes: 13 additions & 6 deletions renderer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2014,19 +2014,26 @@ async function copyError () {
announceStatus('Copied')
}

// A check that couldn't reach GitHub is not "no update": keep the offer already shown.
function updateAfterCheck (shown, answer, reached) {
return reached ? answer : shown
}

// "Update to vX.Y.Z" above the error box when GitHub has a newer release for this platform.
// The Flatpak installs it and restarts; other builds open the release page.
// Returns false when the check itself failed; the 6-hourly calls stay silent about that.
// Returns false when the check itself failed; the 6-hourly calls stay silent about that,
// and an update already offered stays up.
async function checkUpdate () {
let ok = true
state.update = await window.api.checkUpdate().catch(() => { ok = false; return null })
if (ok && !state.update && state.updateError) { state.updateError = null; renderErrors() } // the failed update is gone (installed by hand, or withdrawn)
let reached = true
const answer = await window.api.checkUpdate().catch(() => { reached = false; return null })
state.update = updateAfterCheck(state.update, answer, reached)
if (reached && !state.update && state.updateError) { state.updateError = null; renderErrors() } // the failed update is gone (installed by hand, or withdrawn)
const box = document.getElementById('sidebar-update')
const btn = document.getElementById('btn-update')
if (!box || !btn || btn.disabled) return ok
if (!box || !btn || btn.disabled) return reached
box.style.display = state.update ? 'block' : 'none'
if (state.update) btn.textContent = `Update to v${state.update.version}`
return ok
return reached
}

// Settings → About: a check the user asked for, so it says what happened (textContent: the version comes from the network).
Expand Down
40 changes: 34 additions & 6 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,13 +1042,21 @@ fn get_autostart() -> bool { autostart::get() }
#[tauri::command]
fn set_autostart(on: bool) -> Result<(), String> { autostart::set(on) }

/// `Ok(None)` means this platform is up to date. An error stays an error: the UI reads `None`
/// as "You're on the latest version", so a GitHub failure must not look like that.
fn offered_update(found: Result<Option<Update>, String>, in_flatpak: bool) -> Result<Option<Update>, String> {
found.map(|up| up.map(|u| Update { can_install: u.bundle.is_some() && in_flatpak, ..u }))
}

/// A newer release for this platform, if any (the UI asks at start and every 6 hours).
/// Errors reject the command so Settings → About can say GitHub couldn't be reached.
#[tauri::command]
async fn check_update() -> Option<Update> {
match latest_release().await {
Ok(up) => up.map(|u| Update { can_install: u.bundle.is_some() && update::in_flatpak(), ..u }),
Err(e) => { eprintln!("[update check] {e}"); None }
}
async fn check_update() -> Result<Option<Update>, String> {
let found = match latest_release().await {
Ok(up) => Ok(up),
Err(e) => { eprintln!("[update check] {e}"); Err(e) }
};
offered_update(found, update::in_flatpak())
}

/// Flatpak only: downloads the newest release's bundle, installs it and restarts into it.
Expand Down Expand Up @@ -1217,9 +1225,29 @@ fn main() {

#[cfg(test)]
mod tests {
use super::{LiveSale, days_from_civil, sale_soon_notice, sale_starting_tomorrow, SALES, clamp_cut, dedupe_free, failure, free_notice, fresh_big_deals, fresh_free, fresh_wishlist, is_hidden, keep_or_fail, keep_prices, carry_cheapest, carry_deal_lowest, normalize_hidden, normalize_targets, target_hits, target_notice, price_failure, CS_BLOCKED, normalize_title, remember_big_deals, remember_free, remember_wishlist, restore_by_prefix, safe_https, sale_key, should_prime, store_label, tray_counts, tray_tooltip, with_count, wishlist_notice, load_db, proton_suffix, Acct, Db, Deal, Free, Price, Prime, Settings, SteamProfile, Tokens, WishItem, accounts, same_refresh, BIG_DEAL_CUTS, WISHLIST_CUTS};
use super::{LiveSale, days_from_civil, sale_soon_notice, sale_starting_tomorrow, SALES, clamp_cut, dedupe_free, failure, free_notice, fresh_big_deals, fresh_free, fresh_wishlist, is_hidden, keep_or_fail, keep_prices, carry_cheapest, carry_deal_lowest, normalize_hidden, normalize_targets, target_hits, target_notice, price_failure, CS_BLOCKED, normalize_title, remember_big_deals, remember_free, remember_wishlist, restore_by_prefix, safe_https, sale_key, should_prime, store_label, tray_counts, tray_tooltip, with_count, wishlist_notice, load_db, proton_suffix, Acct, Db, Deal, Free, Price, Prime, Settings, SteamProfile, Tokens, Update, WishItem, accounts, offered_update, same_refresh, BIG_DEAL_CUTS, WISHLIST_CUTS};
use std::collections::{HashMap, HashSet};

#[test]
fn a_failed_update_check_is_not_the_same_as_being_up_to_date() {
let bundle = Some("https://github.com/SusieeTheLinuxUser/dealdrop/releases/download/v0.10.0/DealDrop_0.10.0_x86_64.flatpak".into());
let up = Update {
version: "0.10.0".into(),
page: "https://github.com/SusieeTheLinuxUser/dealdrop/releases/tag/v0.10.0".into(),
bundle: bundle.clone(),
can_install: false,
};
let flatpak = offered_update(Ok(Some(up.clone())), true).unwrap().unwrap();
assert!(flatpak.can_install); // only the Flatpak installs the bundle itself
assert_eq!(flatpak.bundle, bundle);
let other = offered_update(Ok(Some(up)), false).unwrap().unwrap();
assert!(!other.can_install); // Windows, macOS, and a non-Flatpak Linux build open the release page
assert!(!offered_update(Ok(Some(Update { bundle: None, ..other })), true).unwrap().unwrap().can_install);
assert_eq!(offered_update(Ok(None), true).unwrap(), None); // a real "nothing newer"
// the About button reads a rejected command as "Couldn't reach GitHub", not as up to date
assert_eq!(offered_update(Err("HTTP 503".into()), true), Err("HTTP 503".into()));
}

#[cfg(unix)]
#[test]
fn a_config_that_cant_be_moved_aside_stops_saving() {
Expand Down
12 changes: 10 additions & 2 deletions tools/preview/selfcheck.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ vm.runInContext([
between('function nextTheme (', '\nfunction syncSaleTheme'),
between('function wishlistLowestTag (', '\n// ─── Price targets'),
between('const saleKey =', '\n// Live sale →'),
between('function updateAfterCheck (', '\n// "Update to v'),
between('const targetKey', '\nfunction wishlistTargetTag'),
between('const nextIndex', '\n// Moves focus between'),
between('const fmt$ =', '\n// CSP forbids inline onerror'),
'globalThis.helpers = { SALES, state, filteredSorted, storeKeyFor, escHtml, updateRefreshAge, mergeUiPrefs, dealShops, parseClaimed, pruneClaimed, toggleClaimed, claimedLast, normalizeTitle, hiddenTitle, searchRank, parseSeen, newIds, mergeSeen, wishlistKey, seenIds, salesSeenIds, PAGE_SIZE, pageSlice, nextTheme, targetKey, targetPrice, targetHit, parseTarget, protonTier, protonText, protonTag, nextIndex, fmt$, salePriceDescription, dataSig, timeLeft, endsFirst, endDateLabel, startDateLabel, fmtChecked, gameStoreName, srcIcon, wishlistLowestTag, saleKey, BASE_THEMES, SALE_THEMES }',
'globalThis.helpers = { SALES, state, filteredSorted, storeKeyFor, escHtml, updateRefreshAge, mergeUiPrefs, dealShops, parseClaimed, pruneClaimed, toggleClaimed, claimedLast, normalizeTitle, hiddenTitle, searchRank, parseSeen, newIds, mergeSeen, wishlistKey, seenIds, salesSeenIds, PAGE_SIZE, pageSlice, nextTheme, targetKey, targetPrice, targetHit, parseTarget, protonTier, protonText, protonTag, nextIndex, fmt$, salePriceDescription, dataSig, timeLeft, endsFirst, endDateLabel, startDateLabel, fmtChecked, gameStoreName, srcIcon, wishlistLowestTag, saleKey, updateAfterCheck, BASE_THEMES, SALE_THEMES }',
].join('\n'), context)

const h = context.helpers
Expand Down Expand Up @@ -360,6 +361,13 @@ assert.equal(h.saleKey({ name: 'Autumn Sale' }), 'autumn')
assert.equal(h.saleKey({ name: 'Weekly Deals' }), undefined)
assert.equal(h.saleKey({ name: 'Steam Scream Fest' }), undefined) // a fest, not a seasonal sale

// A failed update check keeps the offer already shown; only a reached answer replaces it
const offered = { version: '0.10.0' }
assert.equal(h.updateAfterCheck(offered, null, false), offered)
assert.equal(h.updateAfterCheck(null, null, false), null)
assert.equal(h.updateAfterCheck(offered, null, true), null) // reached, and nothing newer: the offer is gone
assert.equal(h.updateAfterCheck(null, offered, true), offered)

// Countdowns: the d/h/soon split is the floor of the exact instant delta (never a wall-clock
// day count, which would shift across a DST night); relative durations keep this runnable any day
for (const [ms, label] of [[2.5 * 86400000, '2d left'], [86400000 + 60000, '1d left'], [86400000 - 60000, '23h left'], [5 * 3600000 - 60000, '4h left'], [3600000 + 60000, '1h left'], [3600000 - 60000, 'ending soon']]) {
Expand All @@ -381,4 +389,4 @@ for (const t of jsThemes) {
for (const t of cssThemes) assert.equal(jsThemes.has(t), true, `style.css theme "${t}" is missing from BASE_THEMES/SALE_THEMES in app.js`)
assert.equal(jsThemes.has('midnight'), true) // and the root default is offered in Settings

console.log('Renderer self-check passed (price targets, escaping, hidden titles, search ranking, filters, store filter, UI preference merge, claimed games, seen deals, list paging, next sale, refresh age, sale themes, J/K navigation, money and date labels, poll data signature, ProtonDB tiers, lowest-ever rule, sale keys, countdown thresholds, theme/CSS consistency).')
console.log('Renderer self-check passed (price targets, escaping, hidden titles, search ranking, filters, store filter, UI preference merge, claimed games, seen deals, list paging, next sale, refresh age, sale themes, J/K navigation, money and date labels, poll data signature, ProtonDB tiers, lowest-ever rule, sale keys, countdown thresholds, theme/CSS consistency, failed update check).')
Loading