adding browser extension and some QoL
This commit is contained in:
34
README.md
34
README.md
@@ -10,11 +10,13 @@ A modern, high-contrast **KDE Plasma System Tray Uploader** and desktop integrat
|
||||
- **Single-Instance IPC Architecture**: 100% Python-based daemon (`PySide6`). Multiple invocations (hotkeys, Dolphin context menu) delegate directly to the running tray instance without spawning duplicates.
|
||||
- **Dolphin Context Menu Integration**: Right-click any file in Dolphin -> **Actions** -> **Upload to f0ckm**.
|
||||
- **Spectacle Screenshot Capture**: Region capture integration via hotkeys or menu trigger (`f0ckm-uploader-gui --spectacle`).
|
||||
- **Browser Extension Addon (ShareX Style)**: Right-click any image, video, link, or URL on the web in Chrome, Firefox, Brave, or Edge -> **Upload with f0ckm**.
|
||||
- **Custom Protocol Scheme (`f0ckm://`)**: Seamless URL handling (`f0ckm://upload?url=...`) delegating background remote file downloading & live tray progress directly to the system tray app.
|
||||
- **Smart Notification Previews**:
|
||||
- **Images**: Fast native scaling via PySide6.
|
||||
- **Videos** (`.mp4`, `.webm`, etc.): Automatic frame extraction at `00:00:01` via `ffmpeg`.
|
||||
- **Other Files**: Sleek placeholder file badges showing the file extension.
|
||||
- **Clipboard & Settings GUI**: Direct clipboard image/path uploads and a built-in Settings dialog for configuring API keys, default tags, visibility, rating, and autostart.
|
||||
- **Clipboard & Settings GUI**: Direct clipboard image/path uploads, URL dialog uploads, and a built-in Settings dialog for configuring API keys, default tags, visibility, rating, and autostart.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,6 +106,7 @@ To bind a keyboard shortcut to capture a screen region and upload it immediately
|
||||
- **Right Click**: Opens context menu for:
|
||||
- **Capture Region & Upload**
|
||||
- **Upload File...**
|
||||
- **Upload URL...**
|
||||
- **Upload from Clipboard**
|
||||
- **Settings...**
|
||||
- **Quit**
|
||||
@@ -119,15 +122,42 @@ To bind a keyboard shortcut to capture a screen region and upload it immediately
|
||||
# Capture screen region and upload
|
||||
f0ckm-uploader-gui --spectacle
|
||||
|
||||
# Upload a specific file
|
||||
# Upload a specific local file
|
||||
f0ckm-uploader-gui --upload="/path/to/file.mp4"
|
||||
|
||||
# Upload a remote image or media URL
|
||||
f0ckm-uploader-gui --upload="https://example.com/image.png"
|
||||
|
||||
# Launch via custom protocol handler
|
||||
f0ckm-uploader-gui "f0ckm://upload?url=https://example.com/image.png"
|
||||
|
||||
# Open settings dialog
|
||||
f0ckm-uploader-gui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browser Extension (ShareX Addon)
|
||||
|
||||
A WebExtension is provided in the `browser-extension/` directory.
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Chrome / Brave / Edge**:
|
||||
- Go to `chrome://extensions`, enable **Developer mode**.
|
||||
- Click **Load unpacked** and select the `browser-extension` folder.
|
||||
2. **Firefox**:
|
||||
- Go to `about:debugging#/runtime/this-firefox`.
|
||||
- Click **Load Temporary Add-on...** and select `browser-extension/manifest.json`.
|
||||
|
||||
### Features
|
||||
|
||||
- **Context Menu**: Right-click any image, video, audio link, or URL on any webpage -> **Upload to f0ckm**.
|
||||
- **Toolbar Popup**: Click extension icon to upload active tab or paste a URL.
|
||||
- **Keyboard Shortcut**: `Alt+Shift+U` to upload current tab URL.
|
||||
|
||||
---
|
||||
|
||||
## Configuration & Storage Paths
|
||||
|
||||
- **Configuration File**: `~/.config/f0ckm-uploader/config.json`
|
||||
|
||||
58
browser-extension/README.md
Normal file
58
browser-extension/README.md
Normal 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**.
|
||||
242
browser-extension/background.js
Normal file
242
browser-extension/background.js
Normal 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;
|
||||
}
|
||||
});
|
||||
46
browser-extension/manifest.chrome.json
Normal file
46
browser-extension/manifest.chrome.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
browser-extension/manifest.json
Normal file
50
browser-extension/manifest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
197
browser-extension/options/options.css
Normal file
197
browser-extension/options/options.css
Normal 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;
|
||||
}
|
||||
103
browser-extension/options/options.html
Normal file
103
browser-extension/options/options.html
Normal 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>
|
||||
63
browser-extension/options/options.js
Normal file
63
browser-extension/options/options.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
317
browser-extension/popup/popup.css
Normal file
317
browser-extension/popup/popup.css
Normal 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;
|
||||
}
|
||||
61
browser-extension/popup/popup.html
Normal file
61
browser-extension/popup/popup.html
Normal 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>
|
||||
184
browser-extension/popup/popup.js
Normal file
184
browser-extension/popup/popup.js
Normal 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();
|
||||
}
|
||||
@@ -3,11 +3,12 @@ Type=Application
|
||||
Name=f0ckm Uploader
|
||||
GenericName=Image & Media Uploader
|
||||
Comment=System tray GUI uploader for f0ckm
|
||||
Exec=__EXEC_PATH__
|
||||
Exec=__EXEC_PATH__ %u
|
||||
Icon=__ICON_PATH__
|
||||
Terminal=false
|
||||
Categories=Utility;Network;
|
||||
Keywords=upload;screenshot;f0ckm;tray;
|
||||
MimeType=x-scheme-handler/f0ckm;x-scheme-handler/f0ckm-uploader;
|
||||
StartupNotify=false
|
||||
Actions=capture;
|
||||
|
||||
|
||||
378
gui.py
378
gui.py
@@ -13,6 +13,17 @@ import re
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
from PySide6.QtCore import Qt, QThread, QObject, Signal, QUrl, QFile, QIODevice, QTimer, QMimeData, QProcess
|
||||
from PySide6.QtGui import QIcon, QAction, QPixmap, QPainter, QColor, QFont, QPen, QImage, QDesktopServices, QDrag, QClipboard
|
||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart, QNetworkReply, QLocalServer, QLocalSocket
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QGroupBox,
|
||||
QFormLayout, QMessageBox, QFileDialog, QSlider, QScrollArea, QFrame, QGridLayout, QWidget, QSizePolicy, QInputDialog
|
||||
)
|
||||
|
||||
def normalize_file_path(path_str: str) -> str:
|
||||
if not path_str:
|
||||
@@ -26,6 +37,90 @@ def normalize_file_path(path_str: str) -> str:
|
||||
path_str = urllib.parse.unquote(path_str[7:])
|
||||
return os.path.abspath(path_str)
|
||||
|
||||
def parse_upload_target(arg: str) -> tuple[str, bool]:
|
||||
if not arg:
|
||||
return ("", False)
|
||||
|
||||
arg = arg.strip().strip("'\"")
|
||||
|
||||
if arg.startswith("f0ckm://") or arg.startswith("f0ckm-uploader://"):
|
||||
parsed = urllib.parse.urlparse(arg)
|
||||
query_params = urllib.parse.parse_qs(parsed.query)
|
||||
target_url = ""
|
||||
if "url" in query_params and query_params["url"]:
|
||||
target_url = query_params["url"][0]
|
||||
else:
|
||||
raw_path = parsed.netloc + parsed.path
|
||||
if parsed.query:
|
||||
raw_path += "?" + parsed.query
|
||||
raw_path = raw_path.lstrip("/")
|
||||
if raw_path.startswith("upload/"):
|
||||
raw_path = raw_path[7:]
|
||||
elif raw_path.startswith("upload?"):
|
||||
raw_path = raw_path[7:]
|
||||
target_url = raw_path
|
||||
|
||||
target_url = urllib.parse.unquote(target_url)
|
||||
if target_url.startswith("http://") or target_url.startswith("https://"):
|
||||
print(f"[f0ckm-gui] [PARSE] Protocol URL parsed: '{arg}' -> target='{target_url}' (is_url=True)", flush=True)
|
||||
return (target_url, True)
|
||||
|
||||
if arg.startswith("http://") or arg.startswith("https://"):
|
||||
print(f"[f0ckm-gui] [PARSE] Direct URL parsed: '{arg}' (is_url=True)", flush=True)
|
||||
return (arg, True)
|
||||
|
||||
norm = normalize_file_path(arg)
|
||||
print(f"[f0ckm-gui] [PARSE] File path parsed: '{arg}' -> norm='{norm}' (is_url=False)", flush=True)
|
||||
return (norm, False)
|
||||
|
||||
class LocalHTTPBridge(QObject):
|
||||
request_upload = Signal(str)
|
||||
|
||||
local_http_bridge = LocalHTTPBridge()
|
||||
|
||||
class LocalHTTPHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
query = urllib.parse.parse_qs(parsed.query)
|
||||
if parsed.path in ("/upload", "/upload/") and "url" in query:
|
||||
target_url = query["url"][0]
|
||||
print(f"[f0ckm-gui] [LOCAL HTTP API] Received direct upload request for URL: {target_url}", flush=True)
|
||||
local_http_bridge.request_upload.emit(target_url)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"success": true, "msg": "Queued upload"}')
|
||||
elif parsed.path in ("/ping", "/ping/"):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"status": "ok", "app": "f0ckm-uploader"}')
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"error": "Not Found"}')
|
||||
except Exception as e:
|
||||
print(f"[f0ckm-gui] [LOCAL HTTP ERROR] {e}", flush=True)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def start_local_http_server(tray_app, port=18739):
|
||||
try:
|
||||
server = HTTPServer(("127.0.0.1", port), LocalHTTPHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
print(f"[f0ckm-gui] [LOCAL HTTP API] Started loopback server on http://127.0.0.1:{port}", flush=True)
|
||||
return server
|
||||
except Exception as e:
|
||||
print(f"[f0ckm-gui] [LOCAL HTTP API WARNING] Could not start HTTP server on port {port}: {e}", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
def copy_to_clipboard(text: str):
|
||||
if not text:
|
||||
return
|
||||
@@ -53,14 +148,6 @@ def copy_to_clipboard(text: str):
|
||||
subprocess.run(["xsel", "--clipboard", "--input"], input=text.encode("utf-8"), check=False)
|
||||
except Exception as e:
|
||||
print(f"xsel error: {e}")
|
||||
from PySide6.QtCore import Qt, QThread, QObject, Signal, QUrl, QFile, QIODevice, QTimer, QMimeData, QProcess
|
||||
from PySide6.QtGui import QIcon, QAction, QPixmap, QPainter, QColor, QFont, QPen, QImage, QDesktopServices, QDrag, QClipboard
|
||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QHttpMultiPart, QHttpPart, QNetworkReply, QLocalServer, QLocalSocket
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QGroupBox,
|
||||
QFormLayout, QMessageBox, QFileDialog, QSlider, QScrollArea, QFrame, QGridLayout, QWidget, QSizePolicy
|
||||
)
|
||||
|
||||
# Configuration paths
|
||||
CONFIG_DIR = os.path.expanduser("~/.config/f0ckm-uploader")
|
||||
@@ -1047,6 +1134,117 @@ class StreamingMultipartEncoder:
|
||||
self.total_size = len(self.header_bytes) + self.file_size + len(self.footer_bytes)
|
||||
self.content_type = f"multipart/form-data; boundary={self.boundary}"
|
||||
|
||||
class URLPostThread(QThread):
|
||||
finished = Signal(bool, str) # success, response_text_or_error
|
||||
|
||||
def __init__(self, api_url, api_key, url_param, rating, tags, visibility, is_oc, parent=None):
|
||||
super().__init__(parent)
|
||||
self.api_url = api_url
|
||||
self.api_key = api_key
|
||||
self.url_param = url_param
|
||||
self.rating = rating
|
||||
self.tags = tags
|
||||
self.visibility = visibility
|
||||
self.is_oc = is_oc
|
||||
|
||||
def run(self):
|
||||
print(f"[f0ckm-gui] [URL POST START] Sending JSON URL payload '{self.url_param}' to '{self.api_url}'", flush=True)
|
||||
try:
|
||||
payload = {
|
||||
"url": self.url_param,
|
||||
"tags": self.tags if self.tags else "url,upload",
|
||||
"visibility": str(self.visibility) if self.visibility is not None else "0"
|
||||
}
|
||||
if self.rating and str(self.rating).lower() not in ["", "default", "none"]:
|
||||
rating_map = {"s": "sfw", "q": "nsfw", "e": "nsfl", "sfw": "sfw", "nsfw": "nsfw", "nsfl": "nsfl"}
|
||||
payload["rating"] = rating_map.get(str(self.rating).lower(), str(self.rating).lower())
|
||||
if self.is_oc:
|
||||
payload["is_oc"] = "1"
|
||||
|
||||
json_bytes = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
self.api_url,
|
||||
data=json_bytes,
|
||||
headers={
|
||||
"User-Agent": "f0ckm-uploader/2.0",
|
||||
"X-Api-Key": self.api_key,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
resp_text = resp.read().decode("utf-8")
|
||||
print(f"[f0ckm-gui] [URL POST OK] Server Response: {resp_text}", flush=True)
|
||||
self.finished.emit(True, resp_text)
|
||||
except Exception as e:
|
||||
print(f"[f0ckm-gui] [URL POST ERROR] {e}", flush=True)
|
||||
self.finished.emit(False, str(e))
|
||||
|
||||
class StreamingDownloadThread(QThread):
|
||||
progress = Signal(int, int) # bytes_received, total_bytes
|
||||
finished = Signal(bool, str) # success, file_path_or_error_msg
|
||||
|
||||
def __init__(self, url, parent=None):
|
||||
super().__init__(parent)
|
||||
self.url = url
|
||||
self.aborted = False
|
||||
|
||||
def abort(self):
|
||||
self.aborted = True
|
||||
|
||||
def run(self):
|
||||
print(f"[f0ckm-gui] [DOWNLOAD START] Requesting remote URL: {self.url}", flush=True)
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.url,
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
total_size = int(resp.headers.get("Content-Length", 0))
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
print(f"[f0ckm-gui] [DOWNLOAD HTTP OK] Content-Type: '{content_type}', Length: {total_size} bytes", flush=True)
|
||||
|
||||
url_path = urllib.parse.urlparse(self.url).path
|
||||
ext = os.path.splitext(url_path)[1].lower()
|
||||
if not ext or len(ext) > 6:
|
||||
mime_base = content_type.split(";")[0].strip() if content_type else ""
|
||||
ext = mimetypes.guess_extension(mime_base) or ".png"
|
||||
if ext == ".jpe":
|
||||
ext = ".jpg"
|
||||
|
||||
clean_name = os.path.basename(url_path) or f"remote_{int(time.time())}"
|
||||
if not os.path.splitext(clean_name)[1]:
|
||||
clean_name += ext
|
||||
|
||||
temp_dir = tempfile.gettempdir()
|
||||
temp_path = os.path.join(temp_dir, f"f0ckm_remote_{int(time.time())}_{clean_name}")
|
||||
|
||||
received = 0
|
||||
chunk_size = 32768
|
||||
with open(temp_path, "wb") as f:
|
||||
while True:
|
||||
if self.aborted:
|
||||
print("[f0ckm-gui] [DOWNLOAD ABORTED]", flush=True)
|
||||
self.finished.emit(False, "ABORTED")
|
||||
return
|
||||
chunk = resp.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
received += len(chunk)
|
||||
self.progress.emit(received, total_size)
|
||||
|
||||
print(f"[f0ckm-gui] [DOWNLOAD COMPLETE] Saved remote file to: {temp_path} ({received} bytes)", flush=True)
|
||||
self.finished.emit(True, temp_path)
|
||||
except Exception as e:
|
||||
print(f"[f0ckm-gui] [DOWNLOAD ERROR] Failed to fetch {self.url}: {e}", flush=True)
|
||||
if self.aborted:
|
||||
self.finished.emit(False, "ABORTED")
|
||||
else:
|
||||
self.finished.emit(False, str(e))
|
||||
|
||||
class StreamingUploadThread(QThread):
|
||||
progress = Signal(int, int) # bytes_sent, bytes_total
|
||||
finished = Signal(bool, str) # success, response_or_error_msg
|
||||
@@ -1072,6 +1270,7 @@ class StreamingUploadThread(QThread):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
print(f"[f0ckm-gui] [UPLOAD START] File: '{self.file_path}' -> Target API: '{self.api_url}'", flush=True)
|
||||
fields = {
|
||||
"rating": self.rating,
|
||||
"tags": self.tags,
|
||||
@@ -1232,6 +1431,10 @@ class SystemTrayApp(QObject):
|
||||
upload_action.triggered.connect(self.on_upload_file)
|
||||
self.menu.addAction(upload_action)
|
||||
|
||||
upload_url_action = QAction("Upload URL...", self.menu)
|
||||
upload_url_action.triggered.connect(self.on_upload_url)
|
||||
self.menu.addAction(upload_url_action)
|
||||
|
||||
clipboard_action = QAction("Upload from Clipboard", self.menu)
|
||||
clipboard_action.triggered.connect(self.on_upload_clipboard)
|
||||
self.menu.addAction(clipboard_action)
|
||||
@@ -1257,10 +1460,18 @@ class SystemTrayApp(QObject):
|
||||
|
||||
self.tray.setContextMenu(self.menu)
|
||||
self.tray.activated.connect(self.on_tray_activated)
|
||||
self.last_uploaded_url = ""
|
||||
self.tray.messageClicked.connect(self.on_notification_clicked)
|
||||
|
||||
self.settings_dialog = None
|
||||
self.gallery_window = None
|
||||
|
||||
def on_notification_clicked(self):
|
||||
url = getattr(self, "last_uploaded_url", "")
|
||||
if url:
|
||||
print(f"[f0ckm-gui] Notification clicked -> Opening URL in browser: '{url}'", flush=True)
|
||||
QDesktopServices.openUrl(QUrl(url))
|
||||
|
||||
def on_open_gallery(self):
|
||||
if self.gallery_window is None:
|
||||
self.gallery_window = GalleryWindow()
|
||||
@@ -1327,11 +1538,114 @@ class SystemTrayApp(QObject):
|
||||
self.upload_file_direct(save_path)
|
||||
|
||||
def on_abort_upload(self):
|
||||
if hasattr(self, "download_thread") and self.download_thread and self.download_thread.isRunning():
|
||||
self.download_thread.abort()
|
||||
if self.current_reply and self.current_reply.isRunning():
|
||||
self.current_reply.abort()
|
||||
elif hasattr(self, "upload_thread") and self.upload_thread:
|
||||
elif hasattr(self, "upload_thread") and self.upload_thread and self.upload_thread.isRunning():
|
||||
self.upload_thread.abort()
|
||||
|
||||
def upload_target_direct(self, target):
|
||||
target_str, is_url = parse_upload_target(target)
|
||||
print(f"[f0ckm-gui] [TRAY ACTION] Received target: '{target}' -> target_str='{target_str}', is_url={is_url}", flush=True)
|
||||
if not target_str:
|
||||
return
|
||||
if is_url:
|
||||
self.download_and_upload_url(target_str)
|
||||
else:
|
||||
self.upload_file_direct(target_str)
|
||||
|
||||
def download_and_upload_url(self, url):
|
||||
config = get_env_config()
|
||||
api_url = config.get("api_url", "")
|
||||
api_key = config.get("api_key", "")
|
||||
rating = config.get("default_rating", "")
|
||||
tags = config.get("default_tags", "")
|
||||
visibility = config.get("default_visibility", "0")
|
||||
is_oc = config.get("default_is_oc", False)
|
||||
|
||||
print(f"[f0ckm-gui] [URL UPLOAD] Initiating URL upload: '{url}' (API URL: '{api_url}')", flush=True)
|
||||
if not api_url or not api_key:
|
||||
print(f"[f0ckm-gui] [CONFIG ERROR] API URL or API Key missing in configuration!", flush=True)
|
||||
self.show_message("Upload Error", "API URL and API Key must be set in Settings.", QSystemTrayIcon.Critical)
|
||||
return
|
||||
|
||||
self.download_url = url
|
||||
self.download_start_time = time.time()
|
||||
self.tray.setIcon(create_digit_icon(50))
|
||||
self.tray.setToolTip(f"Uploading URL to f0ckm...\n{url[:45]}")
|
||||
|
||||
self.url_post_thread = URLPostThread(api_url, api_key, url, rating, tags, visibility, is_oc)
|
||||
self.url_post_thread.finished.connect(lambda ok, res: self.on_url_post_finished(ok, res, url))
|
||||
self.url_post_thread.start()
|
||||
self.abort_action.setEnabled(True)
|
||||
|
||||
def on_url_post_finished(self, ok, result, url):
|
||||
if ok:
|
||||
try:
|
||||
data = json.loads(result)
|
||||
if data.get("success") and (data.get("url") or data.get("file_url") or data.get("direct_url") or data.get("post_url") or data.get("file")):
|
||||
post_url = data.get("url") or data.get("post_url") or ""
|
||||
direct_url = data.get("file_url") or data.get("direct_url") or data.get("file") or ""
|
||||
config = get_env_config()
|
||||
url_type = config.get("clipboard_url_type", "post").lower()
|
||||
final_url = direct_url if (url_type == "direct" and direct_url) else (post_url or direct_url)
|
||||
|
||||
copy_to_clipboard(final_url)
|
||||
play_success_sound(config.get("sound_file"), config.get("sound_volume"))
|
||||
|
||||
entry = {
|
||||
"timestamp": int(time.time()),
|
||||
"file_path": url,
|
||||
"file_name": url.split("/")[-1] or "url_upload",
|
||||
"file_size": 0,
|
||||
"url": final_url,
|
||||
"post_url": post_url,
|
||||
"file_url": direct_url
|
||||
}
|
||||
add_history_entry(entry)
|
||||
|
||||
self.abort_action.setEnabled(False)
|
||||
self.tray.setIcon(self.default_icon)
|
||||
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
|
||||
self.last_uploaded_url = final_url
|
||||
self.show_success_notification(url, final_url)
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"[f0ckm-gui] JSON parse notice: {e}", flush=True)
|
||||
|
||||
print(f"[f0ckm-gui] Direct URL API notice: Falling back to local download of '{url}'", flush=True)
|
||||
self.download_thread = StreamingDownloadThread(url)
|
||||
self.download_thread.progress.connect(self.on_download_progress)
|
||||
self.download_thread.finished.connect(lambda ok_dl, res_dl: self.on_download_finished(ok_dl, res_dl, url))
|
||||
self.download_thread.start()
|
||||
|
||||
def on_download_progress(self, bytes_received, bytes_total):
|
||||
if bytes_total > 0:
|
||||
pct = min(99, max(0, int((bytes_received / bytes_total) * 100)))
|
||||
self.tray.setIcon(create_digit_icon(pct))
|
||||
rec_str = format_size(bytes_received)
|
||||
tot_str = format_size(bytes_total)
|
||||
url_str = getattr(self, "download_url", "")
|
||||
short_url = (url_str[:35] + "...") if len(url_str) > 35 else url_str
|
||||
self.tray.setToolTip(f"Downloading: {short_url}\n{pct}% ({rec_str} / {tot_str})")
|
||||
else:
|
||||
rec_str = format_size(bytes_received)
|
||||
self.tray.setToolTip(f"Downloading remote file...\n{rec_str}")
|
||||
|
||||
def on_download_finished(self, success, result, url):
|
||||
if not success:
|
||||
self.abort_action.setEnabled(False)
|
||||
self.tray.setIcon(self.default_icon)
|
||||
self.tray.setToolTip("f0ckm Uploader (Click to capture & upload)")
|
||||
if result == "ABORTED":
|
||||
self.show_message("Download Aborted", "Remote URL download was canceled.", QSystemTrayIcon.Information)
|
||||
else:
|
||||
self.show_message("Download Failed", f"Failed to download remote URL: {result}", QSystemTrayIcon.Critical)
|
||||
return
|
||||
|
||||
self.upload_file_direct(result)
|
||||
|
||||
def upload_file_direct(self, file_path):
|
||||
file_path = normalize_file_path(file_path)
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
@@ -1464,6 +1778,7 @@ class SystemTrayApp(QObject):
|
||||
self.show_message("Upload Error", f"Error parsing response: {e}\nResponse: {response_text}", QSystemTrayIcon.Critical)
|
||||
|
||||
def show_success_notification(self, file_path, item_url):
|
||||
self.last_uploaded_url = item_url
|
||||
config = get_env_config()
|
||||
if config.get("play_success_sound", False) or config.get("success_audio_path"):
|
||||
audio_path = config.get("success_audio_path", "")
|
||||
@@ -1525,6 +1840,11 @@ class SystemTrayApp(QObject):
|
||||
)
|
||||
if file_path:
|
||||
self.upload_file_direct(file_path)
|
||||
|
||||
def on_upload_url(self):
|
||||
url, ok = QInputDialog.getText(None, "Upload URL", "Enter image or file URL:")
|
||||
if ok and url.strip():
|
||||
self.upload_target_direct(url.strip())
|
||||
|
||||
def on_upload_clipboard(self):
|
||||
clipboard = QApplication.clipboard()
|
||||
@@ -1586,14 +1906,19 @@ class SingleInstanceApp:
|
||||
if arg in ("--spectacle", "-s"):
|
||||
msg = "spectacle"
|
||||
elif arg.startswith("--upload="):
|
||||
msg = f"upload:{arg[9:]}"
|
||||
target, _ = parse_upload_target(arg[9:])
|
||||
msg = f"upload:{target}"
|
||||
elif arg.startswith("--upload"):
|
||||
if len(sys.argv) > 2:
|
||||
msg = f"upload:{sys.argv[2]}"
|
||||
target, _ = parse_upload_target(sys.argv[2])
|
||||
msg = f"upload:{target}"
|
||||
else:
|
||||
msg = "show"
|
||||
else:
|
||||
norm = normalize_file_path(arg)
|
||||
if os.path.exists(norm):
|
||||
msg = f"upload:{norm}"
|
||||
target, _ = parse_upload_target(arg)
|
||||
if target:
|
||||
msg = f"upload:{target}"
|
||||
print(f"[f0ckm-gui] [IPC CLIENT] Daemon running. Forwarding message via socket: '{msg}'", flush=True)
|
||||
socket.write(msg.encode("utf-8"))
|
||||
socket.waitForBytesWritten(500)
|
||||
socket.disconnectFromServer()
|
||||
@@ -1603,6 +1928,7 @@ class SingleInstanceApp:
|
||||
if not self.server.listen(self.name):
|
||||
return False
|
||||
|
||||
print(f"[f0ckm-gui] [IPC SERVER] Started single-instance IPC listener socket server: '{self.name}'", flush=True)
|
||||
self.server.newConnection.connect(lambda: self._handle_connection(callback_on_message))
|
||||
return True
|
||||
|
||||
@@ -1613,6 +1939,7 @@ class SingleInstanceApp:
|
||||
|
||||
def _read_message(self, socket, callback):
|
||||
data = socket.readAll().data().decode("utf-8").strip()
|
||||
print(f"[f0ckm-gui] [IPC SERVER] Received message from socket: '{data}'", flush=True)
|
||||
callback(data)
|
||||
socket.disconnectFromServer()
|
||||
|
||||
@@ -1637,11 +1964,8 @@ def main():
|
||||
if msg_str == "spectacle":
|
||||
tray_app.on_capture_and_upload()
|
||||
elif msg_str.startswith("upload:"):
|
||||
file_path = normalize_file_path(msg_str[7:])
|
||||
if os.path.exists(file_path):
|
||||
tray_app.upload_file_direct(file_path)
|
||||
else:
|
||||
tray_app.show_message("Upload Error", f"File not found: {file_path}", QSystemTrayIcon.Critical)
|
||||
target = msg_str[7:]
|
||||
tray_app.upload_target_direct(target)
|
||||
else:
|
||||
tray_app.on_open_settings()
|
||||
|
||||
@@ -1652,7 +1976,9 @@ def main():
|
||||
save_config(DEFAULT_CONFIG)
|
||||
|
||||
tray_app = SystemTrayApp()
|
||||
local_http_bridge.request_upload.connect(tray_app.upload_target_direct)
|
||||
tray_app.show()
|
||||
start_local_http_server(tray_app)
|
||||
|
||||
# Check if started with action argument
|
||||
if len(sys.argv) > 1:
|
||||
@@ -1660,16 +1986,16 @@ def main():
|
||||
if arg in ("--spectacle", "-s"):
|
||||
tray_app.on_capture_and_upload()
|
||||
elif arg.startswith("--upload="):
|
||||
file_path = normalize_file_path(arg[9:])
|
||||
tray_app.upload_file_direct(file_path)
|
||||
target, _ = parse_upload_target(arg[9:])
|
||||
tray_app.upload_target_direct(target)
|
||||
elif arg.startswith("--upload"):
|
||||
if len(sys.argv) > 2:
|
||||
file_path = normalize_file_path(sys.argv[2])
|
||||
tray_app.upload_file_direct(file_path)
|
||||
target, _ = parse_upload_target(sys.argv[2])
|
||||
tray_app.upload_target_direct(target)
|
||||
else:
|
||||
norm = normalize_file_path(arg)
|
||||
if os.path.exists(norm):
|
||||
tray_app.upload_file_direct(norm)
|
||||
target, _ = parse_upload_target(arg)
|
||||
if target:
|
||||
tray_app.upload_target_direct(target)
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ sed -e "s|__EXEC_PATH__|$INSTALL_DIR/f0ckm-uploader-gui|g" -e "s|__ICON_PATH__|$
|
||||
chmod +x "$APP_DIR/f0ckm-uploader-gui.desktop"
|
||||
|
||||
update-desktop-database "$APP_DIR" &> /dev/null
|
||||
xdg-mime default f0ckm-uploader-gui.desktop x-scheme-handler/f0ckm x-scheme-handler/f0ckm-uploader &> /dev/null || true
|
||||
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" &> /dev/null || true
|
||||
kbuildsycoca6 --noincremental &> /dev/null || kbuildsycoca5 --noincremental &> /dev/null || true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user