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 @@
@@ -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