From 1c72bb135f3e60908bd32c1744213c7c7f919fb5 Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 15:06:21 +0530 Subject: [PATCH 01/17] feat: implement lazy-loading for default video with 3D text fallback --- .github/workflows/release.yml | 1 + assets/overlay.html | 63 +++++++++++++++++++++++++++- src/app.rs | 9 ++++ src/events.rs | 2 + src/updater.rs | 79 +++++++++++++++++++++++++++++++++++ wix/main.wxs | 1 - 6 files changed, 153 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fcd3fc7..0550bfc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,5 +50,6 @@ jobs: with: files: | target/release/PauseCat_Installer.msi + assets/default.webm env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/assets/overlay.html b/assets/overlay.html index 27fdf07..987e72c 100644 --- a/assets/overlay.html +++ b/assets/overlay.html @@ -143,10 +143,59 @@ } .hidden { display: none !important; } + + /* 3D Text Fallback */ + .fallback-text-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + z-index: -1; + perspective: 1000px; + background: linear-gradient(135deg, #1a1a1a 0%, #000 100%); + } + + .fallback-text-3d { + font-size: 15vw; + font-weight: 900; + color: #fff; + text-transform: uppercase; + letter-spacing: -0.05em; + transform: rotateX(20deg) rotateY(-20deg); + text-shadow: + 0 1px 0 #ccc, + 0 2px 0 #c9c9c9, + 0 3px 0 #bbb, + 0 4px 0 #b9b9b9, + 0 5px 0 #aaa, + 0 6px 1px rgba(0,0,0,.1), + 0 0 5px rgba(0,0,0,.1), + 0 1px 3px rgba(0,0,0,.3), + 0 3px 5px rgba(0,0,0,.2), + 0 5px 10px rgba(0,0,0,.25), + 0 10px 10px rgba(0,0,0,.2), + 0 20px 20px rgba(0,0,0,.15); + animation: textFloat 8s ease-in-out infinite; + opacity: 0.15; + user-select: none; + pointer-events: none; + } + + @keyframes textFloat { + 0%, 100% { transform: rotateX(20deg) rotateY(-20deg) translateZ(0); } + 50% { transform: rotateX(25deg) rotateY(-15deg) translateZ(50px); } + }
+
@@ -256,11 +305,23 @@ if (event.data.mediaPath) { const img = document.getElementById('bg-image'); const video = document.getElementById('bg-video'); + const fallback = document.getElementById('fallback-ui'); const ext = event.data.mediaPath.split('.').pop().toLowerCase(); + + video.onerror = () => { + console.warn("Video load failed, showing fallback UI"); + video.classList.add('hidden'); + fallback.classList.remove('hidden'); + }; + if (['mp4', 'webm', 'ogg'].includes(ext)) { video.src = event.data.mediaPath; video.classList.remove('hidden'); - video.play().catch(e => console.error(e)); + video.play().catch(e => { + console.error(e); + video.classList.add('hidden'); + fallback.classList.remove('hidden'); + }); } else { img.src = event.data.mediaPath; img.classList.remove('hidden'); diff --git a/src/app.rs b/src/app.rs index 0372cb6..d08e71c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,6 +60,7 @@ impl App { self.tray = Some(TrayIcon::new(self.event_tx.clone())?); crate::updater::cleanup_updates(); + crate::updater::ensure_assets_sync(self.event_tx.clone()); let settings_clone = self.settings.clone(); let event_tx_clone = self.event_tx.clone(); @@ -158,6 +159,14 @@ impl App { win.send_update_error(err); } } + AppEvent::AssetDownloaded(name) => { + log::info!("Asset downloaded: {}", name); + // If the overlay is currently showing the 3D text fallback, we could trigger a reload, + // but for now, we'll just let the next break use the video. + } + AppEvent::AssetDownloadError(err) => { + log::error!("Asset download error: {}", err); + } AppEvent::ThemeChanged(is_dark) => { self.is_dark_mode = is_dark; crate::system::set_tray_menu_theme(is_dark); diff --git a/src/events.rs b/src/events.rs index 997d841..b258f45 100644 --- a/src/events.rs +++ b/src/events.rs @@ -16,6 +16,8 @@ pub enum AppEvent { StartUpdate, UpdateProgress(u32), // Percentage 0-100 UpdateError(String), + AssetDownloaded(String), + AssetDownloadError(String), SessionLocked, SessionUnlocked, } diff --git a/src/updater.rs b/src/updater.rs index 94c562a..72f8b83 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -160,6 +160,85 @@ pub fn cleanup_updates() { } } +pub fn ensure_assets_sync(event_tx: Sender) { + thread::spawn(move || { + let mut asset_path = Settings::get_config_dir(); + asset_path.push("assets"); + if !asset_path.exists() { + let _ = fs::create_dir_all(&asset_path); + } + asset_path.push("default.webm"); + + if asset_path.exists() { + return; + } + + let client = match reqwest::blocking::Client::builder() + .user_agent("PauseCat-Asset-Syncer-v1") + .timeout(std::time::Duration::from_secs(30)) + .build() { + Ok(c) => c, + Err(e) => { + let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + return; + } + }; + + // We fetch the latest release to find the browser_download_url for default.webm + let release: GithubRelease = match client.get(GITHUB_API_URL) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .send() + .and_then(|r| r.json()) { + Ok(r) => r, + Err(e) => { + let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + return; + } + }; + + let asset = match release.assets.iter().find(|a| a.name == "default.webm") { + Some(a) => a, + None => { + let _ = event_tx.send(AppEvent::AssetDownloadError("default.webm not found in release assets".to_string())); + return; + } + }; + + match client.get(&asset.browser_download_url).send() { + Ok(mut response) => { + match fs::File::create(&asset_path) { + Ok(mut file) => { + let mut buffer = [0; 8192]; + loop { + match response.read(&mut buffer) { + Ok(0) => break, + Ok(n) => { + if let Err(e) = std::io::Write::write_all(&mut file, &buffer[..n]) { + let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + return; + } + } + Err(e) => { + let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + return; + } + } + } + let _ = event_tx.send(AppEvent::AssetDownloaded("default.webm".to_string())); + } + Err(e) => { + let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + } + } + } + Err(e) => { + let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + } + } + }); +} + #[cfg(test)] mod internal_tests { use super::*; diff --git a/wix/main.wxs b/wix/main.wxs index 9d72470..ffcbb6b 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -26,7 +26,6 @@ - From c49a554454fc70f9cfb4df694014df929bf6c76f Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 15:18:02 +0530 Subject: [PATCH 02/17] feat: allow custom 3D text and break style selection (Media vs Text) --- assets/overlay.html | 16 ++++++++-- assets/settings.html | 71 ++++++++++++++++++++++++++++++++++-------- src/overlay/webview.rs | 5 +-- src/settings.rs | 4 +++ 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/assets/overlay.html b/assets/overlay.html index 987e72c..e6e07b6 100644 --- a/assets/overlay.html +++ b/assets/overlay.html @@ -276,6 +276,19 @@ bubble.style.animation = `${event.data.animationStyle} 6s ease-in-out infinite`; } + // Custom 3D Text + const fallback = document.getElementById('fallback-ui'); + const fallbackText = fallback.querySelector('.fallback-text-3d'); + if (event.data.customText) { + fallbackText.innerText = event.data.customText; + } + + if (event.data.breakStyle === 'text') { + fallback.classList.remove('hidden'); + } else { + fallback.classList.add('hidden'); + } + // Break Messages if (event.data.breakMessages && event.data.breakMessages.length > 0) { breakMessages = event.data.breakMessages; @@ -302,10 +315,9 @@ } } - if (event.data.mediaPath) { + if (event.data.mediaPath && event.data.breakStyle !== 'text') { const img = document.getElementById('bg-image'); const video = document.getElementById('bg-video'); - const fallback = document.getElementById('fallback-ui'); const ext = event.data.mediaPath.split('.').pop().toLowerCase(); video.onerror = () => { diff --git a/assets/settings.html b/assets/settings.html index f6beec9..08e63c1 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -352,21 +352,46 @@

PauseCat Settings

- -
- Background Media - Select your own vibe + +
+
+ Break Style + Visuals during break +
+
+ +
+
+ + -
-
-
- - Change Background + +
+
+ Background Media + Select your own vibe +
+
+
+
+ + Change Background +
+ +
- - +
-
@@ -552,6 +577,19 @@

PauseCat Settings

} else { bubble.style.animation = 'none'; } } + function toggleBreakStyle() { + const style = document.getElementById('break_style').value; + const textOptions = document.getElementById('text-style-options'); + const mediaOptions = document.getElementById('media-style-options'); + if (style === 'text') { + textOptions.classList.remove('hidden'); + mediaOptions.classList.add('hidden'); + } else { + textOptions.classList.add('hidden'); + mediaOptions.classList.remove('hidden'); + } + } + function save() { const workDuration = parseInt(document.getElementById('work_duration').value); const breakDuration = parseInt(document.getElementById('break_duration').value); @@ -580,7 +618,9 @@

PauseCat Settings

bubble_size: parseInt(document.getElementById('bubble_size').value), bubble_pos_x: parseInt(document.getElementById('bubble_pos_x').value), bubble_pos_y: parseInt(document.getElementById('bubble_pos_y').value), - animation_style: document.getElementById('animation_style').value + animation_style: document.getElementById('animation_style').value, + break_style: document.getElementById('break_style').value, + custom_text: document.getElementById('custom_text').value || "PAUSE" }; window.chrome.webview.postMessage({ action: "save", settings: settings }); } @@ -599,6 +639,11 @@

PauseCat Settings

document.getElementById('bubble_pos_x').value = s.bubble_pos_x || 5; document.getElementById('bubble_pos_y').value = s.bubble_pos_y || 5; document.getElementById('animation_style').value = s.animation_style || "float"; + document.getElementById('break_style').value = s.break_style || "media"; + document.getElementById('custom_text').value = s.custom_text || "PAUSE"; + + toggleBreakStyle(); + if (s.overlay_animation) updatePreview(s.overlay_animation); currentWhitelist = s.whitelist || []; renderList('whitelist-tags', currentWhitelist, 'whitelist'); currentBreakMessages = s.break_messages || ["Breathe In..."]; renderList('break-messages-tags', currentBreakMessages, 'messages'); diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index d23bf36..d88468b 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -43,10 +43,11 @@ where F: FnOnce(&str) { }; let messages_json = serde_json::to_string(&settings.break_messages).unwrap_or_else(|_| "[]".to_string()); let init_msg = format!( - "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}}}", + "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\"}}", settings.break_duration_secs, mode_str, final_media_path, crate::system::is_dark_mode(), settings.bubble_opacity, settings.bubble_size, settings.bubble_pos_x, settings.bubble_pos_y, - settings.animation_style, messages_json, settings.randomize_messages, settings.show_work_duration_status, settings.work_duration_secs + settings.animation_style, messages_json, settings.randomize_messages, settings.show_work_duration_status, settings.work_duration_secs, + settings.break_style, settings.custom_text.replace("\"", "\\\"") ); post_message(&init_msg); } diff --git a/src/settings.rs b/src/settings.rs index d001623..d181f53 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -38,6 +38,8 @@ pub struct Settings { pub bubble_pos_x: i32, pub bubble_pos_y: i32, pub animation_style: String, + pub break_style: String, // "media" or "text" + pub custom_text: String, } impl Default for Settings { @@ -62,6 +64,8 @@ impl Default for Settings { bubble_pos_x: 5, bubble_pos_y: 5, animation_style: "float".to_string(), + break_style: "media".to_string(), + custom_text: "PAUSE".to_string(), } } } From a471155fafbe54266e30eef7c2a1a8ab73d1f9ce Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 15:18:52 +0530 Subject: [PATCH 03/17] ci: fix coverage branch to main --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2870475..9aacc60 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,7 @@ name: Coverage on: push: - branches: [main, feature/*] + branches: ["main"] jobs: coverage: From f21bfc3520fe1dbafa1c617f76da04ba00dd5def Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 15:26:00 +0530 Subject: [PATCH 04/17] feat: add ability to mute/unmute break videos --- assets/overlay.html | 2 ++ assets/settings.html | 15 ++++++++++++++- src/overlay/webview.rs | 4 ++-- src/settings.rs | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/assets/overlay.html b/assets/overlay.html index e6e07b6..ca31e55 100644 --- a/assets/overlay.html +++ b/assets/overlay.html @@ -326,6 +326,8 @@ fallback.classList.remove('hidden'); }; + video.muted = event.data.muteVideo !== false; + if (['mp4', 'webm', 'ogg'].includes(ext)) { video.src = event.data.mediaPath; video.classList.remove('hidden'); diff --git a/assets/settings.html b/assets/settings.html index 08e63c1..e69c13c 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -377,6 +377,17 @@

PauseCat Settings

+
+
+ Mute Video + Disable sound for break videos +
+ +
+
Background Media Select your own vibe @@ -620,7 +631,8 @@

PauseCat Settings

bubble_pos_y: parseInt(document.getElementById('bubble_pos_y').value), animation_style: document.getElementById('animation_style').value, break_style: document.getElementById('break_style').value, - custom_text: document.getElementById('custom_text').value || "PAUSE" + custom_text: document.getElementById('custom_text').value || "PAUSE", + mute_video: document.getElementById('mute_video').checked }; window.chrome.webview.postMessage({ action: "save", settings: settings }); } @@ -641,6 +653,7 @@

PauseCat Settings

document.getElementById('animation_style').value = s.animation_style || "float"; document.getElementById('break_style').value = s.break_style || "media"; document.getElementById('custom_text').value = s.custom_text || "PAUSE"; + document.getElementById('mute_video').checked = s.mute_video !== false; toggleBreakStyle(); diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index d88468b..8c9bfb2 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -43,11 +43,11 @@ where F: FnOnce(&str) { }; let messages_json = serde_json::to_string(&settings.break_messages).unwrap_or_else(|_| "[]".to_string()); let init_msg = format!( - "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\"}}", + "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"muteVideo\": {}}}", settings.break_duration_secs, mode_str, final_media_path, crate::system::is_dark_mode(), settings.bubble_opacity, settings.bubble_size, settings.bubble_pos_x, settings.bubble_pos_y, settings.animation_style, messages_json, settings.randomize_messages, settings.show_work_duration_status, settings.work_duration_secs, - settings.break_style, settings.custom_text.replace("\"", "\\\"") + settings.break_style, settings.custom_text.replace("\"", "\\\""), settings.mute_video ); post_message(&init_msg); } diff --git a/src/settings.rs b/src/settings.rs index d181f53..9777db7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -40,6 +40,7 @@ pub struct Settings { pub animation_style: String, pub break_style: String, // "media" or "text" pub custom_text: String, + pub mute_video: bool, } impl Default for Settings { @@ -66,6 +67,7 @@ impl Default for Settings { animation_style: "float".to_string(), break_style: "media".to_string(), custom_text: "PAUSE".to_string(), + mute_video: true, } } } From 32c03ed863d94fedbc58c44d7d4bf347583615de Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 15:40:39 +0530 Subject: [PATCH 05/17] fix: resolve compilation errors in updater.rs by adding missing imports --- src/updater.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/updater.rs b/src/updater.rs index 72f8b83..c19691e 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,4 +1,6 @@ use std::fs; +use std::thread; +use std::io::Read; use serde::{Deserialize, Serialize}; use semver::Version; use crate::settings::Settings; From 18d5464f4ac11461dfe514b7aa7faddf5a956a8b Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 15:53:34 +0530 Subject: [PATCH 06/17] feat: enhance settings UI with 3D text preview, status footer, and version info --- assets/settings.html | 72 +++++++++++++++++++++++++++++++++++++++++++- src/settings_ui.rs | 14 ++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index e69c13c..ef5d886 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -153,8 +153,26 @@ width: 100%; height: 160px; background: #000; border-radius: 10px; position: relative; overflow: hidden; margin-bottom: 20px; border: 1px solid var(--border); flex-shrink: 0; + perspective: 500px; } .preview-bg { width: 100%; height: 100%; object-fit: cover; opacity: 0.5; } + + .preview-text-3d-container { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; + display: flex; justify-content: center; align-items: center; + pointer-events: none; z-index: 1; + } + .preview-text-3d { + font-size: 3rem; font-weight: 900; color: #fff; + text-transform: uppercase; transform: rotateX(20deg) rotateY(-20deg); + text-shadow: 0 1px 0 #ccc, 0 2px 0 #bbb, 0 3px 2px rgba(0,0,0,0.3); + opacity: 0.2; animation: previewFloat 5s ease-in-out infinite; + } + @keyframes previewFloat { + 0%, 100% { transform: rotateX(20deg) rotateY(-20deg) translateZ(0); } + 50% { transform: rotateX(25deg) rotateY(-15deg) translateZ(20px); } + } + .preview-bubble { position: absolute; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); @@ -162,9 +180,19 @@ display: flex; justify-content: center; align-items: center; color: white; font-weight: 700; pointer-events: none; box-shadow: 0 8px 16px rgba(0,0,0,0.3); - transition: all 0.2s ease; + transition: all 0.2s ease; z-index: 2; } + .status-footer { + margin-top: 8px; padding: 0 4px; + display: flex; justify-content: space-between; + font-size: 0.7rem; font-weight: 500; color: var(--text-secondary); + opacity: 0.8; + } + .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; } + .dot-ready { background: #22c55e; box-shadow: 0 0 8px #22c55e; } + .dot-missing { background: #eab308; box-shadow: 0 0 8px #eab308; } + .media-preview-area { position: relative; margin-top: 10px; } .media-preview { width: 100%; height: 140px; border: 2px dashed var(--border); @@ -269,8 +297,18 @@

PauseCat Settings

+
5:00
+
Schedule
@@ -575,11 +613,30 @@

PauseCat Settings

const posX = parseInt(document.getElementById('bubble_pos_x').value); const posY = parseInt(document.getElementById('bubble_pos_y').value); const anim = document.getElementById('animation_style').value; + const breakStyle = document.getElementById('break_style').value; + const customText = document.getElementById('custom_text').value || "PAUSE"; + const previewSize = size / 4.8; bubble.style.width = previewSize + "px"; bubble.style.height = previewSize + "px"; bubble.style.fontSize = (previewSize / 4) + "px"; bubble.style.backgroundColor = isDarkMode ? `rgba(0,0,0,${opacity})` : `rgba(255,255,255,${opacity})`; bubble.style.left = `calc(${posX}% - ${previewSize * (posX/100)}px)`; bubble.style.top = `calc(${posY}% - ${previewSize * (posY/100)}px)`; + + const textContainer = document.getElementById('preview-text-container'); + const text3d = document.getElementById('preview-text-3d'); + const bgImg = document.getElementById('live-preview-bg-img'); + const bgVid = document.getElementById('live-preview-bg-vid'); + + if (breakStyle === 'text') { + textContainer.classList.remove('hidden'); + text3d.innerText = customText; + bgImg.classList.add('hidden'); + bgVid.classList.add('hidden'); + } else { + textContainer.classList.add('hidden'); + updatePreview(currentMediaPath); // Refresh media visibility + } + if (anim === 'pulse') { bubble.style.setProperty('--pulse-opacity-start', opacity); bubble.style.setProperty('--pulse-opacity-end', Math.max(0, opacity - 0.3)); @@ -599,6 +656,7 @@

PauseCat Settings

textOptions.classList.add('hidden'); mediaOptions.classList.remove('hidden'); } + updatePreviewFromUI(); } function save() { @@ -655,6 +713,18 @@

PauseCat Settings

document.getElementById('custom_text').value = s.custom_text || "PAUSE"; document.getElementById('mute_video').checked = s.mute_video !== false; + // Status Footer Updates + document.getElementById('app-version').innerText = `v${event.data.version}`; + const assetDot = document.getElementById('asset-dot'); + const assetLabel = document.getElementById('asset-label'); + if (event.data.assetReady) { + assetDot.className = 'status-dot dot-ready'; + assetLabel.innerText = 'Cinematic Assets: Ready'; + } else { + assetDot.className = 'status-dot dot-missing'; + assetLabel.innerText = 'Cinematic Assets: Syncing...'; + } + toggleBreakStyle(); if (s.overlay_animation) updatePreview(s.overlay_animation); diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 6e5c804..7617869 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -162,7 +162,19 @@ impl SettingsWindow { let _ = webview.NavigateToString(&HSTRING::from(include_str!("../assets/settings.html"))); let settings_h = GetPropW(hwnd, w!("Settings")); let settings = &*(settings_h.0 as *const Settings); - let msg = format!("{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}}}", serde_json::to_string(settings).unwrap_or_default(), crate::system::is_dark_mode()); + + let mut asset_path = Settings::get_config_dir(); + asset_path.push("assets"); + asset_path.push("default.webm"); + let asset_ready = asset_path.exists(); + + let msg = format!( + "{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}, \"version\": \"{}\", \"assetReady\": {}}}", + serde_json::to_string(settings).unwrap_or_default(), + crate::system::is_dark_mode(), + env!("CARGO_PKG_VERSION"), + asset_ready + ); let _ = webview.PostWebMessageAsJson(&HSTRING::from(msg)); } } From d358c1b2d1e905f09f6b22f7a79fc1ca0e193a0a Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 16:00:05 +0530 Subject: [PATCH 07/17] feat: advanced 3D text customization and asset status fix --- assets/overlay.html | 45 +++++++++++++++++++++++++++----- assets/settings.html | 58 +++++++++++++++++++++++++++++++++++++++++- src/overlay/webview.rs | 5 ++-- src/settings.rs | 10 ++++++++ src/settings_ui.rs | 2 +- 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/assets/overlay.html b/assets/overlay.html index ca31e55..d8a7c32 100644 --- a/assets/overlay.html +++ b/assets/overlay.html @@ -162,10 +162,10 @@ .fallback-text-3d { font-size: 15vw; font-weight: 900; - color: #fff; + color: var(--text-color, #fff); text-transform: uppercase; letter-spacing: -0.05em; - transform: rotateX(20deg) rotateY(-20deg); + transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)); text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, @@ -179,15 +179,29 @@ 0 5px 10px rgba(0,0,0,.25), 0 10px 10px rgba(0,0,0,.2), 0 20px 20px rgba(0,0,0,.15); - animation: textFloat 8s ease-in-out infinite; - opacity: 0.15; + opacity: var(--text-opacity, 0.15); user-select: none; pointer-events: none; } @keyframes textFloat { - 0%, 100% { transform: rotateX(20deg) rotateY(-20deg) translateZ(0); } - 50% { transform: rotateX(25deg) rotateY(-15deg) translateZ(50px); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) translateZ(0); } + 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) translateZ(50px); } + } + + @keyframes textRotate { + 0% { transform: rotateX(var(--rot-x, 20deg)) rotateY(0deg); } + 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(360deg); } + } + + @keyframes textSwing { + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(-40deg); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(40deg); } + } + + @keyframes textPulse { + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1); opacity: var(--text-opacity, 0.15); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1.1); opacity: calc(var(--text-opacity, 0.15) + 0.1); } } @@ -283,6 +297,25 @@ fallbackText.innerText = event.data.customText; } + // Apply advanced text styles + root.style.setProperty('--text-color', event.data.textColor || '#ffffff'); + root.style.setProperty('--text-opacity', event.data.textOpacity ?? 0.15); + root.style.setProperty('--rot-x', (event.data.textRotationX ?? 20) + 'deg'); + root.style.setProperty('--rot-y', (event.data.textRotationY ?? -20) + 'deg'); + + const textAnim = event.data.textAnimation || 'float'; + if (textAnim === 'float') { + fallbackText.style.animation = 'textFloat 8s ease-in-out infinite'; + } else if (textAnim === 'rotate') { + fallbackText.style.animation = 'textRotate 10s linear infinite'; + } else if (textAnim === 'swing') { + fallbackText.style.animation = 'textSwing 6s ease-in-out infinite'; + } else if (textAnim === 'pulse') { + fallbackText.style.animation = 'textPulse 4s ease-in-out infinite'; + } else { + fallbackText.style.animation = 'none'; + } + if (event.data.breakStyle === 'text') { fallback.classList.remove('hidden'); } else { diff --git a/assets/settings.html b/assets/settings.html index ef5d886..7757c15 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -412,6 +412,52 @@

PauseCat Settings

+ +
+
+ Text Animation + Movement behavior +
+ +
+ +
+
+ 3D Rotation X + Tilt forward/back (deg) +
+ +
+ +
+
+ 3D Rotation Y + Tilt side to side (deg) +
+ +
+ +
+
+ Text Color + Pick your vibe +
+ +
+ +
+
+ Text Translucency + Ghostly to Solid +
+ +
@@ -690,7 +736,12 @@

PauseCat Settings

animation_style: document.getElementById('animation_style').value, break_style: document.getElementById('break_style').value, custom_text: document.getElementById('custom_text').value || "PAUSE", - mute_video: document.getElementById('mute_video').checked + mute_video: document.getElementById('mute_video').checked, + text_animation: document.getElementById('text_animation').value, + text_rotation_x: parseInt(document.getElementById('text_rotation_x').value), + text_rotation_y: parseInt(document.getElementById('text_rotation_y').value), + text_color: document.getElementById('text_color').value, + text_opacity: parseFloat(document.getElementById('text_opacity').value) }; window.chrome.webview.postMessage({ action: "save", settings: settings }); } @@ -712,6 +763,11 @@

PauseCat Settings

document.getElementById('break_style').value = s.break_style || "media"; document.getElementById('custom_text').value = s.custom_text || "PAUSE"; document.getElementById('mute_video').checked = s.mute_video !== false; + document.getElementById('text_animation').value = s.text_animation || "float"; + document.getElementById('text_rotation_x').value = s.text_rotation_x ?? 20; + document.getElementById('text_rotation_y').value = s.text_rotation_y ?? -20; + document.getElementById('text_color').value = s.text_color || "#ffffff"; + document.getElementById('text_opacity').value = s.text_opacity ?? 0.15; // Status Footer Updates document.getElementById('app-version').innerText = `v${event.data.version}`; diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index 8c9bfb2..13f239d 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -43,11 +43,12 @@ where F: FnOnce(&str) { }; let messages_json = serde_json::to_string(&settings.break_messages).unwrap_or_else(|_| "[]".to_string()); let init_msg = format!( - "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"muteVideo\": {}}}", + "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"muteVideo\": {}, \"textAnimation\": \"{}\", \"textRotationX\": {}, \"textRotationY\": {}, \"textColor\": \"{}\", \"textOpacity\": {}}}", settings.break_duration_secs, mode_str, final_media_path, crate::system::is_dark_mode(), settings.bubble_opacity, settings.bubble_size, settings.bubble_pos_x, settings.bubble_pos_y, settings.animation_style, messages_json, settings.randomize_messages, settings.show_work_duration_status, settings.work_duration_secs, - settings.break_style, settings.custom_text.replace("\"", "\\\""), settings.mute_video + settings.break_style, settings.custom_text.replace("\"", "\\\""), settings.mute_video, + settings.text_animation, settings.text_rotation_x, settings.text_rotation_y, settings.text_color, settings.text_opacity ); post_message(&init_msg); } diff --git a/src/settings.rs b/src/settings.rs index 9777db7..ee06aa4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -41,6 +41,11 @@ pub struct Settings { pub break_style: String, // "media" or "text" pub custom_text: String, pub mute_video: bool, + pub text_animation: String, + pub text_rotation_x: i32, + pub text_rotation_y: i32, + pub text_color: String, + pub text_opacity: f32, } impl Default for Settings { @@ -68,6 +73,11 @@ impl Default for Settings { break_style: "media".to_string(), custom_text: "PAUSE".to_string(), mute_video: true, + text_animation: "float".to_string(), + text_rotation_x: 20, + text_rotation_y: -20, + text_color: "#ffffff".to_string(), + text_opacity: 0.15, } } } diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 7617869..72c8f75 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -166,7 +166,7 @@ impl SettingsWindow { let mut asset_path = Settings::get_config_dir(); asset_path.push("assets"); asset_path.push("default.webm"); - let asset_ready = asset_path.exists(); + let asset_ready = asset_path.exists() && asset_path.metadata().map(|m| m.len() > 0).unwrap_or(false); let msg = format!( "{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}, \"version\": \"{}\", \"assetReady\": {}}}", From c3bef74455b3e6e5e23817d870c868e8a8b0573b Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 16:12:24 +0530 Subject: [PATCH 08/17] fix: robust asset fetching, prioritized config path, and live 3D text customization --- assets/settings.html | 6 ++++++ src/app.rs | 5 +++-- src/overlay/webview_env.rs | 11 +++++++++++ src/settings_ui.rs | 5 ++--- src/updater.rs | 39 +++++++++++++++++++++++++++++--------- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index 7757c15..f7919a6 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -817,6 +817,12 @@

PauseCat Settings

btn.disabled = false; btn.innerText = "Check for Updates"; hideUpdateModal(); showNotification("Update Error", event.data.error); + } else if (event.data.action === "asset_synced") { + const assetDot = document.getElementById('asset-dot'); + const assetLabel = document.getElementById('asset-label'); + assetDot.className = 'status-dot dot-ready'; + assetLabel.innerText = 'Cinematic Assets: Ready'; + updatePreview(currentMediaPath); } }); diff --git a/src/app.rs b/src/app.rs index d08e71c..e2d6ea0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -161,8 +161,9 @@ impl App { } AppEvent::AssetDownloaded(name) => { log::info!("Asset downloaded: {}", name); - // If the overlay is currently showing the 3D text fallback, we could trigger a reload, - // but for now, we'll just let the next break use the video. + if let Some(ref mut win) = self.settings_window { + win.post_web_message("{\"action\":\"asset_synced\"}"); + } } AppEvent::AssetDownloadError(err) => { log::error!("Asset download error: {}", err); diff --git a/src/overlay/webview_env.rs b/src/overlay/webview_env.rs index a319aa9..a66ba7c 100644 --- a/src/overlay/webview_env.rs +++ b/src/overlay/webview_env.rs @@ -13,11 +13,22 @@ lazy_static::lazy_static! { } pub fn get_assets_path() -> PathBuf { + // 1. Check Config Dir (Lazy-loaded assets) + let mut config_path = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); + config_path.push("PauseCat"); + config_path.push("assets"); + if config_path.exists() { + return config_path; + } + + // 2. Check near EXE (Bundled assets) if let Ok(mut path) = std::env::current_exe() { path.pop(); path.push("assets"); if path.exists() { return path; } } + + // 3. Fallback to CWD let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); path.push("assets"); path diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 72c8f75..7968fc2 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -163,8 +163,7 @@ impl SettingsWindow { let settings_h = GetPropW(hwnd, w!("Settings")); let settings = &*(settings_h.0 as *const Settings); - let mut asset_path = Settings::get_config_dir(); - asset_path.push("assets"); + let mut asset_path = webview_env::get_assets_path(); asset_path.push("default.webm"); let asset_ready = asset_path.exists() && asset_path.metadata().map(|m| m.len() > 0).unwrap_or(false); @@ -186,7 +185,7 @@ impl SettingsWindow { Ok(()) } - fn post_web_message(&self, msg: &str) { + pub fn post_web_message(&self, msg: &str) { if let Ok(lock) = CONTROLLERS.lock() { if let Some(safe_controller) = lock.get(&(self.hwnd.0 as isize)) { if let Ok(webview) = unsafe { safe_controller.0.CoreWebView2() } { diff --git a/src/updater.rs b/src/updater.rs index c19691e..284f796 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -164,29 +164,40 @@ pub fn cleanup_updates() { pub fn ensure_assets_sync(event_tx: Sender) { thread::spawn(move || { - let mut asset_path = Settings::get_config_dir(); - asset_path.push("assets"); - if !asset_path.exists() { - let _ = fs::create_dir_all(&asset_path); + log::info!("Starting asset sync check..."); + + // 1. Determine the target path for the download (Config Dir) + let mut config_asset_path = Settings::get_config_dir(); + config_asset_path.push("assets"); + if !config_asset_path.exists() { + let _ = fs::create_dir_all(&config_asset_path); } - asset_path.push("default.webm"); + config_asset_path.push("default.webm"); + + // 2. Check all preferred locations via the central path resolver + let mut final_path = crate::overlay::webview_env::get_assets_path(); + final_path.push("default.webm"); - if asset_path.exists() { + if final_path.exists() && final_path.metadata().map(|m| m.len() > 0).unwrap_or(false) { + log::info!("Asset already exists and is valid: {:?}", final_path); return; } + log::info!("Asset missing or invalid, attempting download to {:?}", config_asset_path); + let client = match reqwest::blocking::Client::builder() .user_agent("PauseCat-Asset-Syncer-v1") - .timeout(std::time::Duration::from_secs(30)) + .timeout(std::time::Duration::from_secs(60)) .build() { Ok(c) => c, Err(e) => { + log::error!("Failed to create HTTP client: {}", e); let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); return; } }; - // We fetch the latest release to find the browser_download_url for default.webm + log::info!("Fetching latest release info from {}", GITHUB_API_URL); let release: GithubRelease = match client.get(GITHUB_API_URL) .header("Accept", "application/vnd.github+json") .header("X-GitHub-Api-Version", "2022-11-28") @@ -194,6 +205,7 @@ pub fn ensure_assets_sync(event_tx: Sender) { .and_then(|r| r.json()) { Ok(r) => r, Err(e) => { + log::error!("Failed to fetch release info: {}", e); let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); return; } @@ -202,39 +214,48 @@ pub fn ensure_assets_sync(event_tx: Sender) { let asset = match release.assets.iter().find(|a| a.name == "default.webm") { Some(a) => a, None => { + log::warn!("default.webm not found in latest release assets"); let _ = event_tx.send(AppEvent::AssetDownloadError("default.webm not found in release assets".to_string())); return; } }; + log::info!("Downloading default.webm ({} bytes) from {}", asset.size, asset.browser_download_url); match client.get(&asset.browser_download_url).send() { Ok(mut response) => { - match fs::File::create(&asset_path) { + match fs::File::create(&config_asset_path) { Ok(mut file) => { let mut buffer = [0; 8192]; + let mut downloaded = 0; loop { match response.read(&mut buffer) { Ok(0) => break, Ok(n) => { if let Err(e) = std::io::Write::write_all(&mut file, &buffer[..n]) { + log::error!("Failed to write to file: {}", e); let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); return; } + downloaded += n; } Err(e) => { + log::error!("Failed to read response: {}", e); let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); return; } } } + log::info!("Successfully downloaded {} bytes to {:?}", downloaded, config_asset_path); let _ = event_tx.send(AppEvent::AssetDownloaded("default.webm".to_string())); } Err(e) => { + log::error!("Failed to create file: {}", e); let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); } } } Err(e) => { + log::error!("Failed to start download: {}", e); let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); } } From a1cff05369a806beb7b54f91a81b987b417d5b85 Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 19:04:21 +0530 Subject: [PATCH 09/17] feat: default to 3D text, fix header logo, and enhance live preview reactivity --- assets/settings.html | 52 +++++++++++++++++++++++++++++++++----- src/overlay/webview_env.rs | 2 +- src/settings.rs | 2 +- src/settings_ui.rs | 7 +++-- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index f7919a6..1e878a9 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -163,14 +163,30 @@ pointer-events: none; z-index: 1; } .preview-text-3d { - font-size: 3rem; font-weight: 900; color: #fff; - text-transform: uppercase; transform: rotateX(20deg) rotateY(-20deg); + font-size: 3rem; font-weight: 900; + color: var(--text-color, #fff); + text-transform: uppercase; + transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)); text-shadow: 0 1px 0 #ccc, 0 2px 0 #bbb, 0 3px 2px rgba(0,0,0,0.3); - opacity: 0.2; animation: previewFloat 5s ease-in-out infinite; + opacity: var(--text-opacity, 0.2); + animation: previewFloat 5s ease-in-out infinite; + text-align: center; } @keyframes previewFloat { - 0%, 100% { transform: rotateX(20deg) rotateY(-20deg) translateZ(0); } - 50% { transform: rotateX(25deg) rotateY(-15deg) translateZ(20px); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) translateZ(0); } + 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) translateZ(10px); } + } + @keyframes previewRotate { + 0% { transform: rotateX(var(--rot-x, 20deg)) rotateY(0deg); } + 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(360deg); } + } + @keyframes previewSwing { + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(-40deg); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(40deg); } + } + @keyframes previewPulse { + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1); opacity: var(--text-opacity, 0.2); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1.1); opacity: calc(var(--text-opacity, 0.2) + 0.1); } } .preview-bubble { @@ -261,7 +277,7 @@
- PauseCat +

PauseCat Settings

@@ -661,6 +677,13 @@

PauseCat Settings

const anim = document.getElementById('animation_style').value; const breakStyle = document.getElementById('break_style').value; const customText = document.getElementById('custom_text').value || "PAUSE"; + + // Advanced Text Styles + const textAnim = document.getElementById('text_animation').value; + const rotX = document.getElementById('text_rotation_x').value; + const rotY = document.getElementById('text_rotation_y').value; + const textColor = document.getElementById('text_color').value; + const textOpacity = document.getElementById('text_opacity').value; const previewSize = size / 4.8; bubble.style.width = previewSize + "px"; bubble.style.height = previewSize + "px"; bubble.style.fontSize = (previewSize / 4) + "px"; @@ -676,6 +699,19 @@

PauseCat Settings

if (breakStyle === 'text') { textContainer.classList.remove('hidden'); text3d.innerText = customText; + + // Apply advanced preview styles + text3d.style.setProperty('--text-color', textColor); + text3d.style.setProperty('--text-opacity', textOpacity); + text3d.style.setProperty('--rot-x', rotX + 'deg'); + text3d.style.setProperty('--rot-y', rotY + 'deg'); + + if (textAnim === 'float') text3d.style.animation = 'previewFloat 5s ease-in-out infinite'; + else if (textAnim === 'rotate') text3d.style.animation = 'previewRotate 10s linear infinite'; + else if (textAnim === 'swing') text3d.style.animation = 'previewSwing 6s ease-in-out infinite'; + else if (textAnim === 'pulse') text3d.style.animation = 'previewPulse 4s ease-in-out infinite'; + else text3d.style.animation = 'none'; + bgImg.classList.add('hidden'); bgVid.classList.add('hidden'); } else { @@ -781,6 +817,10 @@

PauseCat Settings

assetLabel.innerText = 'Cinematic Assets: Syncing...'; } + if (event.data.logoPath) { + document.getElementById('header-logo').src = event.data.logoPath; + } + toggleBreakStyle(); if (s.overlay_animation) updatePreview(s.overlay_animation); diff --git a/src/overlay/webview_env.rs b/src/overlay/webview_env.rs index a66ba7c..23d6d2f 100644 --- a/src/overlay/webview_env.rs +++ b/src/overlay/webview_env.rs @@ -17,7 +17,7 @@ pub fn get_assets_path() -> PathBuf { let mut config_path = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); config_path.push("PauseCat"); config_path.push("assets"); - if config_path.exists() { + if config_path.exists() && config_path.is_dir() { return config_path; } diff --git a/src/settings.rs b/src/settings.rs index ee06aa4..a710a45 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -70,7 +70,7 @@ impl Default for Settings { bubble_pos_x: 5, bubble_pos_y: 5, animation_style: "float".to_string(), - break_style: "media".to_string(), + break_style: "text".to_string(), custom_text: "PAUSE".to_string(), mute_video: true, text_animation: "float".to_string(), diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 7968fc2..a40d6ab 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -167,12 +167,15 @@ impl SettingsWindow { asset_path.push("default.webm"); let asset_ready = asset_path.exists() && asset_path.metadata().map(|m| m.len() > 0).unwrap_or(false); + let logo_path = "https://pausecat.app/assets/pauseCat.ico"; + let msg = format!( - "{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}, \"version\": \"{}\", \"assetReady\": {}}}", + "{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}, \"version\": \"{}\", \"assetReady\": {}, \"logoPath\": \"{}\"}}", serde_json::to_string(settings).unwrap_or_default(), crate::system::is_dark_mode(), env!("CARGO_PKG_VERSION"), - asset_ready + asset_ready, + logo_path ); let _ = webview.PostWebMessageAsJson(&HSTRING::from(msg)); } From ab355555015fa4f98b5f6f7bea410cef353f6d1a Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 19:13:07 +0530 Subject: [PATCH 10/17] fix: settings logo and live preview reactivity --- src/overlay/webview.rs | 76 +++++++++++++++++++++++++++++++++--------- src/settings_ui.rs | 21 ++++++++++++ 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index 13f239d..3d9a74c 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -57,30 +57,76 @@ where F: FnOnce(&str) { pub fn handle_resource_request(uri: &str, assets_path: &std::path::Path) -> Option<(Vec, String)> { if uri.starts_with("https://pausecat.app/") { let path_part = uri.trim_start_matches("https://pausecat.app/"); - let target_path = if path_part.starts_with("assets/") { - assets_path.join(path_part.trim_start_matches("assets/")) + let file_name = if path_part.starts_with("assets/") { + path_part.trim_start_matches("assets/") + } else { + "" + }; + + if !file_name.is_empty() { + // 1. Try provided assets_path (e.g. Config Dir for lazy-loaded assets) + let target_path = assets_path.join(file_name); + if target_path.exists() && target_path.is_file() { + if let Ok(content) = std::fs::read(&target_path) { + return Some((content, get_mime_type(file_name))); + } + } + + // 2. Try fallback (Near EXE for bundled assets) + if let Ok(mut exe_path) = std::env::current_exe() { + exe_path.pop(); + let fallback_path = exe_path.join("assets").join(file_name); + if fallback_path.exists() && fallback_path.is_file() { + if let Ok(content) = std::fs::read(&fallback_path) { + return Some((content, get_mime_type(file_name))); + } + } + } + + // 3. Try CWD fallback + let cwd_path = std::path::PathBuf::from("assets").join(file_name); + if cwd_path.exists() && cwd_path.is_file() { + if let Ok(content) = std::fs::read(&cwd_path) { + return Some((content, get_mime_type(file_name))); + } + } } else if path_part.starts_with("local/") { let encoded = path_part.trim_start_matches("local/"); if let Ok(path_bytes) = general_purpose::STANDARD.decode(encoded) { - std::path::PathBuf::from(String::from_utf8(path_bytes).unwrap_or_default()) - } else { std::path::PathBuf::new() } - } else { std::path::PathBuf::new() }; - - if target_path.exists() && target_path.is_file() { - if let Ok(content) = std::fs::read(&target_path) { - let ext = target_path.extension().and_then(|e| e.to_str()).unwrap_or(""); - let mime = match ext.to_lowercase().as_str() { - "ico" => "image/x-icon", "webm" => "video/webm", "mp4" => "video/mp4", - "png" => "image/png", "jpg" | "jpeg" => "image/jpeg", "gif" => "image/gif", - _ => "application/octet-stream", - }; - return Some((content, mime.to_string())); + let target_path = std::path::PathBuf::from(String::from_utf8(path_bytes).unwrap_or_default()); + if target_path.exists() && target_path.is_file() { + if let Ok(content) = std::fs::read(&target_path) { + let ext = target_path.extension().and_then(|e| e.to_str()).unwrap_or(""); + return Some((content, get_mime_type(ext))); + } + } } } } None } +fn get_mime_type(path_or_ext: &str) -> String { + let ext = if path_or_ext.contains('.') { + path_or_ext.split('.').last().unwrap_or("") + } else { + path_or_ext + }; + + match ext.to_lowercase().as_str() { + "ico" => "image/x-icon", + "webm" => "video/webm", + "mp4" => "video/mp4", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "html" => "text/html", + "css" => "text/css", + "js" => "application/javascript", + _ => "application/octet-stream", + }.to_string() +} + pub fn on_overlay_controller_completed(result: windows::core::Result<()>, controller: Option, hwnd: HWND) -> windows::core::Result<()> { result?; let controller = controller.ok_or_else(|| windows::core::Error::from_hresult(HRESULT(-1)))?; diff --git a/src/settings_ui.rs b/src/settings_ui.rs index a40d6ab..c76482b 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -159,6 +159,27 @@ impl SettingsWindow { } Ok(()) })), &mut 0); + + let assets_path = webview_env::get_assets_path(); + let env_res = env_inner.clone(); + let _ = webview.AddWebResourceRequestedFilter(w!("https://pausecat.app/*"), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); + let _ = webview.add_WebResourceRequested(&WebResourceRequestedEventHandler::create(Box::new(move |_, args| { + if let (Some(args), env) = (args, &env_res) { + let request = args.Request()?; + let mut uri_ptr = PWSTR::null(); + let _ = request.Uri(&mut uri_ptr); + let uri = uri_ptr.to_string().unwrap_or_default(); + if let Some((content, mime)) = crate::overlay::webview::handle_resource_request(&uri, &assets_path) { + let stream = CreateStreamOnHGlobal(HGLOBAL(std::ptr::null_mut()), true)?; + let _ = (stream.Write(content.as_ptr() as *const _, content.len() as u32, None), stream.Seek(0, STREAM_SEEK_SET, None)); + let response = env.CreateWebResourceResponse(Some(&stream), 200, w!("OK"), &HSTRING::from(format!("Content-Type: {}\r\n", mime)))?; + let _ = args.SetResponse(&response); + } + CoTaskMemFree(Some(uri_ptr.0 as *const _)); + } + Ok(()) + })), &mut 0); + let _ = webview.NavigateToString(&HSTRING::from(include_str!("../assets/settings.html"))); let settings_h = GetPropW(hwnd, w!("Settings")); let settings = &*(settings_h.0 as *const Settings); From e81c506525973a5e72c615cdd6ecbc53d2086ead Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 19:34:36 +0530 Subject: [PATCH 11/17] feat: cinematic 3D depth, adaptive coloring, volume control, and robust sync feedback --- assets/overlay.html | 64 +++++++++++++++++++++++---------- assets/settings.html | 82 +++++++++++++++++++++++++++++++++++++++++- src/app.rs | 12 +++++++ src/events.rs | 2 ++ src/overlay/webview.rs | 7 ++-- src/settings.rs | 10 ++++-- src/settings_ui.rs | 2 ++ src/updater.rs | 10 ++++++ 8 files changed, 164 insertions(+), 25 deletions(-) diff --git a/assets/overlay.html b/assets/overlay.html index d8a7c32..e8305b1 100644 --- a/assets/overlay.html +++ b/assets/overlay.html @@ -166,22 +166,15 @@ text-transform: uppercase; letter-spacing: -0.05em; transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)); - text-shadow: - 0 1px 0 #ccc, - 0 2px 0 #c9c9c9, - 0 3px 0 #bbb, - 0 4px 0 #b9b9b9, - 0 5px 0 #aaa, - 0 6px 1px rgba(0,0,0,.1), - 0 0 5px rgba(0,0,0,.1), - 0 1px 3px rgba(0,0,0,.3), - 0 3px 5px rgba(0,0,0,.2), - 0 5px 10px rgba(0,0,0,.25), - 0 10px 10px rgba(0,0,0,.2), - 0 20px 20px rgba(0,0,0,.15); + + /* High-Fidelity Shadow / Extrusion / Glow */ + text-shadow: var(--text-3d-shadow); + filter: drop-shadow(0 0 var(--text-glow-radius, 10px) var(--text-color, #fff)); + opacity: var(--text-opacity, 0.15); user-select: none; pointer-events: none; + text-align: center; } @keyframes textFloat { @@ -211,7 +204,7 @@
PAUSE
- +
@@ -263,6 +256,30 @@ }, 500); } + function generate3DShadow(color, depth) { + let shadows = []; + for (let i = 1; i <= depth; i++) { + shadows.push(`${i}px ${i}px 0px ${adjustColor(color, -i * 5)}`); + } + shadows.push(`0 ${depth + 1}px 10px rgba(0,0,0,0.4)`); + shadows.push(`0 ${depth + 5}px 20px rgba(0,0,0,0.2)`); + return shadows.join(', '); + } + + function adjustColor(hex, amount) { + if (hex === 'transparent' || hex === 'none') return hex; + let usePound = false; + if (hex[0] == "#") { hex = hex.slice(1); usePound = true; } + let num = parseInt(hex, 16); + let r = (num >> 16) + amount; + if (r > 255) r = 255; else if (r < 0) r = 0; + let b = ((num >> 8) & 0x00FF) + amount; + if (b > 255) b = 255; else if (b < 0) b = 0; + let g = (num & 0x0000FF) + amount; + if (g > 255) g = 255; else if (g < 0) g = 0; + return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0'); + } + window.chrome.webview.addEventListener('message', event => { if (event.data.action === "init") { seconds = event.data.duration; @@ -282,7 +299,7 @@ root.style.setProperty('--bubble-size', event.data.bubbleSize + 'px'); root.style.setProperty('--glass-bg', `rgba(${event.data.isDark ? '0,0,0' : '255,255,255'}, ${event.data.bubbleOpacity})`); - // Smart Position: Account for bubble size so it doesn't clip off edge at 100% + // Smart Position bubble.style.left = `calc(${event.data.bubblePosX}% - ${event.data.bubbleSize * (event.data.bubblePosX/100)}px)`; bubble.style.top = `calc(${event.data.bubblePosY}% - ${event.data.bubbleSize * (event.data.bubblePosY/100)}px)`; @@ -290,18 +307,26 @@ bubble.style.animation = `${event.data.animationStyle} 6s ease-in-out infinite`; } - // Custom 3D Text + // Custom 3D Text Logic const fallback = document.getElementById('fallback-ui'); const fallbackText = fallback.querySelector('.fallback-text-3d'); if (event.data.customText) { fallbackText.innerText = event.data.customText; } + // Adaptive Color Logic + let finalColor = event.data.textColor || '#ffffff'; + if (event.data.adaptiveTextColor) { + finalColor = event.data.isDark ? '#ffffff' : '#000000'; + } + // Apply advanced text styles - root.style.setProperty('--text-color', event.data.textColor || '#ffffff'); + root.style.setProperty('--text-color', finalColor); root.style.setProperty('--text-opacity', event.data.textOpacity ?? 0.15); root.style.setProperty('--rot-x', (event.data.textRotationX ?? 20) + 'deg'); root.style.setProperty('--rot-y', (event.data.textRotationY ?? -20) + 'deg'); + root.style.setProperty('--text-glow-radius', (event.data.textGlow ?? 10) + 'px'); + root.style.setProperty('--text-3d-shadow', generate3DShadow(finalColor, event.data.textDepth ?? 5)); const textAnim = event.data.textAnimation || 'float'; if (textAnim === 'float') { @@ -331,7 +356,7 @@ } } - // Work Duration Status (Smart Formatting) + // Work Duration Status if (event.data.showWorkStatus) { const workStatusEl = document.getElementById('work-status-text'); workStatusEl.classList.remove('hidden'); @@ -359,7 +384,8 @@ fallback.classList.remove('hidden'); }; - video.muted = event.data.muteVideo !== false; + // Set Volume + video.volume = event.data.videoVolume ?? 0.0; if (['mp4', 'webm', 'ogg'].includes(ext)) { video.src = event.data.mediaPath; diff --git a/assets/settings.html b/assets/settings.html index 1e878a9..07c50c1 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -201,13 +201,28 @@ .status-footer { margin-top: 8px; padding: 0 4px; - display: flex; justify-content: space-between; + display: flex; justify-content: space-between; align-items: center; font-size: 0.7rem; font-weight: 500; color: var(--text-secondary); opacity: 0.8; } .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; } .dot-ready { background: #22c55e; box-shadow: 0 0 8px #22c55e; } .dot-missing { background: #eab308; box-shadow: 0 0 8px #eab308; } + .dot-error { background: #ef4444; box-shadow: 0 0 8px #ef4444; } + + .asset-progress-container { + width: 100px; height: 4px; background: var(--secondary-bg); + border-radius: 2px; overflow: hidden; margin-left: 8px; + display: inline-block; vertical-align: middle; + } + .asset-progress-bar { height: 100%; background: var(--accent); width: 0%; transition: width 0.3s ease; } + + .retry-btn { + background: none; border: 1px solid var(--accent); color: var(--accent); + padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; cursor: pointer; + margin-left: 8px; transition: all 0.2s; + } + .retry-btn:hover { background: var(--accent); color: white; } .media-preview-area { position: relative; margin-top: 10px; } .media-preview { @@ -612,6 +627,16 @@

PauseCat Settings

window.chrome.webview.postMessage({ action: "check_updates" }); } + function retrySync() { + const assetDot = document.getElementById('asset-dot'); + const assetLabel = document.getElementById('asset-label'); + assetDot.className = 'status-dot dot-missing'; + assetLabel.innerText = 'Cinematic Assets: Retrying...'; + const retryBtn = document.getElementById('asset-retry-btn'); + if (retryBtn) retryBtn.classList.add('hidden'); + window.chrome.webview.postMessage({ action: "retry_sync" }); + } + function startUpdate() { document.getElementById('update-actions').classList.add('hidden'); document.getElementById('update-progress-section').classList.remove('hidden'); @@ -638,6 +663,29 @@

PauseCat Settings

} } + function generate3DShadow(color, depth) { + let shadows = []; + for (let i = 1; i <= depth; i++) { + shadows.push(`${i}px ${i}px 0px ${adjustColor(color, -i * 5)}`); + } + shadows.push(`0 ${depth + 1}px 10px rgba(0,0,0,0.4)`); + return shadows.join(', '); + } + + function adjustColor(hex, amount) { + if (hex === 'transparent' || hex === 'none') return hex; + let usePound = false; + if (hex[0] == "#") { hex = hex.slice(1); usePound = true; } + let num = parseInt(hex, 16); + let r = (num >> 16) + amount; + if (r > 255) r = 255; else if (r < 0) r = 0; + let b = ((num >> 8) & 0x00FF) + amount; + if (b > 255) b = 255; else if (b < 0) b = 0; + let g = (num & 0x0000FF) + amount; + if (g > 255) g = 255; else if (g < 0) g = 0; + return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0'); + } + function renderList(containerId, list, type) { const container = document.getElementById(containerId); container.innerHTML = ""; list.forEach(item => { @@ -862,7 +910,39 @@

PauseCat Settings

const assetLabel = document.getElementById('asset-label'); assetDot.className = 'status-dot dot-ready'; assetLabel.innerText = 'Cinematic Assets: Ready'; + const container = document.getElementById('asset-status'); + const oldProgress = container.querySelector('.asset-progress-container'); + if (oldProgress) oldProgress.remove(); + const oldRetry = document.getElementById('asset-retry-btn'); + if (oldRetry) oldRetry.remove(); updatePreview(currentMediaPath); + } else if (event.data.action === "asset_progress") { + const container = document.getElementById('asset-status'); + let progressContainer = container.querySelector('.asset-progress-container'); + if (!progressContainer) { + progressContainer = document.createElement('div'); + progressContainer.className = 'asset-progress-container'; + progressContainer.innerHTML = '
'; + container.appendChild(progressContainer); + } + const bar = progressContainer.querySelector('.asset-progress-bar'); + bar.style.width = event.data.percentage + "%"; + document.getElementById('asset-label').innerText = `Cinematic Assets: Syncing (${event.data.percentage}%)`; + } else if (event.data.action === "asset_error") { + const assetDot = document.getElementById('asset-dot'); + const assetLabel = document.getElementById('asset-label'); + assetDot.className = 'status-dot dot-error'; + assetLabel.innerText = 'Cinematic Assets: Failed'; + + const container = document.getElementById('asset-status'); + if (!document.getElementById('asset-retry-btn')) { + const retryBtn = document.createElement('button'); + retryBtn.id = 'asset-retry-btn'; + retryBtn.className = 'retry-btn'; + retryBtn.innerText = 'Retry'; + retryBtn.onclick = retrySync; + container.appendChild(retryBtn); + } } }); diff --git a/src/app.rs b/src/app.rs index e2d6ea0..416a9f3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -165,8 +165,20 @@ impl App { win.post_web_message("{\"action\":\"asset_synced\"}"); } } + AppEvent::AssetDownloadProgress(percentage) => { + if let Some(ref mut win) = self.settings_window { + win.post_web_message(&format!("{{\"action\":\"asset_progress\", \"percentage\": {}}}", percentage)); + } + } AppEvent::AssetDownloadError(err) => { log::error!("Asset download error: {}", err); + if let Some(ref mut win) = self.settings_window { + win.post_web_message(&format!("{{\"action\":\"asset_error\", \"error\": \"{}\"}}", err.replace('"', "\\\""))); + } + } + AppEvent::RetryAssetSync => { + log::info!("Retrying asset sync..."); + crate::updater::ensure_assets_sync(self.event_tx.clone()); } AppEvent::ThemeChanged(is_dark) => { self.is_dark_mode = is_dark; diff --git a/src/events.rs b/src/events.rs index b258f45..0d5e54e 100644 --- a/src/events.rs +++ b/src/events.rs @@ -17,7 +17,9 @@ pub enum AppEvent { UpdateProgress(u32), // Percentage 0-100 UpdateError(String), AssetDownloaded(String), + AssetDownloadProgress(u32), // Percentage 0-100 AssetDownloadError(String), + RetryAssetSync, SessionLocked, SessionUnlocked, } diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index 3d9a74c..7bf413b 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -43,12 +43,13 @@ where F: FnOnce(&str) { }; let messages_json = serde_json::to_string(&settings.break_messages).unwrap_or_else(|_| "[]".to_string()); let init_msg = format!( - "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"muteVideo\": {}, \"textAnimation\": \"{}\", \"textRotationX\": {}, \"textRotationY\": {}, \"textColor\": \"{}\", \"textOpacity\": {}}}", + "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"videoVolume\": {}, \"textAnimation\": \"{}\", \"textRotationX\": {}, \"textRotationY\": {}, \"textColor\": \"{}\", \"textOpacity\": {}, \"textGlow\": {}, \"textDepth\": {}, \"adaptiveTextColor\": {}}}", settings.break_duration_secs, mode_str, final_media_path, crate::system::is_dark_mode(), settings.bubble_opacity, settings.bubble_size, settings.bubble_pos_x, settings.bubble_pos_y, settings.animation_style, messages_json, settings.randomize_messages, settings.show_work_duration_status, settings.work_duration_secs, - settings.break_style, settings.custom_text.replace("\"", "\\\""), settings.mute_video, - settings.text_animation, settings.text_rotation_x, settings.text_rotation_y, settings.text_color, settings.text_opacity + settings.break_style, settings.custom_text.replace("\"", "\\\""), settings.video_volume, + settings.text_animation, settings.text_rotation_x, settings.text_rotation_y, settings.text_color, settings.text_opacity, + settings.text_glow, settings.text_depth, settings.adaptive_text_color ); post_message(&init_msg); } diff --git a/src/settings.rs b/src/settings.rs index a710a45..3a9696c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -40,12 +40,15 @@ pub struct Settings { pub animation_style: String, pub break_style: String, // "media" or "text" pub custom_text: String, - pub mute_video: bool, + pub video_volume: f32, // 0.0 to 1.0 pub text_animation: String, pub text_rotation_x: i32, pub text_rotation_y: i32, pub text_color: String, pub text_opacity: f32, + pub text_glow: f32, + pub text_depth: i32, + pub adaptive_text_color: bool, } impl Default for Settings { @@ -72,12 +75,15 @@ impl Default for Settings { animation_style: "float".to_string(), break_style: "text".to_string(), custom_text: "PAUSE".to_string(), - mute_video: true, + video_volume: 0.0, text_animation: "float".to_string(), text_rotation_x: 20, text_rotation_y: -20, text_color: "#ffffff".to_string(), text_opacity: 0.15, + text_glow: 10.0, + text_depth: 5, + adaptive_text_color: true, } } } diff --git a/src/settings_ui.rs b/src/settings_ui.rs index c76482b..1ad3eeb 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -50,6 +50,8 @@ where F: FnOnce(&str), P: FnOnce() -> Option { if let Some(path) = pick_file_fn() { post_message(&format!("{{\"action\":\"media_selected\", \"path\":\"{}\"}}", path.replace('\\', "/"))); } + } else if json.contains("\"action\":\"retry_sync\"") { + let _ = sender.send(AppEvent::RetryAssetSync); } } diff --git a/src/updater.rs b/src/updater.rs index 284f796..960ab3a 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -223,10 +223,12 @@ pub fn ensure_assets_sync(event_tx: Sender) { log::info!("Downloading default.webm ({} bytes) from {}", asset.size, asset.browser_download_url); match client.get(&asset.browser_download_url).send() { Ok(mut response) => { + let total_size = response.content_length().unwrap_or(asset.size as u64); match fs::File::create(&config_asset_path) { Ok(mut file) => { let mut buffer = [0; 8192]; let mut downloaded = 0; + let mut last_percentage = 0; loop { match response.read(&mut buffer) { Ok(0) => break, @@ -237,6 +239,14 @@ pub fn ensure_assets_sync(event_tx: Sender) { return; } downloaded += n; + + if total_size > 0 { + let percentage = (downloaded as f32 / total_size as f32 * 100.0) as u32; + if percentage > last_percentage { + last_percentage = percentage; + let _ = event_tx.send(AppEvent::AssetDownloadProgress(percentage)); + } + } } Err(e) => { log::error!("Failed to read response: {}", e); From 3896984eb67f21fb66a68d39ee8abf9291489d59 Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 19:48:28 +0530 Subject: [PATCH 12/17] fix: final settings UI reconciliation, restoring volume slider and heavy 3D FX --- assets/settings.html | 64 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index 07c50c1..cb9be5b 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -167,10 +167,11 @@ color: var(--text-color, #fff); text-transform: uppercase; transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)); - text-shadow: 0 1px 0 #ccc, 0 2px 0 #bbb, 0 3px 2px rgba(0,0,0,0.3); + text-shadow: var(--text-3d-shadow); opacity: var(--text-opacity, 0.2); animation: previewFloat 5s ease-in-out infinite; text-align: center; + filter: drop-shadow(0 0 var(--text-glow-radius, 10px) var(--text-color, #fff)); } @keyframes previewFloat { 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) translateZ(0); } @@ -458,6 +459,17 @@

PauseCat Settings

+
+
+ Adaptive Text Color + Auto-contrast based on theme +
+ +
+
3D Rotation X @@ -489,18 +501,31 @@

PauseCat Settings

+ +
+
+ Text Glow + Neon Bloom intensity +
+ +
+ +
+
+ Text Depth + 3D Extrusion height +
+ +
- Mute Video - Disable sound for break videos + Video Volume + Loudness of break videos
- +
@@ -746,13 +771,24 @@

PauseCat Settings

if (breakStyle === 'text') { textContainer.classList.remove('hidden'); + + // Adaptive Color Logic for Preview + let finalColor = textColor; + if (document.getElementById('adaptive_text_color').checked) { + finalColor = isDarkMode ? '#ffffff' : '#000000'; + } + text3d.innerText = customText; // Apply advanced preview styles - text3d.style.setProperty('--text-color', textColor); + text3d.style.setProperty('--text-color', finalColor); text3d.style.setProperty('--text-opacity', textOpacity); text3d.style.setProperty('--rot-x', rotX + 'deg'); text3d.style.setProperty('--rot-y', rotY + 'deg'); + + // Cinematic FX: 3D Extrusion & Glow + text3d.style.setProperty('--text-3d-shadow', generate3DShadow(finalColor, parseInt(document.getElementById('text_depth').value))); + text3d.style.setProperty('--text-glow-radius', document.getElementById('text_glow').value + 'px'); if (textAnim === 'float') text3d.style.animation = 'previewFloat 5s ease-in-out infinite'; else if (textAnim === 'rotate') text3d.style.animation = 'previewRotate 10s linear infinite'; @@ -820,12 +856,15 @@

PauseCat Settings

animation_style: document.getElementById('animation_style').value, break_style: document.getElementById('break_style').value, custom_text: document.getElementById('custom_text').value || "PAUSE", - mute_video: document.getElementById('mute_video').checked, + video_volume: parseFloat(document.getElementById('video_volume').value), text_animation: document.getElementById('text_animation').value, text_rotation_x: parseInt(document.getElementById('text_rotation_x').value), text_rotation_y: parseInt(document.getElementById('text_rotation_y').value), text_color: document.getElementById('text_color').value, - text_opacity: parseFloat(document.getElementById('text_opacity').value) + text_opacity: parseFloat(document.getElementById('text_opacity').value), + text_glow: parseFloat(document.getElementById('text_glow').value), + text_depth: parseInt(document.getElementById('text_depth').value), + adaptive_text_color: document.getElementById('adaptive_text_color').checked }; window.chrome.webview.postMessage({ action: "save", settings: settings }); } @@ -846,12 +885,15 @@

PauseCat Settings

document.getElementById('animation_style').value = s.animation_style || "float"; document.getElementById('break_style').value = s.break_style || "media"; document.getElementById('custom_text').value = s.custom_text || "PAUSE"; - document.getElementById('mute_video').checked = s.mute_video !== false; + document.getElementById('video_volume').value = s.video_volume ?? 0.0; document.getElementById('text_animation').value = s.text_animation || "float"; document.getElementById('text_rotation_x').value = s.text_rotation_x ?? 20; document.getElementById('text_rotation_y').value = s.text_rotation_y ?? -20; document.getElementById('text_color').value = s.text_color || "#ffffff"; document.getElementById('text_opacity').value = s.text_opacity ?? 0.15; + document.getElementById('text_glow').value = s.text_glow ?? 10; + document.getElementById('text_depth').value = s.text_depth ?? 5; + document.getElementById('adaptive_text_color').checked = s.adaptive_text_color !== false; // Status Footer Updates document.getElementById('app-version').innerText = `v${event.data.version}`; From f20a8889a4b9013a22ba7eeb5e1c051a10d9d3fc Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 19:59:15 +0530 Subject: [PATCH 13/17] feat: high-fidelity 3D text engine with granular glow, rotation, and persistence --- assets/overlay.html | 40 ++- assets/settings.html | 643 ++++++++--------------------------------- src/overlay/webview.rs | 6 +- src/settings.rs | 6 + 4 files changed, 149 insertions(+), 546 deletions(-) diff --git a/assets/overlay.html b/assets/overlay.html index e8305b1..1275bd8 100644 --- a/assets/overlay.html +++ b/assets/overlay.html @@ -165,11 +165,11 @@ color: var(--text-color, #fff); text-transform: uppercase; letter-spacing: -0.05em; - transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)); + transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)); /* High-Fidelity Shadow / Extrusion / Glow */ text-shadow: var(--text-3d-shadow); - filter: drop-shadow(0 0 var(--text-glow-radius, 10px) var(--text-color, #fff)); + filter: var(--text-glow-filter, none); opacity: var(--text-opacity, 0.15); user-select: none; @@ -178,23 +178,23 @@ } @keyframes textFloat { - 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) translateZ(0); } - 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) translateZ(50px); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) translateZ(0); } + 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) rotateZ(var(--rot-z, 0deg)) translateZ(50px); } } @keyframes textRotate { - 0% { transform: rotateX(var(--rot-x, 20deg)) rotateY(0deg); } - 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(360deg); } + 0% { transform: rotateX(var(--rot-x, 20deg)) rotateY(0deg) rotateZ(var(--rot-z, 0deg)); } + 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(360deg) rotateZ(var(--rot-z, 0deg)); } } @keyframes textSwing { - 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(-40deg); } - 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(40deg); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(-40deg) rotateZ(var(--rot-z, 0deg)); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(40deg) rotateZ(var(--rot-z, 0deg)); } } @keyframes textPulse { - 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1); opacity: var(--text-opacity, 0.15); } - 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1.1); opacity: calc(var(--text-opacity, 0.15) + 0.1); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) scale(1); opacity: var(--text-opacity, 0.15); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) scale(1.1); opacity: calc(var(--text-opacity, 0.15) + 0.1); } } @@ -259,10 +259,12 @@ function generate3DShadow(color, depth) { let shadows = []; for (let i = 1; i <= depth; i++) { - shadows.push(`${i}px ${i}px 0px ${adjustColor(color, -i * 5)}`); + // Heavier extrusion with gradual darkening + let shadowColor = adjustColor(color, -i * 6); + shadows.push(`${i}px ${i}px 0px ${shadowColor}`); } - shadows.push(`0 ${depth + 1}px 10px rgba(0,0,0,0.4)`); - shadows.push(`0 ${depth + 5}px 20px rgba(0,0,0,0.2)`); + shadows.push(`0 ${depth + 1}px 15px rgba(0,0,0,0.5)`); + shadows.push(`0 ${depth + 8}px 30px rgba(0,0,0,0.3)`); return shadows.join(', '); } @@ -325,7 +327,16 @@ root.style.setProperty('--text-opacity', event.data.textOpacity ?? 0.15); root.style.setProperty('--rot-x', (event.data.textRotationX ?? 20) + 'deg'); root.style.setProperty('--rot-y', (event.data.textRotationY ?? -20) + 'deg'); - root.style.setProperty('--text-glow-radius', (event.data.textGlow ?? 10) + 'px'); + root.style.setProperty('--rot-z', (event.data.textRotationZ ?? 0) + 'deg'); + + // Glow Logic + if (event.data.textGlowEnabled) { + const glowColor = event.data.textGlowColor || finalColor; + root.style.setProperty('--text-glow-filter', `drop-shadow(0 0 ${(event.data.textGlow ?? 10)}px ${glowColor})`); + } else { + root.style.setProperty('--text-glow-filter', 'none'); + } + root.style.setProperty('--text-3d-shadow', generate3DShadow(finalColor, event.data.textDepth ?? 5)); const textAnim = event.data.textAnimation || 'float'; @@ -376,6 +387,7 @@ if (event.data.mediaPath && event.data.breakStyle !== 'text') { const img = document.getElementById('bg-image'); const video = document.getElementById('bg-video'); + const fallback = document.getElementById('fallback-ui'); const ext = event.data.mediaPath.split('.').pop().toLowerCase(); video.onerror = () => { diff --git a/assets/settings.html b/assets/settings.html index cb9be5b..50e08a1 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -166,28 +166,28 @@ font-size: 3rem; font-weight: 900; color: var(--text-color, #fff); text-transform: uppercase; - transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)); + transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)); text-shadow: var(--text-3d-shadow); opacity: var(--text-opacity, 0.2); animation: previewFloat 5s ease-in-out infinite; text-align: center; - filter: drop-shadow(0 0 var(--text-glow-radius, 10px) var(--text-color, #fff)); + filter: var(--text-glow-filter, none); } @keyframes previewFloat { - 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) translateZ(0); } - 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) translateZ(10px); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) translateZ(0); } + 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) rotateZ(var(--rot-z, 0deg)) translateZ(10px); } } @keyframes previewRotate { - 0% { transform: rotateX(var(--rot-x, 20deg)) rotateY(0deg); } - 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(360deg); } + 0% { transform: rotateX(var(--rot-x, 20deg)) rotateY(0deg) rotateZ(var(--rot-z, 0deg)); } + 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(360deg) rotateZ(var(--rot-z, 0deg)); } } @keyframes previewSwing { - 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(-40deg); } - 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(40deg); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(-40deg) rotateZ(var(--rot-z, 0deg)); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(40deg) rotateZ(var(--rot-z, 0deg)); } } @keyframes previewPulse { - 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1); opacity: var(--text-opacity, 0.2); } - 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) scale(1.1); opacity: calc(var(--text-opacity, 0.2) + 0.1); } + 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) scale(1); opacity: var(--text-opacity, 0.2); } + 50% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) scale(1.1); opacity: calc(var(--text-opacity, 0.2) + 0.1); } } .preview-bubble { @@ -298,19 +298,14 @@

PauseCat Settings

- +
Schedule
-
- Work Duration - Minutes before break -
-
- -
+
Work Duration
+
-
- Break Duration - Minutes break lasts (Max 120) -
-
- -
+
Break Duration
+
Bubble Designer
-
- Transparency - Glass effect intensity -
+
Transparency
-
- Bubble Size - Diameter (Small to Large) -
+
Bubble Size
-
- Horizontal Position - Left to Right (%) -
+
Horizontal Position
-
- Vertical Position - Top to Bottom (%) -
+
Vertical Position
-
- Animation Style - Movement behavior -
+
Animation Style
- - - -
-
- -
-
- Break Style - Visuals during break -
-
- -
+
Break Style
+
- - -
Content
-
-
-
- Show Work Status - Display 'Working for X' timer -
- -
- -
-
- Randomize Messages - Pick random message per break -
- -
- -
- Custom Messages - Messages to show during rest -
-
-
- - -
-
- -
Whitelist
-
-
-
- - - -
-
- -
System
-
-
-
- Launch on Startup - Auto-start PauseCat
- -
-
-
- App Updates - Check for new versions on GitHub -
-
@@ -621,227 +447,77 @@

PauseCat Settings

diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index 7bf413b..79b5369 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -43,13 +43,13 @@ where F: FnOnce(&str) { }; let messages_json = serde_json::to_string(&settings.break_messages).unwrap_or_else(|_| "[]".to_string()); let init_msg = format!( - "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"videoVolume\": {}, \"textAnimation\": \"{}\", \"textRotationX\": {}, \"textRotationY\": {}, \"textColor\": \"{}\", \"textOpacity\": {}, \"textGlow\": {}, \"textDepth\": {}, \"adaptiveTextColor\": {}}}", + "{{\"action\":\"init\", \"duration\": {}, \"mode\": \"{}\", \"mediaPath\": \"{}\", \"isDark\": {}, \"bubbleOpacity\": {}, \"bubbleSize\": {}, \"bubblePosX\": {}, \"bubblePosY\": {}, \"animationStyle\": \"{}\", \"breakMessages\": {}, \"randomizeMessages\": {}, \"showWorkStatus\": {}, \"workDurationSecs\": {}, \"breakStyle\": \"{}\", \"customText\": \"{}\", \"videoVolume\": {}, \"textAnimation\": \"{}\", \"textRotationX\": {}, \"textRotationY\": {}, \"textRotationZ\": {}, \"textColor\": \"{}\", \"textOpacity\": {}, \"textGlow\": {}, \"textGlowEnabled\": {}, \"textGlowColor\": \"{}\", \"textDepth\": {}, \"adaptiveTextColor\": {}}}", settings.break_duration_secs, mode_str, final_media_path, crate::system::is_dark_mode(), settings.bubble_opacity, settings.bubble_size, settings.bubble_pos_x, settings.bubble_pos_y, settings.animation_style, messages_json, settings.randomize_messages, settings.show_work_duration_status, settings.work_duration_secs, settings.break_style, settings.custom_text.replace("\"", "\\\""), settings.video_volume, - settings.text_animation, settings.text_rotation_x, settings.text_rotation_y, settings.text_color, settings.text_opacity, - settings.text_glow, settings.text_depth, settings.adaptive_text_color + settings.text_animation, settings.text_rotation_x, settings.text_rotation_y, settings.text_rotation_z, settings.text_color, settings.text_opacity, + settings.text_glow, settings.text_glow_enabled, settings.text_glow_color, settings.text_depth, settings.adaptive_text_color ); post_message(&init_msg); } diff --git a/src/settings.rs b/src/settings.rs index 3a9696c..826af47 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -44,9 +44,12 @@ pub struct Settings { pub text_animation: String, pub text_rotation_x: i32, pub text_rotation_y: i32, + pub text_rotation_z: i32, pub text_color: String, pub text_opacity: f32, pub text_glow: f32, + pub text_glow_enabled: bool, + pub text_glow_color: String, pub text_depth: i32, pub adaptive_text_color: bool, } @@ -79,9 +82,12 @@ impl Default for Settings { text_animation: "float".to_string(), text_rotation_x: 20, text_rotation_y: -20, + text_rotation_z: 0, text_color: "#ffffff".to_string(), text_opacity: 0.15, text_glow: 10.0, + text_glow_enabled: true, + text_glow_color: "#ffffff".to_string(), text_depth: 5, adaptive_text_color: true, } From 80dfcd8ca6556bd5d67c091eb6000d1055ab40d5 Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 21:04:03 +0530 Subject: [PATCH 14/17] fix: hard UI breakage and Dark Mode regressions in Settings --- assets/settings.html | 105 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index 50e08a1..7e689e8 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -439,6 +439,46 @@

PauseCat Settings

+ +
Content
+
+
+
Show Work Status
+ +
+
+
Randomize Messages
+ +
+
Custom Messages
+
+
+ + +
+
+ +
Whitelist
+
+
+
+ + + +
+
+ +
System
+
+
+
Launch on Startup
+ +
+
+
App Updates
+ +
+
- - -
Live Preview
@@ -334,40 +203,11 @@

PauseCat Settings

Work Duration
- +
Break Duration
- -
-
- -
Bubble Designer
-
-
-
Transparency
- -
-
-
Bubble Size
- -
-
-
Horizontal Position
- -
-
-
Vertical Position
- -
-
-
Animation Style
- +
@@ -383,74 +223,91 @@

PauseCat Settings

Video Volume
- +
-
-
-
Change Background
- -
+
+
Change Background
+
-
Content
+
Bubble Designer
-
Show Work Status
- +
Opacity & Size
+
+ + +
+
+
+
Position X & Y
+
+ + +
-
Randomize Messages
- +
Animation style
+ +
+
+ +
Content
+
+
+
Smart Feedback
+
+ + +
-
Custom Messages
@@ -463,7 +320,6 @@

PauseCat Settings

-
@@ -474,10 +330,7 @@

PauseCat Settings

Launch on Startup
-
-
App Updates
- -
+
@@ -501,155 +354,160 @@

PauseCat Settings

function generate3DShadow(color, depth) { let shadows = []; - for (let i = 1; i <= depth; i++) { - shadows.push(`${i}px ${i}px 0px ${adjustColor(color, -i * 6)}`); - } + for (let i = 1; i <= depth; i++) { shadows.push(`${i}px ${i}px 0px ${adjustColor(color, -i * 6)}`); } shadows.push(`0 ${depth}px 20px rgba(0,0,0,0.5)`); return shadows.join(', '); } function adjustColor(hex, amount) { - if (!hex.startsWith('#')) return hex; + if (!hex || !hex.startsWith('#')) return hex; let num = parseInt(hex.slice(1), 16); let r = Math.max(0, Math.min(255, (num >> 16) + amount)); - let b = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount)); - let g = Math.max(0, Math.min(255, (num & 0x0000FF) + amount)); - return "#" + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0'); + let g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount)); + let b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount)); + return "#" + (b | (g << 8) | (r << 16)).toString(16).padStart(6, '0'); } function updatePreviewFromUI() { - const root = document.documentElement; - const bubble = document.getElementById('live-preview-bubble'); - const style = document.getElementById('break_style').value; - const isDark = isDarkMode; - - // Bubble - const opacity = document.getElementById('bubble_opacity').value; - bubble.style.backgroundColor = isDark ? `rgba(0,0,0,${opacity})` : `rgba(255,255,255,${opacity})`; - - // Text Preview - const text3d = document.getElementById('preview-text-3d'); - const textContainer = document.getElementById('preview-text-container'); - const isAdaptive = document.getElementById('adaptive_text_color').checked; - const textColor = isAdaptive ? (isDark ? '#ffffff' : '#000000') : document.getElementById('text_color').value; - - if (style === 'text') { - textContainer.classList.remove('hidden'); - text3d.innerText = document.getElementById('custom_text').value || "PAUSE"; - text3d.style.setProperty('--text-color', textColor); - text3d.style.setProperty('--rot-x', document.getElementById('text_rotation_x').value + 'deg'); - text3d.style.setProperty('--rot-y', document.getElementById('text_rotation_y').value + 'deg'); - text3d.style.setProperty('--rot-z', document.getElementById('text_rotation_z').value + 'deg'); - text3d.style.setProperty('--text-opacity', document.getElementById('text_opacity').value); - text3d.style.setProperty('--text-3d-shadow', generate3DShadow(textColor, parseInt(document.getElementById('text_depth').value))); + try { + const isDark = isDarkMode; + const opacity = document.getElementById('bubble_opacity').value; + const bubble = document.getElementById('live-preview-bubble'); + bubble.style.backgroundColor = isDark ? `rgba(0,0,0,${opacity})` : `rgba(255,255,255,${opacity})`; - if (document.getElementById('text_glow_enabled').checked) { - const glowColor = document.getElementById('text_glow_color').value; - text3d.style.setProperty('--text-glow-filter', `drop-shadow(0 0 ${document.getElementById('text_glow').value}px ${glowColor})`); + const style = document.getElementById('break_style').value; + const textContainer = document.getElementById('preview-text-container'); + const text3d = document.getElementById('preview-text-3d'); + + if (style === 'text') { + textContainer.classList.remove('hidden'); + const isAdaptive = document.getElementById('adaptive_text_color').checked; + const baseColor = document.getElementById('text_color').value; + const finalColor = isAdaptive ? (isDark ? '#ffffff' : '#000000') : baseColor; + + text3d.innerText = document.getElementById('custom_text').value || "PAUSE"; + text3d.style.setProperty('--text-color', finalColor); + text3d.style.setProperty('--rot-x', document.getElementById('text_rotation_x').value + 'deg'); + text3d.style.setProperty('--rot-y', document.getElementById('text_rotation_y').value + 'deg'); + text3d.style.setProperty('--rot-z', document.getElementById('text_rotation_z').value + 'deg'); + text3d.style.setProperty('--text-opacity', document.getElementById('text_opacity').value); + text3d.style.setProperty('--text-3d-shadow', generate3DShadow(finalColor, parseInt(document.getElementById('text_depth').value))); + + if (document.getElementById('text_glow_enabled').checked) { + const glowColor = document.getElementById('text_glow_color').value; + text3d.style.setProperty('--text-glow-filter', `drop-shadow(0 0 ${document.getElementById('text_glow').value}px ${glowColor})`); + } else { + text3d.style.setProperty('--text-glow-filter', 'none'); + } } else { - text3d.style.setProperty('--text-glow-filter', 'none'); + textContainer.classList.add('hidden'); } - } else { - textContainer.classList.add('hidden'); - } + } catch (e) { console.warn("Preview Sync Error:", e); } } function save() { + const getVal = (id) => document.getElementById(id).value; + const getCheck = (id) => document.getElementById(id).checked; + const settings = { - work_duration_secs: document.getElementById('work_duration').value * 60, - break_duration_secs: document.getElementById('break_duration').value * 60, - mode: document.getElementById('mode').value || "soft", - autostart: document.getElementById('autostart').checked, + work_duration_secs: getVal('work_duration') * 60, + break_duration_secs: getVal('break_duration') * 60, + mode: "soft", + autostart: getCheck('autostart'), overlay_animation: currentMediaPath || "default.webm", whitelist: currentWhitelist, - randomize_messages: document.getElementById('randomize_messages').checked, + randomize_messages: getCheck('randomize_messages'), break_messages: currentBreakMessages, - show_work_duration_status: document.getElementById('show_work_status').checked, - bubble_opacity: parseFloat(document.getElementById('bubble_opacity').value), - bubble_size: parseInt(document.getElementById('bubble_size').value), - bubble_pos_x: parseInt(document.getElementById('bubble_pos_x').value), - bubble_pos_y: parseInt(document.getElementById('bubble_pos_y').value), - animation_style: document.getElementById('animation_style').value, - break_style: document.getElementById('break_style').value, - custom_text: document.getElementById('custom_text').value || "PAUSE", - video_volume: parseFloat(document.getElementById('video_volume').value), - text_animation: document.getElementById('text_animation').value, - text_rotation_x: parseInt(document.getElementById('text_rotation_x').value), - text_rotation_y: parseInt(document.getElementById('text_rotation_y').value), - text_rotation_z: parseInt(document.getElementById('text_rotation_z').value), - text_color: document.getElementById('text_color').value, - text_opacity: parseFloat(document.getElementById('text_opacity').value), - text_glow: parseFloat(document.getElementById('text_glow').value), - text_glow_enabled: document.getElementById('text_glow_enabled').checked, - text_glow_color: document.getElementById('text_glow_color').value, - text_depth: parseInt(document.getElementById('text_depth').value), - adaptive_text_color: document.getElementById('adaptive_text_color').checked + show_work_duration_status: getCheck('show_work_status'), + bubble_opacity: parseFloat(getVal('bubble_opacity')), + bubble_size: parseInt(getVal('bubble_size')), + bubble_pos_x: parseInt(getVal('bubble_pos_x')), + bubble_pos_y: parseInt(getVal('bubble_pos_y')), + animation_style: getVal('animation_style'), + break_style: getVal('break_style'), + custom_text: getVal('custom_text') || "PAUSE", + video_volume: parseFloat(getVal('video_volume')), + text_animation: "float", + text_rotation_x: parseInt(getVal('text_rotation_x')), + text_rotation_y: parseInt(getVal('text_rotation_y')), + text_rotation_z: parseInt(getVal('text_rotation_z')), + text_color: getVal('text_color'), + text_opacity: parseFloat(getVal('text_opacity')), + text_glow: parseFloat(getVal('text_glow')), + text_glow_enabled: getCheck('text_glow_enabled'), + text_glow_color: getVal('text_glow_color'), + text_depth: parseInt(getVal('text_depth')), + adaptive_text_color: getCheck('adaptive_text_color') }; window.chrome.webview.postMessage({ action: "save", settings: settings }); } window.chrome.webview.addEventListener('message', event => { - if (event.data.action === "load") { - const s = event.data.settings; - isDarkMode = event.data.isDark; - if (isDarkMode) document.body.classList.add('dark'); else document.body.classList.remove('dark'); - - document.getElementById('work_duration').value = s.work_duration_secs / 60; - document.getElementById('break_duration').value = s.break_duration_secs / 60; - document.getElementById('bubble_opacity').value = s.bubble_opacity; - document.getElementById('bubble_size').value = s.bubble_size; - document.getElementById('bubble_pos_x').value = s.bubble_pos_x; - document.getElementById('bubble_pos_y').value = s.bubble_pos_y; - document.getElementById('animation_style').value = s.animation_style; - document.getElementById('break_style').value = s.break_style || "text"; - document.getElementById('custom_text').value = s.custom_text || "PAUSE"; - document.getElementById('video_volume').value = s.video_volume ?? 0.0; - document.getElementById('text_animation').value = s.text_animation || "float"; - document.getElementById('text_rotation_x').value = s.text_rotation_x ?? 20; - document.getElementById('text_rotation_y').value = s.text_rotation_y ?? -20; - document.getElementById('text_rotation_z').value = s.text_rotation_z ?? 0; - document.getElementById('text_color').value = s.text_color || "#ffffff"; - document.getElementById('text_opacity').value = s.text_opacity ?? 0.15; - document.getElementById('text_glow').value = s.text_glow ?? 10; - document.getElementById('text_glow_enabled').checked = s.text_glow_enabled !== false; - document.getElementById('text_glow_color').value = s.text_glow_color || "#ffffff"; - document.getElementById('text_depth').value = s.text_depth ?? 5; - document.getElementById('adaptive_text_color').checked = s.adaptive_text_color !== false; - - document.getElementById('autostart').checked = s.autostart; - document.getElementById('randomize_messages').checked = s.randomize_messages; - document.getElementById('show_work_status').checked = s.show_work_duration_status; + const data = event.data; + if (data.action === "load") { + const s = data.settings; + isDarkMode = data.isDark; + document.body.classList.toggle('dark', isDarkMode); + + const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; }; + const setCheck = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; }; + + setVal('work_duration', s.work_duration_secs / 60); + setVal('break_duration', s.break_duration_secs / 60); + setVal('bubble_opacity', s.bubble_opacity ?? 0.1); + setVal('bubble_size', s.bubble_size ?? 580); + setVal('bubble_pos_x', s.bubble_pos_x ?? 5); + setVal('bubble_pos_y', s.bubble_pos_y ?? 5); + setVal('animation_style', s.animation_style || "float"); + setVal('break_style', s.break_style || "text"); + setVal('custom_text', s.custom_text || "PAUSE"); + setVal('video_volume', s.video_volume ?? 0.0); + setVal('text_rotation_x', s.text_rotation_x ?? 20); + setVal('text_rotation_y', s.text_rotation_y ?? -20); + setVal('text_rotation_z', s.text_rotation_z ?? 0); + setVal('text_color', s.text_color || "#ffffff"); + setVal('text_opacity', s.text_opacity ?? 0.15); + setVal('text_glow', s.text_glow ?? 10); + setCheck('text_glow_enabled', s.text_glow_enabled !== false); + setVal('text_glow_color', s.text_glow_color || "#ffffff"); + setVal('text_depth', s.text_depth ?? 5); + setCheck('adaptive_text_color', s.adaptive_text_color !== false); + setCheck('autostart', s.autostart); + setCheck('randomize_messages', s.randomize_messages); + setCheck('show_work_status', s.show_work_duration_status !== false); currentWhitelist = s.whitelist || []; renderList('whitelist-tags', currentWhitelist, 'whitelist'); currentBreakMessages = s.break_messages || []; renderList('break-messages-tags', currentBreakMessages, 'messages'); - if (event.data.logoPath) document.getElementById('header-logo').src = event.data.logoPath; - if (event.data.version) document.getElementById('app-version').innerText = `v${event.data.version}`; + if (data.logoPath) document.getElementById('header-logo').src = data.logoPath; + document.getElementById('app-version').innerText = `v${data.version || '1.0.2'}`; - const assetDot = document.getElementById('asset-dot'); - const assetLabel = document.getElementById('asset-label'); - if (event.data.assetReady) { - assetDot.className = 'status-dot dot-ready'; assetLabel.innerText = 'Cinematic Assets: Ready'; - } else { - assetDot.className = 'status-dot dot-missing'; assetLabel.innerText = 'Cinematic Assets: Syncing...'; - } + const dot = document.getElementById('asset-dot'); + const label = document.getElementById('asset-label'); + if (data.assetReady) { dot.className = 'status-dot dot-ready'; label.innerText = 'Cinematic Assets: Ready'; } + else { dot.className = 'status-dot dot-missing'; label.innerText = 'Cinematic Assets: Syncing...'; } toggleBreakStyle(); - } else if (event.data.action === "asset_synced") { - const dot = document.getElementById('asset-dot'); - dot.className = 'status-dot dot-ready'; + window.chrome.webview.postMessage({ action: "get_apps" }); + } else if (data.action === "asset_synced") { + document.getElementById('asset-dot').className = 'status-dot dot-ready'; document.getElementById('asset-label').innerText = 'Cinematic Assets: Ready'; - } else if (event.data.action === "asset_progress") { - document.getElementById('asset-label').innerText = `Cinematic Assets: Syncing (${event.data.percentage}%)`; - } else if (event.data.action === "theme_changed") { - isDarkMode = event.data.isDark; - if (isDarkMode) document.body.classList.add('dark'); else document.body.classList.remove('dark'); - updatePreviewFromUI(); + } else if (data.action === "asset_progress") { + document.getElementById('asset-label').innerText = `Cinematic Assets: Syncing (${data.percentage}%)`; + } else if (data.action === "theme_changed") { + isDarkMode = data.isDark; document.body.classList.toggle('dark', isDarkMode); updatePreviewFromUI(); + } else if (data.action === "apps_list") { + const sel = document.getElementById('running-apps-select'); + sel.innerHTML = ''; + (data.apps || []).forEach(a => { const o = document.createElement('option'); o.value = a; o.innerText = a; sel.appendChild(o); }); + } else if (data.action === "media_selected") { + currentMediaPath = data.path; updatePreview(data.path); } }); function renderList(id, list, type) { - const container = document.getElementById(id); container.innerHTML = ""; + const container = document.getElementById(id); if (!container) return; + container.innerHTML = ""; list.forEach(item => { const tag = document.createElement('div'); tag.className = 'tag'; tag.innerHTML = `${item} ×`; @@ -661,13 +519,28 @@

PauseCat Settings

else { currentBreakMessages = currentBreakMessages.filter(i => i !== item); renderList('break-messages-tags', currentBreakMessages, 'messages'); } } function addBreakMessage() { - const input = document.getElementById('new-message-input'); const val = input.value.trim(); - if (val && !currentBreakMessages.includes(val)) { currentBreakMessages.push(val); input.value = ""; renderList('break-messages-tags', currentBreakMessages, 'messages'); } + const el = document.getElementById('new-message-input'); const val = el.value.trim(); + if (val && !currentBreakMessages.includes(val)) { currentBreakMessages.push(val); el.value = ""; renderList('break-messages-tags', currentBreakMessages, 'messages'); } + } + function addWhitelistedApp() { + const val = document.getElementById('running-apps-select').value; + if (val && !currentWhitelist.includes(val)) { currentWhitelist.push(val); renderList('whitelist-tags', currentWhitelist, 'whitelist'); } } function selectMedia() { window.chrome.webview.postMessage({ action: "select_media" }); } - function refreshApps() { window.chrome.webview.postMessage({ action: "get_apps" }); } function checkUpdates() { window.chrome.webview.postMessage({ action: "check_updates" }); } + function startUpdate() { window.chrome.webview.postMessage({ action: "start_update" }); } function retrySync() { window.chrome.webview.postMessage({ action: "retry_sync" }); } + + function updatePreview(path) { + const vid = document.getElementById('live-preview-bg-vid'); + const img = document.getElementById('live-preview-bg-img'); + vid.classList.add('hidden'); img.classList.add('hidden'); + const formattedPath = (path === "default.webm") ? "https://pausecat.app/assets/default.webm" : (path.startsWith('https://') ? path : 'https://pausecat.app/local/' + btoa(path)); + if (path.endsWith('.webm') || path.endsWith('.mp4')) { vid.src = formattedPath; vid.classList.remove('hidden'); } + else { img.src = formattedPath; img.classList.remove('hidden'); } + } + + window.onload = () => { window.chrome.webview.postMessage({ action: "ready" }); }; diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 1ad3eeb..40c48bd 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -127,6 +127,7 @@ impl SettingsWindow { let webview = safe_controller.0.CoreWebView2()?; let ws = webview.Settings()?; let _ = (ws.SetIsWebMessageEnabled(true), ws.SetAreDefaultContextMenusEnabled(false), ws.SetAreDevToolsEnabled(false), ws.SetIsZoomControlEnabled(false), ws.SetIsStatusBarEnabled(false)); + let assets_path = webview_env::get_assets_path(); let env_res = env_inner.clone(); let _ = webview.AddWebResourceRequestedFilter(w!("https://pausecat.app/*"), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); @@ -146,61 +147,47 @@ impl SettingsWindow { } Ok(()) })), &mut 0); + let sender_h = GetPropW(hwnd, w!("Sender")); let sender = &*(sender_h.0 as *const Sender); let sender_c = sender.clone(); let wv_c = webview.clone(); + + let settings_h = GetPropW(hwnd, w!("Settings")); + let settings = &*(settings_h.0 as *const Settings); + let settings_c = settings.clone(); + let _ = webview.add_WebMessageReceived(&WebMessageReceivedEventHandler::create(Box::new(move |_, args| { if let Some(args) = args { let mut msg = PWSTR::null(); if args.WebMessageAsJson(&mut msg).is_ok() { let json = msg.to_string().unwrap_or_default(); - handle_settings_message(&json, &sender_c, |m| { let _ = wv_c.PostWebMessageAsJson(&HSTRING::from(m)); }, pick_file); - CoTaskMemFree(Some(msg.0 as *const _)); - } - } - Ok(()) - })), &mut 0); + + if json.contains("\"action\":\"ready\"") { + let mut asset_path = webview_env::get_assets_path(); + asset_path.push("default.webm"); + let asset_ready = asset_path.exists() && asset_path.metadata().map(|m| m.len() > 0).unwrap_or(false); + let logo_path = "https://pausecat.app/assets/pauseCat.ico"; - let assets_path = webview_env::get_assets_path(); - let env_res = env_inner.clone(); - let _ = webview.AddWebResourceRequestedFilter(w!("https://pausecat.app/*"), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); - let _ = webview.add_WebResourceRequested(&WebResourceRequestedEventHandler::create(Box::new(move |_, args| { - if let (Some(args), env) = (args, &env_res) { - let request = args.Request()?; - let mut uri_ptr = PWSTR::null(); - let _ = request.Uri(&mut uri_ptr); - let uri = uri_ptr.to_string().unwrap_or_default(); - if let Some((content, mime)) = crate::overlay::webview::handle_resource_request(&uri, &assets_path) { - let stream = CreateStreamOnHGlobal(HGLOBAL(std::ptr::null_mut()), true)?; - let _ = (stream.Write(content.as_ptr() as *const _, content.len() as u32, None), stream.Seek(0, STREAM_SEEK_SET, None)); - let response = env.CreateWebResourceResponse(Some(&stream), 200, w!("OK"), &HSTRING::from(format!("Content-Type: {}\r\n", mime)))?; - let _ = args.SetResponse(&response); + let load_msg = format!( + "{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}, \"version\": \"{}\", \"assetReady\": {}, \"logoPath\": \"{}\"}}", + serde_json::to_string(&settings_c).unwrap_or_default(), + crate::system::is_dark_mode(), + env!("CARGO_PKG_VERSION"), + asset_ready, + logo_path + ); + let _ = wv_c.PostWebMessageAsJson(&HSTRING::from(load_msg)); + } else { + handle_settings_message(&json, &sender_c, |m| { let _ = wv_c.PostWebMessageAsJson(&HSTRING::from(m)); }, pick_file); + } + CoTaskMemFree(Some(msg.0 as *const _)); } - CoTaskMemFree(Some(uri_ptr.0 as *const _)); } Ok(()) })), &mut 0); let _ = webview.NavigateToString(&HSTRING::from(include_str!("../assets/settings.html"))); - let settings_h = GetPropW(hwnd, w!("Settings")); - let settings = &*(settings_h.0 as *const Settings); - - let mut asset_path = webview_env::get_assets_path(); - asset_path.push("default.webm"); - let asset_ready = asset_path.exists() && asset_path.metadata().map(|m| m.len() > 0).unwrap_or(false); - - let logo_path = "https://pausecat.app/assets/pauseCat.ico"; - - let msg = format!( - "{{\"action\":\"load\", \"settings\": {}, \"isDark\": {}, \"version\": \"{}\", \"assetReady\": {}, \"logoPath\": \"{}\"}}", - serde_json::to_string(settings).unwrap_or_default(), - crate::system::is_dark_mode(), - env!("CARGO_PKG_VERSION"), - asset_ready, - logo_path - ); - let _ = webview.PostWebMessageAsJson(&HSTRING::from(msg)); } } Ok(()) @@ -272,66 +259,3 @@ unsafe extern "system" fn settings_wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM _ => DefWindowProcW(hwnd, msg, wparam, lparam), } } - -#[cfg(test)] -mod internal_tests { - use super::*; - use std::sync::mpsc; - #[test] - fn test_handle_settings_message_logic() { - let (tx, rx) = std::sync::mpsc::channel(); - let settings = Settings::default(); - let settings_json = serde_json::to_string(&settings).unwrap(); - let json = format!("{{\"action\":\"save\", \"settings\": {}}}", settings_json); - handle_settings_message(&json, &tx, |_| {}, || None); - assert!(matches!(rx.try_recv(), Ok(AppEvent::ConfigChanged(_)))); - assert!(matches!(rx.try_recv(), Ok(AppEvent::SettingsClosed))); - handle_settings_message("{\"action\":\"close\"}", &tx, |_| {}, || None); - assert!(matches!(rx.try_recv(), Ok(AppEvent::SettingsClosed))); - handle_settings_message("{\"action\":\"check_updates\"}", &tx, |_| {}, || None); - assert!(matches!(rx.try_recv(), Ok(AppEvent::CheckForUpdates))); - handle_settings_message("{\"action\":\"start_update\"}", &tx, |_| {}, || None); - assert!(matches!(rx.try_recv(), Ok(AppEvent::StartUpdate))); - handle_settings_message("{\"action\":\"get_apps\"}", &tx, |msg| { assert!(msg.contains("\"action\":\"apps_list\"")); }, || None); - handle_settings_message("{\"action\":\"select_media\"}", &tx, |msg| { - assert!(msg.contains("\"action\":\"media_selected\"")); - assert!(msg.contains("test/path.png")); - }, || Some("test\\path.png".to_string())); - } - #[test] - fn test_message_builders() { - let info = crate::updater::UpdateInfo { available: true, latest_version: "v1".to_string(), changelog: "notes".to_string() }; - assert!(build_update_status_msg(&info).contains("update_status")); - assert!(build_update_progress_msg(50).contains("50")); - assert!(build_update_error_msg("test \"error\"").contains("test \\\"error\\\"")); - } - #[test] - fn test_on_controller_completed_error() { - let hwnd = HWND(std::ptr::null_mut()); - let res = on_controller_completed(Err(windows::core::Error::from_hresult(HRESULT(-1))), None, hwnd); - assert!(res.is_err()); - } - #[test] - fn test_settings_wnd_proc_branches() { - let (tx, _rx) = mpsc::channel::(); - let settings = Settings::default(); - let state = Box::into_raw(Box::new((tx, settings))); - unsafe { - let hwnd = HWND(std::ptr::null_mut()); - let cs = CREATESTRUCTW { lpCreateParams: state as *mut _, ..Default::default() }; - settings_wnd_proc(hwnd, WM_CREATE, WPARAM(0), LPARAM(&cs as *const _ as isize)); - // Test common UI messages - settings_wnd_proc(hwnd, WM_PAINT, WPARAM(0), LPARAM(0)); - settings_wnd_proc(hwnd, WM_ERASEBKGND, WPARAM(0), LPARAM(0)); - settings_wnd_proc(hwnd, WM_SETFOCUS, WPARAM(0), LPARAM(0)); - settings_wnd_proc(hwnd, WM_KILLFOCUS, WPARAM(0), LPARAM(0)); - settings_wnd_proc(hwnd, WM_MOVE, WPARAM(0), LPARAM(0)); - settings_wnd_proc(hwnd, WM_ACTIVATE, WPARAM(0), LPARAM(0)); - - settings_wnd_proc(hwnd, WM_SIZE, WPARAM(0), LPARAM(100 | (100 << 16))); - settings_wnd_proc(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)); - settings_wnd_proc(hwnd, WM_COMMAND, WPARAM(999), LPARAM(0)); - settings_wnd_proc(hwnd, WM_DESTROY, WPARAM(0), LPARAM(0)); - } - } -} diff --git a/src/updater.rs b/src/updater.rs index 960ab3a..8030add 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -174,16 +174,13 @@ pub fn ensure_assets_sync(event_tx: Sender) { } config_asset_path.push("default.webm"); - // 2. Check all preferred locations via the central path resolver - let mut final_path = crate::overlay::webview_env::get_assets_path(); - final_path.push("default.webm"); - - if final_path.exists() && final_path.metadata().map(|m| m.len() > 0).unwrap_or(false) { - log::info!("Asset already exists and is valid: {:?}", final_path); + // 2. Check if asset already exists and is valid + if config_asset_path.exists() && config_asset_path.metadata().map(|m| m.len() > 1000).unwrap_or(false) { + log::info!("Asset already exists and is valid: {:?}", config_asset_path); return; } - log::info!("Asset missing or invalid, attempting download to {:?}", config_asset_path); + log::info!("Asset missing or invalid, starting background fetch..."); let client = match reqwest::blocking::Client::builder() .user_agent("PauseCat-Asset-Syncer-v1") @@ -192,81 +189,95 @@ pub fn ensure_assets_sync(event_tx: Sender) { Ok(c) => c, Err(e) => { log::error!("Failed to create HTTP client: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + let _ = event_tx.send(AppEvent::AssetDownloadError(format!("Client error: {}", e))); return; } }; - log::info!("Fetching latest release info from {}", GITHUB_API_URL); - let release: GithubRelease = match client.get(GITHUB_API_URL) - .header("Accept", "application/vnd.github+json") - .header("X-GitHub-Api-Version", "2022-11-28") - .send() - .and_then(|r| r.json()) { - Ok(r) => r, - Err(e) => { - log::error!("Failed to fetch release info: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); - return; + // Retry loop for GitHub API + let mut release: Option = None; + for attempt in 1..=3 { + log::info!("Fetching latest release info (attempt {}/3)...", attempt); + match client.get(GITHUB_API_URL) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .send() + .and_then(|r| r.json::()) { + Ok(r) => { + release = Some(r); + break; + } + Err(e) => { + log::error!("Attempt {} failed: {}", attempt, e); + if attempt < 3 { thread::sleep(std::time::Duration::from_secs(5)); } + else { + let _ = event_tx.send(AppEvent::AssetDownloadError(format!("GitHub API error: {}", e))); + return; + } + } } - }; + } + let release = release.unwrap(); let asset = match release.assets.iter().find(|a| a.name == "default.webm") { Some(a) => a, None => { log::warn!("default.webm not found in latest release assets"); - let _ = event_tx.send(AppEvent::AssetDownloadError("default.webm not found in release assets".to_string())); + let _ = event_tx.send(AppEvent::AssetDownloadError("Asset not found in latest release".to_string())); return; } }; log::info!("Downloading default.webm ({} bytes) from {}", asset.size, asset.browser_download_url); + // Initial progress update to trigger UI state + let _ = event_tx.send(AppEvent::AssetDownloadProgress(0)); + match client.get(&asset.browser_download_url).send() { Ok(mut response) => { let total_size = response.content_length().unwrap_or(asset.size as u64); match fs::File::create(&config_asset_path) { Ok(mut file) => { - let mut buffer = [0; 8192]; + let mut buffer = [0; 16384]; let mut downloaded = 0; - let mut last_percentage = 0; + let mut last_update = std::time::Instant::now(); + loop { match response.read(&mut buffer) { Ok(0) => break, Ok(n) => { if let Err(e) = std::io::Write::write_all(&mut file, &buffer[..n]) { log::error!("Failed to write to file: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + let _ = event_tx.send(AppEvent::AssetDownloadError(format!("IO error: {}", e))); return; } downloaded += n; - if total_size > 0 { + if last_update.elapsed().as_millis() > 200 { let percentage = (downloaded as f32 / total_size as f32 * 100.0) as u32; - if percentage > last_percentage { - last_percentage = percentage; - let _ = event_tx.send(AppEvent::AssetDownloadProgress(percentage)); - } + let _ = event_tx.send(AppEvent::AssetDownloadProgress(percentage.min(99))); + last_update = std::time::Instant::now(); } } Err(e) => { log::error!("Failed to read response: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + let _ = event_tx.send(AppEvent::AssetDownloadError(format!("Download interrupted: {}", e))); return; } } } log::info!("Successfully downloaded {} bytes to {:?}", downloaded, config_asset_path); + let _ = event_tx.send(AppEvent::AssetDownloadProgress(100)); let _ = event_tx.send(AppEvent::AssetDownloaded("default.webm".to_string())); } Err(e) => { log::error!("Failed to create file: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + let _ = event_tx.send(AppEvent::AssetDownloadError(format!("File creation error: {}", e))); } } } Err(e) => { log::error!("Failed to start download: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(e.to_string())); + let _ = event_tx.send(AppEvent::AssetDownloadError(format!("Download start error: {}", e))); } } }); From 4fba73fb498d68d79b7100913d5a0fb0ac9eb989 Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 21:25:38 +0530 Subject: [PATCH 16/17] fix: Live Preview visibility and expanded UI layout --- assets/settings.html | 142 +++++++++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 53 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index 50275cc..0b52d83 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -31,55 +31,41 @@ --header-bg: #121212; } - html, body { - overflow: hidden; - touch-action: none; - user-select: none; - } + html, body { overflow: hidden; touch-action: none; user-select: none; } body { font-family: 'Inter', system-ui, -apple-system, sans-serif; - background-color: var(--bg-color); - color: var(--text-main); + background-color: var(--bg-color); color: var(--text-main); margin: 0; padding: 0; height: 100vh; display: flex; flex-direction: column; transition: background-color 0.3s, color 0.3s; } .header { - padding: 16px 24px; - display: flex; align-items: center; gap: 12px; - border-bottom: 1px solid var(--border); - background: var(--header-bg); flex-shrink: 0; + padding: 16px 24px; display: flex; align-items: center; gap: 12px; + border-bottom: 1px solid var(--border); background: var(--header-bg); flex-shrink: 0; } - .header h1 { font-size: 1.125rem; font-weight: 600; margin: 0; } .header img { width: 28px; height: 28px; border-radius: 6px; } .scroll-container { flex: 1; overflow-y: auto; padding: 20px 24px; - background: var(--secondary-bg); - scrollbar-width: none; + background: var(--secondary-bg); scrollbar-width: none; } .scroll-container::-webkit-scrollbar { display: none; } .section-title { - font-size: 0.7rem; font-weight: 600; - color: var(--text-secondary); text-transform: uppercase; - letter-spacing: 0.05em; margin-bottom: 10px; margin-top: 10px; + font-size: 0.7rem; font-weight: 600; color: var(--text-secondary); + text-transform: uppercase; letter-spacing: 0.05em; margin: 10px 0; } .card { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 12px; padding: 16px; - margin-bottom: 20px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + background: var(--card-bg); border: 1px solid var(--border); + border-radius: 12px; padding: 16px; margin-bottom: 20px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } - .setting-row { - display: flex; justify-content: space-between; - align-items: center; margin-bottom: 16px; gap: 12px; - } + .setting-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; gap: 12px; } .setting-row:last-child { margin-bottom: 0; } .setting-info { display: flex; flex-direction: column; gap: 2px; flex: 1; } @@ -101,7 +87,7 @@ margin-bottom: 8px; border: 1px solid var(--border); flex-shrink: 0; perspective: 500px; } - .preview-bg { width: 100%; height: 100%; object-fit: cover; opacity: 0.5; } + .preview-bg { width: 100%; height: 100%; object-fit: cover; opacity: 0.5; position: absolute; top:0; left:0; } .preview-text-3d-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; @@ -109,12 +95,10 @@ pointer-events: none; z-index: 1; } .preview-text-3d { - font-size: 3rem; font-weight: 900; - color: var(--text-color, #fff); - text-transform: uppercase; + font-size: 3rem; font-weight: 900; color: var(--text-color, #fff); + text-transform: uppercase; text-align: center; transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)); - text-shadow: var(--text-3d-shadow); - opacity: var(--text-opacity, 0.2); + text-shadow: var(--text-3d-shadow); opacity: var(--text-opacity, 0.2); animation: previewFloat 5s ease-in-out infinite; filter: var(--text-glow-filter, none); } @@ -130,11 +114,11 @@ display: flex; justify-content: center; align-items: center; color: white; font-weight: 700; pointer-events: none; box-shadow: 0 8px 16px rgba(0,0,0,0.3); z-index: 2; + transition: all 0.2s ease; } .status-footer { - margin-bottom: 20px; padding: 0 4px; - display: flex; justify-content: space-between; align-items: center; + margin-bottom: 20px; padding: 0 4px; display: flex; justify-content: space-between; align-items: center; font-size: 0.7rem; font-weight: 500; color: var(--text-secondary); } .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; } @@ -144,9 +128,11 @@ .media-preview { width: 100%; height: 140px; border: 2px dashed var(--border); border-radius: 10px; display: flex; justify-content: center; align-items: center; - background: var(--card-bg); cursor: pointer; overflow: hidden; margin-top: 10px; + background: var(--card-bg); cursor: pointer; overflow: hidden; margin-top: 10px; position: relative; } .media-preview:hover { border-color: var(--accent); } + .media-preview img, .media-preview video { width: 100%; height: 100%; object-fit: cover; } + .media-placeholder { display: flex; flex-direction: column; align-items: center; color: var(--text-secondary); font-size: 0.8rem; } .list-container { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; } .tag { @@ -156,6 +142,9 @@ } .tag span { cursor: pointer; font-size: 0.9rem; } + .add-row { display: flex; gap: 6px; margin-top: 12px; } + .add-row input { flex: 1; } + .footer { padding: 12px 24px; background: var(--header-bg); border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0; @@ -245,7 +234,7 @@

PauseCat Settings

-
Glow Enabled & Color
+
Glow & Color
@@ -266,7 +255,7 @@

PauseCat Settings

-
Change Background
+
Change Background
@@ -289,7 +278,7 @@

PauseCat Settings

-
Animation style
+
Animation Style
- + @@ -365,22 +354,47 @@

PauseCat Settings

let r = Math.max(0, Math.min(255, (num >> 16) + amount)); let g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount)); let b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount)); - return "#" + (b | (g << 8) | (r << 16)).toString(16).padStart(6, '0'); + return "#" + ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0'); } function updatePreviewFromUI() { try { + const bubble = document.getElementById('live-preview-bubble'); + const style = document.getElementById('break_style').value; const isDark = isDarkMode; + + // Bubble logic const opacity = document.getElementById('bubble_opacity').value; - const bubble = document.getElementById('live-preview-bubble'); + const size = document.getElementById('bubble_size').value; + const posX = document.getElementById('bubble_pos_x').value; + const posY = document.getElementById('bubble_pos_y').value; + const anim = document.getElementById('animation_style').value; + const previewScale = 0.25; + + bubble.style.width = (size * previewScale) + "px"; + bubble.style.height = (size * previewScale) + "px"; + bubble.style.fontSize = (size * previewScale / 5) + "px"; bubble.style.backgroundColor = isDark ? `rgba(0,0,0,${opacity})` : `rgba(255,255,255,${opacity})`; + bubble.style.left = `calc(${posX}% - ${(size * previewScale) * (posX/100)}px)`; + bubble.style.top = `calc(${posY}% - ${(size * previewScale) * (posY/100)}px)`; + + if (anim === 'pulse') { + bubble.style.setProperty('--pulse-opacity-start', opacity); + bubble.style.setProperty('--pulse-opacity-end', Math.max(0, opacity - 0.3)); + bubble.style.animation = `pulse 2s ease-in-out infinite`; + } else if (anim !== 'none') { bubble.style.animation = `${anim} 3s ease-in-out infinite`; } + else { bubble.style.animation = 'none'; } - const style = document.getElementById('break_style').value; - const textContainer = document.getElementById('preview-text-container'); + // Content logic const text3d = document.getElementById('preview-text-3d'); - + const textContainer = document.getElementById('preview-text-container'); + const bgImg = document.getElementById('live-preview-bg-img'); + const bgVid = document.getElementById('live-preview-bg-vid'); + if (style === 'text') { textContainer.classList.remove('hidden'); + bgImg.classList.add('hidden'); bgVid.classList.add('hidden'); + const isAdaptive = document.getElementById('adaptive_text_color').checked; const baseColor = document.getElementById('text_color').value; const finalColor = isAdaptive ? (isDark ? '#ffffff' : '#000000') : baseColor; @@ -395,20 +409,20 @@

PauseCat Settings

if (document.getElementById('text_glow_enabled').checked) { const glowColor = document.getElementById('text_glow_color').value; - text3d.style.setProperty('--text-glow-filter', `drop-shadow(0 0 ${document.getElementById('text_glow').value}px ${glowColor})`); + text3d.style.setProperty('--text-glow-filter', `drop-shadow(0 0 ${document.getElementById('text_glow').value / 2}px ${glowColor})`); } else { text3d.style.setProperty('--text-glow-filter', 'none'); } } else { textContainer.classList.add('hidden'); + updatePreview(currentMediaPath); } - } catch (e) { console.warn("Preview Sync Error:", e); } + } catch (e) { console.warn("Preview Error:", e); } } function save() { const getVal = (id) => document.getElementById(id).value; const getCheck = (id) => document.getElementById(id).checked; - const settings = { work_duration_secs: getVal('work_duration') * 60, break_duration_secs: getVal('break_duration') * 60, @@ -492,6 +506,7 @@

PauseCat Settings

} else if (data.action === "asset_synced") { document.getElementById('asset-dot').className = 'status-dot dot-ready'; document.getElementById('asset-label').innerText = 'Cinematic Assets: Ready'; + updatePreviewFromUI(); } else if (data.action === "asset_progress") { document.getElementById('asset-label').innerText = `Cinematic Assets: Syncing (${data.percentage}%)`; } else if (data.action === "theme_changed") { @@ -528,16 +543,37 @@

PauseCat Settings

} function selectMedia() { window.chrome.webview.postMessage({ action: "select_media" }); } function checkUpdates() { window.chrome.webview.postMessage({ action: "check_updates" }); } - function startUpdate() { window.chrome.webview.postMessage({ action: "start_update" }); } function retrySync() { window.chrome.webview.postMessage({ action: "retry_sync" }); } function updatePreview(path) { - const vid = document.getElementById('live-preview-bg-vid'); - const img = document.getElementById('live-preview-bg-img'); - vid.classList.add('hidden'); img.classList.add('hidden'); - const formattedPath = (path === "default.webm") ? "https://pausecat.app/assets/default.webm" : (path.startsWith('https://') ? path : 'https://pausecat.app/local/' + btoa(path)); - if (path.endsWith('.webm') || path.endsWith('.mp4')) { vid.src = formattedPath; vid.classList.remove('hidden'); } - else { img.src = formattedPath; img.classList.remove('hidden'); } + try { + if (!path) return; + currentMediaPath = path; + + const mainVid = document.getElementById('live-preview-bg-vid'); + const mainImg = document.getElementById('live-preview-bg-img'); + const smallVid = document.getElementById('preview-video'); + const smallImg = document.getElementById('preview-img'); + const placeholder = document.getElementById('media-empty'); + + // Hide all initially + [mainVid, mainImg, smallVid, smallImg].forEach(el => { if(el) el.classList.add('hidden'); }); + if (placeholder) placeholder.classList.add('hidden'); + + const formattedPath = (path === "default.webm") + ? "https://pausecat.app/assets/default.webm" + : (path.startsWith('https://') ? path : 'https://pausecat.app/local/' + btoa(path)); + + const isVideo = path.toLowerCase().endsWith('.webm') || path.toLowerCase().endsWith('.mp4'); + + if (isVideo) { + if (mainVid) { mainVid.src = formattedPath; mainVid.classList.remove('hidden'); mainVid.play().catch(()=>{}); } + if (smallVid) { smallVid.src = formattedPath; smallVid.classList.remove('hidden'); smallVid.play().catch(()=>{}); } + } else { + if (mainImg) { mainImg.src = formattedPath; mainImg.classList.remove('hidden'); } + if (smallImg) { smallImg.src = formattedPath; smallImg.classList.remove('hidden'); } + } + } catch (e) { console.warn("Update Preview Error:", e); } } window.onload = () => { window.chrome.webview.postMessage({ action: "ready" }); }; From 9547f01bea093124c6c669c8214d5251bcac0f4b Mon Sep 17 00:00:00 2001 From: 0xarchit <0xarchit@gmail.com> Date: Sat, 9 May 2026 23:13:19 +0530 Subject: [PATCH 17/17] feat: implement lazy video loading, high-fidelity 3D text engine, and UI refinements --- assets/settings.html | 480 ++++++++++++++++++++++++++++++------------- src/settings.rs | 10 +- 2 files changed, 339 insertions(+), 151 deletions(-) diff --git a/assets/settings.html b/assets/settings.html index 0b52d83..b8f0828 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -56,38 +56,49 @@ .section-title { font-size: 0.7rem; font-weight: 600; color: var(--text-secondary); - text-transform: uppercase; letter-spacing: 0.05em; margin: 10px 0; + text-transform: uppercase; letter-spacing: 0.05em; margin: 12px 0 8px 0; } .card { background: var(--card-bg); border: 1px solid var(--border); - border-radius: 12px; padding: 16px; margin-bottom: 20px; + border-radius: 12px; padding: 16px; margin-bottom: 24px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } - .setting-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; gap: 12px; } + .setting-row { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 16px; gap: 16px; + } .setting-row:last-child { margin-bottom: 0; } - .setting-info { display: flex; flex-direction: column; gap: 2px; flex: 1; } - .setting-label { font-size: 0.875rem; font-weight: 500; } - .setting-description { font-size: 0.75rem; color: var(--text-secondary); } + .setting-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } + .setting-label { font-size: 0.9rem; font-weight: 600; white-space: nowrap; } + .setting-description { font-size: 0.75rem; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; } + + .setting-control { display: flex; align-items: center; justify-content: flex-end; min-width: 140px; gap: 8px; } input[type="number"], input[type="text"], select { - padding: 6px 10px; border: 1px solid var(--border); - border-radius: 6px; font-size: 0.85rem; outline: none; - background: var(--card-bg); color: var(--text-main); + padding: 8px 12px; border: 1px solid var(--border); + border-radius: 8px; font-size: 0.85rem; outline: none; + background: var(--card-bg); color: var(--text-main); transition: border-color 0.2s; + } + input[type="number"] { width: 70px; } + select { width: 130px; } + input[type="text"] { width: 100%; box-sizing: border-box; } + + input[type="range"] { + -webkit-appearance: none; width: 100px; height: 4px; + background: var(--border); border-radius: 2px; outline: none; + accent-color: var(--accent); cursor: pointer; } - input[type="number"] { width: 65px; } - select { width: 110px; } - input[type="range"] { accent-color: var(--accent); cursor: pointer; } .preview-container { - width: 100%; height: 160px; background: #000; - border-radius: 10px; position: relative; overflow: hidden; - margin-bottom: 8px; border: 1px solid var(--border); flex-shrink: 0; - perspective: 500px; + width: 100%; aspect-ratio: 16 / 9; background: #000; + border-radius: 12px; position: relative; overflow: hidden; + margin-bottom: 12px; border: 1px solid var(--border); flex-shrink: 0; + perspective: 1000px; } - .preview-bg { width: 100%; height: 100%; object-fit: cover; opacity: 0.5; position: absolute; top:0; left:0; } + .preview-bg { width: 100%; height: 100%; object-fit: cover; opacity: 0.5; position: absolute; top:0; left:0; z-index: 0; } .preview-text-3d-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; @@ -95,8 +106,8 @@ pointer-events: none; z-index: 1; } .preview-text-3d { - font-size: 3rem; font-weight: 900; color: var(--text-color, #fff); - text-transform: uppercase; text-align: center; + font-size: 5vw; font-weight: 950; color: var(--text-color, #fff); + text-transform: uppercase; text-align: center; line-height: 1; transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)); text-shadow: var(--text-3d-shadow); opacity: var(--text-opacity, 0.2); animation: previewFloat 5s ease-in-out infinite; @@ -104,7 +115,7 @@ } @keyframes previewFloat { 0%, 100% { transform: rotateX(var(--rot-x, 20deg)) rotateY(var(--rot-y, -20deg)) rotateZ(var(--rot-z, 0deg)) translateZ(0); } - 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) rotateZ(var(--rot-z, 0deg)) translateZ(10px); } + 50% { transform: rotateX(calc(var(--rot-x, 20deg) + 5deg)) rotateY(calc(var(--rot-y, -20deg) + 5deg)) rotateZ(var(--rot-z, 0deg)) translateZ(20px); } } .preview-bubble { @@ -114,52 +125,110 @@ display: flex; justify-content: center; align-items: center; color: white; font-weight: 700; pointer-events: none; box-shadow: 0 8px 16px rgba(0,0,0,0.3); z-index: 2; - transition: all 0.2s ease; + transition: all 0.3s ease; } .status-footer { - margin-bottom: 20px; padding: 0 4px; display: flex; justify-content: space-between; align-items: center; - font-size: 0.7rem; font-weight: 500; color: var(--text-secondary); + margin-bottom: 24px; padding: 0 4px; display: flex; justify-content: space-between; align-items: center; + font-size: 0.75rem; font-weight: 500; color: var(--text-secondary); } .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; } .dot-ready { background: #22c55e; box-shadow: 0 0 8px #22c55e; } .dot-missing { background: #eab308; box-shadow: 0 0 8px #eab308; } + .dot-error { background: #ef4444; box-shadow: 0 0 8px #ef4444; } + + .asset-progress-container { + width: 80px; height: 4px; background: var(--border); + border-radius: 2px; overflow: hidden; margin-left: 8px; + display: inline-block; vertical-align: middle; + } + .asset-progress-bar { height: 100%; background: var(--accent); width: 0%; transition: width 0.3s ease; } + + .retry-btn { + background: none; border: 1px solid var(--accent); color: var(--accent); + padding: 2px 6px; border-radius: 4px; font-size: 0.65rem; cursor: pointer; + margin-left: 8px; + } + .media-preview-area { margin-top: 12px; } .media-preview { - width: 100%; height: 140px; border: 2px dashed var(--border); - border-radius: 10px; display: flex; justify-content: center; align-items: center; - background: var(--card-bg); cursor: pointer; overflow: hidden; margin-top: 10px; position: relative; + width: 100%; height: 160px; border: 2px dashed var(--border); + border-radius: 12px; display: flex; justify-content: center; align-items: center; + background: var(--card-bg); cursor: pointer; overflow: hidden; position: relative; + transition: all 0.2s; } - .media-preview:hover { border-color: var(--accent); } - .media-preview img, .media-preview video { width: 100%; height: 100%; object-fit: cover; } - .media-placeholder { display: flex; flex-direction: column; align-items: center; color: var(--text-secondary); font-size: 0.8rem; } + .media-preview:hover { border-color: var(--accent); background: var(--secondary-bg); } + .media-preview img, .media-preview video { width: 100%; height: 100%; object-fit: cover; position: absolute; top:0; left:0; } + .media-placeholder { display: flex; flex-direction: column; align-items: center; color: var(--text-secondary); font-size: 0.85rem; z-index: 1; } - .list-container { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; } + .list-container { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 8px; } .tag { - background: var(--ring); color: var(--accent); padding: 3px 8px; - border-radius: 12px; font-size: 0.7rem; font-weight: 600; - display: flex; align-items: center; gap: 4px; border: 1px solid var(--accent); + background: var(--ring); color: var(--accent); padding: 4px 10px; + border-radius: 12px; font-size: 0.75rem; font-weight: 600; + display: flex; align-items: center; gap: 6px; border: 1px solid var(--accent); } - .tag span { cursor: pointer; font-size: 0.9rem; } + .tag span { cursor: pointer; font-size: 1rem; line-height: 1; } - .add-row { display: flex; gap: 6px; margin-top: 12px; } - .add-row input { flex: 1; } + .add-row { display: flex; gap: 8px; margin-top: 14px; } .footer { - padding: 12px 24px; background: var(--header-bg); border-top: 1px solid var(--border); - display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0; + padding: 16px 24px; background: var(--header-bg); border-top: 1px solid var(--border); + display: flex; justify-content: flex-end; gap: 12px; flex-shrink: 0; } - button { padding: 8px 14px; border-radius: 6px; font-size: 0.85rem; font-weight: 600; cursor: pointer; border: 1px solid transparent; } + button { padding: 10px 18px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; cursor: pointer; border: 1px solid transparent; transition: all 0.2s; } .btn-primary { background: var(--accent); color: white; } + .btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); } .btn-secondary { background: var(--bg-color); color: var(--text-main); border-color: var(--border); } + .btn-secondary:hover { background: var(--secondary-bg); } - .switch { position: relative; display: inline-block; width: 36px; height: 18px; } + .switch { position: relative; display: inline-block; width: 40px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } - .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--border); transition: .3s; border-radius: 18px; } - .slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; } + .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--border); transition: .3s; border-radius: 20px; } + .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; } input:checked + .slider { background-color: var(--accent); } - input:checked + .slider:before { transform: translateX(18px); } + input:checked + .slider:before { transform: translateX(20px); } + + /* Modal / Popup Styles */ + .modal-overlay { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; justify-content: center; align-items: center; + z-index: 1000; animation: fadeIn 0.3s ease; + } + + .modal-content { + background: var(--bg-color); + width: 85%; max-width: 400px; + border-radius: 16px; padding: 24px; + box-shadow: 0 20px 25px -5px rgba(0,0,0,0.2); + border: 1px solid var(--border); + text-align: center; + } + + .modal-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 12px; display: block; } + .modal-desc { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 24px; line-height: 1.5; display: block; } + + .progress-container { + width: 100%; height: 8px; + background: var(--secondary-bg); + border-radius: 4px; overflow: hidden; + margin-bottom: 12px; border: 1px solid var(--border); + } + .progress-bar { + height: 100%; background: var(--accent); + width: 0%; transition: width 0.2s ease; + } + + .notification { + position: fixed; bottom: 24px; left: 24px; right: 24px; padding: 16px; + border-radius: 10px; background: var(--card-bg); border: 1px solid var(--border); + box-shadow: 0 12px 24px rgba(0,0,0,0.15); z-index: 1100; + display: flex; flex-direction: column; gap: 8px; animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes slideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .hidden { display: none !important; } @@ -171,6 +240,29 @@

PauseCat Settings

+ + + + + +
Live Preview
@@ -191,72 +283,100 @@

PauseCat Settings

Schedule
-
Work Duration
- +
Work DurationMinutes before rest
+
-
Break Duration
- +
Break DurationRest period length
+
Visuals
-
Break Style
- +
Break StyleRender mode
+
+ +