Skip to content

Commit 4786fe2

Browse files
authored
Merge pull request #8 from mxggle/feat/route
feat: persistent player, transcript UI overhaul, and storage fixes (v0.9.2)
2 parents f097d9e + 652e073 commit 4786fe2

14 files changed

Lines changed: 777 additions & 399 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.9.2] - 2026-03-15
11+
12+
### Added
13+
- **Persistent player**:
14+
- Persistent media player that survives page navigation, preserving playback state across routes
15+
- Virtualized transcript list for smooth rendering of long transcripts without performance degradation
16+
17+
### Changed
18+
- **Transcript controls**:
19+
- Merged transcript action buttons directly into the header for a cleaner, more accessible layout
20+
- Replaced individual export buttons with a single dropdown menu to reduce toolbar clutter
21+
- **Storage cleanup**:
22+
- Transcript records are now deleted atomically alongside media files using a shared `deleteMediaRecords` helper
23+
- `clearAllMediaFiles` now clears both the media store and transcript store in a single transaction
24+
- Cleanup routine removes orphaned transcript records when old media files are purged
25+
26+
### Fixed
27+
- **AI settings**:
28+
- AI provider and model selections now initialize correctly from localStorage on first render
29+
- **Storage**:
30+
- Removed obsolete v3 migration block that incorrectly wiped `mediaTranscripts` on store rehydration
31+
1032
## [0.9.1] - 2026-03-14
1133

1234
### Added

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"loop-station",
2020
"media-player"
2121
],
22-
"version": "0.9.1",
22+
"version": "0.9.2",
2323
"type": "module",
2424
"scripts": {
2525
"dev": "vite",
@@ -36,6 +36,7 @@
3636
"@radix-ui/react-toggle": "^1.1.9",
3737
"@radix-ui/themes": "^3.2.1",
3838
"@tanstack/react-query": "^5.76.2",
39+
"@tanstack/react-virtual": "^3.13.22",
3940
"@types/node": "^22.15.21",
4041
"@types/react-router-dom": "^5.3.3",
4142
"@vercel/analytics": "^1.5.0",

src/components/player/MediaPlayer.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,13 @@ export const MediaPlayer = ({ hiddenMode = false }: MediaPlayerProps) => {
111111
};
112112
}, [currentFile, setIsPlaying]);
113113

114-
// Pause playback when the component unmounts so the store stays in sync
114+
// Pause playback when the component unmounts only if media has been cleared.
115+
// During navigation (settings → player), currentFile stays set so we preserve playback.
115116
useEffect(() => {
116117
return () => {
117-
const { isPlaying: stillPlaying } = usePlayerStore.getState();
118-
if (stillPlaying) {
118+
const { isPlaying: stillPlaying, currentFile: fileAtUnmount } =
119+
usePlayerStore.getState();
120+
if (stillPlaying && !fileAtUnmount) {
119121
usePlayerStore.getState().setIsPlaying(false);
120122
}
121123
};

src/components/transcript/ExplanationDrawer.tsx

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,20 @@ import {
1010
normalizeModelId,
1111
} from "../../types/aiService";
1212
import { aiService } from "../../services/aiService";
13+
import {
14+
ExplanationResult,
15+
globalExplanationListeners,
16+
explanationCache,
17+
setGlobalExplanationState,
18+
getGlobalExplanationState,
19+
} from "./explanationState";
1320

1421
interface ExplanationDrawerProps {
1522
isOpen: boolean;
1623
onClose: () => void;
1724
text: string;
1825
}
1926

20-
interface ExplanationResult {
21-
explanation: string;
22-
usage?: {
23-
promptTokens: number;
24-
completionTokens: number;
25-
totalTokens: number;
26-
};
27-
model: string;
28-
provider: AIProvider;
29-
}
30-
31-
// Global explanation state management (shared with TranscriptSegment)
32-
interface ExplanationState {
33-
text: string;
34-
status: "idle" | "loading" | "completed" | "error";
35-
result?: ExplanationResult;
36-
error?: string;
37-
}
38-
39-
const globalExplanationStates = new Map<string, ExplanationState>();
40-
const globalExplanationListeners = new Set<() => void>();
41-
42-
const setGlobalExplanationState = (
43-
text: string,
44-
state: Partial<ExplanationState>
45-
) => {
46-
const existing = globalExplanationStates.get(text) || {
47-
text,
48-
status: "idle",
49-
};
50-
globalExplanationStates.set(text, { ...existing, ...state });
51-
globalExplanationListeners.forEach((listener) => listener());
52-
};
53-
54-
const getGlobalExplanationState = (text: string): ExplanationState => {
55-
return globalExplanationStates.get(text) || { text, status: "idle" };
56-
};
57-
58-
const explanationCache = new Map<string, ExplanationResult>();
59-
6027
export const ExplanationDrawer: React.FC<ExplanationDrawerProps> = ({
6128
isOpen,
6229
onClose,
@@ -67,9 +34,19 @@ export const ExplanationDrawer: React.FC<ExplanationDrawerProps> = ({
6734
const [isLoading, setIsLoading] = useState(false);
6835
const [error, setError] = useState<string | null>(null);
6936

70-
const [selectedProvider, setSelectedProvider] = useState<AIProvider>("openai");
71-
const [selectedModel, setSelectedModel] = useState("");
72-
const [targetLanguage, setTargetLanguage] = useState("English");
37+
const [selectedProvider, setSelectedProvider] = useState<AIProvider>(
38+
() => (localStorage.getItem("preferred_ai_provider") as AIProvider) || "openai"
39+
);
40+
const [targetLanguage, setTargetLanguage] = useState(
41+
() => localStorage.getItem("target_language") || "English"
42+
);
43+
const [selectedModel, setSelectedModel] = useState(() => {
44+
const provider = (localStorage.getItem("preferred_ai_provider") as AIProvider) || "openai";
45+
return normalizeModelId(
46+
provider,
47+
localStorage.getItem(`${provider}_model`) || DEFAULT_MODELS[provider]
48+
);
49+
});
7350

7451
// Handle ESC key
7552
useEffect(() => {

0 commit comments

Comments
 (0)