diff --git a/README.md b/README.md index 14c2c68..de75983 100644 --- a/README.md +++ b/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` diff --git a/browser-extension/README.md b/browser-extension/README.md new file mode 100644 index 0000000..56f8e7a --- /dev/null +++ b/browser-extension/README.md @@ -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**. diff --git a/browser-extension/background.js b/browser-extension/background.js new file mode 100644 index 0000000..6bb18b2 --- /dev/null +++ b/browser-extension/background.js @@ -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; + } +}); diff --git a/browser-extension/manifest.chrome.json b/browser-extension/manifest.chrome.json new file mode 100644 index 0000000..a82cc08 --- /dev/null +++ b/browser-extension/manifest.chrome.json @@ -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": [ + "" + ], + "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" + } + } +} diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json new file mode 100644 index 0000000..4356e70 --- /dev/null +++ b/browser-extension/manifest.json @@ -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": [ + "" + ], + "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" + } + } +} diff --git a/browser-extension/options/options.css b/browser-extension/options/options.css new file mode 100644 index 0000000..005a05c --- /dev/null +++ b/browser-extension/options/options.css @@ -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; +} diff --git a/browser-extension/options/options.html b/browser-extension/options/options.html new file mode 100644 index 0000000..db19eac --- /dev/null +++ b/browser-extension/options/options.html @@ -0,0 +1,103 @@ + + + + + f0ckm Extension Options + + + +
+
+ +
+

f0ckm Browser Integration Settings

+

Configure browser extension upload modes and API settings

+
+
+ +
+
+

Integration Mode

+
+ + +
+
+ +
+

API & Server Configuration

+
+ + +
+
+ + +
+
+ +
+

Default Upload Preferences

+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+ + + + diff --git a/browser-extension/options/options.js b/browser-extension/options/options.js new file mode 100644 index 0000000..5cbba90 --- /dev/null +++ b/browser-extension/options/options.js @@ -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); + }); + }); +}); diff --git a/browser-extension/popup/popup.css b/browser-extension/popup/popup.css new file mode 100644 index 0000000..5841ca3 --- /dev/null +++ b/browser-extension/popup/popup.css @@ -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; +} diff --git a/browser-extension/popup/popup.html b/browser-extension/popup/popup.html new file mode 100644 index 0000000..8ec1055 --- /dev/null +++ b/browser-extension/popup/popup.html @@ -0,0 +1,61 @@ + + + + + f0ckm Uploader + + + + +
+
+ f0ckm Logo + f0ckm Uploader +
+
+ +
+ + +
+ +
+
+
+ 🌐 Web Page + Loading current tab... +
+
https://...
+ +
+ +
OR PASTE CUSTOM URL
+ +
+ + +
+ + +
+ + + + diff --git a/browser-extension/popup/popup.js b/browser-extension/popup/popup.js new file mode 100644 index 0000000..83e350a --- /dev/null +++ b/browser-extension/popup/popup.js @@ -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(); +} diff --git a/f0ckm-uploader-gui.desktop b/f0ckm-uploader-gui.desktop index b4c897f..b74785b 100755 --- a/f0ckm-uploader-gui.desktop +++ b/f0ckm-uploader-gui.desktop @@ -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; diff --git a/gui.py b/gui.py index 6699426..1befc52 100644 --- a/gui.py +++ b/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()) diff --git a/install.sh b/install.sh index a5079b2..9652b77 100755 --- a/install.sh +++ b/install.sh @@ -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