refactor: remove API key and status from options and popup

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
IgorVolochay
2026-10-25 12:08:49 +03:00
co-authored by Claude Sonnet 5.5
parent fe65aee032
commit 4bb0cf0cb2
5 changed files with 7 additions and 49 deletions
+1 -15
View File
@@ -11,11 +11,7 @@
<h1>Outlier Saver</h1> <h1>Outlier Saver</h1>
<form id="form"> <form id="form">
<label> <label>
API-ключ YouTube Data API v3 Порог outlier score VidIQ (сохранять видео от этого значения)
<input id="apiKey" type="password" autocomplete="off" spellcheck="false">
</label>
<label>
Порог score (просмотры ÷ подписчики)
<input id="threshold" type="number" min="0.1" step="0.1" required> <input id="threshold" type="number" min="0.1" step="0.1" required>
</label> </label>
<label> <label>
@@ -27,16 +23,6 @@
<span id="saved" role="status"></span> <span id="saved" role="status"></span>
</div> </div>
</form> </form>
<details>
<summary>Как получить API-ключ</summary>
<ol>
<li>Откройте <a href="https://console.cloud.google.com/" target="_blank" rel="noreferrer">Google Cloud Console</a> и создайте проект.</li>
<li>В разделе «API и сервисы → Библиотека» включите <b>YouTube Data API v3</b>.</li>
<li>В «Учётные данные» создайте <b>API-ключ</b>.</li>
<li>В ограничениях ключа выберите «Ограничить API → YouTube Data API v3». Ограничение по HTTP-источникам (referrer) <b>не включайте</b>: запросы идут из расширения.</li>
<li>Вставьте ключ в поле выше и нажмите «Сохранить».</li>
</ol>
</details>
</main> </main>
<script type="module" src="options.js"></script> <script type="module" src="options.js"></script>
</body> </body>
+1 -3
View File
@@ -1,13 +1,11 @@
import { getSettings, saveSettings } from '../lib/storage.js'; import { getSettings, saveSettings } from '../lib/storage.js';
const form = document.getElementById('form'); const form = document.getElementById('form');
const apiKey = document.getElementById('apiKey');
const threshold = document.getElementById('threshold'); const threshold = document.getElementById('threshold');
const minViews = document.getElementById('minViews'); const minViews = document.getElementById('minViews');
const savedLabel = document.getElementById('saved'); const savedLabel = document.getElementById('saved');
function fill(settings) { function fill(settings) {
apiKey.value = settings.apiKey;
threshold.value = settings.threshold; threshold.value = settings.threshold;
minViews.value = settings.minViews; minViews.value = settings.minViews;
} }
@@ -16,7 +14,7 @@ fill(await getSettings());
form.addEventListener('submit', async (event) => { form.addEventListener('submit', async (event) => {
event.preventDefault(); event.preventDefault();
fill(await saveSettings({ apiKey: apiKey.value, threshold: threshold.value, minViews: minViews.value })); fill(await saveSettings({ threshold: threshold.value, minViews: minViews.value }));
savedLabel.textContent = 'Сохранено'; savedLabel.textContent = 'Сохранено';
setTimeout(() => { setTimeout(() => {
savedLabel.textContent = ''; savedLabel.textContent = '';
+1 -3
View File
@@ -10,8 +10,6 @@ header { justify-content: space-between; border-bottom: 1px solid var(--line); }
footer { border-top: 1px solid var(--line); } footer { border-top: 1px solid var(--line); }
footer button:last-child { margin-left: auto; } footer button:last-child { margin-left: auto; }
button { padding: 5px 10px; font: inherit; } button { padding: 5px 10px; font: inherit; }
#status { margin: 0; padding: 8px 12px; color: var(--muted); }
#status.warn { color: #d93025; }
#list { list-style: none; margin: 0; padding: 0; max-height: 380px; overflow-y: auto; } #list { list-style: none; margin: 0; padding: 0; max-height: 380px; overflow-y: auto; }
#list li { display: grid; grid-template-columns: 96px 1fr; gap: 10px; padding: 8px 12px; border-bottom: 1px solid var(--line); } #list li { display: grid; grid-template-columns: 96px 1fr; gap: 10px; padding: 8px 12px; border-bottom: 1px solid var(--line); }
#list img { width: 96px; height: 54px; object-fit: cover; border-radius: 4px; background: var(--line); } #list img { width: 96px; height: 54px; object-fit: cover; border-radius: 4px; background: var(--line); }
@@ -19,4 +17,4 @@ button { padding: 5px 10px; font: inherit; }
#list a:hover { text-decoration: underline; } #list a:hover { text-decoration: underline; }
.meta { margin-top: 2px; color: var(--muted); } .meta { margin-top: 2px; color: var(--muted); }
.badge { display: inline-block; margin-right: 6px; padding: 0 6px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 11px; } .badge { display: inline-block; margin-right: 6px; padding: 0 6px; border-radius: 8px; background: var(--accent); color: #fff; font-size: 11px; }
.empty { padding: 24px 12px; text-align: center; color: var(--muted); } .empty { display: block; padding: 24px 12px; text-align: center; color: var(--muted); }
-1
View File
@@ -10,7 +10,6 @@
<strong>Outlier Saver</strong> <strong>Outlier Saver</strong>
<span id="count"></span> <span id="count"></span>
</header> </header>
<p id="status" role="status"></p>
<ul id="list"></ul> <ul id="list"></ul>
<footer> <footer>
<button id="export-json" type="button">Скачать JSON</button> <button id="export-json" type="button">Скачать JSON</button>
+4 -27
View File
@@ -1,34 +1,15 @@
import { countOutliers, listOutliers } from '../lib/db.js'; import { countOutliers, listOutliers } from '../lib/db.js';
import { toCsv, toJson } from '../lib/export.js'; import { toCsv, toJson } from '../lib/export.js';
import { getStatus } from '../lib/storage.js';
const LIST_LIMIT = 50; const LIST_LIMIT = 50;
const compact = new Intl.NumberFormat('ru-RU', { notation: 'compact', maximumFractionDigits: 1 }); const compact = new Intl.NumberFormat('ru-RU', { notation: 'compact', maximumFractionDigits: 1 });
const format = (n) => (n === null || n === undefined ? '—' : compact.format(n)); const format = (n) => (n === null || n === undefined ? '—' : compact.format(n));
const countEl = document.getElementById('count'); const countEl = document.getElementById('count');
const statusEl = document.getElementById('status');
const listEl = document.getElementById('list'); const listEl = document.getElementById('list');
const jsonButton = document.getElementById('export-json'); const jsonButton = document.getElementById('export-json');
const csvButton = document.getElementById('export-csv'); const csvButton = document.getElementById('export-csv');
function statusText(status) {
switch (status.state) {
case 'no-key':
return { text: 'Не задан API-ключ — откройте настройки.', warn: true };
case 'invalid-key':
return { text: `API-ключ отклонён: ${status.message ?? 'неизвестная ошибка'}`, warn: true };
case 'quota':
if (Date.now() >= status.until) return { text: 'Сбор работает.', warn: false };
return {
text: `Квота YouTube API исчерпана, сбор возобновится после ${new Date(status.until).toLocaleString('ru-RU')}.`,
warn: true,
};
default:
return { text: 'Сбор работает.', warn: false };
}
}
function badge(text) { function badge(text) {
const span = document.createElement('span'); const span = document.createElement('span');
span.className = 'badge'; span.className = 'badge';
@@ -51,12 +32,11 @@ function renderRow(outlier) {
title.rel = 'noreferrer'; title.rel = 'noreferrer';
const badges = document.createElement('div'); const badges = document.createElement('div');
if (outlier.score !== null) badges.append(badge(`×${outlier.score}`)); badges.append(badge(`${outlier.outlierScore}x`));
if (outlier.vidiqScore !== null) badges.append(badge(`VidIQ ${outlier.vidiqScore}`));
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'meta'; meta.className = 'meta';
meta.textContent = `${outlier.channelTitle} · ${format(outlier.subscribers)} подп. · ${format(outlier.views)} просм.`; meta.textContent = `${outlier.channelTitle ?? '—'} · ${format(outlier.subscribers)} подп. · ${format(outlier.views)} просм.`;
const body = document.createElement('div'); const body = document.createElement('div');
body.append(title, badges, meta); body.append(title, badges, meta);
@@ -84,17 +64,14 @@ async function exportAs(kind) {
} }
} }
const [count, recent, status] = await Promise.all([countOutliers(), listOutliers(LIST_LIMIT), getStatus()]); const [count, recent] = await Promise.all([countOutliers(), listOutliers(LIST_LIMIT)]);
countEl.textContent = `Сохранено: ${count}`; countEl.textContent = `Сохранено: ${count}`;
const { text, warn } = statusText(status);
statusEl.textContent = text;
statusEl.classList.toggle('warn', warn);
if (recent.length === 0) { if (recent.length === 0) {
const empty = document.createElement('li'); const empty = document.createElement('li');
empty.className = 'empty'; empty.className = 'empty';
empty.textContent = 'Пока ничего не найдено. Откройте YouTube и пролистайте ленту.'; empty.textContent = 'Пока ничего не найдено. Откройте YouTube с включённым VidIQ и пролистайте ленту.';
listEl.append(empty); listEl.append(empty);
jsonButton.disabled = true; jsonButton.disabled = true;
csvButton.disabled = true; csvButton.disabled = true;