From e4484a57f9dc2f36988af531113680625facbf97 Mon Sep 17 00:00:00 2001 From: dillonj Date: Wed, 6 May 2026 16:05:04 -0600 Subject: [PATCH] improve home screen --- frontend/src/App.tsx | 168 +++++++++++++++------- frontend/src/components/LicenseDialog.tsx | 148 +++++++++++++++---- frontend/src/index.css | 15 ++ frontend/src/lib/tauri-bridge.ts | 4 + frontend/src/store/editorStore.test.ts | 12 +- frontend/src/store/licenseStore.ts | 2 +- frontend/src/vite-env.d.ts | 1 + src-tauri/src/lib.rs | 8 ++ src-tauri/src/licensing.rs | 81 +++++++---- 9 files changed, 328 insertions(+), 111 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eea9bd8..bc4f545 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -584,69 +584,133 @@ export default function App() { if (!videoPath) { return ( -
-
-
- -

TalkEdit

-

- Offline AI-powered video editor. +

+ {/* Background pattern */} +
+ + {/* Animated audio waveform background — left and right sides filling to center */} +
+ {[ + [60, 1.3, 0.0], [100, 1.0, 0.12], [40, 1.6, 0.24], [120, 0.9, 0.08], + [80, 1.2, 0.2], [30, 1.8, 0.04], [110, 1.1, 0.28], [50, 1.5, 0.16], + [70, 1.4, 0.08], [140, 0.85, 0.24], [60, 1.3, 0.12], [90, 1.2, 0.0], + [130, 0.95, 0.2], [45, 1.7, 0.28], [80, 1.4, 0.04], [55, 1.1, 0.16], + [100, 1.3, 0.24], [35, 1.6, 0.12], [120, 0.9, 0.0], [65, 1.5, 0.2], + [85, 1.2, 0.08], [150, 0.8, 0.28], [40, 1.9, 0.16], [95, 1.1, 0.04], + [75, 1.4, 0.24], [25, 2.0, 0.12], [105, 1.05, 0.2], [155, 0.82, 0.08], + [50, 1.55, 0.28], [115, 0.92, 0.16], [70, 1.35, 0.04], [135, 0.88, 0.24], + [90, 1.15, 0.12], [42, 1.75, 0.0], [125, 0.98, 0.2], [58, 1.45, 0.08], + ].map(([peak, dur, delay], i) => ( +
+ ))} +
+
+ {[ + [100, 1.0, 0.0], [60, 1.3, 0.08], [130, 0.9, 0.16], [40, 1.6, 0.04], + [80, 1.2, 0.12], [150, 0.85, 0.24], [50, 1.5, 0.2], [110, 1.1, 0.28], + [70, 1.4, 0.08], [30, 1.8, 0.16], [140, 0.9, 0.0], [90, 1.2, 0.24], + [60, 1.3, 0.12], [120, 0.95, 0.04], [85, 1.4, 0.2], [45, 1.7, 0.28], + [160, 0.8, 0.08], [55, 1.5, 0.24], [100, 1.1, 0.0], [75, 1.3, 0.16], + [35, 1.9, 0.2], [115, 1.0, 0.12], [65, 1.6, 0.28], [140, 0.88, 0.04], + [95, 1.25, 0.24], [25, 1.85, 0.08], [125, 0.93, 0.16], [155, 0.78, 0.28], + [48, 1.65, 0.12], [82, 1.32, 0.2], [108, 1.08, 0.04], [72, 1.42, 0.24], + [135, 0.9, 0.16], [38, 1.78, 0.0], [62, 1.48, 0.08], [118, 1.02, 0.28], + ].map(([peak, dur, delay], i) => ( +
+ ))} +
+ +
+
+ +
+ {/* App icon */} +
+ + + + + + +
+ +
+

+ TalkEdit +

+

+ The offline video editor that doesn't slow down on long files.

- {/* Whisper model selector */} -
- - -
-

- For noisy/YouTube videos use large-v3 or large-v3-turbo. - English-only models are ~10% faster and more accurate for English content. -

- -
+ {/* Action row — button + model selector side by side */} +
- +
+ Transcription model: + +
+ +
{licenseStatus?.tag === 'Trial' && ( -
+
Free trial: {licenseStatus.days_remaining} day{licenseStatus.days_remaining !== 1 ? 's' : ''} remaining @@ -661,7 +725,7 @@ export default function App() { )} {licenseStatus?.tag === 'Expired' && ( -
+
Trial expired
- {showDialog && ( - setShowDialog(false)} - onActivate={handleActivate} - keyValue={key} - setKeyValue={setKey} - error={error} - activating={activating} - trialEnding={status.days_remaining <= 3} - /> - )} - - ); - } - - // Expired — show banner + activation dialog (both dismissible) - return ( - <> - setShowDialog(true)} /> - {showDialog && ( setShowDialog(false)} + onClose={() => { setShowDialog(false); handleDeny(); }} onActivate={handleActivate} + onDeny={handleDeny} keyValue={key} setKeyValue={setKey} error={error} activating={activating} - expired + verifying={verifying} + confirmedEmail={confirmedEmail} + trialEnding={status.days_remaining <= 3} /> )} ); } +// Expired — show banner + activation dialog (both dismissible) +return ( + <> + setShowDialog(true)} /> + + {showDialog && ( + { setShowDialog(false); handleDeny(); }} + onActivate={handleActivate} + onDeny={handleDeny} + keyValue={key} + setKeyValue={setKey} + error={error} + activating={activating} + verifying={verifying} + confirmedEmail={confirmedEmail} + expired + /> + )} + +); +} + /** Persistent top banner shown when trial expired — still allows export and loading */ function ExpiredBanner({ onActivate }: { onActivate: () => void }) { return ( @@ -112,22 +144,78 @@ function ExpiredBanner({ onActivate }: { onActivate: () => void }) { function LicenseActivateDialog({ onClose, onActivate, + onDeny, keyValue, setKeyValue, error, activating, + verifying, + confirmedEmail, trialEnding, expired, }: { onClose: () => void; onActivate: () => void; + onDeny: () => void; keyValue: string; setKeyValue: (v: string) => void; error: string | null; activating: boolean; + verifying: boolean; + confirmedEmail: string | null; trialEnding?: boolean; expired?: boolean; }) { + const isProcessing = activating || verifying; + + if (confirmedEmail) { + return ( +
+
e.stopPropagation()} + > +
+ +

Confirm License

+
+ +
+

+ This license key is registered to: +

+

{confirmedEmail}

+
+ +

+ License keys are tied to your email. Sharing this key may result in deactivation. +

+ +
+ + +
+
+
+ ); + } + return (
- {activating ? ( + {isProcessing ? ( ) : ( - + )} - Activate + {verifying ? 'Verifying...' : 'Verify Key'}

diff --git a/frontend/src/index.css b/frontend/src/index.css index 475ba49..dc97a47 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -8,11 +8,26 @@ 100% { transform: scaleY(0.3); opacity: 0.5; } } +@keyframes audioBounce { + 0% { height: 12px; } + 50% { height: var(--bar-peak); } + 100% { height: 12px; } +} + .wave-bar { animation: waveBar 0.9s ease-in-out infinite; transform-origin: bottom; } +.welcome-audio-bar { + width: 4px; + border-radius: 2px; + background: #6366f1; + transform-origin: bottom; + animation: audioBounce var(--bar-duration) ease-in-out infinite; + animation-delay: var(--bar-delay); +} + * { margin: 0; padding: 0; diff --git a/frontend/src/lib/tauri-bridge.ts b/frontend/src/lib/tauri-bridge.ts index a840ea4..73f0a1c 100644 --- a/frontend/src/lib/tauri-bridge.ts +++ b/frontend/src/lib/tauri-bridge.ts @@ -101,6 +101,10 @@ window.electronAPI = { return invoke('activate_license', { licenseKey: key }); }, + verifyLicense: (key: string): Promise => { + return invoke('verify_license', { licenseKey: key }); + }, + getAppStatus: (): Promise => { return invoke('get_app_status'); }, diff --git a/frontend/src/store/editorStore.test.ts b/frontend/src/store/editorStore.test.ts index 4ecedcc..57394e2 100644 --- a/frontend/src/store/editorStore.test.ts +++ b/frontend/src/store/editorStore.test.ts @@ -331,10 +331,14 @@ describe('editorStore', () => { useEditorStore.getState().addSpeedRange(4, 5, 1.5); const project = useEditorStore.getState().saveProject(); - expect(project.cutRanges.length).toBe(1); - expect(project.muteRanges.length).toBe(1); - expect(project.gainRanges.length).toBe(1); - expect(project.speedRanges.length).toBe(1); + expect(project.cutRanges).toBeDefined(); + expect(project.cutRanges!.length).toBe(1); + expect(project.muteRanges).toBeDefined(); + expect(project.muteRanges!.length).toBe(1); + expect(project.gainRanges).toBeDefined(); + expect(project.gainRanges!.length).toBe(1); + expect(project.speedRanges).toBeDefined(); + expect(project.speedRanges!.length).toBe(1); }); test('setProjectFilePath sets and reads back', () => { diff --git a/frontend/src/store/licenseStore.ts b/frontend/src/store/licenseStore.ts index 2601449..f32058b 100644 --- a/frontend/src/store/licenseStore.ts +++ b/frontend/src/store/licenseStore.ts @@ -42,7 +42,7 @@ export const useLicenseStore = create()( status: null, isLoaded: false, showDialog: false, - canEdit: true, + canEdit: false, setStatus: (status) => { const canEdit = status?.tag === 'Licensed' || status?.tag === 'Trial'; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 4d47d46..f25e9f2 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -29,6 +29,7 @@ interface DesktopAPI { writeFile: (path: string, content: string) => Promise; activateLicense: (key: string) => Promise; getAppStatus: () => Promise; + verifyLicense: (key: string) => Promise; deactivateLicense: () => Promise; hasLicenseFeature: (feature: string) => Promise; listModels: () => Promise; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6752e78..db488a2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -245,6 +245,13 @@ fn get_app_status(app_handle: tauri::AppHandle) -> Result Result { + licensing::verify_license_key(&license_key) + .map_err(|e| e.to_string()) +} + /// Verify and activate a license key. #[tauri::command] fn activate_license(app_handle: tauri::AppHandle, license_key: String) -> Result { @@ -417,6 +424,7 @@ pub fn run() { get_app_status, activate_license, deactivate_license, + verify_license, start_trial, has_license_feature, list_models, diff --git a/src-tauri/src/licensing.rs b/src-tauri/src/licensing.rs index d6e1c61..db71304 100644 --- a/src-tauri/src/licensing.rs +++ b/src-tauri/src/licensing.rs @@ -7,7 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; pub const TALKEDIT_PUBLIC_KEY: [u8; 32] = [123, 66, 135, 187, 184, 19, 205, 192, 129, 94, 91, 138, 123, 3, 72, 52, 229, 8, 245, 226, 187, 218, 67, 131, 38, 214, 239, 203, 206, 7, 132, 176]; -pub const TRIAL_DURATION_SECS: u64 = 30 * 86400; +pub const TRIAL_DURATION_SECS: u64 = 7 * 86400; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct LicensePayload { @@ -25,6 +25,13 @@ pub struct TrialState { pub started_at: u64, } +/// On-disk format with integrity seed to deter tampering. +#[derive(Debug, Serialize, Deserialize)] +struct TrialFile { + started_at: u64, + seed: u64, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "tag")] pub enum AppStatus { @@ -113,6 +120,52 @@ pub fn trial_file_path(app_data_dir: &PathBuf) -> PathBuf { app_data_dir.join("trial.json") } +pub fn trial_sentinel_path(app_data_dir: &PathBuf) -> PathBuf { + app_data_dir.join(".trial_lock") +} + +// Simple integrity check constant — not crypto-grade, but deters casual editing. +const TRIAL_SEED: u64 = 0x9F3A_2E7D_C1B8_5604; + +pub fn get_or_start_trial(app_data_dir: &PathBuf) -> TrialState { + let path = trial_file_path(app_data_dir); + let sentinel = trial_sentinel_path(app_data_dir); + let now = now_secs(); + + // If sentinel exists but trial was deleted, refuse to create a new one. + let sentinel_exists = sentinel.exists(); + + if let Ok(content) = std::fs::read_to_string(&path) { + if let Ok(wrapped) = serde_json::from_str::(&content) { + // Verify integrity + if (wrapped.started_at ^ TRIAL_SEED) == wrapped.seed { + return TrialState { started_at: wrapped.started_at }; + } + } + } + + if sentinel_exists { + // Trial was tampered with — return an expired trial + return TrialState { started_at: 0 }; + } + + // Start new trial + let trial = TrialState { started_at: now }; + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = std::fs::write(&sentinel, "1"); + let _ = std::fs::write( + &path, + serde_json::to_string(&TrialFile { + started_at: trial.started_at, + seed: trial.started_at ^ TRIAL_SEED, + }) + .unwrap(), + ); + trial +} + pub fn load_cached_license(app_data_dir: &PathBuf) -> Result { let path = license_file_path(app_data_dir); let content = std::fs::read_to_string(&path).map_err(|_| LicenseError::InvalidFormat)?; @@ -136,26 +189,6 @@ pub fn remove_license(app_data_dir: &PathBuf) { // --- Trial logic --- -pub fn get_or_start_trial(app_data_dir: &PathBuf) -> TrialState { - let path = trial_file_path(app_data_dir); - let now = now_secs(); - - // Try to load existing trial - if let Ok(content) = std::fs::read_to_string(&path) { - if let Ok(trial) = serde_json::from_str::(&content) { - return trial; - } - } - - // Start new trial - let trial = TrialState { started_at: now }; - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - let _ = std::fs::write(&path, serde_json::to_string(&trial).unwrap()); - trial -} - pub fn get_trial_info(trial: &TrialState) -> (u64, u64, bool) { let now = now_secs(); let elapsed = now.saturating_sub(trial.started_at); @@ -176,8 +209,8 @@ pub fn get_trial_days_remaining(trial: &TrialState) -> Option { } pub fn clear_trial(app_data_dir: &PathBuf) { - let path = trial_file_path(app_data_dir); - let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(trial_file_path(app_data_dir)); + let _ = std::fs::remove_file(trial_sentinel_path(app_data_dir)); } /// Get the overall app status: Licensed > Trial > Expired. @@ -248,7 +281,7 @@ mod tests { let trial = TrialState { started_at: now_secs() }; let days = get_trial_days_remaining(&trial); assert!(days.is_some()); - assert_eq!(days.unwrap(), 30); + assert_eq!(days.unwrap(), 7); } #[test]