adding browser extension and some QoL

This commit is contained in:
2026-08-12 00:14:47 +02:00
parent 0ca65ea473
commit d5f092883a
14 changed files with 1708 additions and 29 deletions

View File

@@ -0,0 +1,317 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
html, body {
width: 340px;
min-width: 340px;
min-height: 240px;
margin: 0;
padding: 0;
background-color: #121318;
color: #e2e4ec;
font-size: 13px;
user-select: none;
}
.header {
display: flex;
align-items: center;
justify-content: center;
padding: 12px 14px;
background: #191b22;
border-bottom: 1px solid #282a36;
}
.brand {
display: flex;
align-items: center;
gap: 8px;
}
.brand-icon {
width: 22px;
height: 22px;
}
.brand-name {
font-weight: 700;
font-size: 15px;
color: #ffffff;
letter-spacing: 0.4px;
}
.mode-selector {
display: flex;
background: #181920;
padding: 4px;
margin: 10px 14px 4px 14px;
border-radius: 8px;
border: 1px solid #242634;
}
.mode-option {
flex: 1;
text-align: center;
cursor: pointer;
}
.mode-option input {
display: none;
}
.mode-pill {
display: block;
padding: 6px 4px;
font-size: 11px;
font-weight: 600;
color: #8c92a4;
border-radius: 6px;
transition: all 0.2s ease;
}
.mode-option input:checked + .mode-pill {
background: #2b2f42;
color: #4da6ff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.content {
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.active-tab-card {
background: #191b26;
border: 1px solid #282c3f;
border-radius: 9px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.card-top {
display: flex;
align-items: center;
gap: 8px;
}
.site-badge {
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
background: #0066ff22;
color: #4da6ff;
border: 1px solid #0066ff55;
white-space: nowrap;
}
.site-badge.youtube {
background: #ff000022;
color: #ff4d4d;
border-color: #ff000055;
}
.tab-title {
font-size: 11px;
font-weight: 600;
color: #ffffff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-url-preview {
font-size: 10px;
color: #727a90;
font-family: monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: #111218;
padding: 4px 6px;
border-radius: 4px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
padding: 9px 12px;
border-radius: 7px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.15s ease;
}
.btn-lg {
padding: 11px 14px;
font-size: 13px;
font-weight: 700;
}
.btn-primary {
background: linear-gradient(135deg, #0066ff, #0044cc);
color: #ffffff;
box-shadow: 0 2px 10px rgba(0, 102, 255, 0.3);
}
.btn-primary:hover {
background: linear-gradient(135deg, #1a75ff, #0052e6);
box-shadow: 0 4px 14px rgba(0, 102, 255, 0.5);
}
.btn-primary.youtube-style {
background: linear-gradient(135deg, #e60000, #b30000);
box-shadow: 0 2px 10px rgba(230, 0, 0, 0.3);
}
.btn-primary.youtube-style:hover {
background: linear-gradient(135deg, #ff1a1a, #cc0000);
box-shadow: 0 4px 14px rgba(230, 0, 0, 0.5);
}
.btn-accent {
background: #252836;
color: #4da6ff;
border: 1px solid #363b50;
}
.btn-accent:hover {
background: #2e3346;
color: #80c0ff;
}
.btn-sm {
padding: 5px 8px;
font-size: 11px;
width: auto;
background: #0066ff;
color: #ffffff;
}
.divider {
display: flex;
align-items: center;
text-align: center;
color: #555c70;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
margin: 2px 0;
}
.divider::before, .divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #222532;
}
.divider span {
padding: 0 8px;
}
.input-group {
display: flex;
gap: 6px;
}
.input-group input {
flex: 1;
background: #181a24;
border: 1px solid #282c3d;
color: #ffffff;
padding: 8px 10px;
border-radius: 7px;
font-size: 11px;
outline: none;
transition: border-color 0.2s ease;
}
.input-group input:focus {
border-color: #0066ff;
}
.status-card {
background: #181a24;
border: 1px solid #252838;
border-radius: 7px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
text-align: center;
}
.status-card.hidden {
display: none;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid #2e3448;
border-top-color: #0066ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.status-text {
font-size: 11px;
color: #a0a6b8;
word-break: break-word;
}
.result-container {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
}
.result-container.hidden {
display: none;
}
.result-link {
flex: 1;
font-size: 11px;
color: #4da6ff;
text-decoration: underline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 220px;
display: block;
}
.result-link:hover {
color: #80c0ff;
}
.footer {
display: flex;
justify-content: center;
align-items: center;
padding: 8px 14px;
background: #15161d;
border-top: 1px solid #20222d;
font-size: 10px;
color: #5b6276;
}

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>f0ckm Uploader</title>
<link rel="stylesheet" href="popup.css">
<script src="popup.js" defer></script>
</head>
<body>
<div class="header">
<div class="brand">
<img src="../icons/icon32.png" alt="f0ckm Logo" class="brand-icon">
<span class="brand-name">f0ckm Uploader</span>
</div>
</div>
<div class="mode-selector">
<label class="mode-option">
<input type="radio" name="uploadMode" value="desktop" id="mode-desktop">
<span class="mode-pill">Desktop App (f0ckm://)</span>
</label>
<label class="mode-option">
<input type="radio" name="uploadMode" value="direct" id="mode-direct">
<span class="mode-pill">Direct API</span>
</label>
</div>
<div class="content">
<div id="active-tab-card" class="active-tab-card">
<div class="card-top">
<span id="site-badge" class="site-badge">🌐 Web Page</span>
<span id="tab-title" class="tab-title">Loading current tab...</span>
</div>
<div id="tab-url-preview" class="tab-url-preview">https://...</div>
<button id="btn-upload-tab" class="btn btn-primary btn-lg">
<span class="btn-icon">🚀</span> <span id="btn-upload-text">Upload to f0ckm API</span>
</button>
</div>
<div class="divider"><span>OR PASTE CUSTOM URL</span></div>
<div class="input-group">
<input type="url" id="url-input" placeholder="https://youtube.com/watch?v=..." autocomplete="off" spellcheck="false">
<button id="btn-upload-url" class="btn btn-accent">Upload URL</button>
</div>
<div id="status-card" class="status-card hidden">
<div id="status-spinner" class="spinner hidden"></div>
<div id="status-text" class="status-text"></div>
<div id="result-container" class="result-container hidden">
<a id="result-link" href="#" target="_blank" class="result-link">https://...</a>
<button id="btn-copy-result" class="btn btn-sm">Copy Link</button>
</div>
</div>
</div>
<div class="footer">
<span>f0ckm Integration v2.0</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,184 @@
function initPopup() {
const modeDesktop = document.getElementById("mode-desktop");
const modeDirect = document.getElementById("mode-direct");
const btnUploadTab = document.getElementById("btn-upload-tab");
const btnUploadText = document.getElementById("btn-upload-text");
const btnUploadUrl = document.getElementById("btn-upload-url");
const urlInput = document.getElementById("url-input");
const statusCard = document.getElementById("status-card");
const statusSpinner = document.getElementById("status-spinner");
const statusText = document.getElementById("status-text");
const resultContainer = document.getElementById("result-container");
const resultLink = document.getElementById("result-link");
const btnCopyResult = document.getElementById("btn-copy-result");
const siteBadge = document.getElementById("site-badge");
const tabTitle = document.getElementById("tab-title");
const tabUrlPreview = document.getElementById("tab-url-preview");
let activeTabUrl = "";
// 1. Load settings asynchronously without blocking UI initialization
try {
chrome.storage.sync.get({ uploadMode: "desktop" }, (settings) => {
if (chrome.runtime.lastError || !settings) return;
if (settings.uploadMode === "direct") {
if (modeDirect) modeDirect.checked = true;
} else {
if (modeDesktop) modeDesktop.checked = true;
}
});
} catch (e) {}
if (modeDesktop) {
modeDesktop.addEventListener("change", () => {
chrome.storage.sync.set({ uploadMode: "desktop" });
});
}
if (modeDirect) {
modeDirect.addEventListener("change", () => {
chrome.storage.sync.set({ uploadMode: "direct" });
});
}
// 2. Query Tab Info immediately
try {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs && tabs[0]) {
populateTabInfo(tabs[0]);
} else {
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs2) => {
if (tabs2 && tabs2[0]) {
populateTabInfo(tabs2[0]);
}
});
}
});
} catch (e) {}
function populateTabInfo(tab) {
if (!tab) return;
activeTabUrl = tab.url || tab.pendingUrl || "";
if (tabTitle) tabTitle.textContent = tab.title || "Active Page";
if (tabUrlPreview) tabUrlPreview.textContent = activeTabUrl || "No URL available";
const isYouTube = activeTabUrl.includes("youtube.com") || activeTabUrl.includes("youtu.be");
if (isYouTube) {
if (siteBadge) {
siteBadge.textContent = "▶ YouTube";
siteBadge.classList.add("youtube");
}
if (btnUploadText) btnUploadText.textContent = "Upload YouTube Video to f0ckm";
if (btnUploadTab) btnUploadTab.classList.add("youtube-style");
} else {
if (siteBadge) {
siteBadge.textContent = "🌐 Web Page";
siteBadge.classList.remove("youtube");
}
if (btnUploadText) btnUploadText.textContent = "Upload Page URL to f0ckm";
if (btnUploadTab) btnUploadTab.classList.remove("youtube-style");
}
}
// 3. Upload Active Tab Button Click Handler
if (btnUploadTab) {
btnUploadTab.addEventListener("click", () => {
if (activeTabUrl && (activeTabUrl.startsWith("http://") || activeTabUrl.startsWith("https://"))) {
triggerUpload(activeTabUrl);
} else {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs ? tabs[0] : null;
const url = tab ? (tab.url || tab.pendingUrl) : "";
if (url && (url.startsWith("http://") || url.startsWith("https://"))) {
activeTabUrl = url;
triggerUpload(url);
} else {
showStatus("Could not get active tab URL.", false);
}
});
}
});
}
// 4. Custom pasted URL Upload Button Click Handler
if (btnUploadUrl) {
btnUploadUrl.addEventListener("click", () => {
const url = urlInput ? urlInput.value.trim() : "";
if (url) {
triggerUpload(url);
} else {
showStatus("Please enter a valid URL.", false);
}
});
}
if (urlInput) {
urlInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && btnUploadUrl) {
btnUploadUrl.click();
}
});
}
if (btnCopyResult) {
btnCopyResult.addEventListener("click", () => {
const linkUrl = resultLink ? resultLink.href : "";
if (linkUrl && linkUrl !== "#" && !linkUrl.endsWith("#")) {
navigator.clipboard.writeText(linkUrl);
btnCopyResult.textContent = "Copied!";
setTimeout(() => {
btnCopyResult.textContent = "Copy Link";
}, 1500);
}
});
}
function showStatus(text, loading = false) {
if (!statusCard) return;
statusCard.classList.remove("hidden");
if (statusText) statusText.textContent = text;
if (loading) {
if (statusSpinner) statusSpinner.classList.remove("hidden");
if (resultContainer) resultContainer.classList.add("hidden");
} else {
if (statusSpinner) statusSpinner.classList.add("hidden");
}
}
function showSuccessResult(finalUrl) {
if (!statusCard) return;
statusCard.classList.remove("hidden");
if (statusSpinner) statusSpinner.classList.add("hidden");
if (statusText) statusText.textContent = "Upload Successful! Click link to open:";
if (resultLink) {
resultLink.href = finalUrl;
resultLink.textContent = finalUrl;
}
if (resultContainer) resultContainer.classList.remove("hidden");
}
function triggerUpload(url) {
const currentMode = (modeDirect && modeDirect.checked) ? "direct" : "desktop";
if (currentMode === "desktop") {
showStatus("Sending URL to f0ckm desktop app...", true);
chrome.runtime.sendMessage({ action: "upload_url", url: url }, () => {
showStatus("URL sent to f0ckm app! Check system tray / notifications.", false);
});
} else {
showStatus("Sending URL to f0ckm API...", true);
chrome.runtime.sendMessage({ action: "upload_url", url: url }, (response) => {
if (response && response.resultUrl) {
showSuccessResult(response.resultUrl);
} else {
showStatus("URL Upload queued! Link copied to clipboard.", false);
}
});
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPopup);
} else {
initPopup();
}