added licensing stuff and free trial timer

This commit is contained in:
2026-05-06 01:35:42 -06:00
parent 810957747b
commit 835719a907
13 changed files with 1121 additions and 53 deletions

View File

@ -12,7 +12,9 @@ import SilenceTrimmerPanel from './components/SilenceTrimmerPanel';
import ZoneEditor from './components/ZoneEditor';
import BackgroundMusicPanel from './components/BackgroundMusicPanel';
import AppendClipPanel from './components/AppendClipPanel';
import LicenseDialog from './components/LicenseDialog';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useLicenseStore } from './store/licenseStore';
import {
Film,
FolderOpen,
@ -132,6 +134,8 @@ export default function App() {
const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false);
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
const [lastSavedSignature, setLastSavedSignature] = useState<string | null>(null);
const [showFileMenu, setShowFileMenu] = useState(false);
const canEdit = useLicenseStore((s) => s.canEdit);
const projectSignature = useMemo(() => {
if (!videoPath) return null;
@ -196,7 +200,11 @@ export default function App() {
useKeyboardShortcuts();
// Handle Escape key to exit timeline zone modes
useEffect(() => {
useLicenseStore.getState().checkStatus();
}, []);
// Handle Escape key to exit timeline zone modes and close menus
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
@ -204,9 +212,9 @@ export default function App() {
setMuteMode(false);
setGainMode(false);
setSpeedMode(false);
setShowFileMenu(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
@ -287,7 +295,9 @@ export default function App() {
setProjectFilePath(null);
setProjectName(null);
loadVideo(path);
await transcribeVideo(path);
if (canEdit) {
await transcribeVideo(path);
}
}
});
};
@ -562,44 +572,40 @@ export default function App() {
{/* Top bar */}
<header className="h-12 flex items-center px-4 border-b border-editor-border shrink-0">
<div className="flex items-center gap-0.5">
<ToolbarButton
icon={<FilePlus2 className="w-4 h-4" />}
label="New"
onClick={handleNewProject}
/>
<ToolbarButton
icon={<FolderOpen className="w-4 h-4" />}
label="Open"
onClick={handleOpenFile}
/>
<ToolbarButton
icon={<Save className="w-4 h-4" />}
label="Save"
onClick={handleSaveProject}
disabled={words.length === 0}
/>
<ToolbarButton
icon={<Save className="w-4 h-4" />}
label="Save As"
onClick={handleSaveProjectAs}
disabled={words.length === 0}
/>
<ToolbarButton
icon={<FileInput className="w-4 h-4" />}
label="Load"
onClick={handleLoadProject}
/>
<div className="relative">
<ToolbarButton
icon={<FolderOpen className="w-4 h-4" />}
label="File"
onClick={() => setShowFileMenu((p) => !p)}
active={showFileMenu}
/>
{showFileMenu && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowFileMenu(false)} />
<div className="absolute left-0 top-full mt-1 z-50 w-44 rounded-lg border border-editor-border bg-editor-surface shadow-xl py-1">
<DropdownItem icon={<FilePlus2 className="w-4 h-4" />} label="New Project" onClick={() => { setShowFileMenu(false); handleNewProject(); }} />
<DropdownItem icon={<FolderOpen className="w-4 h-4" />} label="Open File" onClick={() => { setShowFileMenu(false); handleOpenFile(); }} />
<DropdownItem icon={<FileInput className="w-4 h-4" />} label="Load Project" onClick={() => { setShowFileMenu(false); handleLoadProject(); }} />
<div className="border-t border-editor-border my-1" />
<DropdownItem icon={<Save className="w-4 h-4" />} label="Save" onClick={() => { setShowFileMenu(false); handleSaveProject(); }} disabled={words.length === 0} />
<DropdownItem icon={<Save className="w-4 h-4" />} label="Save As" onClick={() => { setShowFileMenu(false); handleSaveProjectAs(); }} disabled={words.length === 0} />
</div>
</>
)}
</div>
<ToolbarButton
icon={<Scissors className="w-4 h-4" />}
label="Cut"
onClick={handleCut}
active={cutMode}
disabled={!canEdit}
/>
<ToolbarButton
icon={<VolumeX className="w-4 h-4" />}
label="Mute"
onClick={handleMute}
active={muteMode}
disabled={!canEdit}
/>
<div className="flex items-center gap-1">
<ToolbarButton
@ -607,6 +613,7 @@ export default function App() {
label="Gain Zone"
onClick={handleGain}
active={gainMode}
disabled={!canEdit}
/>
<input
type="number"
@ -617,6 +624,7 @@ export default function App() {
onChange={(e) => setGainModeDb(Math.max(-24, Math.min(24, Number(e.target.value) || 0)))}
className="w-16 px-1.5 py-1 text-xs bg-editor-surface border border-editor-border rounded text-editor-text focus:outline-none focus:border-editor-accent"
title="Gain dB for new gain zones"
disabled={!canEdit}
/>
</div>
<div className="flex items-center gap-1">
@ -625,6 +633,7 @@ export default function App() {
label="Speed Zone"
onClick={handleSpeed}
active={speedMode}
disabled={!canEdit}
/>
<input
type="number"
@ -635,6 +644,7 @@ export default function App() {
onChange={(e) => setSpeedModeValue(Math.max(0.25, Math.min(4, Number(e.target.value) || 1)))}
className="w-16 px-1.5 py-1 text-xs bg-editor-surface border border-editor-border rounded text-editor-text focus:outline-none focus:border-editor-accent"
title="Playback rate for new speed zones"
disabled={!canEdit}
/>
</div>
<ToolbarButton
@ -642,35 +652,35 @@ export default function App() {
label="Zones"
active={activePanel === 'zones'}
onClick={() => togglePanel('zones')}
disabled={!videoPath}
disabled={!videoPath || !canEdit}
/>
<ToolbarButton
icon={<span className="text-[10px] font-semibold">PA</span>}
label="Pause Trim"
active={activePanel === 'silence'}
onClick={() => togglePanel('silence')}
disabled={!videoPath}
disabled={!videoPath || !canEdit}
/>
<ToolbarButton
icon={<MapPin className="w-4 h-4" />}
label="Markers"
active={activePanel === 'markers'}
onClick={() => togglePanel('markers')}
disabled={!videoPath}
disabled={!videoPath || !canEdit}
/>
<ToolbarButton
icon={<Music className="w-4 h-4" />}
label="Music"
active={activePanel === 'music'}
onClick={() => togglePanel('music')}
disabled={!videoPath}
disabled={!videoPath || !canEdit}
/>
<ToolbarButton
icon={<ListVideo className="w-4 h-4" />}
label="Append"
active={activePanel === 'append'}
onClick={() => togglePanel('append')}
disabled={!videoPath}
disabled={!videoPath || !canEdit}
/>
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-editor-surface border border-editor-border">
<select
@ -700,7 +710,7 @@ export default function App() {
</select>
<button
onClick={handleReprocessProject}
disabled={isTranscribing || !videoPath}
disabled={isTranscribing || !videoPath || !canEdit}
title="Reprocess transcript with selected model"
className="flex items-center gap-1 px-2 py-1 rounded text-xs text-editor-text hover:bg-editor-bg disabled:opacity-40 disabled:cursor-not-allowed"
>
@ -713,7 +723,7 @@ export default function App() {
label="AI"
active={activePanel === 'ai'}
onClick={() => togglePanel('ai')}
disabled={words.length === 0}
disabled={words.length === 0 || !canEdit}
/>
<ToolbarButton
icon={<Download className="w-4 h-4" />}
@ -841,6 +851,8 @@ export default function App() {
</div>
{import.meta.env.DEV && <DevPanel />}
<LicenseDialog />
{showReprocessConfirm && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 px-4"
@ -941,3 +953,30 @@ function ToolbarButton({
</button>
);
}
function DropdownItem({
icon,
label,
onClick,
disabled,
}: {
icon: React.ReactNode;
label: string;
onClick: () => void;
disabled?: boolean;
}) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors ${
disabled
? 'opacity-40 cursor-not-allowed'
: 'text-editor-text-muted hover:text-editor-text hover:bg-editor-bg'
}`}
>
{icon}
{label}
</button>
);
}

View File

@ -0,0 +1,207 @@
import { useState } from 'react';
import { useLicenseStore } from '../store/licenseStore';
import { Key, Check, X, Loader2, Shield, Clock, AlertTriangle } from 'lucide-react';
export default function LicenseDialog() {
const { status, showDialog, setShowDialog, activateLicense } = useLicenseStore();
const [key, setKey] = useState('');
const [error, setError] = useState<string | null>(null);
const [activating, setActivating] = useState(false);
const handleActivate = async () => {
if (!key.trim()) return;
setActivating(true);
setError(null);
const ok = await activateLicense(key.trim());
if (!ok) {
setError('Invalid license key. Check it was entered correctly.');
}
setActivating(false);
};
const formatDate = (ts: number) => {
const d = new Date(ts * 1000);
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
};
if (!status) return null;
if (status.tag === 'Licensed') {
return (
<div className="fixed bottom-4 right-4 z-50">
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-editor-surface border border-editor-border shadow-lg text-xs">
<Shield className="w-3.5 h-3.5 text-editor-success" />
<span className="text-editor-text-muted">
{status.license.tier === 'business' ? 'Business' : 'Pro'} {status.license.customer_email}
</span>
<span className="text-editor-text-muted/50">
expires {formatDate(status.license.expires_at)}
</span>
</div>
</div>
);
}
if (status.tag === 'Trial') {
return (
<>
<div className="fixed bottom-4 right-4 z-50">
<button
onClick={() => setShowDialog(true)}
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-editor-surface border border-editor-border shadow-lg text-xs hover:bg-editor-bg transition-colors"
>
<Clock className="w-3.5 h-3.5 text-editor-accent" />
<span className="text-editor-text-muted">
Trial {status.days_remaining} day{status.days_remaining !== 1 ? 's' : ''} left
</span>
</button>
</div>
{showDialog && (
<LicenseActivateDialog
onClose={() => 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 (
<>
<ExpiredBanner onActivate={() => setShowDialog(true)} />
{showDialog && (
<LicenseActivateDialog
onClose={() => setShowDialog(false)}
onActivate={handleActivate}
keyValue={key}
setKeyValue={setKey}
error={error}
activating={activating}
expired
/>
)}
</>
);
}
/** Persistent top banner shown when trial expired — still allows export and loading */
function ExpiredBanner({ onActivate }: { onActivate: () => void }) {
return (
<div className="h-9 flex items-center justify-center gap-3 px-4 bg-red-500/15 border-b border-red-500/30 shrink-0">
<AlertTriangle className="w-3.5 h-3.5 text-red-400 shrink-0" />
<span className="text-xs text-red-300">
Trial expired export and project loading still work.&nbsp;
<button onClick={onActivate} className="underline font-medium hover:text-red-200">
Activate license
</button>
&nbsp;to restore editing.
</span>
</div>
);
}
function LicenseActivateDialog({
onClose,
onActivate,
keyValue,
setKeyValue,
error,
activating,
trialEnding,
expired,
}: {
onClose: () => void;
onActivate: () => void;
keyValue: string;
setKeyValue: (v: string) => void;
error: string | null;
activating: boolean;
trialEnding?: boolean;
expired?: boolean;
}) {
return (
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/60 px-4">
<div
className="w-full max-w-md rounded-xl border border-editor-border bg-editor-bg p-6 space-y-4"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Key className="w-5 h-5 text-editor-accent" />
<h3 className="text-sm font-semibold">
{expired ? 'Trial Expired' : 'Activate TalkEdit'}
</h3>
</div>
<button
onClick={onClose}
className="p-1 rounded hover:bg-editor-surface text-editor-text-muted"
>
<X className="w-4 h-4" />
</button>
</div>
{expired && (
<div className="text-xs text-editor-text-muted leading-relaxed space-y-1">
<p className="font-medium text-red-300">Your 30-day trial has ended.</p>
<p>
You can still <strong>export videos</strong> and <strong>load projects</strong>.
Enter a license key to restore editing, AI tools, and all other features.
</p>
</div>
)}
{trialEnding && !expired && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<p className="text-xs text-amber-300">Your trial ends soon. Activate now to keep using all features.</p>
</div>
)}
{!expired && !trialEnding && (
<p className="text-xs text-editor-text-muted leading-relaxed">
Enter your license key to activate TalkEdit Pro or Business.
You received this key by email after purchase.
</p>
)}
<div className="space-y-1.5">
<label className="text-xs text-editor-text-muted font-medium">License Key</label>
<textarea
value={keyValue}
onChange={(e) => { setKeyValue(e.target.value); }}
placeholder="talkedit_v1_..."
rows={3}
className="w-full px-3 py-2 text-xs font-mono bg-editor-surface border border-editor-border rounded-lg text-editor-text placeholder:text-editor-text-muted/50 focus:outline-none focus:border-editor-accent resize-none"
/>
{error && <p className="text-xs text-red-400">{error}</p>}
</div>
<button
onClick={onActivate}
disabled={activating || !keyValue.trim()}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-editor-accent hover:bg-editor-accent-hover disabled:opacity-50 rounded-lg text-sm font-medium transition-colors"
>
{activating ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Check className="w-4 h-4" />
)}
Activate
</button>
<p className="text-[10px] text-editor-text-muted text-center">
No license? <a href="#" className="text-editor-accent hover:underline">Purchase at talked.it</a>
</p>
</div>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { useCallback, useRef, useEffect, useMemo, useState } from 'react';
import { useEditorStore } from '../store/editorStore';
import { useLicenseStore } from '../store/licenseStore';
import { Virtuoso } from 'react-virtuoso';
import { Scissors, VolumeX, SlidersHorizontal, Gauge, RotateCcw, Search, ChevronUp, ChevronDown, X, RefreshCw } from 'lucide-react';
@ -42,6 +43,7 @@ export default function TranscriptEditor({
const addGainRange = useEditorStore((s) => s.addGainRange);
const addSpeedRange = useEditorStore((s) => s.addSpeedRange);
const getWordAtTime = useEditorStore((s) => s.getWordAtTime);
const canEdit = useLicenseStore((s) => s.canEdit);
const selectionStart = useRef<number | null>(null);
const wasDragging = useRef(false);
@ -206,7 +208,7 @@ export default function TranscriptEditor({
if (zoneDragStart.current !== null && zoneDragRange) {
const startWord = words[zoneDragRange.start];
const endWord = words[zoneDragRange.end];
if (startWord && endWord) {
if (startWord && endWord && canEdit) {
if (cutMode) addCutRange(startWord.start, endWord.end);
if (muteMode) addMuteRange(startWord.start, endWord.end);
if (gainMode) addGainRange(startWord.start, endWord.end, gainModeDb);
@ -216,7 +218,7 @@ export default function TranscriptEditor({
zoneDragStart.current = null;
setZoneDragRange(null);
selectionStart.current = null;
}, [zoneDragRange, words, cutMode, muteMode, gainMode, gainModeDb, speedMode, speedModeValue, addCutRange, addMuteRange, addGainRange, addSpeedRange]);
}, [zoneDragRange, words, cutMode, muteMode, gainMode, gainModeDb, speedMode, speedModeValue, addCutRange, addMuteRange, addGainRange, addSpeedRange, canEdit]);
const handleClickOutside = useCallback(
(e: React.MouseEvent) => {
@ -303,8 +305,9 @@ export default function TranscriptEditor({
const handleWordDoubleClick = useCallback((index: number) => {
if (cutMode || muteMode || gainMode || speedMode) return;
if (!canEdit) return;
startEditing(index);
}, [cutMode, muteMode, gainMode, speedMode, startEditing]);
}, [cutMode, muteMode, gainMode, speedMode, startEditing, canEdit]);
// Focus edit input when it appears
useEffect(() => {
@ -556,35 +559,39 @@ export default function TranscriptEditor({
<div className="flex items-center gap-1">
<button
onClick={cutSelectedWords}
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-500/20 text-red-300 rounded hover:bg-red-500/30 transition-colors"
disabled={!canEdit}
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-500/20 text-red-300 rounded hover:bg-red-500/30 transition-colors disabled:opacity-40"
>
<Scissors className="w-3 h-3" />
Cut
</button>
<button
onClick={muteSelectedWords}
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-500/20 text-blue-300 rounded hover:bg-blue-500/30 transition-colors"
disabled={!canEdit}
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-500/20 text-blue-300 rounded hover:bg-blue-500/30 transition-colors disabled:opacity-40"
>
<VolumeX className="w-3 h-3" />
Mute
</button>
<button
onClick={gainSelectedWords}
className="flex items-center gap-1 px-2 py-1 text-xs bg-amber-500/20 text-amber-300 rounded hover:bg-amber-500/30 transition-colors"
disabled={!canEdit}
className="flex items-center gap-1 px-2 py-1 text-xs bg-amber-500/20 text-amber-300 rounded hover:bg-amber-500/30 transition-colors disabled:opacity-40"
>
<SlidersHorizontal className="w-3 h-3" />
Gain ({gainModeDb > 0 ? '+' : ''}{gainModeDb.toFixed(1)} dB)
</button>
<button
onClick={speedSelectedWords}
className="flex items-center gap-1 px-2 py-1 text-xs bg-emerald-500/20 text-emerald-300 rounded hover:bg-emerald-500/30 transition-colors"
disabled={!canEdit}
className="flex items-center gap-1 px-2 py-1 text-xs bg-emerald-500/20 text-emerald-300 rounded hover:bg-emerald-500/30 transition-colors disabled:opacity-40"
>
<Gauge className="w-3 h-3" />
Speed {speedModeValue.toFixed(2)}x
</button>
<button
onClick={handleReTranscribe}
disabled={isReTranscribing}
disabled={isReTranscribing || !canEdit}
className="flex items-center gap-1 px-2 py-1 text-xs bg-purple-500/20 text-purple-300 rounded hover:bg-purple-500/30 disabled:opacity-40 transition-colors"
title="Re-run Whisper transcription on this segment"
>

View File

@ -1,5 +1,6 @@
import { useRef, useEffect, useCallback, useState, useMemo } from 'react';
import { useEditorStore } from '../store/editorStore';
import { useLicenseStore } from '../store/licenseStore';
import { AlertTriangle } from 'lucide-react';
import { extractThumbnails } from '../lib/thumbnails';
@ -264,6 +265,7 @@ export default function WaveformTimeline({
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
const removeGainRange = useEditorStore((s) => s.removeGainRange);
const removeSpeedRange = useEditorStore((s) => s.removeSpeedRange);
const canEdit = useLicenseStore((s) => s.canEdit);
const waveformDataRef = useRef<WaveformData | null>(null);
const zoomRef = useRef(1); // 1 = show all, >1 = zoomed in
@ -1025,7 +1027,7 @@ export default function WaveformTimeline({
// Check if clicking on a zone
const zoneHit = getZoneAtPosition(e.clientX, e.clientY);
if (zoneHit) {
if (zoneHit && canEdit) {
if (zoneHit.edge === 'move') {
setSelectedZone({ type: zoneHit.type, id: zoneHit.id });
} else {
@ -1098,7 +1100,7 @@ export default function WaveformTimeline({
// Clear selection if clicking elsewhere
setSelectedZone(null);
if (cutMode || muteMode || gainMode || speedMode) {
if (canEdit && (cutMode || muteMode || gainMode || speedMode)) {
// Range selection mode
const startTime = clientXToTime(e.clientX);
selectionStartRef.current = startTime;
@ -1181,7 +1183,7 @@ export default function WaveformTimeline({
if (e.key === 'Escape') {
setSelectedZone(null);
editingZoneRef.current = null;
} else if (e.key === 'Delete' || e.key === 'Backspace') {
} else if (canEdit && (e.key === 'Delete' || e.key === 'Backspace')) {
if (selectedZone) {
e.preventDefault();
e.stopPropagation();
@ -1204,7 +1206,7 @@ export default function WaveformTimeline({
// Capture phase ensures zone delete runs before app-level bubble shortcuts.
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
}, [selectedZone, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange]);
}, [selectedZone, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange, canEdit]);
useEffect(() => {
if (!selectedZone) return;

View File

@ -96,4 +96,20 @@ window.electronAPI = {
await writeTextFile(path, content);
return true;
},
activateLicense: (key: string): Promise<any> => {
return invoke('activate_license', { licenseKey: key });
},
getAppStatus: (): Promise<any> => {
return invoke('get_app_status');
},
deactivateLicense: (): Promise<void> => {
return invoke('deactivate_license');
},
hasLicenseFeature: (feature: string): Promise<boolean> => {
return invoke('has_license_feature', { feature });
},
};

View File

@ -0,0 +1,105 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface LicensePayload {
license_id: string;
customer_email: string;
tier: 'pro' | 'business';
features: string[];
issued_at: number;
expires_at: number;
max_activations: number;
}
export interface TrialState {
started_at: number;
}
export type AppStatus =
| { tag: 'Licensed'; license: LicensePayload }
| { tag: 'Trial'; days_remaining: number; started_at: number }
| { tag: 'Expired' };
interface LicenseState {
status: AppStatus | null;
isLoaded: boolean;
showDialog: boolean;
canEdit: boolean;
}
interface LicenseActions {
setStatus: (status: AppStatus | null) => void;
setShowDialog: (show: boolean) => void;
checkStatus: () => Promise<void>;
activateLicense: (key: string) => Promise<boolean>;
deactivateLicense: () => Promise<void>;
hasFeature: (feature: string) => Promise<boolean>;
}
export const useLicenseStore = create<LicenseState & LicenseActions>()(
persist(
(set) => ({
status: null,
isLoaded: false,
showDialog: false,
canEdit: true,
setStatus: (status) => {
const canEdit = status?.tag === 'Licensed' || status?.tag === 'Trial';
set({ status, isLoaded: true, canEdit });
},
setShowDialog: (show) => set({ showDialog: show }),
checkStatus: async () => {
try {
const status = await window.electronAPI?.getAppStatus();
const canEdit = status?.tag === 'Licensed' || status?.tag === 'Trial';
set({ status: status || { tag: 'Expired' }, isLoaded: true, canEdit });
} catch {
set({ status: { tag: 'Expired' }, isLoaded: true, canEdit: false });
}
},
activateLicense: async (key: string): Promise<boolean> => {
try {
const license = await window.electronAPI?.activateLicense(key);
if (!license) return false;
set({ status: { tag: 'Licensed', license }, showDialog: false, canEdit: true });
return true;
} catch {
return false;
}
},
deactivateLicense: async () => {
try {
await window.electronAPI?.deactivateLicense();
const s = await window.electronAPI?.getAppStatus();
const canEdit = s?.tag === 'Licensed' || s?.tag === 'Trial';
set({ status: s || { tag: 'Expired' }, isLoaded: true, canEdit });
} catch {
set({ status: { tag: 'Expired' }, isLoaded: true, canEdit: false });
}
},
hasFeature: async (feature: string): Promise<boolean> => {
try {
return await window.electronAPI?.hasLicenseFeature(feature) || false;
} catch {
return false;
}
},
}),
{
name: 'talkedit-license',
partialize: (state) => {
// Only persist Licensed status (trial is ephemeral)
if (state.status?.tag === 'Licensed') {
return { status: state.status };
}
return {};
},
},
),
);

View File

@ -20,6 +20,10 @@ interface DesktopAPI {
transcribe: (filePath: string, modelName: string, language?: string) => Promise<any>;
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<boolean>;
activateLicense: (key: string) => Promise<any>;
getAppStatus: () => Promise<any>;
deactivateLicense: () => Promise<void>;
hasLicenseFeature: (feature: string) => Promise<boolean>;
}
interface Window {

View File

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/AppendClipPanel.tsx","./src/components/BackgroundMusicPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/MarkersPanel.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/VolumePanel.tsx","./src/components/WaveformTimeline.tsx","./src/components/ZoneEditor.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/keybindings.ts","./src/lib/tauri-bridge.ts","./src/lib/thumbnails.ts","./src/store/aiStore.ts","./src/store/editorStore.test.ts","./src/store/editorStore.ts","./src/types/project.ts"],"version":"5.9.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/AppendClipPanel.tsx","./src/components/BackgroundMusicPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/LicenseDialog.tsx","./src/components/MarkersPanel.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/VolumePanel.tsx","./src/components/WaveformTimeline.tsx","./src/components/ZoneEditor.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/keybindings.ts","./src/lib/tauri-bridge.ts","./src/lib/thumbnails.ts","./src/store/aiStore.ts","./src/store/editorStore.test.ts","./src/store/editorStore.ts","./src/store/licenseStore.ts","./src/types/project.ts"],"version":"5.9.3"}