Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/chrome-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
1. Соберите расширение из корня репозитория:

```bash
git pull origin main
npm install
npm run ext:build
```

2. Откройте Chrome → `chrome://extensions` → включите **Developer mode**.
3. **Load unpacked** → выберите папку `extension/` (не `extension/src`).
4. На [kar-tochki.pages.dev](https://kar-tochki.pages.dev/?ext_connect=1) войдите в облачный аккаунт (если ещё не вошли). Баннер подтвердит подключение.
5. Откройте любое видео на YouTube → нажмите **Карточки** → выберите режим и папку → **Сформировать** → отметьте карточки → **Создать**.
3. **Load unpacked** → выберите папку `extension/` (не `extension/src` и не `extension/dist`).
4. После обновления кода нажмите **Reload** на карточке расширения (иначе останется старая пустая панель).
5. На [kar-tochki.pages.dev](https://kar-tochki.pages.dev/?ext_connect=1) войдите в облачный аккаунт (если ещё не вошли). Баннер подтвердит подключение.
6. Откройте любое видео на YouTube → нажмите **Карточки** → выберите режим и папку → **Сформировать** → отметьте карточки → **Создать**.

Если Side Panel пустой: правый клик внутри панели → **Inspect** → вкладка Console; также смотри **Errors** на `chrome://extensions`.

## Что нужно заранее

Expand Down
8 changes: 5 additions & 3 deletions extension/dist/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ async function setAuth(auth) {
else await chrome.storage.local.remove(STORAGE_KEYS.auth);
}
async function setVideo(video) {
if (video) await chrome.storage.session.set({ [STORAGE_KEYS.video]: video });
else await chrome.storage.session.remove(STORAGE_KEYS.video);
try {
if (video) await chrome.storage.local.set({ [STORAGE_KEYS.video]: video });
else await chrome.storage.local.remove(STORAGE_KEYS.video);
} catch {
}
}

// src/background.ts
Expand Down Expand Up @@ -79,4 +82,3 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
})();
return true;
});
//# sourceMappingURL=background.js.map
62 changes: 32 additions & 30 deletions extension/dist/content-app-bridge.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
// src/content-app-bridge.ts
var PAGE_TYPE = "KAR_EXT_CONNECT";
var PAGE_ACK = "KAR_EXT_CONNECT_ACK";
var PAGE_STATUS = "KAR_EXT_CONNECT_STATUS";
window.addEventListener("message", (event) => {
if (event.source !== window) return;
const data = event.data;
if (!data || data.type !== PAGE_TYPE) return;
chrome.runtime.sendMessage(
{
type: "AUTH_CONNECT",
session: data.session,
supabaseUrl: data.supabaseUrl,
anonKey: data.anonKey
},
(response) => {
const err = chrome.runtime.lastError;
window.postMessage(
{
type: PAGE_ACK,
ok: !err && !!response?.ok,
error: err?.message || response?.error || null
},
"*"
);
}
);
});
window.postMessage({ type: PAGE_STATUS, installed: true }, "*");
//# sourceMappingURL=content-app-bridge.js.map
"use strict";
(() => {
// src/content-app-bridge.ts
var PAGE_TYPE = "KAR_EXT_CONNECT";
var PAGE_ACK = "KAR_EXT_CONNECT_ACK";
var PAGE_STATUS = "KAR_EXT_CONNECT_STATUS";
window.addEventListener("message", (event) => {
if (event.source !== window) return;
const data = event.data;
if (!data || data.type !== PAGE_TYPE) return;
chrome.runtime.sendMessage(
{
type: "AUTH_CONNECT",
session: data.session,
supabaseUrl: data.supabaseUrl,
anonKey: data.anonKey
},
(response) => {
const err = chrome.runtime.lastError;
window.postMessage(
{
type: PAGE_ACK,
ok: !err && !!response?.ok,
error: err?.message || response?.error || null
},
"*"
);
}
);
});
window.postMessage({ type: PAGE_STATUS, installed: true }, "*");
})();
150 changes: 76 additions & 74 deletions extension/dist/content-youtube.js
Original file line number Diff line number Diff line change
@@ -1,81 +1,83 @@
// src/content-youtube.ts
var BTN_ID = "kar-ext-yt-fab";
function isWatchPage(href = location.href) {
try {
const u = new URL(href);
if (u.pathname === "/watch" && u.searchParams.get("v")) return true;
if (u.pathname.startsWith("/shorts/")) return true;
return false;
} catch {
return false;
"use strict";
(() => {
// src/content-youtube.ts
var BTN_ID = "kar-ext-yt-fab";
function isWatchPage(href = location.href) {
try {
const u = new URL(href);
if (u.pathname === "/watch" && u.searchParams.get("v")) return true;
if (u.pathname.startsWith("/shorts/")) return true;
return false;
} catch {
return false;
}
}
}
function currentVideoUrl() {
if (!isWatchPage()) return null;
const u = new URL(location.href);
if (u.pathname.startsWith("/shorts/")) {
const id = u.pathname.split("/")[2];
return id ? `https://www.youtube.com/shorts/${id}` : null;
function currentVideoUrl() {
if (!isWatchPage()) return null;
const u = new URL(location.href);
if (u.pathname.startsWith("/shorts/")) {
const id = u.pathname.split("/")[2];
return id ? `https://www.youtube.com/shorts/${id}` : null;
}
const v = u.searchParams.get("v");
return v ? `https://www.youtube.com/watch?v=${v}` : null;
}
const v = u.searchParams.get("v");
return v ? `https://www.youtube.com/watch?v=${v}` : null;
}
function videoTitle() {
const el = document.querySelector("h1.ytd-watch-metadata yt-formatted-string") || document.querySelector("h1 yt-formatted-string") || document.querySelector("h1.ytd-video-primary-info-renderer") || document.querySelector("#title h1");
return (el?.textContent || document.title || "").replace(/ - YouTube$/, "").trim();
}
function ensureButton() {
const url = currentVideoUrl();
let btn = document.getElementById(BTN_ID);
if (!url) {
btn?.remove();
return;
function videoTitle() {
const el = document.querySelector("h1.ytd-watch-metadata yt-formatted-string") || document.querySelector("h1 yt-formatted-string") || document.querySelector("h1.ytd-video-primary-info-renderer") || document.querySelector("#title h1");
return (el?.textContent || document.title || "").replace(/ - YouTube$/, "").trim();
}
if (!btn) {
btn = document.createElement("button");
btn.id = BTN_ID;
btn.type = "button";
btn.title = "\u041A\u0410\u0420-\u0442\u043E\u0447\u043A\u0438: \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u0438\u0437 \u044D\u0442\u043E\u0433\u043E \u0432\u0438\u0434\u0435\u043E";
btn.setAttribute("aria-label", "\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u041A\u0410\u0420-\u0442\u043E\u0447\u043A\u0438");
btn.innerHTML = '<span class="kar-ext-fab-mark" aria-hidden="true">\u041A</span><span class="kar-ext-fab-label">\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0438</span>';
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const videoUrl = currentVideoUrl();
if (!videoUrl) return;
chrome.runtime.sendMessage({
type: "OPEN_SIDEPANEL",
url: videoUrl,
title: videoTitle()
function ensureButton() {
const url = currentVideoUrl();
let btn = document.getElementById(BTN_ID);
if (!url) {
btn?.remove();
return;
}
if (!btn) {
btn = document.createElement("button");
btn.id = BTN_ID;
btn.type = "button";
btn.title = "\u041A\u0410\u0420-\u0442\u043E\u0447\u043A\u0438: \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u0438\u0437 \u044D\u0442\u043E\u0433\u043E \u0432\u0438\u0434\u0435\u043E";
btn.setAttribute("aria-label", "\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u041A\u0410\u0420-\u0442\u043E\u0447\u043A\u0438");
btn.innerHTML = '<span class="kar-ext-fab-mark" aria-hidden="true">\u041A</span><span class="kar-ext-fab-label">\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0438</span>';
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const videoUrl = currentVideoUrl();
if (!videoUrl) return;
chrome.runtime.sendMessage({
type: "OPEN_SIDEPANEL",
url: videoUrl,
title: videoTitle()
});
});
document.documentElement.appendChild(btn);
}
btn.hidden = false;
}
function notifyVideo() {
const url = currentVideoUrl();
if (!url) return;
chrome.runtime.sendMessage({
type: "SET_VIDEO",
url,
title: videoTitle()
}).catch(() => {
});
document.documentElement.appendChild(btn);
}
btn.hidden = false;
}
function notifyVideo() {
const url = currentVideoUrl();
if (!url) return;
chrome.runtime.sendMessage({
type: "SET_VIDEO",
url,
title: videoTitle()
}).catch(() => {
});
}
function sync() {
ensureButton();
notifyVideo();
}
sync();
document.addEventListener("yt-navigate-finish", () => sync());
window.addEventListener("popstate", () => sync());
var lastHref = location.href;
var mo = new MutationObserver(() => {
if (location.href !== lastHref) {
lastHref = location.href;
sync();
function sync() {
ensureButton();
notifyVideo();
}
});
mo.observe(document.documentElement, { childList: true, subtree: true });
//# sourceMappingURL=content-youtube.js.map
sync();
document.addEventListener("yt-navigate-finish", () => sync());
window.addEventListener("popstate", () => sync());
var lastHref = location.href;
var mo = new MutationObserver(() => {
if (location.href !== lastHref) {
lastHref = location.href;
sync();
}
});
mo.observe(document.documentElement, { childList: true, subtree: true });
})();
Loading
Loading