Initial commit: Outlook Web Inbox Count extension v1.4
Chrome/Firefox extension that displays the total inbox item count next to the Inbox label in Outlook Web. Includes build+publish automation via the Chrome Web Store API (see PUBLISHING.md).
This commit is contained in:
commit
922d520f02
9 changed files with 641 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
outlook_web_inbox_count_v*.zip
|
||||||
41
CLAUDE.md
Normal file
41
CLAUDE.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a browser extension (v1.1) that enhances Outlook Web by displaying the total number of items next to the Inbox label. The extension uses a content script to monitor DOM changes and extract inbox count information from the page title.
|
||||||
|
|
||||||
|
### Version History
|
||||||
|
- **v1.1**: Fixed browser hanging issues with throttling, improved MutationObserver, enhanced DOM selectors, reduced polling frequency
|
||||||
|
- **v1.0**: Initial release
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **manifest.json**: Chrome extension manifest (v3) defining permissions, content scripts, and metadata
|
||||||
|
- **content.js**: Main content script that runs on Outlook Web pages, implementing:
|
||||||
|
- DOM element detection for inbox labels
|
||||||
|
- Item count extraction from page titles using regex
|
||||||
|
- Dynamic count display with styling
|
||||||
|
- MutationObserver for real-time DOM change monitoring
|
||||||
|
- Periodic refresh mechanism (every 5 seconds after initial 10-second startup)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Testing the Extension
|
||||||
|
1. Load as unpacked extension in Chrome/Edge developer mode
|
||||||
|
2. Navigate to https://outlook.office.com or https://outlook.office365.com
|
||||||
|
3. Check browser console for debug logs starting with "Outlook Inbox Count Extension"
|
||||||
|
|
||||||
|
### Packaging
|
||||||
|
The .zip file contains the packaged extension ready for distribution.
|
||||||
|
|
||||||
|
## Key Implementation Details
|
||||||
|
|
||||||
|
- **Performance optimizations (v1.1)**: Throttling mechanism prevents excessive function calls, selective MutationObserver reduces CPU usage
|
||||||
|
- **Enhanced DOM detection**: Multiple fallback selectors and regex patterns for different Outlook Web versions
|
||||||
|
- **CSS selectors**: Primary `div[title^="Inbox -"][data-folder-name="inbox"]` with fallbacks for `button` elements and various title formats
|
||||||
|
- **Count extraction**: Multiple regex patterns including `/Inbox - (\d+) items/`, `/Inbox \((\d+)\)/`, `/(\d+) items/`
|
||||||
|
- **Duplicate prevention**: Uses `.added-count` class marking to avoid multiple count displays
|
||||||
|
- **Dynamic content handling**: Optimized interval polling (10s) and intelligent mutation observation
|
||||||
|
- **Graceful fallback**: Displays "(-)" when count unavailable
|
||||||
128
PUBLISHING.md
Normal file
128
PUBLISHING.md
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Publishing to the Chrome Web Store
|
||||||
|
|
||||||
|
This document describes how to release a new version of the Outlook Web Inbox
|
||||||
|
Count extension to the Chrome Web Store using the `publish.sh` script.
|
||||||
|
|
||||||
|
## Quick release workflow
|
||||||
|
|
||||||
|
For a routine release (once the one-time setup below is done):
|
||||||
|
|
||||||
|
1. Edit `content.js` and bump the version in `manifest.json` (e.g. `1.4` -> `1.5`).
|
||||||
|
2. Add a changelog entry in the header of `content.js`.
|
||||||
|
3. Run:
|
||||||
|
```bash
|
||||||
|
./publish.sh
|
||||||
|
```
|
||||||
|
This builds `outlook_web_inbox_count_v<VERSION>.zip` from `manifest.json`,
|
||||||
|
uploads it, and publishes it. Chrome Web Store review typically takes a
|
||||||
|
few hours to a day before the new version goes live.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `./publish.sh --no-publish` - upload only, leave as Draft in the dashboard.
|
||||||
|
- `./publish.sh path/to/existing.zip` - upload a specific zip without rebuilding.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
`publish.sh` uses the [Chrome Web Store API](https://developer.chrome.com/docs/webstore/api):
|
||||||
|
|
||||||
|
1. Exchanges the OAuth **refresh token** (from `.env`) for a short-lived
|
||||||
|
**access token** via `https://oauth2.googleapis.com/token`.
|
||||||
|
2. `PUT`s the zip to `upload/chromewebstore/v1.1/items/$EXTENSION_ID`.
|
||||||
|
3. `POST`s to `chromewebstore/v1.1/items/$EXTENSION_ID/publish` to submit the
|
||||||
|
uploaded draft for review.
|
||||||
|
|
||||||
|
The script reads all credentials from a `.env` file in the project root
|
||||||
|
(gitignored). Required variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLIENT_ID=<google oauth client id>
|
||||||
|
CLIENT_SECRET=<google oauth client secret>
|
||||||
|
REFRESH_TOKEN=<long-lived oauth refresh token>
|
||||||
|
EXTENSION_ID=<32-char chrome web store extension id>
|
||||||
|
```
|
||||||
|
|
||||||
|
The current extension ID is `mjdfjopdcoiojbjnfkpjhcnpefjknkdn`.
|
||||||
|
|
||||||
|
## One-time setup (already done, for reference)
|
||||||
|
|
||||||
|
### 1. Google Cloud OAuth project
|
||||||
|
|
||||||
|
1. Create a project at https://console.cloud.google.com/.
|
||||||
|
2. Enable the **Chrome Web Store API** under "APIs & Services".
|
||||||
|
3. Configure the OAuth consent screen:
|
||||||
|
- User type: External
|
||||||
|
- Add yourself as a **test user** (required while the app is in Testing mode).
|
||||||
|
4. Create OAuth credentials:
|
||||||
|
- "Credentials" -> "Create Credentials" -> "OAuth client ID"
|
||||||
|
- Application type: **Desktop app**
|
||||||
|
- Save the `client_id` and `client_secret` to `.env`.
|
||||||
|
|
||||||
|
### 2. Obtain a refresh token
|
||||||
|
|
||||||
|
Open this URL in a browser (replace `$CLIENT_ID`):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=$CLIENT_ID&redirect_uri=http://localhost&access_type=offline&prompt=consent
|
||||||
|
```
|
||||||
|
|
||||||
|
After approval, the browser will attempt to redirect to `http://localhost/?code=...`
|
||||||
|
and fail to connect - that is expected. **Copy the `code` value from the URL bar.**
|
||||||
|
|
||||||
|
Exchange the code for a refresh token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://oauth2.googleapis.com/token" \
|
||||||
|
-d "client_id=$CLIENT_ID" \
|
||||||
|
-d "client_secret=$CLIENT_SECRET" \
|
||||||
|
-d "code=$CODE" \
|
||||||
|
-d "grant_type=authorization_code" \
|
||||||
|
-d "redirect_uri=http://localhost"
|
||||||
|
```
|
||||||
|
|
||||||
|
Save the `refresh_token` value from the JSON response into `.env`.
|
||||||
|
|
||||||
|
### 3. Find the extension ID
|
||||||
|
|
||||||
|
Log into https://chrome.google.com/webstore/devconsole and open the extension's
|
||||||
|
edit page. The URL contains the 32-character extension ID:
|
||||||
|
`.../devconsole/<account>/<EXTENSION_ID>/edit`.
|
||||||
|
|
||||||
|
## Refreshing credentials
|
||||||
|
|
||||||
|
### If the refresh token has expired (7-day limit)
|
||||||
|
|
||||||
|
While the OAuth consent screen is in **Testing** mode, refresh tokens expire
|
||||||
|
after **7 days**. If `publish.sh` fails with an access-token error, redo step
|
||||||
|
2 above (obtain a new code and exchange it) and update `REFRESH_TOKEN` in `.env`.
|
||||||
|
|
||||||
|
**To avoid this**, publish the OAuth app (consent screen -> "Publish App").
|
||||||
|
Since the only user is the developer and the scope
|
||||||
|
(`chromewebstore`) is self-owned, verification is not strictly required for
|
||||||
|
personal use - the app just needs to be in "In production" state.
|
||||||
|
|
||||||
|
### Rotating the client secret
|
||||||
|
|
||||||
|
If the `client_secret` is ever exposed, rotate it in the Google Cloud console
|
||||||
|
(Credentials page -> edit client -> Reset Secret) and update `.env`. You will
|
||||||
|
also need to obtain a new refresh token since the old one is tied to the old
|
||||||
|
secret.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **`Error 403: access_denied` during auth** - you are not added as a test
|
||||||
|
user on the OAuth consent screen. Add yourself in the console and retry.
|
||||||
|
- **`uploadState: FAILURE`** - usually means the zip is malformed, or the
|
||||||
|
version in `manifest.json` is not greater than the currently published
|
||||||
|
version. Bump the version and rebuild.
|
||||||
|
- **Publish returns `ITEM_PENDING_REVIEW`** - normal. The item has been
|
||||||
|
submitted and is in the Chrome review queue.
|
||||||
|
- **Refresh token suddenly invalid** - most likely the 7-day testing-mode
|
||||||
|
expiry. Obtain a new one (see above).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `publish.sh` - build + upload + publish script.
|
||||||
|
- `.env` - credentials (gitignored; never commit).
|
||||||
|
- `.gitignore` - excludes `.env`.
|
||||||
|
- `manifest.json` - source of truth for the version number.
|
||||||
|
- `outlook_web_inbox_count_v<VERSION>.zip` - build artifact produced by `publish.sh`.
|
||||||
323
content.js
Normal file
323
content.js
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
// Outlook Web Inbox Count Extension v1.4
|
||||||
|
// Changelog v1.4:
|
||||||
|
// - Added outlook.cloud.microsoft to manifest matches (Microsoft migrated Outlook Web to new domain)
|
||||||
|
// Changelog v1.3:
|
||||||
|
// - Fixed Firefox compatibility by adding browser_specific_settings to manifest
|
||||||
|
// - Fixed bug where findTotalItems() wasn't returning values due to forEach callbacks
|
||||||
|
// - Changed forEach loops to for loops to enable proper return statements
|
||||||
|
// - Extended retry mechanism with more attempts (1s, 2s, 3s, 5s, 8s, 10s, 15s, 20s)
|
||||||
|
// - Added re-logging of environment info on each retry to track page load progress
|
||||||
|
// Changelog v1.2:
|
||||||
|
// - Added comprehensive debugging information to help diagnose user issues
|
||||||
|
// - Added URL detection and page structure analysis
|
||||||
|
// - Enhanced logging for DOM element discovery
|
||||||
|
// - Added language/locale detection
|
||||||
|
// Changelog v1.1:
|
||||||
|
// - Fixed browser hanging issue by adding throttling mechanism
|
||||||
|
// - Improved MutationObserver to be more selective and prevent infinite loops
|
||||||
|
// - Enhanced DOM selectors with multiple fallbacks for different Outlook versions
|
||||||
|
// - Reduced polling frequency to improve performance
|
||||||
|
// - Added better pattern matching for inbox count extraction
|
||||||
|
|
||||||
|
console.log("Outlook Inbox Count Extension v1.4: Script started");
|
||||||
|
|
||||||
|
// Enhanced debugging information
|
||||||
|
function logEnvironmentInfo() {
|
||||||
|
console.log("=== OUTLOOK INBOX EXTENSION DEBUG INFO ===");
|
||||||
|
console.log("URL:", window.location.href);
|
||||||
|
console.log("Domain:", window.location.hostname);
|
||||||
|
console.log("User Agent:", navigator.userAgent);
|
||||||
|
console.log("Language:", navigator.language);
|
||||||
|
console.log("Document ready state:", document.readyState);
|
||||||
|
console.log("Page title:", document.title);
|
||||||
|
|
||||||
|
// Check for New Outlook indicators
|
||||||
|
const isNewOutlook = document.querySelector('[data-app-section="NewMailModule"]') ||
|
||||||
|
document.querySelector('div[role="main"][data-app-section]') ||
|
||||||
|
document.body.classList.contains('new-outlook') ||
|
||||||
|
window.location.href.includes('mail.office365.com');
|
||||||
|
console.log("Detected New Outlook interface:", isNewOutlook);
|
||||||
|
|
||||||
|
// Log all elements that might be inbox-related
|
||||||
|
console.log("=== DOM ANALYSIS ===");
|
||||||
|
const potentialInboxElements = document.querySelectorAll('[data-folder-name], [title*="Inbox"], [aria-label*="Inbox"], button[title*="nbox"], div[title*="nbox"]');
|
||||||
|
console.log("Found", potentialInboxElements.length, "potential inbox elements:");
|
||||||
|
potentialInboxElements.forEach((el, i) => {
|
||||||
|
console.log(`Element ${i}:`, {
|
||||||
|
tagName: el.tagName,
|
||||||
|
className: el.className,
|
||||||
|
title: el.getAttribute('title'),
|
||||||
|
dataFolderName: el.getAttribute('data-folder-name'),
|
||||||
|
ariaLabel: el.getAttribute('aria-label'),
|
||||||
|
textContent: el.textContent?.substring(0, 100)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log("=== END DOM ANALYSIS ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
let isProcessing = false;
|
||||||
|
let lastProcessTime = 0;
|
||||||
|
const THROTTLE_DELAY = 1000; // 1 second throttle
|
||||||
|
|
||||||
|
function findInboxElement() {
|
||||||
|
console.log("=== SEARCHING FOR INBOX ELEMENT ===");
|
||||||
|
|
||||||
|
// Try multiple selectors to find inbox element
|
||||||
|
const selectors = [
|
||||||
|
'div[title^="Inbox -"][data-folder-name="inbox"]',
|
||||||
|
'div[data-folder-name="inbox"]',
|
||||||
|
'div[title*="Inbox"]',
|
||||||
|
'button[title^="Inbox -"]',
|
||||||
|
'button[data-folder-name="inbox"]',
|
||||||
|
// Additional selectors for different Outlook versions
|
||||||
|
'button[aria-label*="Inbox"]',
|
||||||
|
'div[aria-label*="Inbox"]',
|
||||||
|
'[data-folder-id="inbox"]',
|
||||||
|
'[data-automation-id*="inbox"]',
|
||||||
|
'span[title*="Inbox"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log("Trying", selectors.length, "different selectors...");
|
||||||
|
|
||||||
|
for (let i = 0; i < selectors.length; i++) {
|
||||||
|
const selector = selectors[i];
|
||||||
|
console.log(`Trying selector ${i + 1}/${selectors.length}:`, selector);
|
||||||
|
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
console.log(`Found ${elements.length} elements with this selector`);
|
||||||
|
|
||||||
|
if (elements.length > 0) {
|
||||||
|
elements.forEach((el, idx) => {
|
||||||
|
console.log(` Element ${idx}:`, {
|
||||||
|
tagName: el.tagName,
|
||||||
|
className: el.className,
|
||||||
|
title: el.getAttribute('title'),
|
||||||
|
ariaLabel: el.getAttribute('aria-label'),
|
||||||
|
dataFolderName: el.getAttribute('data-folder-name'),
|
||||||
|
textContent: el.textContent?.substring(0, 50) + '...'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const inboxDiv = elements[0]; // Use the first match
|
||||||
|
console.log("✓ Using first element as inbox:", inboxDiv);
|
||||||
|
|
||||||
|
const span = inboxDiv.querySelector('span:not(.added-count)');
|
||||||
|
if (span) {
|
||||||
|
console.log("✓ Found inner span element:", span);
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
// If no span found, try to use the element itself
|
||||||
|
console.log("ℹ No inner span found, using element itself");
|
||||||
|
return inboxDiv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("❌ Inbox element not found with any selector");
|
||||||
|
console.log("=== END INBOX ELEMENT SEARCH ===");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTotalItems() {
|
||||||
|
console.log("=== SEARCHING FOR TOTAL ITEMS COUNT ===");
|
||||||
|
|
||||||
|
// Try multiple selectors and patterns
|
||||||
|
const selectors = [
|
||||||
|
'div[title^="Inbox -"][data-folder-name="inbox"]',
|
||||||
|
'div[data-folder-name="inbox"]',
|
||||||
|
'div[title*="Inbox"]',
|
||||||
|
'button[title^="Inbox -"]',
|
||||||
|
'button[data-folder-name="inbox"]',
|
||||||
|
'button[aria-label*="Inbox"]',
|
||||||
|
'div[aria-label*="Inbox"]',
|
||||||
|
'[data-folder-id="inbox"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
/Inbox - (\d+) items/i,
|
||||||
|
/Inbox \((\d+)\)/i,
|
||||||
|
/(\d+) items/i,
|
||||||
|
/\((\d+)\)/i,
|
||||||
|
// Additional patterns for different languages/formats
|
||||||
|
/Inbox.*?(\d+)/i,
|
||||||
|
/(\d+).*?unread/i,
|
||||||
|
/(\d+).*?message/i,
|
||||||
|
/(\d+).*?email/i
|
||||||
|
];
|
||||||
|
|
||||||
|
// Also check document title which often contains count
|
||||||
|
console.log("Checking document title:", document.title);
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = document.title.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
const totalItems = match[1];
|
||||||
|
console.log(`✓ Total items found in document title: ${totalItems}`);
|
||||||
|
return totalItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Checking DOM elements...");
|
||||||
|
for (const selector of selectors) {
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
console.log(`Checking selector: ${selector} (${elements.length} elements)`);
|
||||||
|
|
||||||
|
for (let idx = 0; idx < elements.length; idx++) {
|
||||||
|
const inboxDiv = elements[idx];
|
||||||
|
const sources = [
|
||||||
|
{ name: 'title', value: inboxDiv.getAttribute('title') },
|
||||||
|
{ name: 'aria-label', value: inboxDiv.getAttribute('aria-label') },
|
||||||
|
{ name: 'textContent', value: inboxDiv.textContent }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
if (!source.value) continue;
|
||||||
|
|
||||||
|
console.log(` Element ${idx} ${source.name}:`, source.value.substring(0, 100));
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = source.value.match(pattern);
|
||||||
|
if (match) {
|
||||||
|
const totalItems = match[1];
|
||||||
|
console.log(`✓ Total items found in ${source.name}: ${totalItems}`);
|
||||||
|
return totalItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("❌ Total items count not found in any source");
|
||||||
|
console.log("=== END TOTAL ITEMS SEARCH ===");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInboxCount() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Throttle execution to prevent excessive calls
|
||||||
|
if (isProcessing || (now - lastProcessTime) < THROTTLE_DELAY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true;
|
||||||
|
lastProcessTime = now;
|
||||||
|
console.log("addInboxCount function called");
|
||||||
|
|
||||||
|
const inboxElement = findInboxElement();
|
||||||
|
if (!inboxElement) {
|
||||||
|
console.log("Inbox element not found, will try again later");
|
||||||
|
isProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalItems = findTotalItems();
|
||||||
|
console.log("Total items:", totalItems);
|
||||||
|
|
||||||
|
// Check if we've already added the count
|
||||||
|
let countSpan = inboxElement.querySelector('.added-count');
|
||||||
|
if (!countSpan) {
|
||||||
|
// Create a new span element for the count
|
||||||
|
countSpan = document.createElement('span');
|
||||||
|
countSpan.style.marginLeft = '5px';
|
||||||
|
countSpan.style.fontWeight = 'bold';
|
||||||
|
countSpan.className = 'added-count';
|
||||||
|
|
||||||
|
// Add the count next to the inbox label
|
||||||
|
inboxElement.appendChild(countSpan);
|
||||||
|
console.log("Count span added to inbox label");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the count
|
||||||
|
if (totalItems) {
|
||||||
|
countSpan.textContent = ` (${totalItems})`;
|
||||||
|
countSpan.style.color = ''; // Reset to default color
|
||||||
|
} else {
|
||||||
|
countSpan.textContent = ' (-)';
|
||||||
|
countSpan.style.color = '#888'; // Grey color
|
||||||
|
}
|
||||||
|
console.log("Count span updated");
|
||||||
|
isProcessing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
console.log("=== INITIALIZING OUTLOOK INBOX COUNT EXTENSION ===");
|
||||||
|
|
||||||
|
// Log environment information first
|
||||||
|
logEnvironmentInfo();
|
||||||
|
|
||||||
|
console.log("Starting initial inbox count attempt...");
|
||||||
|
addInboxCount();
|
||||||
|
|
||||||
|
// Try to add the inbox count multiple times during startup with extended delays
|
||||||
|
// Firefox may need more time for Outlook's dynamic content to load
|
||||||
|
console.log("Scheduling additional attempts at 1s, 2s, 3s, 5s, 8s, 10s, 15s, and 20s...");
|
||||||
|
const retryTimes = [1000, 2000, 3000, 5000, 8000, 10000, 15000, 20000];
|
||||||
|
|
||||||
|
retryTimes.forEach(delay => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`=== ${delay/1000}-SECOND RETRY ===`);
|
||||||
|
logEnvironmentInfo(); // Re-log to see if page has loaded
|
||||||
|
addInboxCount();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then, check every 15 seconds (slightly increased for debugging)
|
||||||
|
setInterval(() => {
|
||||||
|
console.log("=== PERIODIC CHECK ===");
|
||||||
|
addInboxCount();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
console.log("=== INITIALIZATION COMPLETE ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a MutationObserver to watch for changes in the DOM
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
let shouldUpdate = false;
|
||||||
|
|
||||||
|
for (let mutation of mutations) {
|
||||||
|
// Only update if attributes changed on elements that might be inbox-related
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'title') {
|
||||||
|
const target = mutation.target;
|
||||||
|
if (target.hasAttribute && target.hasAttribute('data-folder-name')) {
|
||||||
|
shouldUpdate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Or if new inbox elements were added
|
||||||
|
else if (mutation.type === 'childList') {
|
||||||
|
const nodes = [...(mutation.addedNodes || [])];
|
||||||
|
if (nodes.some(node => node.nodeType === 1 &&
|
||||||
|
(node.hasAttribute?.('data-folder-name') ||
|
||||||
|
node.querySelector?.('[data-folder-name="inbox"]')))) {
|
||||||
|
shouldUpdate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUpdate) {
|
||||||
|
addInboxCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start observing with more targeted configuration
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['title', 'data-folder-name']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the init function when the DOM is fully loaded
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for messages from the background script
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.message === "URL_CHANGED") {
|
||||||
|
console.log("URL changed, running inbox count function");
|
||||||
|
addInboxCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
icon128.png
Normal file
BIN
icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
icon16.png
Normal file
BIN
icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 749 B |
BIN
icon64.png
Normal file
BIN
icon64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
34
manifest.json
Normal file
34
manifest.json
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Outlook Web Inbox Count",
|
||||||
|
"version": "1.4",
|
||||||
|
"description": "Adds the total number of items next to the Inbox label in Outlook Web",
|
||||||
|
"author": "Giorgio Gilestro",
|
||||||
|
"homepage_url": "https://lab.gilest.ro",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "outlook-inbox-count@gilest.ro"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://outlook.office.com/*",
|
||||||
|
"https://outlook.office365.com/*",
|
||||||
|
"https://outlook.cloud.microsoft/*"
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://outlook.office.com/*",
|
||||||
|
"https://outlook.office365.com/*",
|
||||||
|
"https://outlook.cloud.microsoft/*"
|
||||||
|
],
|
||||||
|
"js": ["content.js"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "icon16.png",
|
||||||
|
"64": "icon64.png",
|
||||||
|
"128": "icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
113
publish.sh
Executable file
113
publish.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build and publish the extension to the Chrome Web Store.
|
||||||
|
#
|
||||||
|
# Reads the version from manifest.json, builds outlook_web_inbox_count_v<VERSION>.zip
|
||||||
|
# (overwriting if present), then uploads and publishes it via the Chrome Web Store API.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./publish.sh # build from manifest.json, upload, and publish
|
||||||
|
# ./publish.sh --no-publish # upload only (leave in Draft state in the dashboard)
|
||||||
|
# ./publish.sh path/to/extension.zip # skip build, upload a specific zip
|
||||||
|
#
|
||||||
|
# Requires a .env file in the project root with:
|
||||||
|
# CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN, EXTENSION_ID
|
||||||
|
# See PUBLISHING.md for how to obtain these.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# --- Parse arguments ---
|
||||||
|
PUBLISH=true
|
||||||
|
ZIP_ARG=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-publish) PUBLISH=false ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,15p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) ZIP_ARG="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Load credentials ---
|
||||||
|
if [[ ! -f .env ]]; then
|
||||||
|
echo "Error: .env file not found (see PUBLISHING.md)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
for var in CLIENT_ID CLIENT_SECRET REFRESH_TOKEN EXTENSION_ID; do
|
||||||
|
if [[ -z "${!var:-}" ]]; then
|
||||||
|
echo "Error: $var is not set in .env" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Determine / build zip ---
|
||||||
|
if [[ -n "$ZIP_ARG" ]]; then
|
||||||
|
ZIP="$ZIP_ARG"
|
||||||
|
if [[ ! -f "$ZIP" ]]; then
|
||||||
|
echo "Error: $ZIP not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
VERSION=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
|
||||||
|
ZIP="outlook_web_inbox_count_v${VERSION}.zip"
|
||||||
|
echo "Building $ZIP from manifest version $VERSION..."
|
||||||
|
rm -f "$ZIP"
|
||||||
|
zip -q "$ZIP" manifest.json content.js icon16.png icon64.png icon128.png
|
||||||
|
fi
|
||||||
|
echo "Package: $ZIP"
|
||||||
|
|
||||||
|
# --- Get a fresh access token ---
|
||||||
|
echo "Fetching access token..."
|
||||||
|
ACCESS_TOKEN=$(curl -s "https://oauth2.googleapis.com/token" \
|
||||||
|
-d "client_id=${CLIENT_ID}" \
|
||||||
|
-d "client_secret=${CLIENT_SECRET}" \
|
||||||
|
-d "refresh_token=${REFRESH_TOKEN}" \
|
||||||
|
-d "grant_type=refresh_token" \
|
||||||
|
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token',''))")
|
||||||
|
|
||||||
|
if [[ -z "$ACCESS_TOKEN" ]]; then
|
||||||
|
echo "Error: failed to obtain access token. Refresh token may have expired." >&2
|
||||||
|
echo "See PUBLISHING.md 'Refreshing credentials' section." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Upload ---
|
||||||
|
echo "Uploading to extension $EXTENSION_ID..."
|
||||||
|
UPLOAD_RESPONSE=$(curl -s \
|
||||||
|
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||||
|
-H "x-goog-api-version: 2" \
|
||||||
|
-X PUT \
|
||||||
|
-T "$ZIP" \
|
||||||
|
"https://www.googleapis.com/upload/chromewebstore/v1.1/items/$EXTENSION_ID")
|
||||||
|
echo "Upload response: $UPLOAD_RESPONSE"
|
||||||
|
|
||||||
|
UPLOAD_STATE=$(echo "$UPLOAD_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('uploadState',''))" 2>/dev/null || echo "")
|
||||||
|
if [[ "$UPLOAD_STATE" != "SUCCESS" ]]; then
|
||||||
|
echo "Error: upload did not succeed (state: $UPLOAD_STATE)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Publish ---
|
||||||
|
if [[ "$PUBLISH" == "true" ]]; then
|
||||||
|
echo "Publishing..."
|
||||||
|
PUBLISH_RESPONSE=$(curl -s \
|
||||||
|
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||||
|
-H "x-goog-api-version: 2" \
|
||||||
|
-H "Content-Length: 0" \
|
||||||
|
-X POST \
|
||||||
|
"https://www.googleapis.com/chromewebstore/v1.1/items/$EXTENSION_ID/publish")
|
||||||
|
echo "Publish response: $PUBLISH_RESPONSE"
|
||||||
|
else
|
||||||
|
echo "Skipping publish step (--no-publish). The upload is in Draft state in the dashboard."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue