skillok meeting
a008f4d72f373bf56330f826ea2ad0912d19c3a7
Перейти к файлу
Код
meeting srcviewsnew.ts
anc Интерфейс по руководству «Стиль Skillok»; одни часы записи на весь экран По skillok/skillok-develop design/SKILLOK_STYLE.md: - кнопки и поля — --radius-control 2px; поля — --field-bg, 9/11, 13px, фокус терракотой внутрь; неактивная кнопка — 0,5; .ui-danger; мишени 44px под пальцем; - подписи полей — 12px капс с разрядкой; карточки без заливки, отступы 14–16px; вкладки 32px без жирного; моноширинный — только код, числа — tabular-nums; тень — только у сообщения; - значки — icons.ts (SVG 16, контур 1,5) вместо «★ ▶ ❚❚ ✕»; - обращение на «ты» в интерфейсе и сообщениях ядра; сняты пояснения «как пользоваться»; блок проверки звука — .audio-check (не пересекается с галочкой .check). Светлая тема остаётся единственной — решение владельца для Meet (руководство просит обе темы). Шапка и меню показывают время по записанному звуку, как панель записи. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 955f445 4 октября 2026 г.
123 строки 6.0 КБ
new.ts
// Экран «Новая запись»: «Записать встречу» ведёт сюда, а не запускает запись
// сразу (решение владельца 01.10.2026). Здесь проверка звука — две живые
// полоски, микрофон и звук компьютера, — и совет про наушники; запись
// начинается только кнопкой «Начать запись».
//
// Проверка — главная защита от встречи, которая молча не записалась: что
// звука собеседников нет, человек видит до встречи, а не после.

import { api, events, type CheckView } from "../api";
import { esc } from "../dom";
import type { Scope } from "../scope";
import { bindPermissionHelp, permissionHelp } from "../recording";
import { startRecording, state } from "../state";

export async function renderNew(root: HTMLElement, scope: Scope): Promise<void> {
  // Запись уже идёт — вторую не начать, показываем идущую.
  if (state.recording) {
    location.replace(`#/m/${encodeURIComponent(state.recording.meeting_id)}`);
    return;
  }
  const view = await api.getSettings();
  if (!scope.alive) return;

  if (!view.capture_supported) {
    root.innerHTML = `
      <div class="page">
        <section class="card">
          <h1>Новая запись</h1>
          <p>На этой системе запись не поддерживается. Добавь готовый аудиофайл — перетащи его в окно или выбери в меню.</p>
        </section>
      </div>`;
    return;
  }

  root.innerHTML = `
    <div class="page">
      <section class="card prep">
        <h1>Новая запись</h1>
        <div class="prep-block">
          <p class="field-label">Проверка звука</p>
          <div class="audio-check" data-check>
            ${checkRow("me", "Микрофон", "скажи что-нибудь")}
            ${checkRow("others", "Звук компьютера", "включи любой звук — видео или музыку")}
          </div>
        </div>
        <p class="muted">Лучше в наушниках — так голоса собеседников не попадут в микрофон второй раз.</p>
        ${
          view.openrouter_key
            ? ""
            : `<p class="notice">Записать можно и без ИИ. Чтобы потом расшифровать, подключи его в <a href="#/settings">настройках</a>.</p>`
        }
        <button class="ui-btn hero-btn" data-start>Начать запись</button>
      </section>
    </div>`;
  // Проверка звука: уровни приходят, пока открыт экран; уходим — устройства
  // закрываются. «Начать запись» ядро гасит проверку само.
  let check: CheckView | null = null;
  try {
    check = await api.audioCheckStart();
  } catch (e) {
    check = { mic_error: String(e), system_error: String(e), os: "" };
  }
  if (!scope.alive) {
    void api.audioCheckStop();
    return;
  }
  showError(root, "me", check.mic_error);
  showError(root, "others", check.system_error);
  const heard: Record<string, number> = {};
  scope.on(
    events.recorder(({ meeting_id, event }) => {
      if (meeting_id || event.kind !== "level") return;
      const key = event.data.speaker.kind === "me" ? "me" : "others";
      const row = root.querySelector<HTMLElement>(`[data-check-row="${key}"]`);
      if (!row || row.dataset.error) return;
      const fill = row.querySelector<HTMLElement>(".meter-fill");
      if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((event.data.dbfs + 60) / 55) * 100))}%`;
      if (event.data.dbfs > -45) heard[key] = performance.now();
      const ok = performance.now() - (heard[key] ?? -1e9) < 3_000;
      row.classList.toggle("is-ok", ok);
      row.querySelector("[data-check-state]")!.textContent = ok ? "слышно" : "тишина";
    }),
  );
  scope.on(Promise.resolve(() => void api.audioCheckStop()));

  root.querySelector<HTMLButtonElement>("[data-start]")!.addEventListener("click", async (e) => {
    const b = e.currentTarget as HTMLButtonElement;
    b.disabled = true;
    b.textContent = "Начинаю…";
    await startRecording();
    // Не началась — ошибку уже показало сообщение внизу, кнопку возвращаем.
    if (scope.alive && !state.recording) {
      b.disabled = false;
      b.textContent = "Начать запись";
    }
  });
}

function checkRow(key: "me" | "others", title: string, hint: string): string {
  return `
    <div class="check-row" data-check-row="${key}">
      <span class="check-title">${title}</span>
      <span class="meter-bar"><span class="meter-fill"></span></span>
      <span class="check-state" data-check-state>тишина</span>
      <span class="check-hint xs muted" data-check-hint>${esc(hint)}</span>
    </div>`;
}

/** Устройство не открылось — вместо полоски что делать и кнопка в настройки. */
function showError(root: HTMLElement, key: "me" | "others", error: string | null): void {
  if (!error) return;
  const row = root.querySelector<HTMLElement>(`[data-check-row="${key}"]`);
  if (!row) return;
  row.dataset.error = "1";
  row.classList.add("is-error");
  row.querySelector("[data-check-state]")!.textContent = "не пишется";
  const help = permissionHelp(key);
  const hint = row.querySelector<HTMLElement>("[data-check-hint]")!;
  hint.innerHTML = help || esc(error);
  // Текст ошибки системы — мелко, для того, кто будет разбираться.
  if (help) hint.insertAdjacentHTML("beforeend", `<p class="xs muted perm-raw">${esc(error)}</p>`);
  bindPermissionHelp(row);
}