@@ -259,6 +302,8 @@
display: flex;
flex-direction: column;
height: 100%;
+ /* Enable GPU acceleration */
+ will-change: contents;
}
.go-back {
@@ -270,9 +315,18 @@
cursor: pointer;
margin-right: auto;
opacity: 0.75;
+ /* Remove default button styles */
+ background: none;
+ border: none;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ /* Optimize hover performance */
+ transition: opacity 0.15s ease;
+ will-change: opacity;
}
.go-back:hover {
opacity: 1;
}
-
+
\ No newline at end of file
diff --git a/src/ui/PodcastView/PodcastView_backup.svelte b/src/ui/PodcastView/PodcastView_backup.svelte
new file mode 100644
index 0000000..ab296f7
--- /dev/null
+++ b/src/ui/PodcastView/PodcastView_backup.svelte
@@ -0,0 +1,288 @@
+
+
+
+
+
+ {#if $viewState === ViewState.Player}
+
+ {:else if $viewState === ViewState.EpisodeList}
+
+
+ {#if selectedFeed}
+ {
+ selectedFeed = null;
+ displayedEpisodes = latestEpisodes;
+ viewState.set(ViewState.EpisodeList);
+ }}
+ >
+ Latest Episodes
+
+
+ {:else if selectedPlaylist}
+ {
+ selectedPlaylist = null;
+ displayedEpisodes = latestEpisodes;
+ viewState.set(ViewState.EpisodeList);
+ }}
+ >
+ Latest Episodes
+
+
+
+
+
+ {:else}
+
+ {/if}
+
+
+ {:else if $viewState === ViewState.PodcastGrid}
+
+ {/if}
+
+
+
diff --git a/src/ui/PodcastView/index.ts b/src/ui/PodcastView/index.ts
index 3d8a458..3cd7306 100644
--- a/src/ui/PodcastView/index.ts
+++ b/src/ui/PodcastView/index.ts
@@ -1,4 +1,4 @@
-import { IPodNotes } from "../../types/IPodNotes";
+import type { IPodNotes } from "../../types/IPodNotes";
import { ItemView, WorkspaceLeaf } from "obsidian";
import { VIEW_TYPE } from "../../constants";
import PodcastView from './PodcastView.svelte';
diff --git a/src/ui/PodcastView/spawnEpisodeContextMenu.ts b/src/ui/PodcastView/spawnEpisodeContextMenu.ts
index 8811789..4eff977 100644
--- a/src/ui/PodcastView/spawnEpisodeContextMenu.ts
+++ b/src/ui/PodcastView/spawnEpisodeContextMenu.ts
@@ -2,7 +2,7 @@ import { Menu, Notice } from "obsidian";
import createPodcastNote, { getPodcastNote, openPodcastNote } from "src/createPodcastNote";
import downloadEpisodeWithProgessNotice from "src/downloadEpisode";
import { currentEpisode, downloadedEpisodes, favorites, playedEpisodes, playlists, plugin, queue, viewState } from "src/store";
-import { Episode } from "src/types/Episode";
+import type { Episode } from "src/types/Episode";
import { ViewState } from "src/types/ViewState";
import { get } from "svelte/store";
@@ -16,12 +16,55 @@ interface DisabledMenuItems {
playlists: boolean;
}
+// Cache episode lookups to avoid repeated searches
+const episodeLookupCache = new Map
;
+}>();
+
+function getCacheKey(episode: Episode): string {
+ return `${episode.title}-${episode.podcastName}`;
+}
+
+function getEpisodeCachedState(episode: Episode) {
+ const cacheKey = getCacheKey(episode);
+ let cached = episodeLookupCache.get(cacheKey);
+
+ if (!cached) {
+ const playedEps = get(playedEpisodes);
+ const favs = get(favorites);
+ const q = get(queue);
+ const pls = get(playlists);
+
+ cached = {
+ isPlayed: Object.values(playedEps).some(e => e.title === episode.title && e.finished),
+ isFavorite: favs.episodes.some(e => e.title === episode.title),
+ isInQueue: q.episodes.some(e => e.title === episode.title),
+ playlists: new Set(
+ Object.entries(pls)
+ .filter(([_, playlist]) => playlist.episodes.some(e => e.title === episode.title))
+ .map(([name]) => name)
+ )
+ };
+
+ episodeLookupCache.set(cacheKey, cached);
+
+ // Clear cache after a short delay to handle rapid updates
+ setTimeout(() => episodeLookupCache.delete(cacheKey), 5000);
+ }
+
+ return cached;
+}
+
export default function spawnEpisodeContextMenu(
episode: Episode,
event: MouseEvent,
disabledMenuItems?: Partial
) {
const menu = new Menu();
+ const cachedState = getEpisodeCachedState(episode);
if (!disabledMenuItems?.play) {
menu.addItem(item => item
@@ -34,12 +77,12 @@ export default function spawnEpisodeContextMenu(
}
if (!disabledMenuItems?.markPlayed) {
- const episodeIsPlayed = Object.values(get(playedEpisodes)).find(e => (e.title === episode.title && e.finished));
menu.addItem(item => item
- .setIcon(episodeIsPlayed ? "cross" : "check")
- .setTitle(`Mark as ${episodeIsPlayed ? "Unplayed" : "Played"}`)
+ .setIcon(cachedState.isPlayed ? "cross" : "check")
+ .setTitle(`Mark as ${cachedState.isPlayed ? "Unplayed" : "Played"}`)
.onClick(() => {
- if (episodeIsPlayed) {
+ episodeLookupCache.delete(getCacheKey(episode)); // Invalidate cache
+ if (cachedState.isPlayed) {
playedEpisodes.markAsUnplayed(episode);
} else {
playedEpisodes.markAsPlayed(episode);
@@ -93,12 +136,12 @@ export default function spawnEpisodeContextMenu(
}
if (!disabledMenuItems?.favorite) {
- const episodeIsFavorite = get(favorites).episodes.find(e => e.title === episode.title);
menu.addItem(item => item
.setIcon("lucide-star")
- .setTitle(`${episodeIsFavorite ? "Remove from" : "Add to"} Favorites`)
+ .setTitle(`${cachedState.isFavorite ? "Remove from" : "Add to"} Favorites`)
.onClick(() => {
- if (episodeIsFavorite) {
+ episodeLookupCache.delete(getCacheKey(episode)); // Invalidate cache
+ if (cachedState.isFavorite) {
favorites.update(playlist => {
playlist.episodes = playlist.episodes.filter(e => e.title !== episode.title);
return playlist;
@@ -115,12 +158,12 @@ export default function spawnEpisodeContextMenu(
}
if (!disabledMenuItems?.queue) {
- const episodeIsInQueue = get(queue).episodes.find(e => e.title === episode.title);
menu.addItem(item => item
.setIcon("list-ordered")
- .setTitle(`${episodeIsInQueue ? "Remove from" : "Add to"} Queue`)
+ .setTitle(`${cachedState.isInQueue ? "Remove from" : "Add to"} Queue`)
.onClick(() => {
- if (episodeIsInQueue) {
+ episodeLookupCache.delete(getCacheKey(episode)); // Invalidate cache
+ if (cachedState.isInQueue) {
queue.update(playlist => {
playlist.episodes = playlist.episodes.filter(e => e.title !== episode.title);
@@ -142,12 +185,13 @@ export default function spawnEpisodeContextMenu(
const playlistsInStore = get(playlists);
for (const playlist of Object.values(playlistsInStore)) {
- const episodeIsInPlaylist = playlist.episodes.find(e => e.title === episode.title);
+ const episodeIsInPlaylist = cachedState.playlists.has(playlist.name);
menu.addItem(item => item
.setIcon(playlist.icon)
.setTitle(`${episodeIsInPlaylist ? "Remove from" : "Add to"} ${playlist.name}`)
.onClick(() => {
+ episodeLookupCache.delete(getCacheKey(episode)); // Invalidate cache
if (episodeIsInPlaylist) {
playlists.update(playlists => {
playlists[playlist.name].episodes = playlists[playlist.name].episodes.filter(e => e.title !== episode.title);
@@ -168,4 +212,4 @@ export default function spawnEpisodeContextMenu(
menu.showAtMouseEvent(event);
-}
+}
\ No newline at end of file
diff --git a/src/ui/common/Image.svelte b/src/ui/common/Image.svelte
index f0901a1..a67b027 100644
--- a/src/ui/common/Image.svelte
+++ b/src/ui/common/Image.svelte
@@ -4,49 +4,64 @@
export let src: string;
export let alt: string;
export let fadeIn: boolean = false;
- export let opacity: number = 0; // Falsey value so condition isn't triggered if not set.
+ export let opacity: number = 1;
export {_class as class};
let _class = "";
let loaded = false;
- let loading = true;
let failed = false;
const dispatcher = createEventDispatcher();
- function onClick(event: MouseEvent) {
+ function handleClick(event: MouseEvent) {
dispatcher("click", { event });
}
+
+ function handleLoad() {
+ loaded = true;
+ }
+
+ function handleError() {
+ failed = true;
+ dispatcher("error");
+ }
-{#if loading || loaded}
-
-
onClick(e)}
- draggable="false"
- {src}
- {alt}
+{#if !failed}
+
{loaded = true; loading = false;}}
- on:error={() => {failed = true; loading = false;}}
+ draggable="false"
+ style:opacity={fadeIn && !loaded ? 0 : opacity}
+ style:transition={fadeIn ? "opacity 0.2s ease-out" : "none"}
/>
-
-{:else if failed}
-
+{:else}
+
+ Failed to load image
+
{/if}
+
\ No newline at end of file
diff --git a/src/ui/common/Image_backup.svelte b/src/ui/common/Image_backup.svelte
new file mode 100644
index 0000000..f0901a1
--- /dev/null
+++ b/src/ui/common/Image_backup.svelte
@@ -0,0 +1,52 @@
+
+
+{#if loading || loaded}
+
+
onClick(e)}
+ draggable="false"
+ {src}
+ {alt}
+ class={_class}
+ style:opacity={opacity ? opacity : !fadeIn ? 1 : loaded ? 1 : 0}
+ style:transition={fadeIn ? "opacity 0.5s ease-out" : ""}
+ on:load={() => {loaded = true; loading = false;}}
+ on:error={() => {failed = true; loading = false;}}
+ />
+
+{:else if failed}
+
+{/if}
+
+
diff --git a/src/ui/common/Progressbar.svelte b/src/ui/common/Progressbar.svelte
index 9165fd3..484c0a8 100644
--- a/src/ui/common/Progressbar.svelte
+++ b/src/ui/common/Progressbar.svelte
@@ -1,5 +1,5 @@
-
{
- const { container } = render(Progressbar, { props: { value: 0, max: 100}});
-
- expect(container).toBeVisible();
-});
\ No newline at end of file
diff --git a/src/ui/obsidian/Button.svelte b/src/ui/obsidian/Button.svelte
index 68dbaf1..ec127c4 100644
--- a/src/ui/obsidian/Button.svelte
+++ b/src/ui/obsidian/Button.svelte
@@ -1,10 +1,11 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/ui/obsidian/Dropdown.svelte b/src/ui/obsidian/Dropdown.svelte
index 642cc49..3a678c3 100644
--- a/src/ui/obsidian/Dropdown.svelte
+++ b/src/ui/obsidian/Dropdown.svelte
@@ -1,41 +1,49 @@
-
+
diff --git a/src/ui/obsidian/Icon.svelte b/src/ui/obsidian/Icon.svelte
index 645a001..367ee64 100644
--- a/src/ui/obsidian/Icon.svelte
+++ b/src/ui/obsidian/Icon.svelte
@@ -1,9 +1,9 @@
-
+
\ No newline at end of file
diff --git a/src/ui/settings/PlaylistItem.svelte b/src/ui/settings/PlaylistItem.svelte
index 09d1bb4..ffd0668 100644
--- a/src/ui/settings/PlaylistItem.svelte
+++ b/src/ui/settings/PlaylistItem.svelte
@@ -1,5 +1,5 @@
{#each playlistArr as playlist}
@@ -85,9 +97,9 @@
-
+
-
+
diff --git a/src/ui/settings/PodNotesSettingsTab.ts b/src/ui/settings/PodNotesSettingsTab.ts
index f76631d..242800b 100644
--- a/src/ui/settings/PodNotesSettingsTab.ts
+++ b/src/ui/settings/PodNotesSettingsTab.ts
@@ -17,6 +17,7 @@ import {
import { FilePathTemplateEngine } from "../../TemplateEngine";
import { episodeCache, savedFeeds } from "src/store/index";
import type { Episode } from "src/types/Episode";
+import type { TimestampRange } from "src/types/TimestampRange";
import { get } from "svelte/store";
import { exportOPML, importOPML } from "src/opml";
@@ -149,7 +150,9 @@ export class PodNotesSettingsTab extends PluginSettingTab {
const updateTimestampDemo = (value: string) => {
if (!this.plugin.api.podcast) return;
- const demoVal = TimestampTemplateEngine(value);
+ // Create a dummy timestamp range for the demo
+ const dummyRange = { start: 60, end: 90 }; // 1 minute to 1:30
+ const demoVal = TimestampTemplateEngine(value, dummyRange);
timestampFormatDemoEl.empty();
MarkdownRenderer.renderMarkdown(
demoVal,
@@ -402,6 +405,33 @@ export class PodNotesSettingsTab extends PluginSettingTab {
transcriptTemplateSetting.settingEl.style.flexDirection = "column";
transcriptTemplateSetting.settingEl.style.alignItems = "unset";
transcriptTemplateSetting.settingEl.style.gap = "10px";
+
+ // Add timestamp settings
+ new Setting(container)
+ .setName("Include timestamps in transcripts")
+ .setDesc("When enabled, transcripts will include timestamps linking to specific points in the episode.")
+ .addToggle((toggle) => {
+ toggle
+ .setValue(this.plugin.settings.transcript.includeTimestamps)
+ .onChange(async (value) => {
+ this.plugin.settings.transcript.includeTimestamps = value;
+ await this.plugin.saveSettings();
+ });
+ });
+
+ new Setting(container)
+ .setName("Timestamp range (seconds)")
+ .setDesc("The minimum time gap between timestamps in the transcript. Lower values create more timestamps.")
+ .addSlider((slider) => {
+ slider
+ .setLimits(1, 10, 1)
+ .setValue(this.plugin.settings.transcript.timestampRange)
+ .setDynamicTooltip()
+ .onChange(async (value) => {
+ this.plugin.settings.transcript.timestampRange = value;
+ await this.plugin.saveSettings();
+ });
+ });
}
}
@@ -429,4 +459,4 @@ function getRandomEpisode(): Episode {
randomFeed[Math.floor(Math.random() * randomFeed.length)];
return randomEpisode;
-}
+}
\ No newline at end of file
diff --git a/src/ui/settings/PodcastQueryGrid.svelte b/src/ui/settings/PodcastQueryGrid.svelte
index 9200ddf..e8dd182 100644
--- a/src/ui/settings/PodcastQueryGrid.svelte
+++ b/src/ui/settings/PodcastQueryGrid.svelte
@@ -1,93 +1,101 @@
{/each}
diff --git a/src/ui/settings/PodcastResultCard.svelte b/src/ui/settings/PodcastResultCard.svelte
index b2b2b36..caede31 100644
--- a/src/ui/settings/PodcastResultCard.svelte
+++ b/src/ui/settings/PodcastResultCard.svelte
@@ -1,13 +1,14 @@
@@ -25,14 +26,14 @@
{#if isSaved}
dispatch("removePodcast", { podcast })}
+ tooltip={`Remove ${podcast.title} podcast`}
+ onclick={() => onremovePodcast?.(podcast)}
/>
{:else}
dispatch("addPodcast", { podcast })}
+ tooltip={`Add ${podcast.title} podcast`}
+ onclick={() => onaddPodcast?.(podcast)}
/>
{/if}
@@ -47,8 +48,11 @@
border-radius: 8px;
background-color: var(--background-secondary);
max-width: 100%;
- transition: all 0.3s ease;
+ /* Optimize transitions - only transition what changes */
+ transition: border-color 0.15s ease, background-color 0.15s ease;
position: relative;
+ /* Force hardware acceleration */
+ transform: translateZ(0);
}
.podcast-artwork-container {
@@ -71,8 +75,8 @@
}
.podcast-result-card:hover {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- transform: translateY(-2px);
+ border-color: var(--interactive-hover);
+ background-color: var(--background-secondary-alt);
}
.podcast-info {
diff --git a/src/utility/extractStylesFromObj.ts b/src/utility/extractStylesFromObj.ts
index 4f0e22f..462bf82 100644
--- a/src/utility/extractStylesFromObj.ts
+++ b/src/utility/extractStylesFromObj.ts
@@ -1,4 +1,4 @@
-import { CSSObject } from "src/types/CSSObject";
+import type { CSSObject } from "src/types/CSSObject";
export default function extractStylesFromObj(obj: CSSObject): string {
return Object.entries(obj)
diff --git a/src/utility/findPlayedEpisodes.ts b/src/utility/findPlayedEpisodes.ts
index a6a202d..7c24fe5 100644
--- a/src/utility/findPlayedEpisodes.ts
+++ b/src/utility/findPlayedEpisodes.ts
@@ -1,7 +1,7 @@
import FeedParser from "src/parser/feedParser";
-import { Episode } from "src/types/Episode";
-import { PlayedEpisode } from "src/types/PlayedEpisode"
-import { PodcastFeed } from "src/types/PodcastFeed";
+import type { Episode } from "src/types/Episode";
+import type { PlayedEpisode } from "src/types/PlayedEpisode"
+import type { PodcastFeed } from "src/types/PodcastFeed";
export default async function findPlayedEpisodesInFeeds(
playedEpisodes: PlayedEpisode[],
diff --git a/src/utility/isLocalFile.ts b/src/utility/isLocalFile.ts
index 0d2f204..ced633a 100644
--- a/src/utility/isLocalFile.ts
+++ b/src/utility/isLocalFile.ts
@@ -1,5 +1,5 @@
-import { LocalEpisode } from "src/types/LocalEpisode";
-import { Episode } from "src/types/Episode";
+import type { LocalEpisode } from "src/types/LocalEpisode";
+import type { Episode } from "src/types/Episode";
export function isLocalFile(ep: Episode): ep is LocalEpisode {
return ep.podcastName === "local file";
diff --git a/src/utility/searchEpisodes.ts b/src/utility/searchEpisodes.ts
index 9fb135c..d6c537b 100644
--- a/src/utility/searchEpisodes.ts
+++ b/src/utility/searchEpisodes.ts
@@ -1,5 +1,5 @@
import Fuse from "fuse.js";
-import { Episode } from "src/types/Episode";
+import type { Episode } from "src/types/Episode";
export default function searchEpisodes(query: string, episodes: Episode[]): Episode[] {
if (episodes.length === 0) return [];
diff --git a/svelte.config.js b/svelte.config.js
new file mode 100644
index 0000000..e972021
--- /dev/null
+++ b/svelte.config.js
@@ -0,0 +1,24 @@
+const sveltePreprocess = require('svelte-preprocess');
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Use sveltePreprocess for handling TypeScript
+ preprocess: sveltePreprocess({
+ typescript: true,
+ }),
+
+ compilerOptions: {
+ // CSS is injected by esbuild
+ css: 'injected',
+ // Enable all warnings in development
+ dev: process.env.NODE_ENV !== 'production',
+ // Disable runes mode for gradual migration
+ runes: false,
+ // Enable compatibility mode for Svelte 5
+ compatibility: {
+ componentApi: 4
+ }
+ }
+};
+
+module.exports = config;
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 6483100..8a49192 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,7 @@
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
+ "verbatimModuleSyntax": true,
"types": ["svelte", "node"],
"strictNullChecks": true,
"lib": [
diff --git a/vitest.config.ts b/vitest.config.ts
index 6b5a1d1..b136334 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -20,4 +20,4 @@ export default defineConfig({
globals: true,
environment: "jsdom",
},
-});
+});
\ No newline at end of file