From b8849677de53859aa7f49d944984d7de06890e73 Mon Sep 17 00:00:00 2001
From: advplyr <advplyr@protonmail.com>
Date: Wed, 18 Jun 2025 17:20:36 -0500
Subject: [PATCH] Episode view modal makes timestamps in description clickable

---
 .../components/modals/podcast/ViewEpisode.vue | 40 +++++++++++++++++--
 1 file changed, 37 insertions(+), 3 deletions(-)

diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue
index 5a520ef4..b4358c5d 100644
--- a/client/components/modals/podcast/ViewEpisode.vue
+++ b/client/components/modals/podcast/ViewEpisode.vue
@@ -16,7 +16,7 @@
         </div>
       </div>
       <p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
-      <div v-if="description" dir="auto" class="default-style less-spacing" v-html="description" />
+      <div v-if="description" dir="auto" class="default-style less-spacing" @click="handleDescriptionClick" v-html="description" />
       <p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
 
       <div class="w-full h-px bg-white/5 my-4" />
@@ -68,7 +68,7 @@ export default {
       return this.episode.title || 'No Episode Title'
     },
     description() {
-      return this.episode.description || ''
+      return this.parseDescription(this.episode.description || '')
     },
     media() {
       return this.libraryItem?.media || {}
@@ -94,7 +94,41 @@ export default {
       return this.$store.getters['libraries/getBookCoverAspectRatio']
     }
   },
-  methods: {},
+  methods: {
+    handleDescriptionClick(e) {
+      if (e.target.matches('span.time-marker')) {
+        const time = parseInt(e.target.dataset.time)
+        if (!isNaN(time)) {
+          this.$eventBus.$emit('play-item', {
+            episodeId: this.episodeId,
+            libraryItemId: this.libraryItem.id,
+            startTime: time
+          })
+        }
+        e.preventDefault()
+      }
+    },
+    parseDescription(description) {
+      const timeMarkerLinkRegex = /<a href="#([^"]*?\b\d{1,2}:\d{1,2}(?::\d{1,2})?)">(.*?)<\/a>/g
+      const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
+
+      function convertToSeconds(time) {
+        const timeParts = time.split(':').map(Number)
+        return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
+      }
+
+      return description
+        .replace(timeMarkerLinkRegex, (match, href, displayTime) => {
+          const time = displayTime.match(timeMarkerRegex)[0]
+          const seekTimeInSeconds = convertToSeconds(time)
+          return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${displayTime}</span>`
+        })
+        .replace(timeMarkerRegex, (match) => {
+          const seekTimeInSeconds = convertToSeconds(match)
+          return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${match}</span>`
+        })
+    }
+  },
   mounted() {}
 }
 </script>