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:
co-authored by
Claude Sonnet 5.5
parent
fe65aee032
commit
4bb0cf0cb2
@@ -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,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
@@ -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); }
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user