adding browser extension and some QoL
This commit is contained in:
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();
|
||||
}
|
||||
Reference in New Issue
Block a user