skillok meeting
96a28a9c42ff4eab2c832f9e7b418f3fdf45bc26
Перейти к файлу
Код
meeting srcrecording.ts
anc Запись всего звука, папка хранения, точнее распознавание - Звук собеседников — весь звук компьютера, кроме самого Skillok Meet: Zoom, Google Meet, Телемост, что угодно. Windows — process loopback в режиме исключения своего процесса, macOS — ScreenCaptureKit без фильтра по приложению. Поиск Zoom и зависимость windows убраны. - Если звук компьютера недоступен (на Mac — нет разрешения), запись идёт с микрофона, а интерфейс говорит почему. - Настройки: папка хранения записей (выбрать, открыть, вернуть по умолчанию; записи переносятся целиком), точность распознавания — по умолчанию полная whisper-large-v3. - «Открыть папку с файлами» у записи; микрофон на Mac просит у CoreAudio сразу 16 кГц. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 96a28a9 30 сентября 2026 г.
recording.ts
// Запись: панель с таймером, уровнями и кнопками — одна на главной и на
// экране записи — и метка «идёт запись» в шапке, видная с любого экрана.

import { api, elapsed, events } from "./api";
import { esc } from "./dom";
import type { Scope } from "./scope";
import { changed, copy, refreshRecording, state, toast } from "./state";

export function recordingPanel(warning: string): string {
  const r = state.recording;
  if (!r) return "";
  const meter = (label: string, key: string) => `
    <div class="meter"><span class="meter-label small">${label}</span>
      <span class="meter-bar"><span class="meter-fill" data-level="${key}"></span></span></div>`;
  return `
    <div class="rec" data-rec>
      <p class="rec-state"><span class="dot dot-live${r.paused ? " is-paused" : ""}"></span>${
        r.paused ? "Запись на паузе" : r.system ? "Идёт запись: собеседники и вы" : "Идёт запись: только ваш микрофон"
      }</p>
      ${r.system_error ? `<p class="rec-warn small">Голоса собеседников не пишутся: ${esc(r.system_error)}</p>` : ""}
      <p class="rec-timer mono-num" data-timer>${elapsed(r.started_at)}</p>
      <div class="meters">${meter("Вы", "me")}${r.system ? meter("Собеседники", "others") : ""}</div>
      <div class="actions">
        <button class="ui-btn" data-rec-stop>Остановить</button>
        <button class="link-btn" data-rec-pause>${r.paused ? "Продолжить" : "Пауза"}</button>
        <button class="link-btn" data-rec-warning>Скопировать предупреждение для чата</button>
      </div>
      <template data-warning>${esc(warning)}</template>
    </div>`;
}

/** Кнопки панели и живые уровни звука. `onStop` — после остановки. */
export function bindRecording(root: HTMLElement, scope: Scope, warning: string, onStop: () => void): void {
  root.querySelector<HTMLButtonElement>("[data-rec-stop]")?.addEventListener("click", async (e) => {
    const b = e.currentTarget as HTMLButtonElement;
    b.disabled = true;
    b.textContent = "Останавливаю…";
    try {
      await api.recorderStop();
      await refreshRecording();
      onStop();
    } catch (err) {
      b.disabled = false;
      toast(err);
    }
  });
  root.querySelector("[data-rec-pause]")?.addEventListener("click", async () => {
    if (!state.recording) return;
    try {
      await api.recorderPause(!state.recording.paused);
      state.recording.paused = !state.recording.paused;
      changed();
    } catch (err) {
      toast(err);
    }
  });
  root.querySelector("[data-rec-warning]")?.addEventListener("click", () => copy(warning, "Предупреждение скопировано — вставьте его в чат встречи"));
  scope.on(
    events.recorder(({ event }) => {
      if (event.kind === "level") {
        const key = event.data.speaker.kind === "me" ? "me" : "others";
        const fill = root.querySelector<HTMLElement>(`[data-level="${key}"]`);
        if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((event.data.dbfs + 60) / 55) * 100))}%`;
      } else {
        toast(`Дорожка «${event.data.speaker.kind === "me" ? "вы" : "собеседники"}» перестала писаться: ${event.data.error}`);
      }
    }),
  );
  scope.every(1000, () => {
    const t = root.querySelector("[data-timer]");
    if (t && state.recording) t.textContent = elapsed(state.recording.started_at);
  });
}

/** Метка в шапке: запись идёт, а открыт другой экран. */
export function drawHeaderBadge(): void {
  const el = document.getElementById("rec-badge");
  if (!el) return;
  const r = state.recording;
  const onItsPage = r && location.hash === `#/m/${encodeURIComponent(r.meeting_id)}`;
  const onHome = r && (location.hash === "" || location.hash === "#/");
  if (!r || onItsPage || onHome) {
    el.hidden = true;
    return;
  }
  el.hidden = false;
  el.setAttribute("href", `#/m/${encodeURIComponent(r.meeting_id)}`);
  el.innerHTML = `<span class="dot dot-live${r.paused ? " is-paused" : ""}"></span>${r.paused ? "Пауза" : "Запись"} <span class="mono-num" data-badge-timer>${elapsed(r.started_at)}</span>`;
}

export function startHeaderBadge(): void {
  window.addEventListener("hashchange", drawHeaderBadge);
  window.setInterval(() => {
    const t = document.querySelector("[data-badge-timer]");
    if (t && state.recording) t.textContent = elapsed(state.recording.started_at);
  }, 1000);
}