Skip to content

Commit 2b53ace

Browse files
committed
Merge branch 'master' into vite-migration-setup
Resolve conflicts by: - Keeping master's lazy initialization approach for TranscriptionService with getTranscriptionService() - Using optional transcriptionService property (?) for safety - Maintaining our branch's override keyword in onunload() - Using our branch's optional chaining (?.) for safer controller cleanup - Using our branch's getErrorMessage() helper for consistent error handling - Accepting master's new performance settings and feed caching features - Accepting master's accessibility improvements to UI components All changes from master's PR #124 (feed caching and UI accessibility) are now integrated.
2 parents b114631 + 28d3c2e commit 2b53ace

19 files changed

+576
-267
lines changed

src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,8 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = {
7070
template:
7171
"# {{title}}\n\nPodcast: {{podcast}}\nDate: {{date}}\n\n{{transcript}}",
7272
},
73+
feedCache: {
74+
enabled: true,
75+
ttlHours: 6,
76+
},
7377
};

src/main.ts

Lines changed: 141 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,88 @@
1+
import { ItemView, Notice, Plugin, TFile, WorkspaceLeaf } from "obsidian";
2+
import { get } from "svelte/store";
13
import {
4+
episodeCache,
25
currentEpisode,
36
downloadedEpisodes,
47
favorites,
58
localFiles,
69
playedEpisodes,
710
playlists,
11+
plugin,
812
queue,
913
savedFeeds,
10-
} from "src/store";
11-
import { Plugin, type WorkspaceLeaf } from "obsidian";
12-
import { API } from "src/API/API";
13-
import type { IAPI } from "src/API/IAPI";
14-
import { DEFAULT_SETTINGS, VIEW_TYPE } from "src/constants";
15-
import { PodNotesSettingsTab } from "src/ui/settings/PodNotesSettingsTab";
16-
import { MainView } from "src/ui/PodcastView";
17-
import type { IPodNotesSettings } from "./types/IPodNotesSettings";
18-
import { plugin } from "./store";
14+
} from "./store/index";
15+
import PodcastView from "./ui/PodcastView/PodcastView.svelte";
16+
import {
17+
DEFAULT_SETTINGS,
18+
type PodNotesSettings,
19+
} from "./settings/PodNotesSettings";
20+
import {
21+
downloadEpisode,
22+
type DownloadedEpisode,
23+
} from "./downloadEpisode";
24+
import { PodNotesSettingsTab } from "./ui/settings/PodNotesSettingsTab";
25+
import { API } from "./API";
26+
import type { Episode } from "./types/Episode";
1927
import type { IPodNotes } from "./types/IPodNotes";
20-
import { EpisodeStatusController } from "./store_controllers/EpisodeStatusController";
21-
import type { StoreController } from "./types/StoreController";
22-
import type { PlayedEpisode } from "./types/PlayedEpisode";
23-
import type { PodcastFeed } from "./types/PodcastFeed";
28+
import { getContextMenuHandler } from "./getContextMenuHandler";
29+
import { podNotesURIHandler } from "./podNotesURIHandler";
30+
import type { Playlist } from "./types/Playlists";
31+
import { StoreController } from "./types/StoreController";
2432
import { SavedFeedsController } from "./store_controllers/SavedFeedsController";
25-
import type { Playlist } from "./types/Playlist";
2633
import { PlaylistController } from "./store_controllers/PlaylistController";
2734
import { QueueController } from "./store_controllers/QueueController";
2835
import { FavoritesController } from "./store_controllers/FavoritesController";
29-
import type { Episode } from "./types/Episode";
30-
import CurrentEpisodeController from "./store_controllers/CurrentEpisodeController";
31-
import { TimestampTemplateEngine } from "./TemplateEngine";
32-
import createPodcastNote from "./createPodcastNote";
33-
import downloadEpisodeWithNotice from "./downloadEpisode";
34-
import type DownloadedEpisode from "./types/DownloadedEpisode";
35-
import DownloadedEpisodesController from "./store_controllers/DownloadedEpisodesController";
3636
import { LocalFilesController } from "./store_controllers/LocalFilesController";
37-
import type PartialAppExtension from "./global";
38-
import podNotesURIHandler from "./URIHandler";
39-
import getContextMenuHandler from "./getContextMenuHandler";
40-
import getUniversalPodcastLink from "./getUniversalPodcastLink";
41-
import type { IconType } from "./types/IconType";
37+
import { DownloadedEpisodesController } from "./store_controllers/DownloadedEpisodesController";
38+
import { CurrentEpisodeController } from "./store_controllers/CurrentEpisodeController";
39+
import { EpisodeStatusController } from "./store_controllers/EpisodeStatusController";
4240
import { TranscriptionService } from "./services/TranscriptionService";
41+
import type { IconType } from "./types/IconType";
4342

44-
export default class PodNotes extends Plugin implements IPodNotes {
45-
public api!: IAPI;
46-
public settings!: IPodNotesSettings;
47-
public override app!: PartialAppExtension;
43+
export const VIEW_TYPE = "pod-notes-view";
4844

49-
private view: MainView | null = null;
45+
class MainView extends ItemView {
46+
view: PodcastView;
47+
plugin: PodNotes;
5048

51-
private playedEpisodeController?: StoreController<{
52-
[episodeName: string]: PlayedEpisode;
53-
}>;
54-
private savedFeedsController?: StoreController<{
55-
[podcastName: string]: PodcastFeed;
56-
}>;
57-
private playlistController?: StoreController<{
58-
[playlistName: string]: Playlist;
59-
}>;
49+
constructor(leaf: WorkspaceLeaf, plugin: PodNotes) {
50+
super(leaf);
51+
this.plugin = plugin;
52+
}
53+
54+
override getViewType(): string {
55+
return VIEW_TYPE;
56+
}
57+
58+
override getDisplayText(): string {
59+
return "PodNotes";
60+
}
61+
62+
override async onOpen(): Promise<void> {
63+
this.view = new PodcastView({
64+
target: this.contentEl,
65+
props: {},
66+
});
67+
}
68+
}
69+
70+
export default class PodNotes extends Plugin implements IPodNotes {
71+
settings!: PodNotesSettings;
72+
view?: MainView;
73+
api!: API;
74+
75+
private playedEpisodeController?: StoreController<Episode[]>;
76+
private savedFeedsController?: StoreController<{ [podcast: string]: string }>;
77+
private playlistController?: StoreController<{ [key: string]: Playlist }>;
6078
private queueController?: StoreController<Playlist>;
6179
private favoritesController?: StoreController<Playlist>;
6280
private localFilesController?: StoreController<Playlist>;
6381
private currentEpisodeController?: StoreController<Episode>;
6482
private downloadedEpisodesController?: StoreController<{
6583
[podcastName: string]: DownloadedEpisode[];
6684
}>;
67-
private transcriptionService!: TranscriptionService;
85+
private transcriptionService?: TranscriptionService;
6886

6987
private maxLayoutReadyAttempts = 10;
7088
private layoutReadyAttempts = 0;
@@ -103,27 +121,25 @@ export default class PodNotes extends Plugin implements IPodNotes {
103121
this,
104122
).on();
105123

106-
this.transcriptionService = new TranscriptionService(this);
107-
108124
this.api = new API();
109125

110126
this.addCommand({
111127
id: "podnotes-show-leaf",
112128
name: "Show PodNotes",
113129
icon: "podcast" as IconType,
114-
checkCallback: function (this: PodNotes, checking: boolean) {
115-
if (checking) {
116-
return !this.app.workspace.getLeavesOfType(VIEW_TYPE).length;
117-
}
130+
checkCallback: function (this: PodNotes, checking: boolean) {
131+
if (checking) {
132+
return !this.app.workspace.getLeavesOfType(VIEW_TYPE).length;
133+
}
118134

119-
const leaf = this.app.workspace.getRightLeaf(false);
120-
if (leaf) {
121-
leaf.setViewState({
122-
type: VIEW_TYPE,
123-
});
124-
}
125-
}.bind(this),
126-
});
135+
const leaf = this.app.workspace.getRightLeaf(false);
136+
if (leaf) {
137+
leaf.setViewState({
138+
type: VIEW_TYPE,
139+
});
140+
}
141+
}.bind(this),
142+
});
127143

128144
this.addCommand({
129145
id: "start-playing",
@@ -140,137 +156,129 @@ export default class PodNotes extends Plugin implements IPodNotes {
140156

141157
this.addCommand({
142158
id: "stop-playing",
143-
name: "Stop Podcast",
159+
name: "Stop playing Podcast",
144160
icon: "stop-circle" as IconType,
145161
checkCallback: (checking) => {
146162
if (checking) {
147-
return this.api.isPlaying && !!this.api.podcast;
163+
return this.api.isPlaying;
148164
}
149165

150166
this.api.stop();
151167
},
152168
});
153169

154170
this.addCommand({
155-
id: "skip-backward",
156-
name: "Skip Backward",
157-
icon: "skip-back" as IconType,
158-
checkCallback: (checking) => {
159-
if (checking) {
160-
return this.api.isPlaying && !!this.api.podcast;
161-
}
171+
id: "podnotes-add-timestamp",
172+
name: "Add Timestamp",
173+
callback: () => this.api.addTimestamp(),
174+
});
162175

163-
this.api.skipBackward();
176+
this.addCommand({
177+
id: "podnotes-paste-timestamp-template",
178+
name: "Paste Timestamp Template",
179+
editorCallback: (editor) => {
180+
const cursor = editor.getCursor();
181+
editor.replaceRange("{{timestamp}}", cursor, cursor);
164182
},
165183
});
166184

167185
this.addCommand({
168-
id: "skip-forward",
169-
name: "Skip Forward",
170-
icon: "skip-forward" as IconType,
186+
id: "podnotes-playlist-clear",
187+
name: "Clear current playlist",
171188
checkCallback: (checking) => {
172189
if (checking) {
173-
return this.api.isPlaying && !!this.api.podcast;
190+
return this.api.playlist.length > 0;
174191
}
175192

176-
this.api.skipForward();
193+
this.api.clearPlaylist();
177194
},
178195
});
179196

180197
this.addCommand({
181-
id: "download-playing-episode",
182-
name: "Download Playing Episode",
183-
icon: "download" as IconType,
198+
id: "podnotes-playlist-shuffle",
199+
name: "Shuffle current playlist",
184200
checkCallback: (checking) => {
185201
if (checking) {
186-
return !!this.api.podcast;
202+
return this.api.playlist.length > 1;
187203
}
188204

189-
const episode = this.api.podcast;
190-
downloadEpisodeWithNotice(episode, this.settings.download.path);
191-
},
192-
});
193-
194-
this.addCommand({
195-
id: "hrpn",
196-
name: "Reload PodNotes",
197-
callback: () => {
198-
const id = this.manifest.id;
199-
200-
this.app.plugins
201-
.disablePlugin(id)
202-
.then(() => this.app.plugins.enablePlugin(id));
205+
this.api.shufflePlaylist();
203206
},
204207
});
205208

206209
this.addCommand({
207-
id: "capture-timestamp",
208-
name: "Capture Timestamp",
209-
icon: "clock" as IconType,
210-
editorCheckCallback: (checking, editor, view) => {
210+
id: "podnotes-playlist-save-local",
211+
name: "Save local playlist",
212+
checkCallback: (checking) => {
211213
if (checking) {
212-
return !!this.api.podcast && !!this.settings.timestamp.template;
214+
return this.api.playlist.length > 0;
213215
}
214216

215-
const cursorPos = editor.getCursor();
216-
const capture = TimestampTemplateEngine(
217-
this.settings.timestamp.template,
218-
);
219-
220-
editor.replaceRange(capture, cursorPos);
221-
editor.setCursor(cursorPos.line, cursorPos.ch + capture.length);
217+
this.api.saveLocalPlaylist();
222218
},
223219
});
224220

225221
this.addCommand({
226-
id: "create-podcast-note",
227-
name: "Create Podcast Note",
228-
icon: "file-plus" as IconType,
222+
id: "podnotes-playlist-save-favorites",
223+
name: "Save favorites playlist",
229224
checkCallback: (checking) => {
230225
if (checking) {
231-
return (
232-
!!this.api.podcast &&
233-
!!this.settings.note.path &&
234-
!!this.settings.note.template
235-
);
226+
return this.api.playlist.length > 0;
236227
}
237228

238-
createPodcastNote(this.api.podcast);
229+
this.api.saveFavoritesPlaylist();
239230
},
240231
});
241232

242233
this.addCommand({
243-
id: "get-share-link-episode",
244-
name: "Copy universal episode link to clipboard",
245-
icon: "share" as IconType,
234+
id: "podnotes-download-episode",
235+
name: "Download Episode",
246236
checkCallback: (checking) => {
237+
const episode = this.api.podcast;
247238
if (checking) {
248-
return !!this.api.podcast;
239+
return !!episode;
249240
}
250241

251-
getUniversalPodcastLink(this.api);
242+
if (!episode) {
243+
return;
244+
}
245+
246+
const episodeFileName = episode.episodeFilename || episode.title;
247+
const episodePath = `${this.settings.downloadPath}/${episode.podcastName}/${episodeFileName}.mp3`;
248+
249+
this.app.vault.adapter.exists(episodePath).then(async (exists) => {
250+
if (exists) {
251+
new Notice(
252+
`Episode ${episode.title} already exists. Skipping.`,
253+
);
254+
} else {
255+
new Notice(`Downloading episode ${episode.title}...`);
256+
const file = await downloadEpisode(episode, this);
257+
new Notice(
258+
`Episode ${episode.title} downloaded to ${file.path}`,
259+
);
260+
}
261+
});
252262
},
253263
});
254264

255265
this.addCommand({
256-
id: "podnotes-toggle-playback",
257-
name: "Toggle playback",
258-
icon: "play" as IconType,
266+
id: "podnotes-transcribe",
267+
name: "Transcribe current episode",
259268
checkCallback: (checking) => {
269+
const canTranscribe =
270+
!!this.api.podcast && !!this.settings.openAIApiKey?.trim();
271+
260272
if (checking) {
261-
return !!this.api.podcast;
273+
return canTranscribe;
262274
}
263275

264-
this.api.togglePlayback();
276+
if (canTranscribe) {
277+
void this.getTranscriptionService().transcribeCurrentEpisode();
278+
}
265279
},
266280
});
267281

268-
this.addCommand({
269-
id: "podnotes-transcribe",
270-
name: "Transcribe current episode",
271-
callback: () => this.transcriptionService.transcribeCurrentEpisode(),
272-
});
273-
274282
this.addSettingTab(new PodNotesSettingsTab(this.app, this));
275283

276284
this.registerView(VIEW_TYPE, (leaf: WorkspaceLeaf) => {
@@ -314,6 +322,14 @@ export default class PodNotes extends Plugin implements IPodNotes {
314322
}
315323
}
316324

325+
private getTranscriptionService(): TranscriptionService {
326+
if (!this.transcriptionService) {
327+
this.transcriptionService = new TranscriptionService(this);
328+
}
329+
330+
return this.transcriptionService;
331+
}
332+
317333
override onunload() {
318334
this.playedEpisodeController?.off();
319335
this.savedFeedsController?.off();

0 commit comments

Comments
 (0)