added close; fixed some issues
This commit is contained in:
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' http://localhost:* ws://localhost:*; media-src 'self' file: blob: http://localhost:*;" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' ipc: http://ipc.localhost http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; media-src 'self' file: blob: http://localhost:* http://127.0.0.1:*; img-src 'self' data: blob:;" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { useEditorStore } from './store/editorStore';
|
import { useEditorStore } from './store/editorStore';
|
||||||
import VideoPlayer from './components/VideoPlayer';
|
import VideoPlayer from './components/VideoPlayer';
|
||||||
import TranscriptEditor from './components/TranscriptEditor';
|
import TranscriptEditor from './components/TranscriptEditor';
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const IS_ELECTRON = !!window.electronAPI;
|
const IS_ELECTRON = !!window.electronAPI;
|
||||||
|
const IS_TAURI = !IS_ELECTRON && '__TAURI_INTERNALS__' in window;
|
||||||
|
|
||||||
type Panel = 'ai' | 'settings' | 'export' | null;
|
type Panel = 'ai' | 'settings' | 'export' | null;
|
||||||
|
|
||||||
@ -45,6 +47,8 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (IS_ELECTRON) {
|
if (IS_ELECTRON) {
|
||||||
window.electronAPI!.getBackendUrl().then(setBackendUrl);
|
window.electronAPI!.getBackendUrl().then(setBackendUrl);
|
||||||
|
} else if (IS_TAURI) {
|
||||||
|
invoke<string>('get_backend_url').then(setBackendUrl).catch(console.error);
|
||||||
}
|
}
|
||||||
}, [setBackendUrl]);
|
}, [setBackendUrl]);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,28 @@
|
|||||||
import { useRef, useEffect, useCallback, useState } from 'react';
|
import { useRef, useEffect, useCallback, useState } from 'react';
|
||||||
import { useEditorStore } from '../store/editorStore';
|
import { useEditorStore } from '../store/editorStore';
|
||||||
import { ZoomIn, ZoomOut, AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
const RULER_H = 20; // px reserved at top of canvas for the time ruler
|
||||||
|
|
||||||
|
function formatTime(secs: number): string {
|
||||||
|
const m = Math.floor(secs / 60);
|
||||||
|
const s = secs % 60;
|
||||||
|
if (m > 0) return `${m}:${String(Math.floor(s)).padStart(2, '0')}.${Math.floor((s % 1) * 10)}`;
|
||||||
|
return `${s.toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickInterval(pxPerSec: number): { major: number; minor: number } {
|
||||||
|
const NICE = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
|
||||||
|
let major = NICE[NICE.length - 1];
|
||||||
|
for (const n of NICE) {
|
||||||
|
if (n * pxPerSec >= 70) { major = n; break; }
|
||||||
|
}
|
||||||
|
let minor = major;
|
||||||
|
for (const n of NICE) {
|
||||||
|
if (n * pxPerSec >= 6 && n < major) { minor = n; }
|
||||||
|
}
|
||||||
|
return { major, minor };
|
||||||
|
}
|
||||||
|
|
||||||
export default function WaveformTimeline() {
|
export default function WaveformTimeline() {
|
||||||
const waveCanvasRef = useRef<HTMLCanvasElement>(null);
|
const waveCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
@ -17,7 +39,8 @@ export default function WaveformTimeline() {
|
|||||||
|
|
||||||
const audioContextRef = useRef<AudioContext | null>(null);
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
const audioBufferRef = useRef<AudioBuffer | null>(null);
|
const audioBufferRef = useRef<AudioBuffer | null>(null);
|
||||||
const zoomRef = useRef(1);
|
const zoomRef = useRef(1); // 1 = show all, >1 = zoomed in
|
||||||
|
const scrollSecsRef = useRef(0); // seconds scrolled from left
|
||||||
const rafRef = useRef(0);
|
const rafRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -27,7 +50,8 @@ export default function WaveformTimeline() {
|
|||||||
const loadAudio = async () => {
|
const loadAudio = async () => {
|
||||||
try {
|
try {
|
||||||
const waveformUrl = `${backendUrl}/audio/waveform?path=${encodeURIComponent(videoPath!)}`;
|
const waveformUrl = `${backendUrl}/audio/waveform?path=${encodeURIComponent(videoPath!)}`;
|
||||||
console.log('[WaveformTimeline] Loading audio from waveform endpoint:', waveformUrl);
|
console.log('[WaveformTimeline] backendUrl:', backendUrl, '| videoPath:', videoPath);
|
||||||
|
console.log('[WaveformTimeline] Fetching:', waveformUrl);
|
||||||
const ctx = new AudioContext();
|
const ctx = new AudioContext();
|
||||||
audioContextRef.current = ctx;
|
audioContextRef.current = ctx;
|
||||||
|
|
||||||
@ -81,7 +105,8 @@ export default function WaveformTimeline() {
|
|||||||
drawStaticWaveform();
|
drawStaticWaveform();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[WaveformTimeline] Waveform load failed:', err);
|
console.error('[WaveformTimeline] Waveform load failed:', err);
|
||||||
setAudioError(`Waveform unavailable — ${err instanceof Error ? err.message : 'audio could not be decoded'}`);
|
const waveformUrl2 = `${backendUrl}/audio/waveform?path=${encodeURIComponent(videoPath ?? '')}`;
|
||||||
|
setAudioError(`Waveform unavailable — ${err instanceof Error ? err.message : 'audio could not be decoded'} [URL: ${waveformUrl2}]`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,38 +133,95 @@ export default function WaveformTimeline() {
|
|||||||
|
|
||||||
const width = rect.width;
|
const width = rect.width;
|
||||||
const height = rect.height;
|
const height = rect.height;
|
||||||
|
const dur = buffer.duration;
|
||||||
|
const zoom = zoomRef.current;
|
||||||
|
const scroll = scrollSecsRef.current;
|
||||||
|
const pxPerSec = (width * zoom) / dur;
|
||||||
|
const sampleRate = buffer.sampleRate;
|
||||||
const channelData = buffer.getChannelData(0);
|
const channelData = buffer.getChannelData(0);
|
||||||
const samplesPerPixel = Math.floor(channelData.length / width);
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
for (const range of deletedRanges) {
|
// --- Ruler background ---
|
||||||
const x1 = (range.start / buffer.duration) * width;
|
ctx.fillStyle = '#13141f';
|
||||||
const x2 = (range.end / buffer.duration) * width;
|
ctx.fillRect(0, 0, width, RULER_H);
|
||||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
|
|
||||||
ctx.fillRect(x1, 0, x2 - x1, height);
|
// Separator line
|
||||||
|
ctx.strokeStyle = '#2a2d3e';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, RULER_H);
|
||||||
|
ctx.lineTo(width, RULER_H);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// --- Ruler ticks & labels ---
|
||||||
|
const { major, minor } = pickInterval(pxPerSec);
|
||||||
|
const visibleDur = width / pxPerSec;
|
||||||
|
|
||||||
|
// Minor ticks
|
||||||
|
const minorStart = Math.floor(scroll / minor) * minor;
|
||||||
|
ctx.strokeStyle = '#3a3d52';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let t = minorStart; t <= scroll + visibleDur + minor; t = Math.round((t + minor) * 1e6) / 1e6) {
|
||||||
|
const x = (t - scroll) * pxPerSec;
|
||||||
|
if (x < 0 || x > width) continue;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, RULER_H);
|
||||||
|
ctx.lineTo(x, RULER_H * 0.45);
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mid = height / 2;
|
// Major ticks + labels
|
||||||
|
const majorStart = Math.floor(scroll / major) * major;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.font = `9px "JetBrains Mono", "Courier New", monospace`;
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
for (let t = majorStart; t <= scroll + visibleDur + major; t = Math.round((t + major) * 1e6) / 1e6) {
|
||||||
|
const x = (t - scroll) * pxPerSec;
|
||||||
|
if (x < -50 || x > width + 50) continue;
|
||||||
|
ctx.strokeStyle = '#4a4f6a';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, RULER_H);
|
||||||
|
ctx.lineTo(x, 0);
|
||||||
|
ctx.stroke();
|
||||||
|
if (x >= 2 && x < width - 2) {
|
||||||
|
ctx.fillStyle = '#6b7280';
|
||||||
|
ctx.fillText(formatTime(t), x + 3, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Waveform ---
|
||||||
|
const waveTop = RULER_H + 1;
|
||||||
|
const waveH = height - waveTop;
|
||||||
|
|
||||||
|
for (const range of deletedRanges) {
|
||||||
|
const x1 = (range.start - scroll) * pxPerSec;
|
||||||
|
const x2 = (range.end - scroll) * pxPerSec;
|
||||||
|
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
|
||||||
|
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mid = waveTop + waveH / 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = '#4a4d5e';
|
ctx.strokeStyle = '#4a4d5e';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
for (let x = 0; x < width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
const start = x * samplesPerPixel;
|
const tStart = scroll + x / pxPerSec;
|
||||||
const end = Math.min(start + samplesPerPixel, channelData.length);
|
const tEnd = scroll + (x + 1) / pxPerSec;
|
||||||
|
const sStart = Math.floor(tStart * sampleRate);
|
||||||
|
const sEnd = Math.min(Math.ceil(tEnd * sampleRate), channelData.length);
|
||||||
|
if (sStart >= channelData.length) break;
|
||||||
|
|
||||||
let min = 0;
|
let min = 0, max = 0;
|
||||||
let max = 0;
|
for (let i = sStart; i < sEnd; i++) {
|
||||||
for (let i = start; i < end; i++) {
|
|
||||||
if (channelData[i] < min) min = channelData[i];
|
if (channelData[i] < min) min = channelData[i];
|
||||||
if (channelData[i] > max) max = channelData[i];
|
if (channelData[i] > max) max = channelData[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
const yMin = mid + min * mid * 0.9;
|
const amp = (waveH / 2) * 0.9;
|
||||||
const yMax = mid + max * mid * 0.9;
|
ctx.moveTo(x, mid + min * amp);
|
||||||
ctx.moveTo(x, yMin);
|
ctx.lineTo(x, mid + max * amp);
|
||||||
ctx.lineTo(x, yMax);
|
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}, [deletedRanges]);
|
}, [deletedRanges]);
|
||||||
@ -177,13 +259,16 @@ export default function WaveformTimeline() {
|
|||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
if (dur > 0 && video) {
|
if (dur > 0 && video) {
|
||||||
const px = (video.currentTime / dur) * width;
|
const pxPerSec = (width * zoomRef.current) / dur;
|
||||||
ctx.beginPath();
|
const px = (video.currentTime - scrollSecsRef.current) * pxPerSec;
|
||||||
ctx.strokeStyle = '#6366f1';
|
if (px >= 0 && px <= width) {
|
||||||
ctx.lineWidth = 2;
|
ctx.beginPath();
|
||||||
ctx.moveTo(px, 0);
|
ctx.strokeStyle = '#6366f1';
|
||||||
ctx.lineTo(px, height);
|
ctx.lineWidth = 2;
|
||||||
ctx.stroke();
|
ctx.moveTo(px, 0);
|
||||||
|
ctx.lineTo(px, height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rafRef.current = requestAnimationFrame(tick);
|
rafRef.current = requestAnimationFrame(tick);
|
||||||
@ -201,17 +286,50 @@ export default function WaveformTimeline() {
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [drawStaticWaveform]);
|
}, [drawStaticWaveform]);
|
||||||
|
|
||||||
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const buffer = audioBufferRef.current;
|
||||||
|
const canvas = waveCanvasRef.current;
|
||||||
|
if (!buffer || !canvas) return;
|
||||||
|
const dur = buffer.duration;
|
||||||
|
const width = canvas.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
// Zoom around the cursor position
|
||||||
|
const mouseX = e.clientX - canvas.getBoundingClientRect().left;
|
||||||
|
const pxPerSecBefore = (width * zoomRef.current) / dur;
|
||||||
|
const timeCursor = scrollSecsRef.current + mouseX / pxPerSecBefore;
|
||||||
|
const factor = e.deltaY < 0 ? 1.25 : 1 / 1.25;
|
||||||
|
zoomRef.current = Math.max(1, Math.min(100, zoomRef.current * factor));
|
||||||
|
const pxPerSecAfter = (width * zoomRef.current) / dur;
|
||||||
|
scrollSecsRef.current = timeCursor - mouseX / pxPerSecAfter;
|
||||||
|
} else {
|
||||||
|
// Scroll horizontally
|
||||||
|
const pxPerSec = (width * zoomRef.current) / dur;
|
||||||
|
scrollSecsRef.current += (e.deltaY || e.deltaX) / pxPerSec * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp scroll
|
||||||
|
const pxPerSec = (width * zoomRef.current) / dur;
|
||||||
|
const maxScroll = Math.max(0, dur - width / pxPerSec);
|
||||||
|
scrollSecsRef.current = Math.max(0, Math.min(scrollSecsRef.current, maxScroll));
|
||||||
|
drawStaticWaveform();
|
||||||
|
}, [drawStaticWaveform]);
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (!headCanvasRef.current || duration === 0) return;
|
const buffer = audioBufferRef.current;
|
||||||
const rect = headCanvasRef.current.getBoundingClientRect();
|
const canvas = headCanvasRef.current;
|
||||||
const ratio = (e.clientX - rect.left) / rect.width;
|
if (!canvas || !buffer) return;
|
||||||
const newTime = ratio * duration;
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const pxPerSec = (rect.width * zoomRef.current) / buffer.duration;
|
||||||
|
const newTime = Math.max(0, Math.min(buffer.duration, scrollSecsRef.current + x / pxPerSec));
|
||||||
setCurrentTime(newTime);
|
setCurrentTime(newTime);
|
||||||
const video = document.querySelector('video');
|
const video = document.querySelector('video');
|
||||||
if (video) video.currentTime = newTime;
|
if (video) video.currentTime = newTime;
|
||||||
},
|
},
|
||||||
[duration, setCurrentTime],
|
[setCurrentTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!videoUrl) {
|
if (!videoUrl) {
|
||||||
@ -228,22 +346,9 @@ export default function WaveformTimeline() {
|
|||||||
<span className="text-[10px] text-editor-text-muted font-medium uppercase tracking-wider">
|
<span className="text-[10px] text-editor-text-muted font-medium uppercase tracking-wider">
|
||||||
Timeline
|
Timeline
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<span className="text-[10px] text-editor-text-muted">
|
||||||
<button
|
Scroll · Ctrl+Scroll to zoom
|
||||||
onClick={() => { zoomRef.current = Math.max(0.5, zoomRef.current - 0.5); drawStaticWaveform(); }}
|
</span>
|
||||||
className="p-0.5 text-editor-text-muted hover:text-editor-text"
|
|
||||||
title="Zoom out"
|
|
||||||
>
|
|
||||||
<ZoomOut className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { zoomRef.current = Math.min(10, zoomRef.current + 0.5); drawStaticWaveform(); }}
|
|
||||||
className="p-0.5 text-editor-text-muted hover:text-editor-text"
|
|
||||||
title="Zoom in"
|
|
||||||
>
|
|
||||||
<ZoomIn className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{audioError ? (
|
{audioError ? (
|
||||||
<div className="flex-1 flex items-center justify-center gap-2 text-editor-text-muted text-xs">
|
<div className="flex-1 flex items-center justify-center gap-2 text-editor-text-muted text-xs">
|
||||||
@ -257,6 +362,7 @@ export default function WaveformTimeline() {
|
|||||||
ref={headCanvasRef}
|
ref={headCanvasRef}
|
||||||
className="absolute inset-0 w-full h-full cursor-crosshair"
|
className="absolute inset-0 w-full h-full cursor-crosshair"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onWheel={handleWheel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const initialState: EditorState = {
|
|||||||
transcriptionStatus: '',
|
transcriptionStatus: '',
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
exportProgress: 0,
|
exportProgress: 0,
|
||||||
backendUrl: 'http://localhost:8642',
|
backendUrl: 'http://127.0.0.1:8000',
|
||||||
};
|
};
|
||||||
|
|
||||||
let nextRangeId = 1;
|
let nextRangeId = 1;
|
||||||
|
|||||||
@ -9,13 +9,10 @@ mod ai_provider;
|
|||||||
mod caption_generator;
|
mod caption_generator;
|
||||||
mod background_removal;
|
mod background_removal;
|
||||||
|
|
||||||
/// Returns the backend URL. Stubbed for now; will be replaced once the
|
/// Returns the backend URL.
|
||||||
/// Python/Rust backend is fully wired up.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_backend_url() -> String {
|
fn get_backend_url() -> String {
|
||||||
// During development the Python backend still runs on 8642.
|
"http://127.0.0.1:8000".to_string()
|
||||||
// In production this will be replaced with a local Rust server or IPC.
|
|
||||||
"http://localhost:8642".to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimal encrypt: base64-encodes the string as a placeholder until a proper
|
/// Minimal encrypt: base64-encodes the string as a placeholder until a proper
|
||||||
|
|||||||
@ -45,18 +45,6 @@ pub fn python_exe() -> PathBuf {
|
|||||||
root.join(".venv").join("bin").join("python3")
|
root.join(".venv").join("bin").join("python3")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Absolute path to the bundled ffmpeg binary.
|
|
||||||
/// Uses a sidecar in resources/bin/ when packaged, otherwise expects it on PATH.
|
|
||||||
pub fn ffmpeg_exe() -> PathBuf {
|
|
||||||
let root = project_root();
|
|
||||||
let bundled = root.join("bin").join("ffmpeg");
|
|
||||||
if bundled.exists() {
|
|
||||||
return bundled;
|
|
||||||
}
|
|
||||||
// Fallback to system ffmpeg during development
|
|
||||||
PathBuf::from("ffmpeg")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Absolute path to a script in the backend directory.
|
/// Absolute path to a script in the backend directory.
|
||||||
pub fn backend_script(name: &str) -> PathBuf {
|
pub fn backend_script(name: &str) -> PathBuf {
|
||||||
project_root().join("backend").join(name)
|
project_root().join("backend").join(name)
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": null
|
"csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:8000; media-src 'self' http://127.0.0.1:8000; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#!/home/dillon/_code/TalkEdit/.venv/bin/python3.10
|
#!/home/dillon/_code/TalkEdit/.venv312/bin/python3.12
|
||||||
"""
|
"""
|
||||||
Test script for the TalkEdit API.
|
Test script for the TalkEdit API.
|
||||||
This script tests the new Tauri commands that expose all backend functions.
|
This script tests the new Tauri commands that expose all backend functions.
|
||||||
|
|||||||
Reference in New Issue
Block a user