Skip to content

Commit 229239d

Browse files
committed
feat: overhaul AI settings and shadowing cleanup
1 parent 2594868 commit 229239d

15 files changed

Lines changed: 1121 additions & 592 deletions

src/components/controls/CombinedControls.tsx

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { useState, useEffect, useMemo } from "react";
1+
import { useState, useEffect } from "react";
22
import { usePlayerStore } from "../../stores/playerStore";
33
import { useTranslation } from "react-i18next";
44
import { formatTime } from "../../utils/formatTime";
5-
import { checkAudioRecordingSupport, getRecordingUnsupportedMessage } from "../../utils/browserCheck";
65
import {
76
Play,
87
Pause,
@@ -20,11 +19,8 @@ import {
2019
Bookmark,
2120
ListMusic,
2221
X,
23-
Mic,
2422
RotateCcw,
2523
} from "lucide-react";
26-
import { useShadowingStore } from "../../stores/shadowingStore";
27-
import { useShadowingRecorder } from "../../hooks/useShadowingRecorder";
2824
import { Slider } from "../ui/slider";
2925
import { Button } from "../ui/button";
3026
import { Plus } from "lucide-react";
@@ -69,15 +65,6 @@ export const CombinedControls = () => {
6965
selectedBookmarkId,
7066
} = usePlayerStore();
7167

72-
const {
73-
isShadowingMode,
74-
setShadowingMode,
75-
isRecording,
76-
} = useShadowingStore();
77-
78-
// Initialize shadowing recorder
79-
useShadowingRecorder();
80-
8168
const [rangeValues, setRangeValues] = useState<[number, number]>([0, 100]);
8269
const [showABControls, setShowABControls] = useState(false);
8370
// Get current media bookmarks for the bookmark button
@@ -93,20 +80,6 @@ export const CombinedControls = () => {
9380
setRangeValues([(start / duration) * 100, (end / duration) * 100]);
9481
}, [loopStart, loopEnd, duration]);
9582

96-
// Check if audio recording is supported in this browser
97-
const recordingCapabilities = useMemo(() => checkAudioRecordingSupport(), []);
98-
const canRecord = recordingCapabilities.supportsAudioRecording;
99-
100-
// Handle shadowing mode toggle
101-
const handleShadowingToggle = () => {
102-
if (!isShadowingMode && !canRecord) {
103-
const errorMessage = getRecordingUnsupportedMessage(recordingCapabilities);
104-
toast.error(errorMessage, { duration: 5000 });
105-
return;
106-
}
107-
setShadowingMode(!isShadowingMode);
108-
};
109-
11083
// Toggle play/pause
11184
const togglePlayPause = () => {
11285
setIsPlaying(!isPlaying);
@@ -413,22 +386,6 @@ export const CombinedControls = () => {
413386
)}
414387
</button>
415388

416-
<button
417-
onClick={handleShadowingToggle}
418-
className={`p-2.5 rounded-full transition-all duration-150 ${isRecording
419-
? "bg-red-600 text-white animate-pulse"
420-
: isShadowingMode
421-
? "bg-orange-600 text-white hover:bg-orange-700"
422-
: canRecord
423-
? "bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
424-
: "bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-50"
425-
}`}
426-
aria-label={isShadowingMode ? t("shadowing.disable") : t("shadowing.enable")}
427-
title={!canRecord ? "Audio recording is not supported on this device/browser" : (isShadowingMode ? t("shadowing.disable") : t("shadowing.enable"))}
428-
>
429-
<Mic size={18} className="sm:w-[20px] sm:h-[20px]" />
430-
</button>
431-
432389
<button
433390
onClick={seekForward}
434391
className="p-2.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300 transition-colors"

src/components/controls/MobileControls.tsx

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { useState, useMemo } from "react";
1+
import { useState } from "react";
22
import { usePlayerStore } from "../../stores/playerStore";
33
import { useTranslation } from "react-i18next";
4-
import { checkAudioRecordingSupport, getRecordingUnsupportedMessage } from "../../utils/browserCheck";
54

65
import { toast } from "react-hot-toast";
76
import {
@@ -21,18 +20,12 @@ import {
2120
ChevronLeft,
2221
ChevronRight,
2322
ChevronsRight,
24-
Mic,
2523
} from "lucide-react";
2624
import { Button } from "../ui/button";
27-
import { useShadowingStore } from "../../stores/shadowingStore";
28-
import { useShadowingRecorder } from "../../hooks/useShadowingRecorder";
2925

3026
export const MobileControls = () => {
3127
const { t } = useTranslation();
3228

33-
// Initialize shadowing recorder
34-
useShadowingRecorder();
35-
3629
const {
3730
isPlaying,
3831
duration,
@@ -69,20 +62,10 @@ export const MobileControls = () => {
6962
toggleLooping,
7063
} = usePlayerStore();
7164

72-
const {
73-
isShadowingMode,
74-
setShadowingMode,
75-
isRecording,
76-
} = useShadowingStore();
77-
7865
const [showSpeedControls, setShowSpeedControls] = useState(false);
7966
const [showVolumeDrawer, setShowVolumeDrawer] = useState(false);
8067
const [showPlaylistDrawer, setShowPlaylistDrawer] = useState(false);
8168

82-
// Check if audio recording is supported in this browser
83-
const recordingCapabilities = useMemo(() => checkAudioRecordingSupport(), []);
84-
const canRecord = recordingCapabilities.supportsAudioRecording;
85-
8669
// Get current media bookmarks for the bookmark button
8770
const bookmarks = getCurrentMediaBookmarks();
8871

@@ -236,19 +219,6 @@ export const MobileControls = () => {
236219

237220
// Clear loop points function moved to waveform footer
238221

239-
// Handle shadowing mode toggle
240-
const handleShadowingToggle = () => {
241-
// If trying to enable shadowing mode, check browser support first
242-
if (!isShadowingMode && !canRecord) {
243-
const errorMessage = getRecordingUnsupportedMessage(recordingCapabilities);
244-
toast.error(errorMessage, { duration: 5000 });
245-
return;
246-
}
247-
248-
// Toggle shadowing mode
249-
setShadowingMode(!isShadowingMode);
250-
};
251-
252222
// Note: Loop jump functions removed as they're not used in the mobile interface
253223

254224
return (
@@ -293,23 +263,6 @@ export const MobileControls = () => {
293263
)}
294264
</button>
295265

296-
{/* Shadowing toggle button */}
297-
<button
298-
onClick={handleShadowingToggle}
299-
className={`p-3 rounded-full transition-all duration-150 ${isRecording
300-
? "bg-red-600 text-white animate-pulse"
301-
: isShadowingMode
302-
? "bg-orange-600 text-white hover:bg-orange-700"
303-
: canRecord
304-
? "bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
305-
: "bg-gray-200 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed opacity-50"
306-
}`}
307-
aria-label={isShadowingMode ? t("shadowing.disable") : t("shadowing.enable")}
308-
title={!canRecord ? "Audio recording is not supported on this device/browser" : undefined}
309-
>
310-
<Mic size={24} />
311-
</button>
312-
313266
<button
314267
onClick={seekForward}
315268
className="p-3 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"

src/components/player/MediaHistory.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ export const MediaHistory = ({
304304
const [hoveredFolder, setHoveredFolder] = useState<
305305
string | "unfiled" | "all" | null
306306
>(null);
307+
const [pendingDeleteItem, setPendingDeleteItem] =
308+
useState<MediaHistoryItem | null>(null);
307309

308310
const {
309311
mediaHistory,
@@ -473,7 +475,10 @@ export const MediaHistory = ({
473475
// Remove an item from history
474476
const handleRemoveFromHistory = (id: string, e: React.MouseEvent) => {
475477
e.stopPropagation();
476-
removeFromHistory(id);
478+
const item = mediaHistory.find((historyItem) => historyItem.id === id);
479+
if (item) {
480+
setPendingDeleteItem(item);
481+
}
477482
};
478483

479484
// Clear all history
@@ -1200,6 +1205,20 @@ export const MediaHistory = ({
12001205
setConfirmClearOpen(false);
12011206
}}
12021207
/>
1208+
<ConfirmDeleteDialog
1209+
open={!!pendingDeleteItem}
1210+
itemName={pendingDeleteItem?.name ?? ""}
1211+
onOpenChange={(open) => {
1212+
if (!open) {
1213+
setPendingDeleteItem(null);
1214+
}
1215+
}}
1216+
onConfirm={async () => {
1217+
if (!pendingDeleteItem) return;
1218+
await removeFromHistory(pendingDeleteItem.id);
1219+
setPendingDeleteItem(null);
1220+
}}
1221+
/>
12031222
</>
12041223
);
12051224
};
@@ -1427,6 +1446,40 @@ function ConfirmClearDialog({
14271446
);
14281447
}
14291448

1449+
function ConfirmDeleteDialog({
1450+
open,
1451+
itemName,
1452+
onOpenChange,
1453+
onConfirm,
1454+
}: {
1455+
open: boolean;
1456+
itemName: string;
1457+
onOpenChange: (v: boolean) => void;
1458+
onConfirm: () => void | Promise<void>;
1459+
}) {
1460+
const { t } = useTranslation();
1461+
return (
1462+
<Dialog open={open} onOpenChange={onOpenChange}>
1463+
<DialogContent>
1464+
<DialogHeader>
1465+
<DialogTitle>{t("history.deleteItemTitle")}</DialogTitle>
1466+
<DialogDescription>
1467+
{t("history.deleteItemDescription", { name: itemName })}
1468+
</DialogDescription>
1469+
</DialogHeader>
1470+
<DialogFooter>
1471+
<Button variant="outline" onClick={() => onOpenChange(false)}>
1472+
{t("common.cancel")}
1473+
</Button>
1474+
<Button className="bg-red-600 hover:bg-red-700" onClick={onConfirm}>
1475+
{t("history.removeFromHistory")}
1476+
</Button>
1477+
</DialogFooter>
1478+
</DialogContent>
1479+
</Dialog>
1480+
);
1481+
}
1482+
14301483
function RenameItemButton({ item }: { item: MediaHistoryItem }) {
14311484
const { renameHistoryItem } = usePlayerStore();
14321485
const [open, setOpen] = useState(false);

src/components/player/MediaPlayer.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,36 @@ export const MediaPlayer = ({ hiddenMode = false }: MediaPlayerProps) => {
118118
}
119119
}, [isPlaying, currentFile, setIsPlaying, safePlay]);
120120

121+
// Keep the global playback state aligned with actual media element state.
122+
// This is required for features like shadowing recording that react to store playback.
123+
useEffect(() => {
124+
const mediaElement = currentFile?.type.includes("video")
125+
? videoRef.current
126+
: audioRef.current;
127+
if (!mediaElement) return;
128+
129+
const handlePlay = () => {
130+
if (!usePlayerStore.getState().isPlaying) {
131+
setIsPlaying(true);
132+
}
133+
};
134+
135+
const handlePause = () => {
136+
if (isDelayingRef.current || mediaElement.ended) return;
137+
if (usePlayerStore.getState().isPlaying) {
138+
setIsPlaying(false);
139+
}
140+
};
141+
142+
mediaElement.addEventListener("play", handlePlay);
143+
mediaElement.addEventListener("pause", handlePause);
144+
145+
return () => {
146+
mediaElement.removeEventListener("play", handlePlay);
147+
mediaElement.removeEventListener("pause", handlePause);
148+
};
149+
}, [currentFile, setIsPlaying]);
150+
121151
// Handle volume changes
122152
useEffect(() => {
123153
const mediaElement = currentFile?.type.includes("video")

src/components/ui/ModelSelector.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
8787
const getProviderColor = (provider: AIProvider) => {
8888
switch (provider) {
8989
case "openai":
90-
return "text-green-600 bg-green-50";
90+
return "bg-green-50 text-green-600 dark:bg-green-950/40 dark:text-green-400";
9191
case "gemini":
92-
return "text-blue-600 bg-blue-50";
92+
return "bg-blue-50 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400";
9393
case "grok":
94-
return "text-purple-600 bg-purple-50";
94+
return "bg-purple-50 text-purple-600 dark:bg-purple-950/40 dark:text-purple-400";
9595
default:
96-
return "text-gray-600 bg-gray-50";
96+
return "bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-300";
9797
}
9898
};
9999

@@ -178,12 +178,12 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
178178
</Button>
179179

180180
{isOpen && (
181-
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-96 overflow-auto">
181+
<div className="absolute z-50 mt-1 max-h-96 w-full overflow-auto rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-900">
182182
{Object.entries(groupedModels).map(
183183
([providerKey, providerModels]) => (
184184
<div key={providerKey}>
185185
{showAllProviders && (
186-
<div className="px-3 py-2 text-xs font-semibold text-gray-500 bg-gray-50 border-b">
186+
<div className="border-b border-gray-200 bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
187187
<div className="flex items-center gap-2">
188188
{getProviderIcon(providerKey as AIProvider)}
189189
{providerKey.toUpperCase()}
@@ -198,15 +198,15 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
198198
setIsOpen(false);
199199
}}
200200
className={cn(
201-
"w-full px-3 py-2 text-left hover:bg-gray-50 border-b border-gray-100 last:border-b-0",
201+
"w-full border-b border-gray-100 px-3 py-2 text-left hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-800/80 last:border-b-0",
202202
selectedModel === model.id &&
203-
"bg-blue-50 border-blue-200",
203+
"border-blue-200 bg-blue-50 dark:border-blue-900/60 dark:bg-blue-950/30",
204204
compact && "py-1"
205205
)}
206206
>
207207
<div className="flex items-start justify-between gap-2">
208208
<div className="min-w-0 flex-1">
209-
<div className="flex items-center gap-2 mb-1">
209+
<div className="mb-1 flex items-center gap-2">
210210
{!showAllProviders && (
211211
<div
212212
className={cn(
@@ -221,11 +221,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
221221
{model.name}
222222
</span>
223223
{selectedModel === model.id && (
224-
<CheckCircle className="w-4 h-4 text-blue-600" />
224+
<CheckCircle className="w-4 h-4 text-blue-600 dark:text-blue-400" />
225225
)}
226226
</div>
227227
{!compact && (
228-
<p className="text-xs text-gray-600 mb-2">
228+
<p className="mb-2 text-xs text-gray-600 dark:text-gray-400">
229229
{model.description}
230230
</p>
231231
)}
@@ -236,14 +236,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
236236
.map((capability) => (
237237
<span
238238
key={capability}
239-
className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-gray-100 text-gray-700 text-xs rounded"
239+
className="inline-flex items-center gap-1 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300"
240240
>
241241
{getCapabilityIcon(capability)}
242242
{getCapabilityLabel(capability)}
243243
</span>
244244
))}
245245
{model.capabilities.length > 4 && (
246-
<span className="text-xs text-gray-500">
246+
<span className="text-xs text-gray-500 dark:text-gray-400">
247247
{t("modelSelector.moreCapabilities", {
248248
count: model.capabilities.length - 4,
249249
})}
@@ -252,7 +252,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
252252
</div>
253253
)}
254254
{showPricing && !compact && (
255-
<div className="flex items-center gap-4 text-xs text-gray-500">
255+
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
256256
<div className="flex items-center gap-1">
257257
<DollarSign className="w-3 h-3" />
258258
{t("modelSelector.inputPrice", {
@@ -281,7 +281,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
281281
)
282282
)}
283283
{models.length === 0 && (
284-
<div className="px-3 py-4 text-center text-gray-500">
284+
<div className="px-3 py-4 text-center text-gray-500 dark:text-gray-400">
285285
<AlertCircle className="w-6 h-6 mx-auto mb-2" />
286286
<p className="text-sm">{t("modelSelector.noModels")}</p>
287287
{!showAllProviders && !provider && (

0 commit comments

Comments
 (0)