<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";
import RecordingComposer from "./RecordingComposer.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);
}
/** recording composer: Save → patch the report, copy the share link; Discard → delete it */
async function onRecordingSaved(reportToken: string) {
props.state.pending = false;
props.state.pendingToken = null;
if (reportToken) await copyReportLink(reportToken);
}
function onRecordingDiscarded() {
props.state.pending = false;
props.state.pendingToken = null;
}
/**
* 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"
/>
<RecordingComposer
v-if="state.pending && !composerOpen"
class="ltt-interactive"
:report-token="state.pendingToken"
@saved="onRecordingSaved"
@discarded="onRecordingDiscarded"
/>
<div v-if="linkCopied" class="ltt-copied-toast">
<i class="ph ph-check-circle" /> Link copied to clipboard
</div>
<div v-if="state.flash" class="ltt-flash-toast">
<i class="ph ph-play-circle" /> {{ state.flash }}
</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;
}
.ltt-flash-toast {
position: fixed;
left: 50%;
bottom: 76px;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--color-surface, #1f2335);
border: 2px solid var(--color-accent, #7aa2f7);
color: var(--color-text, #c0caf5);
font-size: 13px;
z-index: 2;
}
</style>