spot: refactor popup code, split audio logic from ui code
This commit is contained in:
parent
a13f427816
commit
a009ff928c
8 changed files with 428 additions and 300 deletions
|
|
@ -1,165 +1,49 @@
|
|||
import orLogo from "~/assets/orSpot.svg";
|
||||
import micOff from "~/assets/mic-off-red.svg";
|
||||
import micOn from "~/assets/mic-on-dark.svg";
|
||||
import { createEffect, onMount } from "solid-js";
|
||||
import Login from "~/entrypoints/popup/Login";
|
||||
import Settings from "~/entrypoints/popup/Settings";
|
||||
import { createSignal, createEffect, onMount } from "solid-js";
|
||||
import Dropdown from "~/entrypoints/popup/Dropdown";
|
||||
import Button from "~/entrypoints/popup/Button";
|
||||
import {
|
||||
ChevronSvg,
|
||||
RecordDesktopSvg,
|
||||
RecordTabSvg,
|
||||
HomePageSvg,
|
||||
SlackSvg,
|
||||
SettingsSvg,
|
||||
} from "./Icons";
|
||||
|
||||
async function getAudioDevices() {
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevices = devices
|
||||
.filter((device) => device.kind === "audioinput")
|
||||
.map((device) => ({ label: device.label, id: device.deviceId }));
|
||||
|
||||
return { granted: true, audioDevices };
|
||||
} catch (error) {
|
||||
console.error("Error accessing audio devices:", error);
|
||||
const msg = error.message ?? "";
|
||||
return {
|
||||
granted: false,
|
||||
denied: msg.includes("denied"),
|
||||
audioDevices: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const orSite = () => {
|
||||
window.open("https://openreplay.com", "_blank");
|
||||
};
|
||||
|
||||
function Header({ openSettings }: { openSettings: () => void }) {
|
||||
const openHomePage = async () => {
|
||||
const { settings } = await chrome.storage.local.get("settings");
|
||||
return window.open(`${settings.ingestPoint}/spots`, "_blank");
|
||||
};
|
||||
return (
|
||||
<div class={"flex items-center gap-1"}>
|
||||
<div
|
||||
class="flex items-center gap-1 cursor-pointer hover:opacity-50"
|
||||
onClick={orSite}
|
||||
>
|
||||
<img src={orLogo} class="w-5" alt={"OpenReplay Spot"} />
|
||||
<div class={"text-neutral-600"}>
|
||||
<span class={"text-lg font-semibold text-black"}>OpenReplay Spot</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={"ml-auto flex items-center gap-2"}>
|
||||
<div class="text-sm tooltip tooltip-bottom" data-tip="My Spots">
|
||||
<div onClick={openHomePage}>
|
||||
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
|
||||
<HomePageSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Get help on Slack"
|
||||
>
|
||||
<a
|
||||
href={
|
||||
"https://join.slack.com/t/openreplay/shared_invite/zt-2brqlwcis-k7OtqHkW53EAoTRqPjCmyg"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
|
||||
<SlackSvg />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Settings"
|
||||
onClick={openSettings}
|
||||
>
|
||||
<div class={"cursor-pointer p-2 hover:bg-indigo-50 rounded-full"}>
|
||||
<SettingsSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const STATE = {
|
||||
empty: "empty",
|
||||
login: "login",
|
||||
ready: "ready",
|
||||
starting: "starting",
|
||||
recording: "recording",
|
||||
};
|
||||
import Header from "./components/Header";
|
||||
import RecordingControls from "./components/RecordingControls";
|
||||
import AudioPicker from "./components/AudioPicker";
|
||||
import { useAppState } from "./hooks/useAppState";
|
||||
import { useAudioDevices } from "./hooks/useAudioDevices";
|
||||
import { AppState } from "./types";
|
||||
|
||||
function App() {
|
||||
const [state, setState] = createSignal(STATE.empty);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = createSignal(false);
|
||||
const [mic, setMic] = createSignal(false);
|
||||
const [selectedAudioDevice, setSelectedAudioDevice] = createSignal("");
|
||||
const [hasPermissions, setHasPermissions] = createSignal(false);
|
||||
const {
|
||||
state,
|
||||
isSettingsOpen,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
openSettings,
|
||||
closeSettings,
|
||||
} = useAppState();
|
||||
|
||||
const {
|
||||
audioDevices,
|
||||
selectedAudioDevice,
|
||||
mic,
|
||||
hasPermissions,
|
||||
isChecking,
|
||||
checkAudioDevices,
|
||||
handleMicToggle,
|
||||
selectAudioDevice,
|
||||
} = useAudioDevices();
|
||||
|
||||
// Listen for mic status updates from background
|
||||
onMount(() => {
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "popup:no-login") {
|
||||
setState(STATE.login);
|
||||
}
|
||||
if (message.type === "popup:login") {
|
||||
setState(STATE.ready);
|
||||
}
|
||||
if (message.type === "popup:stopped") {
|
||||
setState(STATE.ready);
|
||||
}
|
||||
if (message.type === "popup:started") {
|
||||
setState(STATE.recording);
|
||||
}
|
||||
if (message.type === "popup:mic-status") {
|
||||
setMic(message.status);
|
||||
}
|
||||
});
|
||||
void browser.runtime.sendMessage({ type: "popup:check-status" });
|
||||
});
|
||||
|
||||
const startRecording = async (reqTab: "tab" | "desktop") => {
|
||||
setState(STATE.starting);
|
||||
await browser.runtime.sendMessage({
|
||||
type: "popup:start",
|
||||
area: reqTab,
|
||||
mic: mic(),
|
||||
audioId: selectedAudioDevice(),
|
||||
permissions: hasPermissions(),
|
||||
});
|
||||
window.close();
|
||||
const handleStartRecording = (area: "tab" | "desktop") => {
|
||||
startRecording(area, mic(), selectedAudioDevice(), hasPermissions());
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
void browser.runtime.sendMessage({
|
||||
type: "popup:stop",
|
||||
mic: mic(),
|
||||
audioId: selectedAudioDevice(),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleMic = async () => {
|
||||
setMic(!mic());
|
||||
};
|
||||
|
||||
const openSettings = () => {
|
||||
setIsSettingsOpen(true);
|
||||
};
|
||||
const closeSettings = () => {
|
||||
setIsSettingsOpen(false);
|
||||
const handleStopRecording = () => {
|
||||
stopRecording(mic(), selectedAudioDevice());
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -167,58 +51,30 @@ function App() {
|
|||
{isSettingsOpen() ? (
|
||||
<Settings goBack={closeSettings} />
|
||||
) : (
|
||||
<div class={"flex flex-col gap-4 p-5"}>
|
||||
<div class="flex flex-col gap-4 p-5">
|
||||
<Header openSettings={openSettings} />
|
||||
|
||||
{state() === STATE.login ? (
|
||||
{state() === AppState.LOGIN ? (
|
||||
<Login />
|
||||
) : (
|
||||
<>
|
||||
{state() === STATE.recording ? (
|
||||
<Button
|
||||
name={"End Recording"}
|
||||
onClick={() => stopRecording()}
|
||||
/>
|
||||
) : null}
|
||||
{state() === STATE.starting ? (
|
||||
<div
|
||||
class={
|
||||
"flex flex-row items-center gap-2 w-full justify-center"
|
||||
}
|
||||
>
|
||||
<div class="py-4">Your recording is starting</div>
|
||||
</div>
|
||||
) : null}
|
||||
{state() === STATE.ready ? (
|
||||
<>
|
||||
<div class="flex flex-row items-center gap-2 w-full justify-center">
|
||||
<button
|
||||
class="btn bg-indigo-100 text-base hover:bg-primary hover:text-white w-6/12"
|
||||
name="Record Tab"
|
||||
onClick={() => startRecording("tab")}
|
||||
>
|
||||
<RecordTabSvg />
|
||||
Record Tab
|
||||
</button>
|
||||
<RecordingControls
|
||||
state={state()}
|
||||
startRecording={handleStartRecording}
|
||||
stopRecording={handleStopRecording}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn bg-teal-50 text-base hover:bg-primary hover:text-white"
|
||||
name={"Record Desktop"}
|
||||
onClick={() => startRecording("desktop")}
|
||||
>
|
||||
<RecordDesktopSvg />
|
||||
Record Desktop
|
||||
</button>
|
||||
</div>
|
||||
<AudioPicker
|
||||
mic={mic}
|
||||
toggleMic={toggleMic}
|
||||
selectedAudioDevice={selectedAudioDevice}
|
||||
setSelectedAudioDevice={setSelectedAudioDevice}
|
||||
setHasPermissions={setHasPermissions}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{state() === AppState.READY && (
|
||||
<AudioPicker
|
||||
mic={mic}
|
||||
audioDevices={audioDevices}
|
||||
selectedAudioDevice={selectedAudioDevice}
|
||||
isChecking={isChecking}
|
||||
onMicToggle={handleMicToggle}
|
||||
onCheckAudio={checkAudioDevices}
|
||||
onSelectDevice={selectAudioDevice}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -227,111 +83,4 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
interface IAudioPicker {
|
||||
mic: () => boolean;
|
||||
toggleMic: () => void;
|
||||
selectedAudioDevice: () => string;
|
||||
setSelectedAudioDevice: (value: string) => void;
|
||||
setHasPermissions: (value: boolean) => void;
|
||||
}
|
||||
function AudioPicker(props: IAudioPicker) {
|
||||
const [audioDevices, setAudioDevices] = createSignal(
|
||||
[] as { label: string; id: string }[],
|
||||
);
|
||||
const [checkedAudioDevices, setCheckedAudioDevices] = createSignal(0);
|
||||
|
||||
createEffect(() => {
|
||||
chrome.storage.local.get("audioPerm", (data) => {
|
||||
if (data.audioPerm && audioDevices().length === 0) {
|
||||
props.setHasPermissions(true);
|
||||
void checkAudioDevices();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const checkAudioDevices = async () => {
|
||||
const { granted, audioDevices, denied } = await getAudioDevices();
|
||||
if (!granted && !denied) {
|
||||
void browser.runtime.sendMessage({
|
||||
type: "popup:get-audio-perm",
|
||||
});
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "popup:audio-perm") {
|
||||
void checkAudioDevices();
|
||||
}
|
||||
});
|
||||
} else if (audioDevices.length > 0) {
|
||||
chrome.storage.local.set({ audioPerm: granted });
|
||||
setAudioDevices(audioDevices);
|
||||
props.setSelectedAudioDevice(audioDevices[0]?.id || "");
|
||||
}
|
||||
};
|
||||
|
||||
const checkAudio = async () => {
|
||||
if (checkedAudioDevices() > 0) {
|
||||
return;
|
||||
}
|
||||
setCheckedAudioDevices(1);
|
||||
await checkAudioDevices();
|
||||
setCheckedAudioDevices(2);
|
||||
};
|
||||
const onSelect = (value) => {
|
||||
props.setSelectedAudioDevice(value);
|
||||
if (!props.mic()) {
|
||||
props.toggleMic();
|
||||
}
|
||||
};
|
||||
|
||||
const onMicToggle = async () => {
|
||||
if (!audioDevices().length) {
|
||||
return await checkAudioDevices();
|
||||
}
|
||||
if (!props.selectedAudioDevice() && audioDevices().length) {
|
||||
onSelect(audioDevices()[0].id);
|
||||
} else {
|
||||
props.toggleMic();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={"inline-flex items-center gap-1 text-xs"}>
|
||||
<div
|
||||
class={
|
||||
"p-1 cursor-pointer btn btn-xs bg-white hover:bg-indigo-50 pointer-events-auto tooltip tooltip-right text-sm font-normal"
|
||||
}
|
||||
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
|
||||
onClick={onMicToggle}
|
||||
>
|
||||
<img
|
||||
src={props.mic() ? micOn : micOff}
|
||||
alt={props.mic() ? "microphone on" : "microphone off"}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class={
|
||||
"flex items-center gap-1 btn btn-xs btn-ghost hover:bg-neutral/20 rounded-lg pointer-events-auto"
|
||||
}
|
||||
onClick={checkAudio}
|
||||
>
|
||||
{audioDevices().length === 0 ? (
|
||||
<div class="max-w-64 block leading-tight cursor-pointer whitespace-nowrap overflow-hidden font-normal">
|
||||
{checkedAudioDevices() === 1
|
||||
? "Loading audio devices"
|
||||
: "Grant microphone access"}
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
options={audioDevices()}
|
||||
selected={props.selectedAudioDevice()}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
)}
|
||||
<ChevronSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
57
spot/entrypoints/popup/components/AudioPicker.tsx
Normal file
57
spot/entrypoints/popup/components/AudioPicker.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, For } from "solid-js";
|
||||
import micOff from "~/assets/mic-off-red.svg";
|
||||
import micOn from "~/assets/mic-on-dark.svg";
|
||||
import Dropdown from "~/entrypoints/popup/Dropdown";
|
||||
import { ChevronSvg } from "../Icons";
|
||||
import { AudioDevice } from "../types";
|
||||
|
||||
interface AudioPickerProps {
|
||||
mic: () => boolean;
|
||||
audioDevices: () => AudioDevice[];
|
||||
selectedAudioDevice: () => string;
|
||||
isChecking: () => boolean;
|
||||
onMicToggle: () => void;
|
||||
onCheckAudio: () => void;
|
||||
onSelectDevice: (deviceId: string) => void;
|
||||
}
|
||||
|
||||
const AudioPicker: Component<AudioPickerProps> = (props) => {
|
||||
return (
|
||||
<div class="inline-flex items-center gap-1 text-xs">
|
||||
<div
|
||||
class="p-1 cursor-pointer btn btn-xs bg-white hover:bg-indigo-50 pointer-events-auto tooltip tooltip-right text-sm font-normal"
|
||||
data-tip={props.mic() ? "Switch Off Mic" : "Switch On Mic"}
|
||||
onClick={props.onMicToggle}
|
||||
>
|
||||
<img
|
||||
src={props.mic() ? micOn : micOff}
|
||||
alt={props.mic() ? "microphone on" : "microphone off"}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-1 btn btn-xs btn-ghost hover:bg-neutral/20 rounded-lg pointer-events-auto"
|
||||
onClick={props.onCheckAudio}
|
||||
>
|
||||
{props.audioDevices().length === 0 ? (
|
||||
<div class="max-w-64 block leading-tight cursor-pointer whitespace-nowrap overflow-hidden font-normal">
|
||||
{props.isChecking()
|
||||
? "Loading audio devices"
|
||||
: "Grant microphone access"}
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
options={props.audioDevices()}
|
||||
selected={props.selectedAudioDevice()}
|
||||
onChange={props.onSelectDevice}
|
||||
/>
|
||||
)}
|
||||
<ChevronSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioPicker;
|
||||
73
spot/entrypoints/popup/components/Header.tsx
Normal file
73
spot/entrypoints/popup/components/Header.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Component } from "solid-js";
|
||||
import orLogo from "~/assets/orSpot.svg";
|
||||
import {
|
||||
HomePageSvg,
|
||||
SlackSvg,
|
||||
SettingsSvg,
|
||||
} from "../Icons";
|
||||
|
||||
interface HeaderProps {
|
||||
openSettings: () => void;
|
||||
}
|
||||
|
||||
const Header: Component<HeaderProps> = (props) => {
|
||||
const openHomePage = async () => {
|
||||
const { settings } = await chrome.storage.local.get("settings");
|
||||
return window.open(`${settings.ingestPoint}/spots`, "_blank");
|
||||
};
|
||||
|
||||
const openOrSite = () => {
|
||||
window.open("https://openreplay.com", "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="flex items-center gap-1 cursor-pointer hover:opacity-50"
|
||||
onClick={openOrSite}
|
||||
>
|
||||
<img src={orLogo} class="w-5" alt="OpenReplay Spot" />
|
||||
<div class="text-neutral-600">
|
||||
<span class="text-lg font-semibold text-black">OpenReplay Spot</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<div class="text-sm tooltip tooltip-bottom" data-tip="My Spots">
|
||||
<div onClick={openHomePage}>
|
||||
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
|
||||
<HomePageSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Get help on Slack"
|
||||
>
|
||||
<a
|
||||
href="https://join.slack.com/t/openreplay/shared_invite/zt-2brqlwcis-k7OtqHkW53EAoTRqPjCmyg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
|
||||
<SlackSvg />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm tooltip tooltip-bottom"
|
||||
data-tip="Settings"
|
||||
onClick={props.openSettings}
|
||||
>
|
||||
<div class="cursor-pointer p-2 hover:bg-indigo-50 rounded-full">
|
||||
<SettingsSvg />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
48
spot/entrypoints/popup/components/RecordingControls.tsx
Normal file
48
spot/entrypoints/popup/components/RecordingControls.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { Component } from "solid-js";
|
||||
import { RecordTabSvg, RecordDesktopSvg } from "../Icons";
|
||||
import Button from "~/entrypoints/popup/Button";
|
||||
import { AppState, RecordingArea } from "../types";
|
||||
|
||||
interface RecordingControlsProps {
|
||||
state: AppState;
|
||||
startRecording: (area: RecordingArea) => void;
|
||||
stopRecording: () => void;
|
||||
}
|
||||
|
||||
const RecordingControls: Component<RecordingControlsProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
{props.state === AppState.RECORDING && (
|
||||
<Button name="End Recording" onClick={props.stopRecording} />
|
||||
)}
|
||||
|
||||
{props.state === AppState.STARTING && (
|
||||
<div class="flex flex-row items-center gap-2 w-full justify-center">
|
||||
<div class="py-4">Your recording is starting</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.state === AppState.READY && (
|
||||
<div class="flex flex-row items-center gap-2 w-full justify-center">
|
||||
<button
|
||||
class="btn bg-indigo-100 text-base hover:bg-primary hover:text-white w-6/12"
|
||||
onClick={() => props.startRecording("tab")}
|
||||
>
|
||||
<RecordTabSvg />
|
||||
Record Tab
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn bg-teal-50 text-base hover:bg-primary hover:text-white"
|
||||
onClick={() => props.startRecording("desktop")}
|
||||
>
|
||||
<RecordDesktopSvg />
|
||||
Record Desktop
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingControls;
|
||||
63
spot/entrypoints/popup/hooks/useAppState.ts
Normal file
63
spot/entrypoints/popup/hooks/useAppState.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { createSignal, onMount } from "solid-js";
|
||||
import { AppState, RecordingArea } from "../types";
|
||||
|
||||
export function useAppState() {
|
||||
const [state, setState] = createSignal<AppState>(AppState.EMPTY);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "popup:no-login") {
|
||||
setState(AppState.LOGIN);
|
||||
}
|
||||
if (message.type === "popup:login") {
|
||||
setState(AppState.READY);
|
||||
}
|
||||
if (message.type === "popup:stopped") {
|
||||
setState(AppState.READY);
|
||||
}
|
||||
if (message.type === "popup:started") {
|
||||
setState(AppState.RECORDING);
|
||||
}
|
||||
});
|
||||
|
||||
void browser.runtime.sendMessage({ type: "popup:check-status" });
|
||||
});
|
||||
|
||||
const startRecording = async (
|
||||
area: RecordingArea,
|
||||
mic: boolean,
|
||||
audioId: string,
|
||||
permissions: boolean
|
||||
) => {
|
||||
setState(AppState.STARTING);
|
||||
await browser.runtime.sendMessage({
|
||||
type: "popup:start",
|
||||
area,
|
||||
mic,
|
||||
audioId,
|
||||
permissions,
|
||||
});
|
||||
window.close();
|
||||
};
|
||||
|
||||
const stopRecording = (mic: boolean, audioId: string) => {
|
||||
void browser.runtime.sendMessage({
|
||||
type: "popup:stop",
|
||||
mic,
|
||||
audioId,
|
||||
});
|
||||
};
|
||||
|
||||
const openSettings = () => setIsSettingsOpen(true);
|
||||
const closeSettings = () => setIsSettingsOpen(false);
|
||||
|
||||
return {
|
||||
state,
|
||||
isSettingsOpen,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
openSettings,
|
||||
closeSettings,
|
||||
};
|
||||
}
|
||||
100
spot/entrypoints/popup/hooks/useAudioDevices.ts
Normal file
100
spot/entrypoints/popup/hooks/useAudioDevices.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { createSignal, createEffect } from "solid-js";
|
||||
import { AudioDevice } from "../types";
|
||||
import { getAudioDevices } from "../utils/audio";
|
||||
|
||||
export function useAudioDevices() {
|
||||
const [audioDevices, setAudioDevices] = createSignal<AudioDevice[]>([]);
|
||||
const [selectedAudioDevice, setSelectedAudioDevice] = createSignal("");
|
||||
const [mic, setMic] = createSignal(false);
|
||||
const [hasPermissions, setHasPermissions] = createSignal(false);
|
||||
const [isChecking, setIsChecking] = createSignal(false);
|
||||
|
||||
createEffect(() => {
|
||||
chrome.storage.local.get("audioPerm", (data) => {
|
||||
if (data.audioPerm && audioDevices().length === 0) {
|
||||
setHasPermissions(true);
|
||||
checkAudioDevices().then(async (devices) => {
|
||||
const { selectedAudioId, micOn } = await chrome.storage.local.get([
|
||||
"selectedAudioId",
|
||||
"micOn",
|
||||
]);
|
||||
|
||||
if (selectedAudioId) {
|
||||
const selectedDevice = devices.find(
|
||||
(device) => device.id === selectedAudioId
|
||||
);
|
||||
if (selectedDevice) {
|
||||
setSelectedAudioDevice(selectedDevice.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (micOn) {
|
||||
toggleMic();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const checkAudioDevices = async (): Promise<AudioDevice[]> => {
|
||||
setIsChecking(true);
|
||||
|
||||
const { granted, audioDevices, denied } = await getAudioDevices();
|
||||
|
||||
if (!granted && !denied) {
|
||||
void browser.runtime.sendMessage({
|
||||
type: "popup:get-audio-perm",
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === "popup:audio-perm") {
|
||||
void checkAudioDevices();
|
||||
}
|
||||
});
|
||||
} else if (audioDevices.length > 0) {
|
||||
chrome.storage.local.set({ audioPerm: granted });
|
||||
setAudioDevices(audioDevices);
|
||||
setSelectedAudioDevice(audioDevices[0]?.id || "");
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
return audioDevices;
|
||||
};
|
||||
|
||||
const toggleMic = () => {
|
||||
setMic(!mic());
|
||||
};
|
||||
|
||||
const selectAudioDevice = (deviceId: string) => {
|
||||
setSelectedAudioDevice(deviceId);
|
||||
if (!mic()) {
|
||||
toggleMic();
|
||||
}
|
||||
chrome.storage.local.set({ selectedAudioId: deviceId, micOn: true });
|
||||
};
|
||||
|
||||
const handleMicToggle = async () => {
|
||||
if (!audioDevices().length) {
|
||||
return await checkAudioDevices();
|
||||
}
|
||||
|
||||
if (!selectedAudioDevice() && audioDevices().length) {
|
||||
selectAudioDevice(audioDevices()[0].id);
|
||||
} else {
|
||||
chrome.storage.local.set({ micOn: !mic() });
|
||||
toggleMic();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
audioDevices,
|
||||
selectedAudioDevice,
|
||||
mic,
|
||||
hasPermissions,
|
||||
isChecking,
|
||||
checkAudioDevices,
|
||||
toggleMic,
|
||||
selectAudioDevice,
|
||||
handleMicToggle,
|
||||
};
|
||||
}
|
||||
14
spot/entrypoints/popup/types/index.ts
Normal file
14
spot/entrypoints/popup/types/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type AudioDevice = {
|
||||
label: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export enum AppState {
|
||||
EMPTY = "empty",
|
||||
LOGIN = "login",
|
||||
READY = "ready",
|
||||
STARTING = "starting",
|
||||
RECORDING = "recording",
|
||||
}
|
||||
|
||||
export type RecordingArea = "tab" | "desktop";
|
||||
24
spot/entrypoints/popup/utils/audio.ts
Normal file
24
spot/entrypoints/popup/utils/audio.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { AudioDevice } from "../types";
|
||||
export async function getAudioDevices(): Promise<{
|
||||
granted: boolean;
|
||||
denied?: boolean;
|
||||
audioDevices: AudioDevice[];
|
||||
}> {
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevices = devices
|
||||
.filter((device) => device.kind === "audioinput")
|
||||
.map((device) => ({ label: device.label, id: device.deviceId }));
|
||||
|
||||
return { granted: true, audioDevices };
|
||||
} catch (error) {
|
||||
console.error("Error accessing audio devices:", error);
|
||||
const msg = error.message ?? "";
|
||||
return {
|
||||
granted: false,
|
||||
denied: msg.includes("denied"),
|
||||
audioDevices: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue