Skip to content

Commit 52afe37

Browse files
committed
Deduplicate repeated code patterns
- `format_size` in `search.rs`: 4 identical if/else blocks → loop over units array - `three_months_ago`/`six_months_ago` → `n_months_ago(now, n)`, `jan1_this_year`/`jan1_last_year` → `jan1(now, offset)` in `ai_query_builder.rs` - `icons.rs`: replace `RwLock<Option<HashMap>>` + `ensure_cache()` with `LazyLock<RwLock<HashMap>>` - `FilePane.svelte`: extract `resetLoadingState()` for 3 duplicated reset blocks - `reactive-settings.svelte.ts`: remove 7 redundant per-case debug logs (outer log already covers it) - `paddle-api.ts`: extract `unwrapPaddleData()` for shared Paddle API envelope unwrap - `fetch-all.ts`: extract `guardedFetch()` for repeated env-check-or-missing-error pattern - `reporter.go`: extract `printIssueSection()` for 3 identical issue-printing blocks
1 parent a1becca commit 52afe37

9 files changed

Lines changed: 99 additions & 214 deletions

File tree

apps/analytics-dashboard/src/lib/server/fetch-all.ts

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export interface DashboardData {
2323
license: SourceResult<LicenseData>
2424
}
2525

26-
function missingEnv(name: string): SourceResult<never> {
27-
return { ok: false, error: `${name}: not configured (missing env vars)` }
26+
/** Runs `fn` if `envKey` is set, otherwise returns a "not configured" error. */
27+
function guardedFetch<T>(envKey: string | undefined, name: string, fn: () => Promise<SourceResult<T>>): Promise<SourceResult<T>> {
28+
return envKey ? fn() : Promise.resolve({ ok: false, error: `${name}: not configured (missing env vars)` })
2829
}
2930

3031
/** Returns the env object from CF Pages platform, falling back to $env/dynamic/private for local dev. */
@@ -59,41 +60,27 @@ export async function fetchDashboardData(
5960
const env = await resolveEnv(platform)
6061

6162
const [umami, cloudflare, paddle, github, posthog, license] = await Promise.all([
62-
env?.UMAMI_API_URL
63-
? fetchUmamiData(
64-
{
65-
UMAMI_API_URL: env.UMAMI_API_URL,
66-
UMAMI_USERNAME: env.UMAMI_USERNAME,
67-
UMAMI_PASSWORD: env.UMAMI_PASSWORD,
68-
UMAMI_WEBSITE_ID: env.UMAMI_WEBSITE_ID,
69-
UMAMI_BLOG_WEBSITE_ID: env.UMAMI_BLOG_WEBSITE_ID,
70-
},
71-
range
72-
)
73-
: Promise.resolve(missingEnv('Umami')),
74-
env?.CLOUDFLARE_API_TOKEN
75-
? fetchCloudflareData(
76-
{ CLOUDFLARE_API_TOKEN: env.CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID },
77-
range
78-
)
79-
: Promise.resolve(missingEnv('Cloudflare')),
80-
env?.PADDLE_API_KEY_LIVE
81-
? fetchPaddleData({ PADDLE_API_KEY_LIVE: env.PADDLE_API_KEY_LIVE }, range)
82-
: Promise.resolve(missingEnv('Paddle')),
63+
guardedFetch(env?.UMAMI_API_URL, 'Umami', () =>
64+
fetchUmamiData({
65+
UMAMI_API_URL: env.UMAMI_API_URL,
66+
UMAMI_USERNAME: env.UMAMI_USERNAME,
67+
UMAMI_PASSWORD: env.UMAMI_PASSWORD,
68+
UMAMI_WEBSITE_ID: env.UMAMI_WEBSITE_ID,
69+
UMAMI_BLOG_WEBSITE_ID: env.UMAMI_BLOG_WEBSITE_ID,
70+
}, range)),
71+
guardedFetch(env?.CLOUDFLARE_API_TOKEN, 'Cloudflare', () =>
72+
fetchCloudflareData({ CLOUDFLARE_API_TOKEN: env.CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID }, range)),
73+
guardedFetch(env?.PADDLE_API_KEY_LIVE, 'Paddle', () =>
74+
fetchPaddleData({ PADDLE_API_KEY_LIVE: env.PADDLE_API_KEY_LIVE }, range)),
8375
fetchGitHubData({ GITHUB_TOKEN: env?.GITHUB_TOKEN }),
84-
env?.POSTHOG_API_KEY
85-
? fetchPostHogData(
86-
{
87-
POSTHOG_API_KEY: env.POSTHOG_API_KEY,
88-
POSTHOG_PROJECT_ID: env.POSTHOG_PROJECT_ID,
89-
POSTHOG_API_URL: env.POSTHOG_API_URL,
90-
},
91-
range
92-
)
93-
: Promise.resolve(missingEnv('PostHog')),
94-
env?.LICENSE_SERVER_ADMIN_TOKEN
95-
? fetchLicenseData({ LICENSE_SERVER_ADMIN_TOKEN: env.LICENSE_SERVER_ADMIN_TOKEN })
96-
: Promise.resolve(missingEnv('License server')),
76+
guardedFetch(env?.POSTHOG_API_KEY, 'PostHog', () =>
77+
fetchPostHogData({
78+
POSTHOG_API_KEY: env.POSTHOG_API_KEY,
79+
POSTHOG_PROJECT_ID: env.POSTHOG_PROJECT_ID,
80+
POSTHOG_API_URL: env.POSTHOG_API_URL,
81+
}, range)),
82+
guardedFetch(env?.LICENSE_SERVER_ADMIN_TOKEN, 'License server', () =>
83+
fetchLicenseData({ LICENSE_SERVER_ADMIN_TOKEN: env.LICENSE_SERVER_ADMIN_TOKEN })),
9784
])
9885

9986
return { range, updatedAt: new Date().toISOString(), umami, cloudflare, paddle, github, posthog, license }

apps/desktop/src-tauri/src/commands/ai_query_builder.rs

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ pub fn time_to_range(t: &str) -> (Option<u64>, Option<u64>) {
9494
"last_month" => (Some(first_of_last_month(now)), Some(first_of_this_month(now))),
9595
"this_quarter" => (Some(first_of_this_quarter(now)), None),
9696
"last_quarter" => (Some(first_of_last_quarter(now)), Some(first_of_this_quarter(now))),
97-
"this_year" => (Some(jan1_this_year(now)), None),
98-
"last_year" => (Some(jan1_last_year(now)), Some(jan1_this_year(now))),
99-
"recent" => (Some(three_months_ago(now)), None),
100-
"last_3_months" => (Some(three_months_ago(now)), None),
101-
"last_6_months" => (Some(six_months_ago(now)), None),
97+
"this_year" => (Some(jan1(now, 0)), None),
98+
"last_year" => (Some(jan1(now, -1)), Some(jan1(now, 0))),
99+
"recent" => (Some(n_months_ago(now, 3)), None),
100+
"last_3_months" => (Some(n_months_ago(now, 3)), None),
101+
"last_6_months" => (Some(n_months_ago(now, 6)), None),
102102
"old" => (None, Some(one_year_ago(now))),
103103
_ => {
104104
if is_year(t) {
@@ -194,42 +194,20 @@ fn first_of_last_quarter(now: OffsetDateTime) -> u64 {
194194
to_timestamp(first.with_hms(0, 0, 0).expect("valid").assume_utc())
195195
}
196196

197-
fn jan1_this_year(now: OffsetDateTime) -> u64 {
198-
let first = time::Date::from_calendar_date(now.year(), time::Month::January, 1).unwrap_or(now.date());
197+
fn jan1(now: OffsetDateTime, year_offset: i32) -> u64 {
198+
let first = time::Date::from_calendar_date(now.year() + year_offset, time::Month::January, 1).unwrap_or(now.date());
199199
to_timestamp(first.with_hms(0, 0, 0).expect("valid").assume_utc())
200200
}
201201

202-
fn jan1_last_year(now: OffsetDateTime) -> u64 {
203-
let first = time::Date::from_calendar_date(now.year() - 1, time::Month::January, 1).unwrap_or(now.date());
204-
to_timestamp(first.with_hms(0, 0, 0).expect("valid").assume_utc())
205-
}
206-
207-
fn three_months_ago(now: OffsetDateTime) -> u64 {
208-
let date = now.date();
209-
// Subtract 3 months
210-
let mut year = date.year();
211-
let mut month_num = date.month() as u8;
212-
if month_num <= 3 {
213-
year -= 1;
214-
month_num += 9; // wrap around
215-
} else {
216-
month_num -= 3;
217-
}
218-
let month = time::Month::try_from(month_num).unwrap_or(time::Month::January);
219-
let day = date.day().min(days_in_month(year, month));
220-
let target = time::Date::from_calendar_date(year, month, day).unwrap_or(date);
221-
to_timestamp(target.with_hms(0, 0, 0).expect("valid").assume_utc())
222-
}
223-
224-
fn six_months_ago(now: OffsetDateTime) -> u64 {
202+
fn n_months_ago(now: OffsetDateTime, n: u8) -> u64 {
225203
let date = now.date();
226204
let mut year = date.year();
227205
let mut month_num = date.month() as u8;
228-
if month_num <= 6 {
206+
if month_num <= n {
229207
year -= 1;
230-
month_num += 6; // wrap around
208+
month_num += 12 - n;
231209
} else {
232-
month_num -= 6;
210+
month_num -= n;
233211
}
234212
let month = time::Month::try_from(month_num).unwrap_or(time::Month::January);
235213
let day = date.day().min(days_in_month(year, month));

apps/desktop/src-tauri/src/icons.rs

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,41 @@ use rayon::prelude::*;
1313
use std::collections::HashMap;
1414
use std::io::Cursor;
1515
use std::path::{Path, PathBuf};
16-
use std::sync::RwLock;
16+
use std::sync::{LazyLock, RwLock};
1717

1818
// file_icon_provider uses GTK on Linux which requires main-thread access and
1919
// fails silently from rayon/tokio threads. On Linux we use freedesktop-icons instead.
2020
#[cfg(target_os = "macos")]
2121
use file_icon_provider::get_file_icon;
2222

2323
/// Cache for generated icons (icon_id -> base64 WebP data URL)
24-
static ICON_CACHE: RwLock<Option<HashMap<String, String>>> = RwLock::new(None);
25-
26-
/// Initializes the icon cache if not already done.
27-
fn ensure_cache() {
28-
let cache = ICON_CACHE.read().unwrap();
29-
if cache.is_some() {
30-
return;
31-
}
32-
drop(cache);
33-
let mut cache = ICON_CACHE.write().unwrap();
34-
if cache.is_none() {
35-
*cache = Some(HashMap::new());
36-
}
37-
}
24+
static ICON_CACHE: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
3825

3926
/// Gets cached icon data URL for the given icon ID, if available.
4027
fn get_cached_icon(icon_id: &str) -> Option<String> {
41-
ensure_cache();
42-
let cache = ICON_CACHE.read().unwrap();
43-
cache.as_ref()?.get(icon_id).cloned()
28+
ICON_CACHE.read().unwrap().get(icon_id).cloned()
4429
}
4530

4631
/// Caches an icon data URL.
4732
fn cache_icon(icon_id: String, data_url: String) {
48-
ensure_cache();
49-
let mut cache = ICON_CACHE.write().unwrap();
50-
if let Some(ref mut map) = *cache {
51-
map.insert(icon_id, data_url);
52-
}
33+
ICON_CACHE.write().unwrap().insert(icon_id, data_url);
5334
}
5435

5536
/// Clears all cached icons for extension-based entries.
5637
/// Called when the "use app icons for documents" setting changes.
5738
pub fn clear_extension_icon_cache() {
58-
ensure_cache();
59-
let mut cache = ICON_CACHE.write().unwrap();
60-
if let Some(ref mut map) = *cache {
61-
// Only remove extension-based icons (ext:xxx), keep directory icons
62-
map.retain(|key, _| !key.starts_with("ext:"));
63-
}
39+
// Only remove extension-based icons (ext:xxx), keep directory icons
40+
ICON_CACHE.write().unwrap().retain(|key, _| !key.starts_with("ext:"));
6441
}
6542

6643
/// Clears all cached icons for directory entries (`dir`, `symlink-dir`, `path:*`).
6744
/// Called when the system theme or accent color changes, since macOS folder icons
6845
/// are tinted by the current appearance.
6946
pub fn clear_directory_icon_cache() {
70-
ensure_cache();
71-
let mut cache = ICON_CACHE.write().unwrap();
72-
if let Some(ref mut map) = *cache {
73-
map.retain(|key, _| key != "dir" && key != "symlink-dir" && !key.starts_with("path:"));
74-
}
47+
ICON_CACHE
48+
.write()
49+
.unwrap()
50+
.retain(|key, _| key != "dir" && key != "symlink-dir" && !key.starts_with("path:"));
7551
}
7652

7753
/// Converts an image to a base64 WebP data URL.

apps/desktop/src-tauri/src/indexing/search.rs

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -349,38 +349,19 @@ pub(crate) fn format_size(bytes: u64) -> String {
349349
const MB: u64 = 1_024 * KB;
350350
const GB: u64 = 1_024 * MB;
351351
const TB: u64 = 1_024 * GB;
352+
const UNITS: &[(u64, &str)] = &[(TB, "TB"), (GB, "GB"), (MB, "MB"), (KB, "KB")];
352353

353-
if bytes >= TB {
354-
let val = bytes as f64 / TB as f64;
355-
if val.fract() == 0.0 {
356-
format!("{} TB", val as u64)
357-
} else {
358-
format!("{val:.1} TB")
359-
}
360-
} else if bytes >= GB {
361-
let val = bytes as f64 / GB as f64;
362-
if val.fract() == 0.0 {
363-
format!("{} GB", val as u64)
364-
} else {
365-
format!("{val:.1} GB")
366-
}
367-
} else if bytes >= MB {
368-
let val = bytes as f64 / MB as f64;
369-
if val.fract() == 0.0 {
370-
format!("{} MB", val as u64)
371-
} else {
372-
format!("{val:.1} MB")
373-
}
374-
} else if bytes >= KB {
375-
let val = bytes as f64 / KB as f64;
376-
if val.fract() == 0.0 {
377-
format!("{} KB", val as u64)
378-
} else {
379-
format!("{val:.1} KB")
354+
for &(threshold, unit) in UNITS {
355+
if bytes >= threshold {
356+
let val = bytes as f64 / threshold as f64;
357+
return if val.fract() == 0.0 {
358+
format!("{} {unit}", val as u64)
359+
} else {
360+
format!("{val:.1} {unit}")
361+
};
380362
}
381-
} else {
382-
format!("{bytes} B")
383363
}
364+
format!("{bytes} B")
384365
}
385366

386367
pub(crate) fn format_timestamp(ts: u64) -> String {

apps/desktop/src/lib/file-explorer/pane/FilePane.svelte

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,16 @@
525525
// Finalizing state (read_dir done, now sorting/caching)
526526
let finalizingCount = $state<number | undefined>(undefined)
527527
let unlistenReadComplete: UnlistenFn | undefined
528+
function resetLoadingState(errorMessage?: string, preserveTotalCount = false) {
529+
if (errorMessage) error = errorMessage
530+
listingId = ''
531+
if (!preserveTotalCount) totalCount = 0
532+
loading = false
533+
openingFolder = false
534+
loadingCount = undefined
535+
finalizingCount = undefined
536+
}
537+
528538
// Sync status map for visible files
529539
let syncStatusMap = $state<Record<string, SyncStatus>>({})
530540
const syncPollIntervalMs = 3000
@@ -775,13 +785,7 @@
775785
if (event.payload.listingId === newListingId && thisGeneration === loadGeneration) {
776786
// For MTP volumes, trigger fallback on error (device likely disconnected)
777787
if (isMtpView) {
778-
error = event.payload.message
779-
listingId = ''
780-
totalCount = 0
781-
loading = false
782-
openingFolder = false
783-
loadingCount = undefined
784-
finalizingCount = undefined
788+
resetLoadingState(event.payload.message)
785789
log.warn('MTP listing error, triggering fallback: {error}', {
786790
error: event.payload.message,
787791
})
@@ -803,25 +807,15 @@
803807
})
804808
} else {
805809
// Path exists but has another error (permission denied, etc.)
806-
error = event.payload.message
807-
listingId = ''
808-
totalCount = 0
809-
loading = false
810-
openingFolder = false
811-
loadingCount = undefined
812-
finalizingCount = undefined
810+
resetLoadingState(event.payload.message)
813811
}
814812
})
815813
}
816814
}),
817815
listen<ListingCancelledEvent>('listing-cancelled', (event) => {
818816
if (event.payload.listingId === newListingId && thisGeneration === loadGeneration) {
819817
// Cancellation handled by onCancelLoading callback
820-
listingId = ''
821-
loading = false
822-
openingFolder = false
823-
loadingCount = undefined
824-
finalizingCount = undefined
818+
resetLoadingState(undefined, true)
825819
}
826820
}),
827821
])
@@ -854,13 +848,7 @@
854848
}
855849
} catch (e) {
856850
if (thisGeneration !== loadGeneration) return
857-
error = e instanceof Error ? e.message : String(e)
858-
listingId = ''
859-
totalCount = 0
860-
loading = false
861-
openingFolder = false
862-
loadingCount = undefined
863-
finalizingCount = undefined
851+
resetLoadingState(e instanceof Error ? e.message : String(e))
864852
}
865853
}
866854

apps/desktop/src/lib/settings/reactive-settings.svelte.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,29 +63,23 @@ export async function initReactiveSettings(): Promise<void> {
6363

6464
switch (id) {
6565
case 'appearance.uiDensity':
66-
log.debug('Applying UI density change: {value}', { value })
6766
uiDensity = value as UiDensity
6867
break
6968
case 'appearance.dateTimeFormat':
70-
log.debug('Applying date/time format change: {value}', { value })
7169
dateTimeFormat = value as DateTimeFormat
7270
break
7371
case 'appearance.customDateTimeFormat':
74-
log.debug('Applying custom date format change: {value}', { value })
7572
customDateTimeFormat = value as string
7673
break
7774
case 'appearance.fileSizeFormat':
78-
log.debug('Applying file size format change: {value}', { value })
7975
fileSizeFormat = value as FileSizeFormat
8076
break
8177
case 'appearance.useAppIconsForDocuments':
82-
log.debug('Applying app icons for documents change: {value}', { value })
8378
useAppIconsForDocuments = value as boolean
8479
// Clear the icon cache so icons are re-fetched with the new setting
8580
void clearExtensionIconCache()
8681
break
8782
case 'listing.directorySortMode':
88-
log.debug('Applying directory sort mode change: {value}', { value })
8983
directorySortMode = value as DirectorySortMode
9084
break
9185
case 'appearance.appColor':

0 commit comments

Comments
 (0)