Merge pull request #1 from usetrmnl/catchup_to_chrome

Parity with Chrome extension
This commit is contained in:
Ryan Kulp
2026-01-27 14:18:03 -05:00
committed by GitHub
31 changed files with 12040 additions and 2031 deletions
Vendored
BIN
View File
Binary file not shown.
Binary file not shown.
+3 -19
View File
@@ -6,7 +6,7 @@ A Firefox-only browser extension that displays images from your TRMNL device in
This extension brings TRMNL's calm, distraction-free environment directly to your Firefox new tab page. It connects to your TRMNL account and displays the current screen from your selected device, automatically refreshing at configurable intervals.
**Note**: This extension is specifically designed for Firefox and uses Firefox-native APIs. It will not work in Chrome or other browsers.
**Note**: This extension was originally designed for Chrome but adopts [these changes](https://github.com/usetrmnl/trmnl-chrome/pull/5) to work with Firefox-native APIs.
## Requirements
@@ -78,26 +78,10 @@ This will create `trmnl-firefox.xpi` which can be installed in Firefox.
- Use the "Logout" button in the bottom overlay of any new tab page
- **Developer Mode**: Use the Firefox Developer Tools to access the TRMNL panel for environment switching
## Project Structure
```
trmnl-firefox/
├── code/ # Extension source code
│ ├── manifest.json # Firefox extension configuration
│ ├── newtab.html/js # New tab page implementation
│ ├── popup.html/js # Settings popup
│ ├── background.js # Background script for API calls
│ ├── devtools.html/js # Developer tools integration
│ ├── panel.html/js # DevTools panel
│ └── styles.css # Styling
├── pack.rb # Ruby script to package extension
└── README.md # This file
```
## Development
The extension uses:
- Firefox WebExtensions APIs (Manifest V2)
- Firefox WebExtensions APIs (Manifest V3)
- Vanilla JavaScript
- Native Firefox storage and messaging APIs
- CSS with dark mode support
@@ -108,4 +92,4 @@ Pull requests are welcome. Please ensure all changes maintain Firefox compatibil
## License
[MIT](https://choosealicense.com/licenses/mit/)
[MIT](https://choosealicense.com/licenses/mit/)
File diff suppressed because it is too large Load Diff
-110
View File
@@ -1,110 +0,0 @@
# TRMNL Firefox Extension
A Firefox extension that displays images from TRMNL's API in your new tab page with automatic refresh functionality. **Note**: requires a TRMNL account with a physical device or [BYOD](https://shop.usetrmnl.com/products/byod) license.
## Features
- Displays TRMNL images in new tab pages
- Automatic image refresh with configurable intervals
- Device selection for users with multiple TRMNL devices
- Automatic login flow - no manual API key entry required
- Developer tools panel for environment switching
- Offline-friendly caching
- Logout & reset functionality to clear all extension data
## Installation
### From Source (Development)
1. Clone this repository:
```bash
git clone git@github.com:usetrmnl/trmnl-firefox.git
```
2. Open Firefox and navigate to `about:debugging`
3. Click "This Firefox" in the left sidebar
4. Click "Load Temporary Add-on..." and select the `manifest.json` file from the `trmnl-firefox/code` directory
### Production Installation
This extension is designed for Firefox and uses Firefox-specific APIs. For production use, the extension would need to be signed by Mozilla and distributed through Firefox Add-ons.
## Setup
1. After installation, open a new tab or click the TRMNL extension icon
2. Click "Login to TRMNL" to open the TRMNL website in a new tab
3. Complete your login on the TRMNL website
4. The extension will automatically detect your login and fetch your devices
5. Your TRMNL device screen will appear in new tabs automatically
## Usage
### First Time Setup
1. **Login**: Click "Login to TRMNL" when prompted
2. **Authenticate**: Complete login on the TRMNL website
3. **Automatic Setup**: Extension automatically fetches your devices and API keys
### Daily Use
- **New Tab**: Open a new tab to see the current TRMNL image
- **Settings**: Click the TRMNL toolbar icon to access device settings and refresh options
- **Device Selection**: Choose between multiple devices if you have them
- **Manual Refresh**: Use the "Refresh Now" button in settings or on the new tab page
### Advanced Options
- **Manual API Key**: Advanced users can still enter API keys manually via the settings
- **Logout**: Clear all extension data and reset to initial state:
- Click the TRMNL toolbar icon and use the "Logout & Reset" button, or
- Use the "Logout" button in the bottom overlay of any new tab page
- **Developer Mode**: Use the Firefox Developer Tools to access the TRMNL panel for environment switching
## Development
The extension is built specifically for Firefox using:
- Vanilla JavaScript with Firefox WebExtensions APIs
- HTML/CSS for UI components
- Native Firefox storage and messaging APIs
- TRMNL API integration
### Key Files
- `manifest.json` - Firefox extension configuration (Manifest V2)
- `newtab.js/.html` - New tab page implementation
- `popup.js/.html` - Settings popup accessible from toolbar
- `background.js` - Background script for API calls, login flow, and data management
- `dashboard-content.js` - Content script for TRMNL website integration and login detection
- `devtools.js/.html` - Developer tools panel integration
- `panel.js/.html` - Developer tools panel implementation
- `styles.css` - Styling for new tab and popup interfaces
### Firefox-Specific Features
- Uses `browser.*` APIs natively (no polyfill required)
- Leverages Firefox's `chrome_url_overrides` for new tab functionality
- Integrated with Firefox Developer Tools
- Uses Firefox's persistent background script model
- Native Firefox storage management with automatic logout detection
- Automatic login detection via content scripts across TRMNL domains
### Login Flow Implementation
- **Automatic Device Fetching**: Connects to `https://usetrmnl.com/devices.json` to retrieve user devices and API keys
- **Login Detection**: Content script monitors TRMNL website for successful authentication
- **Seamless Integration**: No manual API key copying required
- **Fallback Support**: Manual API key entry still available for advanced users
- **Cross-Domain Support**: Works with all TRMNL subdomains and development environments
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please ensure all changes maintain Firefox compatibility and follow Firefox extension best practices.
## License
[MIT](https://choosealicense.com/licenses/mit/)
-62
View File
@@ -1,62 +0,0 @@
(function () {
// Function to check if user is authenticated
function checkAuthentication() {
// Check if we can access devices.json to verify authentication
fetch('/devices.json')
.then(response => {
if (response.ok) {
console.log("User is authenticated - devices.json accessible");
browser.runtime.sendMessage({ action: "loginSuccess" });
}
})
.catch(error => {
console.log("Authentication check failed:", error);
});
}
// Function to detect login success indicators
function detectLoginSuccess() {
// Check for dashboard or authenticated pages
const authenticatedPaths = ['/dashboard', '/devices', '/settings', '/account'];
const currentPath = window.location.pathname;
if (authenticatedPaths.some(path => currentPath.startsWith(path))) {
console.log("User on authenticated page:", currentPath);
browser.runtime.sendMessage({ action: "loginSuccess" });
browser.runtime.sendMessage({ action: "refreshDevices" });
return;
}
// Check for authentication cookies
const hasAuthCookie = document.cookie.includes('session') ||
document.cookie.includes('auth') ||
document.cookie.includes('_session') ||
document.cookie.includes('remember_user_token');
// Check for user navigation elements that appear when logged in
const userNavElements = document.querySelector('nav [href*="logout"], nav [href*="sign_out"], .user-menu, .account-menu');
if (hasAuthCookie || userNavElements) {
console.log("Login indicators found - cookies:", !!hasAuthCookie, "nav elements:", !!userNavElements);
checkAuthentication();
}
}
// Run detection immediately
detectLoginSuccess();
// Also run after DOM content loads in case elements load later
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', detectLoginSuccess);
}
// Watch for navigation changes (SPA behavior)
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
setTimeout(detectLoginSuccess, 500); // Small delay for page to settle
}
}).observe(document, { subtree: true, childList: true });
})();
-49
View File
@@ -1,49 +0,0 @@
{
"manifest_version": 2,
"name": "TRMNL New Tab - Firefox",
"version": "1.0",
"description": "Firefox extension that brings TRMNL's calm, distraction-free environment to your new tab page for seamless clarity and focus.",
"permissions": [
"storage",
"alarms",
"http://localhost:3000/*",
"https://usetrmnl.com/*"
],
"browser_action": {
"default_popup": "popup.html",
"default_icon": {
"16": "trmnl-icon.png",
"48": "trmnl-icon.png",
"128": "trmnl-icon.png"
}
},
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"background": {
"scripts": ["background.js"],
"persistent": true
},
"content_scripts": [
{
"matches": [
"https://usetrmnl.com/*",
"https://www.usetrmnl.com/*",
"https://app.usetrmnl.com/*",
"http://localhost:3000/*"
],
"js": ["dashboard-content.js"]
}
],
"devtools_page": "devtools.html",
"icons": {
"16": "trmnl-icon.png",
"48": "trmnl-icon.pngp",
"128": "trmnl-icon.png"
},
"browser_specific_settings": {
"gecko": {
"id": "trmnl-newtab@usetrmnl.com"
}
}
}
-416
View File
@@ -1,416 +0,0 @@
// JavaScript for the new tab page
document.addEventListener("DOMContentLoaded", initNewTab);
// Listen for storage changes to detect logout
browser.storage.onChanged.addListener((changes, namespace) => {
if (namespace === "local") {
// Check if critical data was cleared (logout)
if (changes.devices && !changes.devices.newValue) {
console.log("Devices cleared - user logged out");
redirectToLogin();
}
}
});
// DOM references
const imageElement = document.getElementById("trmnl-image");
const loadingElement = document.getElementById("loading");
const errorContainer = document.getElementById("error-container");
const infoOverlay = document.getElementById("info-overlay");
const nextRefreshElement = document.getElementById("next-refresh-time");
const refreshButton = document.getElementById("refresh-now");
const settingsButton = document.getElementById("open-settings");
const logoutButton = document.getElementById("logout-btn");
// State
let refreshTimeoutId = null;
let countdownIntervalId = null;
// Initialize the new tab page
async function initNewTab() {
try {
// Check if user is logged out
if (await isLoggedOut()) {
console.log("User is logged out, redirecting to login");
await redirectToLogin();
return;
}
// First try to get the environment setting
const { environment } = await browser.storage.local.get("environment");
const baseUrl =
environment === "development"
? "http://localhost:3000"
: "https://usetrmnl.com";
// Try to get devices from local storage first
const { devices: storedDevices } =
await browser.storage.local.get("devices");
// If we have devices in storage, use them
if (storedDevices && storedDevices.length > 0) {
console.log("Using devices from local storage:", storedDevices);
// Check if we have a selected device
const { selectedDevice } =
await browser.storage.local.get("selectedDevice");
if (!selectedDevice) {
await browser.storage.local.set({ selectedDevice: storedDevices[0] });
}
// Continue with normal initialization
setupEventListeners();
await loadImage();
return;
}
// If we don't have devices in storage, fetch from server
console.log("No devices in local storage, fetching from server");
const response = await fetch(`${baseUrl}/devices.json`);
// If unauthorized or forbidden, redirect to login
if (response.status === 401 || response.status === 403) {
await redirectToLogin();
return;
}
// If response is not OK for any other reason
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// If we get here, we have the devices, proceed with normal initialization
const devices = await response.json();
if (!devices || devices.length === 0) {
// No devices available, redirect to login
await redirectToLogin();
return;
}
// Store devices and selected device if not already set
await browser.storage.local.set({ devices });
const { selectedDevice } =
await browser.storage.local.get("selectedDevice");
if (!selectedDevice) {
await browser.storage.local.set({ selectedDevice: devices[0] });
}
// Continue with normal initialization
setupEventListeners();
await loadImage();
} catch (error) {
console.error("Error during initialization:", error);
// On any error, redirect to login
await redirectToLogin();
}
}
// Set up event listeners
function setupEventListeners() {
// Refresh now button
refreshButton.addEventListener("click", () => {
browser.runtime.sendMessage({ action: "forceRefresh" }).then(() => {
loadImage();
});
});
// Settings button
settingsButton.addEventListener("click", () => {
// For Firefox, show an inline settings panel instead of popup
showInlineSettings();
});
// Logout button
logoutButton.addEventListener("click", () => {
performLogout();
});
// Login button - add event listener when it appears
document.addEventListener("click", (e) => {
if (e.target && e.target.id === "login-now") {
startLogin();
}
});
// Listen for messages from background
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "imageUpdated") {
console.log("Received image update notification");
loadImage();
if (sendResponse) sendResponse({ received: true });
} else if (message.action === "loginSuccess") {
console.log("Login success detected, refreshing new tab");
window.location.reload();
}
return false;
});
}
// Load the image from storage
async function loadImage() {
loadingElement.classList.remove("hidden");
imageElement.classList.add("hidden"); // Hide the image while loading
browser.runtime
.sendMessage({ action: "getCurrentImage" })
.then((response) => {
if (!response || !response.currentImage) {
showLoginPrompt();
return;
}
// Hide error container if it was showing
errorContainer.classList.add("hidden");
// Create a new image element to force a reload
const newImage = new Image();
newImage.onload = () => {
// Update the src of the actual image element
imageElement.src = newImage.src;
loadingElement.classList.add("hidden");
imageElement.classList.remove("hidden");
};
newImage.onerror = () => {
console.error("Error loading image data URL");
loadingElement.textContent = "Error loading image";
// Request a fresh image
browser.runtime.sendMessage({ action: "forceRefresh" });
};
// Add a cache-busting parameter to force reload
newImage.src = `${response.currentImage.url}#t=${Date.now()}`;
// Update next refresh info
updateRefreshTimer(response.nextFetch);
});
}
// Start login flow
function startLogin() {
browser.runtime.sendMessage({ action: "startLogin" }).then((response) => {
if (response && response.success) {
const errorText = errorContainer.querySelector("p");
if (errorText) {
errorText.textContent = "Login page opened - please complete login in the new tab.";
}
// Check for login success
checkForLoginSuccess();
} else {
const errorText = errorContainer.querySelector("p");
if (errorText) {
errorText.textContent = "Error opening login page. Please try again.";
}
}
}).catch((error) => {
console.error("Login error:", error);
const errorText = errorContainer.querySelector("p");
if (errorText) {
errorText.textContent = "Error opening login page. Please try again.";
}
});
}
// Check for login success periodically
function checkForLoginSuccess() {
const checkInterval = setInterval(async () => {
const { devices } = await browser.storage.local.get(["devices"]);
if (devices && devices.length > 0) {
clearInterval(checkInterval);
window.location.reload();
}
}, 2000);
// Stop checking after 2 minutes
setTimeout(() => {
clearInterval(checkInterval);
}, 120000);
}
// Show the login prompt
function showLoginPrompt() {
loadingElement.classList.add("hidden");
imageElement.classList.add("hidden");
errorContainer.classList.remove("hidden");
const errorText = errorContainer.querySelector("p");
if (errorText) {
errorText.textContent = "Please log in to your TRMNL account to view your devices.";
}
}
// Update the refresh countdown timer
function updateRefreshTimer(nextFetchTimestamp) {
if (!nextFetchTimestamp) {
nextRefreshElement.textContent = "Unknown";
return;
}
// Clear existing timeouts and intervals
if (refreshTimeoutId) clearTimeout(refreshTimeoutId);
if (countdownIntervalId) clearInterval(countdownIntervalId);
// Set timeout to load image at refresh time
const now = Date.now();
const timeToRefresh = Math.max(0, nextFetchTimestamp - now);
if (timeToRefresh > 0) {
// Add a small buffer (2 seconds) to ensure the background has time to fetch
refreshTimeoutId = setTimeout(() => {
// Check if the image has been updated in background
browser.runtime
.sendMessage({ action: "getCurrentImage" })
.then((response) => {
const currentTime = Date.now();
// Only reload if the last fetch time is recent (within last 10 seconds)
if (
response &&
response.lastFetch &&
currentTime - response.lastFetch < 10000
) {
loadImage();
} else {
// If not updated recently, the background refresh might have failed
// Request a refresh and then load the image
browser.runtime.sendMessage({ action: "forceRefresh" }).then(() => {
setTimeout(loadImage, 2000);
});
}
});
}, timeToRefresh);
// Update countdown display
updateCountdown(nextFetchTimestamp);
countdownIntervalId = setInterval(() => {
updateCountdown(nextFetchTimestamp);
}, 1000);
}
}
// Update the countdown display
function updateCountdown(nextFetchTimestamp) {
const now = Date.now();
const timeRemaining = Math.max(0, nextFetchTimestamp - now);
if (timeRemaining <= 0) {
nextRefreshElement.textContent = "Now";
if (countdownIntervalId) {
clearInterval(countdownIntervalId);
}
return;
}
// Format the time remaining
const seconds = Math.floor((timeRemaining / 1000) % 60);
const minutes = Math.floor((timeRemaining / (1000 * 60)) % 60);
const hours = Math.floor(timeRemaining / (1000 * 60 * 60));
nextRefreshElement.textContent = `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`;
}
// Show inline settings panel (Firefox-compatible)
function showInlineSettings() {
// Create or show a simple inline settings overlay
let settingsOverlay = document.getElementById("settings-overlay");
if (!settingsOverlay) {
settingsOverlay = document.createElement("div");
settingsOverlay.id = "settings-overlay";
settingsOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
`;
const settingsPanel = document.createElement("div");
settingsPanel.style.cssText = `
background: #1a1a1a;
padding: 20px;
border-radius: 8px;
color: white;
max-width: 400px;
width: 90%;
`;
settingsPanel.innerHTML = `
<h3 style="margin-top: 0;">TRMNL Settings</h3>
<p>To access full settings, click the TRMNL icon in your browser toolbar.</p>
<button id="close-settings" style="background: #4a5568; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin-top: 16px;">Close</button>
`;
settingsOverlay.appendChild(settingsPanel);
document.body.appendChild(settingsOverlay);
// Close button functionality
document.getElementById("close-settings").addEventListener("click", () => {
settingsOverlay.remove();
});
// Close on overlay click
settingsOverlay.addEventListener("click", (e) => {
if (e.target === settingsOverlay) {
settingsOverlay.remove();
}
});
} else {
settingsOverlay.style.display = "flex";
}
}
// Helper to pad numbers with leading zeros
function padZero(num) {
return num.toString().padStart(2, "0");
}
// Check if user is logged out (no critical data)
async function isLoggedOut() {
const storage = await browser.storage.local.get([
"devices",
"selectedDevice",
"apiKey"
]);
// If we have no devices AND no API key, consider user logged out
return (!storage.devices || storage.devices.length === 0) && !storage.apiKey;
}
// Redirect to login page
async function redirectToLogin() {
const { environment } = await browser.storage.local.get("environment");
const baseUrl =
environment === "development"
? "http://localhost:3000"
: "https://usetrmnl.com";
console.log("Redirecting to login:", `${baseUrl}/login`);
window.location.href = `${baseUrl}/login`;
}
// Perform logout
function performLogout() {
if (!confirm("Are you sure you want to logout? This will clear all extension data and you'll need to login again.")) {
return;
}
browser.runtime.sendMessage({ action: "logout" }).then((response) => {
if (response && response.success) {
console.log("Logout successful");
// The background script will redirect us to login
} else {
console.error("Logout failed:", response);
alert("Logout failed. Please try again.");
}
}).catch((error) => {
console.error("Logout error:", error);
alert("Logout failed. Please try again.");
});
}
-408
View File
@@ -1,408 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TRMNL Settings</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<style>
body {
width: 350px;
padding: 20px;
font-family: Inter, sans-serif;
margin: 0;
background-color: #f8f9fa;
color: #333;
}
h2 {
margin-top: 0;
margin-bottom: 20px;
color: #2c3e50;
font-size: 20px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
font-size: 14px;
}
input,
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
background-color: #fff;
color: #333;
box-sizing: border-box;
}
input:focus,
select:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.button-group button {
flex: 1;
}
.button-group button:only-child {
max-width: 200px;
margin: 0 auto;
}
#status {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
font-size: 14px;
text-align: center;
min-height: 20px;
font-weight: 600;
}
.status-success {
color: #155724 !important;
background-color: #d4edda !important;
border: 1px solid #c3e6cb !important;
}
.status-error {
color: #721c24 !important;
background-color: #f8d7da !important;
border: 1px solid #f5c6cb !important;
}
.status-info {
font-size: 12px;
color: #666;
margin-top: 15px;
line-height: 1.4;
}
.logout-section {
border-top: 1px solid #dee2e6;
padding-top: 20px;
margin-top: 25px;
}
.logout-btn {
background-color: #dc3545 !important;
color: white !important;
width: 100%;
}
.logout-btn:hover {
background-color: #c82333 !important;
}
.logout-warning {
font-size: 12px;
color: #6c757d;
margin-bottom: 10px;
text-align: center;
}
.login-prompt {
text-align: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 15px;
}
.login-prompt p {
margin-bottom: 15px;
color: #6c757d;
font-size: 14px;
}
.login-btn {
background-color: #28a745 !important;
color: white !important;
width: 100%;
font-weight: 600;
}
.login-btn:hover {
background-color: #218838 !important;
}
.advanced-section {
margin-top: 20px;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 0;
background-color: #f8f9fa;
}
.advanced-section summary {
cursor: pointer;
font-weight: 600;
color: #555;
margin: 0;
padding: 15px;
outline: none;
border-radius: 6px 6px 0 0;
user-select: none;
}
.advanced-section summary:hover {
color: #007bff;
background-color: #e9ecef;
}
.advanced-section[open] summary {
border-bottom: 1px solid #dee2e6;
border-radius: 6px 6px 0 0;
}
.advanced-section > div {
padding: 15px;
}
.advanced-section .button-group {
margin: 15px 0;
display: flex;
gap: 10px;
justify-content: center;
}
.advanced-section .button-group button {
flex: 1;
max-width: 150px;
}
.advanced-section .button-group button:only-child {
max-width: 200px;
}
.advanced-section .toggle-manual-btn {
margin-bottom: 15px;
width: 100%;
max-width: 250px;
}
.manual-api-section {
margin-bottom: 15px;
}
.toggle-manual-btn {
background-color: #6c757d !important;
color: white !important;
font-size: 13px;
padding: 10px 20px;
margin-top: 8px;
border-radius: 6px;
font-weight: 500;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.toggle-manual-btn:hover {
background-color: #545b62 !important;
}
.hidden {
display: none !important;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #e9ecef;
}
h2 {
color: #f8f9fa;
}
label {
color: #ced4da;
}
input,
select {
background-color: #333;
color: #fff;
border-color: #555;
}
input:focus,
select:focus {
border-color: #0d6efd;
}
select option {
background-color: #333;
color: #fff;
}
.login-prompt {
background-color: #2d3748;
}
.login-prompt p {
color: #a0aec0;
}
.status-info {
color: #adb5bd;
}
.advanced-section {
background-color: #2d3748;
border-color: #333;
}
.advanced-section summary {
color: #ced4da;
}
.advanced-section summary:hover {
color: #0d6efd;
background-color: #3a4a5c;
}
.advanced-section[open] summary {
border-bottom-color: #333;
}
.logout-section {
border-top-color: #333;
}
.logout-warning {
color: #adb5bd;
}
.status-success {
color: #d1e7dd !important;
background-color: #0f2419 !important;
border: 1px solid #1c4532 !important;
}
.status-error {
color: #f8d7da !important;
background-color: #2c0b0e !important;
border: 1px solid #58151c !important;
}
}
</style>
</head>
<body class="popup-body">
<div>
<h2 class="popup-title">TRMNL New Tab Settings</h2>
<div id="device-selector-group" class="form-group hidden">
<label for="device-select" class="popup-label"
>Select Device</label
>
<select id="device-select" class="popup-select">
<option value="">Loading devices...</option>
</select>
</div>
<div class="form-group" id="login-section">
<div id="login-prompt" class="login-prompt">
<p>
Please log in to your TRMNL account to access your
devices.
</p>
<button id="login-btn" class="login-btn">
Login to TRMNL
</button>
</div>
</div>
<div id="status"></div>
<details id="advanced-section" class="advanced-section">
<summary>Advanced Settings</summary>
<div style="text-align: center; margin-bottom: 15px;">
<button id="toggle-manual" class="toggle-manual-btn">
Use Manual API Key
</button>
</div>
<div id="manual-api-section" class="manual-api-section hidden">
<label for="api-key">Manual API Key</label>
<input
type="password"
id="api-key"
placeholder="Enter your API key manually"
/>
</div>
<div class="button-group" style="margin-bottom: 15px;">
<button id="save-settings" style="display: none;">Save Settings</button>
<button id="refresh-now" style="display: none;">Refresh Now</button>
</div>
<div class="status-info">
<div id="last-updated"></div>
<div id="next-update"></div>
<div id="refresh-rate"></div>
</div>
</details>
<div id="logout-section" class="logout-section hidden">
<div class="logout-warning">
This will clear all extension data and log you out.
</div>
<button id="logout-btn" class="logout-btn">
Logout & Reset
</button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
-531
View File
File diff suppressed because it is too large Load Diff
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>trmnl--fav-icon</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="trmnl--fav-icon">
<path d="M101.273465,0 L410.726535,7.25467311e-15 C445.941525,-4.215174e-15 458.711331,3.66661164 471.585405,10.5517418 C484.459479,17.436872 494.563128,27.5405206 501.448258,40.4145947 C508.333388,53.2886688 512,66.0584752 512,101.273465 L512,410.726535 C512,445.941525 508.333388,458.711331 501.448258,471.585405 C494.563128,484.459479 484.459479,494.563128 471.585405,501.448258 C458.711331,508.333388 445.941525,512 410.726535,512 L101.273465,512 C66.0584752,512 53.2886688,508.333388 40.4145947,501.448258 C27.5405206,494.563128 17.436872,484.459479 10.5517418,471.585405 C3.66661164,458.711331 0,445.941525 0,410.726535 L0,101.273465 C0,66.0584752 3.66661164,53.2886688 10.5517418,40.4145947 C17.436872,27.5405206 27.5405206,17.436872 40.4145947,10.5517418 C53.2886688,3.66661164 66.0584752,0 101.273465,0 Z" id="Rectangle" fill="#F86527"></path>
<polygon id="Path" fill="#FFFFFF" points="173.227881 101 266 135.905584 248.772355 182 156 147.094416"></polygon>
<polygon id="Path" fill="#FFFFFF" points="321.477308 90 352 184.742164 305.523867 200 275 105.258073"></polygon>
<polygon id="Path" fill="#FFFFFF" points="423 199.08638 368.734902 282 328 254.91256 382.266271 172"></polygon>
<polygon id="Path" fill="#FFFFFF" points="401 347.057326 302.406877 356 298 306.942674 396.593123 298"></polygon>
<polygon id="Path" fill="#FFFFFF" points="272.526812 422 204 350.073199 239.474369 316 308 387.926801"></polygon>
<polygon id="Path" fill="#FFFFFF" points="133 367.412771 146.295792 269 195 275.587229 181.704208 374"></polygon>
<polygon id="Path" fill="#FFFFFF" points="89 224.865546 173.872813 174 199 216.133279 114.126739 267"></polygon>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

+7
View File
@@ -0,0 +1,7 @@
(function () {
// Check if we're on the dashboard page post-login
if (window.location.pathname === "/dashboard") {
// Send message to background script to refresh devices
chrome.runtime.sendMessage({ action: "refreshDevices" });
}
})();
-1
View File
@@ -1,7 +1,6 @@
<!doctype html>
<html>
<head>
<script src="devtools.js"></script>
</head>
<body>
+1 -1
View File
@@ -1,4 +1,4 @@
browser.devtools.panels.create(
chrome.devtools.panels.create(
"TRMNL",
"",
"/panel.html", // Make sure this path is correct
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
{
"manifest_version": 3,
"name": "TRMNL New Tab Display",
"version": "1.1",
"description": "Displays images from TRMNL API on new tab pages",
"permissions": ["storage", "alarms"],
"host_permissions": ["http://localhost:3000/*", "https://usetrmnl.com/*"],
"devtools_page": "devtools.html",
"chrome_url_overrides": {
"newtab": "newtab.html"
},
"content_scripts": [
{
"matches": ["https://usetrmnl.com/*", "http://localhost:3000/*"],
"js": ["dashboard-content.js"]
}
],
"background": {
"scripts": ["background.js"],
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"browser_specific_settings": {
"gecko": {
"id": "{fa015ea5-5b74-40d8-b4d5-75f5f0edbfda}"
}
}
}

Some files were not shown because too many files have changed in this diff Show More