Create settings
This commit is contained in:
parent
f6cc5d380f
commit
a462a22468
3 changed files with 165 additions and 130 deletions
43
src/App.tsx
43
src/App.tsx
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { FileSelector } from './components/FileSelector';
|
import { FileSelector } from './components/FileSelector';
|
||||||
import { GeminiSettings, GeminiConfig } from './components/GeminiSettings';
|
import { GeminiSettings } from './components/GeminiSettings';
|
||||||
|
import { Settings } from './components/Settings';
|
||||||
import { VideoTimeline } from './components/VideoTimeline';
|
import { VideoTimeline } from './components/VideoTimeline';
|
||||||
import './styles/index.css';
|
import './styles/index.css';
|
||||||
|
|
||||||
|
|
@ -29,14 +30,12 @@ function App() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [addBlackBar, setAddBlackBar] = useState(true);
|
const [addBlackBar, setAddBlackBar] = useState(true);
|
||||||
const [resizeTo720p, setResizeTo720p] = useState(true);
|
const [resizeTo720p, setResizeTo720p] = useState(true);
|
||||||
const [geminiConfig, setGeminiConfig] = useState<GeminiConfig>({
|
const [geminiEnabled, setGeminiEnabled] = useState(false);
|
||||||
enabled: false,
|
const [apiKey, setApiKey] = useState('AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc');
|
||||||
apiKey: 'AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc',
|
|
||||||
isDefault: true
|
|
||||||
});
|
|
||||||
const [selectedTime, setSelectedTime] = useState<number>(10);
|
const [selectedTime, setSelectedTime] = useState<number>(10);
|
||||||
const [transcription, setTranscription] = useState<TranscriptionState | null>(null);
|
const [transcription, setTranscription] = useState<TranscriptionState | null>(null);
|
||||||
const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]);
|
const [thumbnails, setThumbnails] = useState<Thumbnail[]>([]);
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invoke('check_dependencies')
|
invoke('check_dependencies')
|
||||||
|
|
@ -97,12 +96,12 @@ function App() {
|
||||||
subtitlePath: subtitleFile.path,
|
subtitlePath: subtitleFile.path,
|
||||||
addBlackBar,
|
addBlackBar,
|
||||||
resizeTo720p,
|
resizeTo720p,
|
||||||
geminiEnabled: geminiConfig.enabled,
|
geminiEnabled,
|
||||||
frameTime: selectedTime
|
frameTime: selectedTime
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process transcription first if Gemini is enabled
|
// Process transcription first if Gemini is enabled
|
||||||
if (geminiConfig.enabled) {
|
if (geminiEnabled) {
|
||||||
try {
|
try {
|
||||||
console.log('Starting transcription process...');
|
console.log('Starting transcription process...');
|
||||||
const transcriptionResult = await invoke<TranscriptionState>('process_transcription', {
|
const transcriptionResult = await invoke<TranscriptionState>('process_transcription', {
|
||||||
|
|
@ -110,7 +109,7 @@ function App() {
|
||||||
video_path: videoFile.path,
|
video_path: videoFile.path,
|
||||||
subtitle_path: subtitleFile.path,
|
subtitle_path: subtitleFile.path,
|
||||||
frame_time: selectedTime,
|
frame_time: selectedTime,
|
||||||
api_key: geminiConfig.apiKey
|
api_key: apiKey
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('Transcription completed with full response:', transcriptionResult);
|
console.log('Transcription completed with full response:', transcriptionResult);
|
||||||
|
|
@ -180,7 +179,19 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 py-8 px-4">
|
<div className="min-h-screen bg-gray-100 py-8 px-4">
|
||||||
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||||
<h1 className="text-2xl font-bold text-center mb-8">Video Subtitle Merger <span className="text-blue-500">2.0</span></h1>
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-2xl font-bold">Video Subtitle Merger <span className="text-blue-500">3.0</span></h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-gray-700 bg-gray-100 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -208,12 +219,13 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GeminiSettings
|
<GeminiSettings
|
||||||
onSettingsChange={setGeminiConfig}
|
enabled={geminiEnabled}
|
||||||
|
onEnableChange={setGeminiEnabled}
|
||||||
videoFile={videoFile?.path || null}
|
videoFile={videoFile?.path || null}
|
||||||
onThumbnailsGenerated={handleThumbnailsGenerated}
|
onThumbnailsGenerated={handleThumbnailsGenerated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{geminiConfig.enabled && videoFile && (
|
{geminiEnabled && videoFile && (
|
||||||
<VideoTimeline
|
<VideoTimeline
|
||||||
videoPath={videoFile.path}
|
videoPath={videoFile.path}
|
||||||
onTimeSelect={setSelectedTime}
|
onTimeSelect={setSelectedTime}
|
||||||
|
|
@ -295,6 +307,13 @@ function App() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Settings
|
||||||
|
isOpen={isSettingsOpen}
|
||||||
|
onClose={() => setIsSettingsOpen(false)}
|
||||||
|
apiKey={apiKey}
|
||||||
|
onApiKeyChange={setApiKey}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,72 +2,24 @@ import React, { useState, useCallback } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
interface GeminiSettingsProps {
|
interface GeminiSettingsProps {
|
||||||
onSettingsChange: (settings: GeminiConfig) => void;
|
enabled: boolean;
|
||||||
|
onEnableChange: (enabled: boolean) => void;
|
||||||
videoFile: string | null;
|
videoFile: string | null;
|
||||||
onThumbnailsGenerated: (thumbnails: { path: string; time: number; }[]) => void;
|
onThumbnailsGenerated: (thumbnails: { path: string; time: number; }[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeminiConfig {
|
export const GeminiSettings: React.FC<GeminiSettingsProps> = ({
|
||||||
enabled: boolean;
|
enabled,
|
||||||
apiKey: string;
|
onEnableChange,
|
||||||
isDefault: boolean;
|
videoFile,
|
||||||
}
|
onThumbnailsGenerated
|
||||||
|
}) => {
|
||||||
const DEFAULT_API_KEY = 'AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc';
|
|
||||||
|
|
||||||
export const GeminiSettings: React.FC<GeminiSettingsProps> = ({ onSettingsChange, videoFile, onThumbnailsGenerated }) => {
|
|
||||||
const [settings, setSettings] = useState<GeminiConfig>({
|
|
||||||
enabled: false,
|
|
||||||
apiKey: DEFAULT_API_KEY,
|
|
||||||
isDefault: true
|
|
||||||
});
|
|
||||||
const [testing, setTesting] = useState(false);
|
|
||||||
const [testResult, setTestResult] = useState<string | null>(null);
|
|
||||||
const [generatingThumbnails, setGeneratingThumbnails] = useState(false);
|
const [generatingThumbnails, setGeneratingThumbnails] = useState(false);
|
||||||
const [thumbnailResult, setThumbnailResult] = useState<string | null>(null);
|
const [thumbnailResult, setThumbnailResult] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleEnableChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEnableChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newSettings = {
|
onEnableChange(e.target.checked);
|
||||||
...settings,
|
|
||||||
enabled: e.target.checked
|
|
||||||
};
|
};
|
||||||
setSettings(newSettings);
|
|
||||||
onSettingsChange(newSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newSettings = {
|
|
||||||
...settings,
|
|
||||||
apiKey: e.target.value,
|
|
||||||
isDefault: e.target.value === DEFAULT_API_KEY
|
|
||||||
};
|
|
||||||
setSettings(newSettings);
|
|
||||||
onSettingsChange(newSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetToDefault = () => {
|
|
||||||
const newSettings = {
|
|
||||||
...settings,
|
|
||||||
apiKey: DEFAULT_API_KEY,
|
|
||||||
isDefault: true
|
|
||||||
};
|
|
||||||
setSettings(newSettings);
|
|
||||||
onSettingsChange(newSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const testApiKey = useCallback(async () => {
|
|
||||||
setTesting(true);
|
|
||||||
setTestResult(null);
|
|
||||||
try {
|
|
||||||
// We'll implement this Rust command later
|
|
||||||
await invoke('test_gemini_api', { apiKey: settings.apiKey });
|
|
||||||
setTestResult('API key is valid');
|
|
||||||
} catch (error) {
|
|
||||||
setTestResult(`Error: ${error}`);
|
|
||||||
} finally {
|
|
||||||
setTesting(false);
|
|
||||||
}
|
|
||||||
}, [settings.apiKey]);
|
|
||||||
|
|
||||||
const generateThumbnails = useCallback(async () => {
|
const generateThumbnails = useCallback(async () => {
|
||||||
if (!videoFile) {
|
if (!videoFile) {
|
||||||
|
|
@ -94,7 +46,7 @@ export const GeminiSettings: React.FC<GeminiSettingsProps> = ({ onSettingsChange
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="enableGemini"
|
id="enableGemini"
|
||||||
checked={settings.enabled}
|
checked={enabled}
|
||||||
onChange={handleEnableChange}
|
onChange={handleEnableChange}
|
||||||
className="h-4 w-4 text-blue-500 rounded border-gray-300 focus:ring-blue-500"
|
className="h-4 w-4 text-blue-500 rounded border-gray-300 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
|
@ -103,50 +55,8 @@ export const GeminiSettings: React.FC<GeminiSettingsProps> = ({ onSettingsChange
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settings.enabled && (
|
{enabled && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
|
||||||
<label htmlFor="apiKey" className="block text-sm font-medium text-gray-700">
|
|
||||||
Gemini API Key
|
|
||||||
</label>
|
|
||||||
<div className="mt-1 flex rounded-md shadow-sm">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="apiKey"
|
|
||||||
value={settings.apiKey}
|
|
||||||
onChange={handleApiKeyChange}
|
|
||||||
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
||||||
placeholder="Enter your Gemini API key"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={testApiKey}
|
|
||||||
disabled={testing}
|
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{testing ? 'Testing...' : 'Test API Key'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{!settings.isDefault && (
|
|
||||||
<button
|
|
||||||
onClick={resetToDefault}
|
|
||||||
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
Reset to Default
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{testResult && (
|
|
||||||
<div className={`text-sm ${testResult.startsWith('Error') ? 'text-red-600' : 'text-green-600'}`}>
|
|
||||||
{testResult}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<button
|
<button
|
||||||
onClick={generateThumbnails}
|
onClick={generateThumbnails}
|
||||||
disabled={generatingThumbnails || !videoFile}
|
disabled={generatingThumbnails || !videoFile}
|
||||||
|
|
@ -164,7 +74,6 @@ export const GeminiSettings: React.FC<GeminiSettingsProps> = ({ onSettingsChange
|
||||||
'Generate Thumbnails'
|
'Generate Thumbnails'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{thumbnailResult && (
|
{thumbnailResult && (
|
||||||
<div className={`text-sm ${thumbnailResult.startsWith('Error') || thumbnailResult.startsWith('Please') ? 'text-red-600' : 'text-green-600'}`}>
|
<div className={`text-sm ${thumbnailResult.startsWith('Error') || thumbnailResult.startsWith('Please') ? 'text-red-600' : 'text-green-600'}`}>
|
||||||
|
|
|
||||||
107
src/components/Settings.tsx
Normal file
107
src/components/Settings.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
interface SettingsProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
apiKey: string;
|
||||||
|
onApiKeyChange: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_API_KEY = 'AIzaSyAF825tPTh77oL0knsGFEyvsN0iPUO_bXc';
|
||||||
|
|
||||||
|
export const Settings: React.FC<SettingsProps> = ({ isOpen, onClose, apiKey, onApiKeyChange }) => {
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [testResult, setTestResult] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onApiKeyChange(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetToDefault = () => {
|
||||||
|
onApiKeyChange(DEFAULT_API_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testApiKey = async () => {
|
||||||
|
setTesting(true);
|
||||||
|
setTestResult(null);
|
||||||
|
try {
|
||||||
|
await invoke('test_gemini_api', { apiKey });
|
||||||
|
setTestResult('API key is valid');
|
||||||
|
} catch (error) {
|
||||||
|
setTestResult(`Error: ${error}`);
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-[500px] max-w-full">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-xl font-semibold">Settings</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="apiKey" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Gemini API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="apiKey"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={handleApiKeyChange}
|
||||||
|
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
placeholder="Enter your Gemini API key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={testApiKey}
|
||||||
|
disabled={testing}
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{testing ? 'Testing...' : 'Test API Key'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{apiKey !== DEFAULT_API_KEY && (
|
||||||
|
<button
|
||||||
|
onClick={resetToDefault}
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Reset to Default
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testResult && (
|
||||||
|
<div className={`text-sm ${testResult.startsWith('Error') ? 'text-red-600' : 'text-green-600'}`}>
|
||||||
|
{testResult}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue