Browse Source

feat: add support to audio and video API responses (#3044)

Liyas Thomas 1 year ago
parent
commit
ddaec1b9ac

+ 2 - 0
packages/hoppscotch-common/locales/en.json

@@ -432,6 +432,7 @@
     "view_my_links": "View my links"
   },
   "response": {
+    "audio": "Audio",
     "body": "Response Body",
     "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
     "headers": "Headers",
@@ -445,6 +446,7 @@
     "status": "Status",
     "time": "Time",
     "title": "Response",
+    "video": "Video",
     "waiting_for_connection": "waiting for connection",
     "xml": "XML"
   },

+ 79 - 0
packages/hoppscotch-common/src/components/lenses/renderers/AudioLensRenderer.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="flex flex-col flex-1">
+    <div
+      class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
+    >
+      <label class="font-semibold truncate text-secondaryLight">
+        {{ t("response.body") }}
+      </label>
+      <div class="flex">
+        <HoppButtonSecondary
+          v-if="response.body"
+          v-tippy="{ theme: 'tooltip', allowHTML: true }"
+          :title="`${t(
+            'action.download_file'
+          )} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
+          :icon="downloadIcon"
+          @click="downloadResponse"
+        />
+      </div>
+    </div>
+    <div class="flex flex-1 items-center justify-center overflow-auto">
+      <audio controls :src="audiosrc"></audio>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue"
+import { useI18n } from "@composables/i18n"
+import { useDownloadResponse } from "@composables/lens-actions"
+import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
+import { defineActionHandler } from "~/helpers/actions"
+import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
+import { flow, pipe } from "fp-ts/function"
+import * as S from "fp-ts/string"
+import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
+import * as A from "fp-ts/Array"
+import * as O from "fp-ts/Option"
+import { objFieldMatches } from "~/helpers/functional/object"
+
+const t = useI18n()
+
+const props = defineProps<{
+  response: HoppRESTResponse & {
+    type: "success" | "fail"
+  }
+}>()
+
+const audiosrc = computed(() =>
+  URL.createObjectURL(
+    new Blob([props.response.body], {
+      type: "audio/mp3",
+    })
+  )
+)
+
+const responseType = computed(() =>
+  pipe(
+    props.response,
+    O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
+    O.chain(
+      // Try getting content-type
+      flow(
+        (res) => res.headers,
+        A.findFirst((h) => h.key.toLowerCase() === "content-type"),
+        O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
+      )
+    ),
+    O.getOrElse(() => "text/plain")
+  )
+)
+
+const { downloadIcon, downloadResponse } = useDownloadResponse(
+  responseType.value,
+  computed(() => props.response.body)
+)
+
+defineActionHandler("response.file.download", () => downloadResponse())
+</script>

+ 79 - 0
packages/hoppscotch-common/src/components/lenses/renderers/VideoLensRenderer.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="flex flex-col flex-1">
+    <div
+      class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
+    >
+      <label class="font-semibold truncate text-secondaryLight">
+        {{ t("response.body") }}
+      </label>
+      <div class="flex">
+        <HoppButtonSecondary
+          v-if="response.body"
+          v-tippy="{ theme: 'tooltip', allowHTML: true }"
+          :title="`${t(
+            'action.download_file'
+          )} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
+          :icon="downloadIcon"
+          @click="downloadResponse"
+        />
+      </div>
+    </div>
+    <div class="flex flex-1 items-center justify-center overflow-auto">
+      <video controls :src="videosrc"></video>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue"
+import { useI18n } from "@composables/i18n"
+import { useDownloadResponse } from "@composables/lens-actions"
+import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
+import { defineActionHandler } from "~/helpers/actions"
+import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
+import { flow, pipe } from "fp-ts/function"
+import * as S from "fp-ts/string"
+import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
+import * as A from "fp-ts/Array"
+import * as O from "fp-ts/Option"
+import { objFieldMatches } from "~/helpers/functional/object"
+
+const t = useI18n()
+
+const props = defineProps<{
+  response: HoppRESTResponse & {
+    type: "success" | "fail"
+  }
+}>()
+
+const videosrc = computed(() =>
+  URL.createObjectURL(
+    new Blob([props.response.body], {
+      type: "video/mp4",
+    })
+  )
+)
+
+const responseType = computed(() =>
+  pipe(
+    props.response,
+    O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
+    O.chain(
+      // Try getting content-type
+      flow(
+        (res) => res.headers,
+        A.findFirst((h) => h.key.toLowerCase() === "content-type"),
+        O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
+      )
+    ),
+    O.getOrElse(() => "text/plain")
+  )
+)
+
+const { downloadIcon, downloadResponse } = useDownloadResponse(
+  responseType.value,
+  computed(() => props.response.body)
+)
+
+defineActionHandler("response.file.download", () => downloadResponse())
+</script>

+ 16 - 0
packages/hoppscotch-common/src/helpers/lenses/audioLens.ts

@@ -0,0 +1,16 @@
+import { defineAsyncComponent } from "vue"
+import { Lens } from "./lenses"
+
+const audioLens: Lens = {
+  lensName: "response.audio",
+  isSupportedContentType: (contentType) =>
+    /\baudio\/(?:wav|mpeg|mp4|aac|aacp|ogg|webm|x-caf|flac|mp3|)\b/i.test(
+      contentType
+    ),
+  renderer: "audiores",
+  rendererImport: defineAsyncComponent(
+    () => import("~/components/lenses/renderers/AudioLensRenderer.vue")
+  ),
+}
+
+export default audioLens

+ 4 - 0
packages/hoppscotch-common/src/helpers/lenses/lenses.ts

@@ -5,6 +5,8 @@ import imageLens from "./imageLens"
 import htmlLens from "./htmlLens"
 import xmlLens from "./xmlLens"
 import pdfLens from "./pdfLens"
+import audioLens from "./audioLens"
+import videoLens from "./videoLens"
 import { defineAsyncComponent } from "vue"
 
 export type Lens = {
@@ -20,6 +22,8 @@ export const lenses: Lens[] = [
   htmlLens,
   xmlLens,
   pdfLens,
+  audioLens,
+  videoLens,
   rawLens,
 ]
 

+ 16 - 0
packages/hoppscotch-common/src/helpers/lenses/videoLens.ts

@@ -0,0 +1,16 @@
+import { defineAsyncComponent } from "vue"
+import { Lens } from "./lenses"
+
+const videoLens: Lens = {
+  lensName: "response.video",
+  isSupportedContentType: (contentType) =>
+    /\bvideo\/(?:webm|x-m4v|quicktime|x-ms-wmv|x-flv|mpeg|x-msvideo|x-ms-asf|mp4|)\b/i.test(
+      contentType
+    ),
+  renderer: "videores",
+  rendererImport: defineAsyncComponent(
+    () => import("~/components/lenses/renderers/VideoLensRenderer.vue")
+  ),
+}
+
+export default videoLens