Newer
Older
bugtrail / packages / extension / src / content / overlay / App.vue
<script setup lang="ts">
import { ref } from "vue";
import browser from "webextension-polyfill";
import type { ElementContext } from "@ltt/shared";
import type { OverlayState } from "./mount";
import PickerLayer from "./PickerLayer.vue";
import RegionSelector from "./RegionSelector.vue";
import NoteComposer from "./NoteComposer.vue";
import RecorderBar from "./RecorderBar.vue";

const props = defineProps<{ state: OverlayState }>();

const pickedElement = ref<ElementContext | null>(null);
/** screenshot chosen in the region selector, passed to the composer */
const screenshotDataUrl = ref<string | null>(null);
const composerOpen = ref(false);
/** brief confirmation after the report link was copied */
const linkCopied = ref(false);
let linkCopiedTimer: number | null = null;

function onPick(element: ElementContext) {
  pickedElement.value = element;
}

function onRegionConfirmed(dataUrl: string) {
  screenshotDataUrl.value = dataUrl;
  composerOpen.value = true;
}

async function onSubmitted(reportToken: string) {
  closeComposer();
  await copyReportLink(reportToken);
}

/**
 * Share links are copied automatically so they can go straight into the
 * bug tracker. The page is usually focused (the user just clicked around),
 * but a hidden textarea fallback covers the unfocused case.
 */
async function copyReportLink(reportToken: string) {
  try {
    const settings = (await browser.runtime.sendMessage({ type: "settings_get" })) as {
      ok: boolean;
      data?: { serverUrl: string; panelUrl: string | null };
    };
    const base = (settings?.data?.panelUrl ?? settings?.data?.serverUrl ?? "").replace(/\/+$/, "");
    const url = `${base}/r/${reportToken}`;
    try {
      await navigator.clipboard.writeText(url);
    } catch {
      const textarea = document.createElement("textarea");
      textarea.value = url;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.select();
      document.execCommand("copy");
      textarea.remove();
    }
    linkCopied.value = true;
    if (linkCopiedTimer) window.clearTimeout(linkCopiedTimer);
    linkCopiedTimer = window.setTimeout(() => (linkCopied.value = false), 2500);
  } catch (error) {
    console.error("[ltt] failed to copy report link:", error);
  }
}

function onRegionCancelled() {
  pickedElement.value = null;
  // keep the picker alive while recording so another element can be picked
  props.state.mode = props.state.recording ? "picker" : "idle";
}

function closeComposer() {
  composerOpen.value = false;
  screenshotDataUrl.value = null;
  pickedElement.value = null;
  // keep the picker alive while recording so another element can be picked
  props.state.mode = props.state.recording ? "picker" : "idle";
}

function cancelPicker() {
  props.state.mode = "idle";
}

async function stopRecorder() {
  const response = (await browser.runtime.sendMessage({ type: "recorder_stop" })) as {
    ok: boolean;
    error?: string;
  };
  if (!response?.ok) console.error("[ltt] recorder stop failed:", response?.error);
  props.state.recording = false;
  props.state.mode = "idle";
}
</script>

<template>
  <div class="ltt-overlay-root">
    <PickerLayer
      v-if="state.mode === 'picker' && !composerOpen"
      class="ltt-interactive"
      @pick="onPick"
      @cancel="cancelPicker"
    />
    <RegionSelector
      v-if="pickedElement && !composerOpen"
      class="ltt-interactive"
      @confirm="onRegionConfirmed"
      @cancel="onRegionCancelled"
    />
    <NoteComposer
      v-if="composerOpen && pickedElement"
      class="ltt-interactive"
      :element="pickedElement"
      :screenshot-data-url="screenshotDataUrl"
      @close="closeComposer"
      @submitted="onSubmitted"
    />
    <RecorderBar
      v-if="state.recording && !composerOpen"
      class="ltt-interactive"
      @note="state.mode = 'picker'"
      @stop="stopRecorder"
    />

    <div v-if="linkCopied" class="ltt-copied-toast">
      <i class="ph ph-check-circle" /> Link copied to clipboard
    </div>
  </div>
</template>

<style scoped>
.ltt-copied-toast {
  position: fixed;
  left: 50%;
  bottom: 32px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  background: var(--color-surface, #1f2335);
  border: 2px solid var(--color-success, #9ece6a);
  color: var(--color-text, #c0caf5);
  font-size: 13px;
  z-index: 2;
}
</style>