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,58 @@
# f0ckm Browser Integration Addon
A ShareX-style browser extension for **Chrome, Brave, Edge, Opera, and Firefox** that allows you to upload images, media, links, and URLs directly to **f0ckm** from any web browser context menu or toolbar popup.
---
## Features
- **Right-Click Context Menu (ShareX Style)**:
- **Upload image to f0ckm** (on any image)
- **Upload media to f0ckm** (on video/audio elements)
- **Upload link with f0ckm** (on links)
- **Upload selected URL with f0ckm** (on highlighted text URLs)
- **Upload page URL to f0ckm** (on page background)
- **Dual Upload Modes**:
- **Desktop App Mode (`f0ckm://`)** *(Recommended)*: Hands off downloads and uploads to the KDE desktop system tray app. Gives you live system tray digit progress (`0``99`%), Spectacle notifications, audio sounds, and local history gallery.
- **Direct API Mode**: Uploads directly from the browser background page using API URL and API Key without needing the tray app running.
- **Toolbar Popup Interface**:
- Upload current active tab URL with one click.
- Paste any URL for quick upload.
- Quick toggle between Desktop App Mode and Direct API Mode.
- **Global Keyboard Shortcut**: `Alt+Shift+U` to immediately upload current page URL.
---
## Installation Instructions
### Chrome, Brave, Edge, Opera (Chromium)
1. Open your browser and navigate to `chrome://extensions` (or `edge://extensions` in Edge).
2. Enable **Developer mode** (toggle in the top-right corner).
3. Click **Load unpacked** in the top-left menu.
4. Select the `browser-extension` folder in this repository (`/home/kibi/Projects/f0ckm-uploader/browser-extension`).
5. Pin the **f0ckm Uploader** icon to your toolbar for easy access!
### Mozilla Firefox
1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`.
2. Click **Load Temporary Add-on...**
3. Select `manifest.json` from the `browser-extension` folder.
---
## Usage
1. **Right-Click Context Menu**: Right-click any image, video, audio link, or page -> select **Upload to f0ckm**.
2. **Toolbar Popup**: Click the f0ckm icon in your browser toolbar to upload the active tab or paste a URL.
3. **Keyboard Shortcut**: Press `Alt+Shift+U` on any tab.
---
## Configuration
Click **Settings** (or right-click extension icon -> Options) to configure:
- **Upload Mode**: Desktop App (f0ckm://) vs Direct API.
- **API Endpoint URL**: Default `https://your-f0ckm-instance.com/api/v2/upload`.
- **API Key**: Secret API key for direct API mode.
- **Default Rating / Tags / Visibility / OC / Copied Link Format**.

View File

@@ -0,0 +1,242 @@
// f0ckm Uploader - Background Service Worker
const DEFAULT_SETTINGS = {
uploadMode: "desktop", // "desktop" (f0ckm://) or "direct" (API)
apiUrl: "https://f0ckm.com/api/v2/upload",
apiKey: "",
rating: "",
tags: "",
visibility: "0",
isOc: false,
urlType: "post"
};
async function getSettings() {
return new Promise((resolve) => {
chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => {
resolve(items || DEFAULT_SETTINGS);
});
});
}
const badgeCache = {};
function updateBadge(tabId, url) {
if (!tabId) return;
const isWeb = url && (url.startsWith("http://") || url.startsWith("https://"));
const isYT = isWeb && (url.includes("youtube.com") || url.includes("youtu.be"));
const targetText = isWeb ? "1" : "";
const targetColor = isYT ? "#ff0000" : "#0066ff";
const cacheKey = `${targetText}:${targetColor}`;
if (badgeCache[tabId] === cacheKey) {
return;
}
badgeCache[tabId] = cacheKey;
try {
if (targetText) {
chrome.action.setBadgeText({ text: targetText, tabId: tabId });
chrome.action.setBadgeBackgroundColor({ color: targetColor, tabId: tabId });
} else {
chrome.action.setBadgeText({ text: "", tabId: tabId });
}
} catch (e) {}
}
chrome.tabs.onActivated.addListener(async (activeInfo) => {
try {
const tab = await chrome.tabs.get(activeInfo.tabId);
if (tab) updateBadge(activeInfo.tabId, tab.url);
} catch (e) {}
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (tab && (changeInfo.status === "complete" || changeInfo.url)) {
updateBadge(tabId, tab.url);
}
});
function setupContextMenus() {
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: "f0ckm_upload_image",
title: "Upload image to f0ckm",
contexts: ["image"]
});
chrome.contextMenus.create({
id: "f0ckm_upload_video",
title: "Upload media to f0ckm",
contexts: ["video", "audio"]
});
chrome.contextMenus.create({
id: "f0ckm_upload_link",
title: "Upload link with f0ckm",
contexts: ["link"]
});
chrome.contextMenus.create({
id: "f0ckm_upload_selection",
title: "Upload selected URL with f0ckm",
contexts: ["selection"]
});
chrome.contextMenus.create({
id: "f0ckm_upload_page",
title: "Upload page URL to f0ckm",
contexts: ["page"]
});
});
}
chrome.runtime.onInstalled.addListener(() => {
setupContextMenus();
});
chrome.runtime.onStartup.addListener(() => {
setupContextMenus();
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
let targetUrl = "";
if (info.menuItemId === "f0ckm_upload_image" || info.menuItemId === "f0ckm_upload_video") {
targetUrl = info.srcUrl || info.linkUrl;
} else if (info.menuItemId === "f0ckm_upload_link") {
targetUrl = info.linkUrl;
} else if (info.menuItemId === "f0ckm_upload_selection") {
targetUrl = info.selectionText ? info.selectionText.trim() : "";
} else if (info.menuItemId === "f0ckm_upload_page") {
targetUrl = info.pageUrl || (tab ? tab.url : "");
}
if (targetUrl) {
await processUpload(targetUrl);
}
});
chrome.commands.onCommand.addListener(async (command) => {
if (command === "upload-page") {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs && tabs.length > 0 && tabs[0].url) {
await processUpload(tabs[0].url);
}
}
});
async function processUpload(targetUrl) {
if (!targetUrl) return;
const settings = await getSettings();
if (settings.uploadMode === "desktop") {
// 1. Try local loopback HTTP API first (Direct desktop handoff)
const localApiUrl = "http://127.0.0.1:18739/upload?url=" + encodeURIComponent(targetUrl);
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500);
const res = await fetch(localApiUrl, { signal: controller.signal });
clearTimeout(timeoutId);
if (res.ok) {
return;
}
} catch (err) {}
// 2. Fallback: Custom protocol scheme in active tab
const customUrl = "f0ckm://upload?url=" + encodeURIComponent(targetUrl);
try {
chrome.tabs.create({ url: customUrl, active: true }, (newTab) => {
if (newTab && newTab.id) {
setTimeout(() => {
chrome.tabs.remove(newTab.id).catch(() => {});
}, 4500);
}
});
} catch (e) {}
} else {
// Direct API Upload mode -> POST JSON payload with url to f0ckm API!
showNotification("Uploading...", `Uploading ${targetUrl.substring(0, 45)}...`, "uploading");
try {
if (!settings.apiUrl || !settings.apiKey) {
showNotification("Upload Error", "API URL and API Key must be configured in extension options.", "error");
return;
}
const payload = {
url: targetUrl,
tags: settings.tags || "url,upload",
visibility: settings.visibility || "0"
};
if (settings.rating && settings.rating !== "default") {
const ratingMap = { s: "sfw", q: "nsfw", e: "nsfl", sfw: "sfw", nsfw: "nsfw", nsfl: "nsfl" };
payload.rating = ratingMap[settings.rating.toLowerCase()] || settings.rating;
}
if (settings.isOc) payload.is_oc = "1";
const uploadRes = await fetch(settings.apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": settings.apiKey
},
body: JSON.stringify(payload)
});
const json = await uploadRes.json();
if (json.success && (json.url || json.file_url || json.direct_url || json.target_url || json.file)) {
const postUrl = json.url || json.post_url || "";
const directUrl = json.file_url || json.direct_url || json.target_url || json.file || "";
const resultUrl = (settings.urlType === "direct" && directUrl) ? directUrl : (postUrl || directUrl);
showNotification("Upload Successful!", `Link: ${resultUrl}`, "success");
return resultUrl;
} else {
const msg = json.msg || "Upload failed";
showNotification("Upload Failed", msg, "error");
}
} catch (err) {
showNotification("Upload Error", err.message || "An error occurred", "error");
}
}
}
function showNotification(title, message, iconType = "info") {
if (chrome.notifications) {
chrome.notifications.create({
type: "basic",
iconUrl: "icons/icon128.png",
title: title,
message: message,
priority: 2
});
}
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "get_active_tab") {
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
if (tabs && tabs.length > 0 && tabs[0].url) {
sendResponse({ tab: tabs[0] });
} else {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs2) => {
if (tabs2 && tabs2.length > 0 && tabs2[0].url) {
sendResponse({ tab: tabs2[0] });
} else {
chrome.tabs.query({ active: true }, (tabs3) => {
sendResponse({ tab: (tabs3 && tabs3.length > 0) ? tabs3[0] : null });
});
}
});
}
});
return true;
} else if (request.action === "upload_url") {
processUpload(request.url).then((resUrl) => {
sendResponse({ status: "ok", resultUrl: resUrl || "" });
});
return true;
}
});

View File

@@ -0,0 +1,46 @@
{
"manifest_version": 3,
"name": "f0ckm Uploader",
"version": "2.0",
"description": "Upload images, media, links, and URLs directly to f0ckm from context menu or toolbar.",
"permissions": [
"contextMenus",
"storage",
"notifications",
"activeTab",
"clipboardWrite"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": true
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"commands": {
"upload-page": {
"suggested_key": {
"default": "Alt+Shift+U"
},
"description": "Upload current page URL to f0ckm"
}
}
}

View File

@@ -0,0 +1,50 @@
{
"manifest_version": 3,
"name": "f0ckm Uploader",
"version": "2.0",
"description": "Upload images, media, links, and URLs directly to f0ckm from context menu or toolbar.",
"permissions": [
"contextMenus",
"storage",
"notifications",
"activeTab",
"tabs",
"clipboardWrite"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"scripts": ["background.js"]
},
"action": {
"default_popup": "popup/popup.html",
"default_title": "f0ckm Uploader",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"default_area": "navbar"
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": true
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"commands": {
"upload-page": {
"suggested_key": {
"default": "Alt+Shift+U"
},
"description": "Upload current page URL to f0ckm"
}
}
}

View File

@@ -0,0 +1,197 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
background-color: #0f1015;
color: #dce0ed;
font-size: 14px;
line-height: 1.5;
padding: 40px 20px;
display: flex;
justify-content: center;
}
.container {
width: 100%;
max-width: 640px;
background: #161822;
border: 1px solid #242738;
border-radius: 12px;
padding: 28px 32px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
}
.header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 20px;
border-bottom: 1px solid #242738;
margin-bottom: 24px;
}
.logo {
width: 48px;
height: 48px;
}
h1 {
font-size: 20px;
font-weight: 700;
color: #ffffff;
}
.subtitle {
font-size: 13px;
color: #7a829a;
}
.section {
margin-bottom: 24px;
}
h2 {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #4da6ff;
margin-bottom: 12px;
}
.field-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.radio-label {
display: flex;
align-items: flex-start;
gap: 12px;
background: #1c1e2d;
border: 1px solid #2a2e44;
padding: 12px 14px;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.radio-label:hover {
border-color: #3d4464;
}
.radio-label input[type="radio"] {
margin-top: 3px;
accent-color: #0066ff;
}
.radio-content strong {
display: block;
font-size: 13px;
color: #ffffff;
}
.radio-content p {
font-size: 12px;
color: #8c93aa;
margin-top: 2px;
}
.field {
margin-bottom: 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-size: 12px;
font-weight: 600;
color: #b0b6cc;
}
input[type="text"],
input[type="url"],
input[type="password"],
select {
background: #12131b;
border: 1px solid #292d42;
color: #ffffff;
padding: 9px 12px;
border-radius: 7px;
font-size: 13px;
outline: none;
width: 100%;
transition: border-color 0.2s ease;
}
input:focus, select:focus {
border-color: #0066ff;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.field-row {
margin-bottom: 14px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 13px;
color: #dce0ed;
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #0066ff;
cursor: pointer;
}
.actions {
display: flex;
align-items: center;
gap: 14px;
margin-top: 28px;
padding-top: 20px;
border-top: 1px solid #242738;
}
.btn-save {
background: linear-gradient(135deg, #0066ff, #0044cc);
color: #ffffff;
border: none;
padding: 10px 20px;
font-size: 13px;
font-weight: 700;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-save:hover {
background: linear-gradient(135deg, #1a75ff, #0052e6);
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.3);
}
.toast {
font-size: 13px;
font-weight: 600;
color: #2ed573;
}
.toast.hidden {
display: none;
}

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>f0ckm Extension Options</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="container">
<div class="header">
<img src="../icons/icon48.png" alt="f0ckm Icon" class="logo">
<div>
<h1>f0ckm Browser Integration Settings</h1>
<p class="subtitle">Configure browser extension upload modes and API settings</p>
</div>
</div>
<form id="settings-form">
<div class="section">
<h2>Integration Mode</h2>
<div class="field-group">
<label class="radio-label">
<input type="radio" name="uploadMode" value="desktop" id="mode-desktop">
<div class="radio-content">
<strong>Desktop Application Integration (f0ckm://)</strong>
<p>Delegates downloads & uploads directly to the KDE system tray app. Provides live tray digit icons, Spectacle notifications, sound effects, and local gallery history.</p>
</div>
</label>
<label class="radio-label">
<input type="radio" name="uploadMode" value="direct" id="mode-direct">
<div class="radio-content">
<strong>Direct Browser Extension API Upload</strong>
<p>Uploads files directly from the browser background page using API URL and API Key set below.</p>
</div>
</label>
</div>
</div>
<div class="section">
<h2>API & Server Configuration</h2>
<div class="field">
<label for="apiUrl">API Endpoint URL</label>
<input type="url" id="apiUrl" placeholder="https://your-f0ckm-instance.com/api/v2/upload">
</div>
<div class="field">
<label for="apiKey">API Key</label>
<input type="password" id="apiKey" placeholder="Enter your secret API key">
</div>
</div>
<div class="section">
<h2>Default Upload Preferences</h2>
<div class="grid-2">
<div class="field">
<label for="rating">Default Rating</label>
<select id="rating">
<option value="">Default (Untagged)</option>
<option value="sfw">Safe (sfw)</option>
<option value="nsfw">Questionable (nsfw)</option>
<option value="nsfl">Explicit (nsfl)</option>
</select>
</div>
<div class="field">
<label for="visibility">Default Visibility</label>
<select id="visibility">
<option value="0">Public (0)</option>
<option value="1">Unlisted (1)</option>
<option value="2">Private (2)</option>
</select>
</div>
</div>
<div class="field">
<label for="tags">Default Tags</label>
<input type="text" id="tags" placeholder="e.g. browser, web, media">
</div>
<div class="field-row">
<label class="checkbox-label">
<input type="checkbox" id="isOc">
<span>Mark uploads as Original Content (OC)</span>
</label>
</div>
<div class="field">
<label for="urlType">Copied Link Format</label>
<select id="urlType">
<option value="post">f0ckm Post URL (Viewer page)</option>
<option value="direct">Direct File Link (Raw image/media file)</option>
</select>
</div>
</div>
<div class="actions">
<button type="submit" id="btn-save" class="btn-save">Save Settings</button>
<span id="save-toast" class="toast hidden">✓ Settings saved successfully!</span>
</div>
</form>
</div>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,63 @@
const DEFAULT_SETTINGS = {
uploadMode: "desktop",
apiUrl: "https://f0ckm.com/api/v2/upload",
apiKey: "",
rating: "s",
tags: "",
visibility: "0",
isOc: false,
urlType: "post"
};
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("settings-form");
const modeDesktop = document.getElementById("mode-desktop");
const modeDirect = document.getElementById("mode-direct");
const apiUrl = document.getElementById("apiUrl");
const apiKey = document.getElementById("apiKey");
const rating = document.getElementById("rating");
const visibility = document.getElementById("visibility");
const tags = document.getElementById("tags");
const isOc = document.getElementById("isOc");
const urlType = document.getElementById("urlType");
const saveToast = document.getElementById("save-toast");
// Load existing settings
chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => {
if (items.uploadMode === "direct") {
modeDirect.checked = true;
} else {
modeDesktop.checked = true;
}
apiUrl.value = items.apiUrl || "";
apiKey.value = items.apiKey || "";
rating.value = items.rating || "s";
visibility.value = items.visibility !== undefined ? String(items.visibility) : "0";
tags.value = items.tags || "";
isOc.checked = !!items.isOc;
urlType.value = items.urlType || "post";
});
// Save settings on form submit
form.addEventListener("submit", (e) => {
e.preventDefault();
const newSettings = {
uploadMode: modeDirect.checked ? "direct" : "desktop",
apiUrl: apiUrl.value.trim(),
apiKey: apiKey.value.trim(),
rating: rating.value,
visibility: visibility.value,
tags: tags.value.trim(),
isOc: isOc.checked,
urlType: urlType.value
};
chrome.storage.sync.set(newSettings, () => {
saveToast.classList.remove("hidden");
setTimeout(() => {
saveToast.classList.add("hidden");
}, 2500);
});
});
});

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();
}