diff --git a/.gitignore b/.gitignore index 79faf8f..2ce9e32 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,16 @@ -extension.zip \ No newline at end of file +# OS +.DS_Store + +# Build outputs +/dist/ +*.zip + +# Local scripts cache or temp +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor +.idea/ +.vscode/ diff --git a/README.md b/README.md index 6b84b00..780342d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # TranscripTonic -Simple Google Meet transcripts. Private and open source. +Simple Google Meet transcripts. Private and open source. Works on Chrome and Firefox. ![marquee-large](/assets/marquee-large.png) @@ -21,10 +21,17 @@ View video on [YouTube](https://www.youtube.com/watch?v=ARL6HbkakX4) # Installation + +## Chrome +## Firefox +Firefox users can install the extension using the unpacked installation method described below, or wait for the official Firefox Add-ons store release. + +**Note:** Firefox requires Manifest V2 format, so a separate `manifest-firefox.json` file is provided for Firefox compatibility. +

@@ -69,7 +76,7 @@ When this happens, it might be possible to recover the transcript, but recovery
# Privacy policy -TranscripTonic Chrome extension does not collect any information from users in any manner, except anonymous errors and transcript download timestamp. All processing/transcript storage happens within the user's Chrome browser and does not leave the device, unless you configure a webhook and choose to post data to your webhook URL. +TranscripTonic browser extension does not collect any information from users in any manner, except anonymous errors and transcript download timestamp. All processing/transcript storage happens within the user's browser and does not leave the device, unless you configure a webhook and choose to post data to your webhook URL.

@@ -79,3 +86,26 @@ The transcript may not always be accurate and is only intended to aid in improvi

+ +# Installing unpacked extension +This method works for both Chrome and Firefox browsers. + +1. Download the unpacked extension zip file from GitHub using this [link](https://raw.githubusercontent.com/vivek-nexus/transcriptonic/refs/heads/main/extension-unpacked.zip) + +## For Chrome: +2. Open `chrome://extensions` in a new Chrome tab +3. Enable "Developer mode" from top right corner +4. Drag and drop the unpacked extension zip file to complete the installation process +5. If drag and drop of zip file does not work, unzip the file. Click on "Load unpacked" in chrome extensions page and select the `extension-unpacked` folder to complete the installation process. + +## For Firefox: +2. Open `about:debugging` in a new Firefox tab +3. Click "This Firefox" in the left sidebar +4. Click "Load Temporary Add-on" +5. Unzip the downloaded file and navigate to the `extension-unpacked` folder +6. **Important:** Select specifically the `manifest-firefox.json` file (NOT `manifest.json`) + +**Note:** Remove unpacked extension when no longer needed. Your meeting data of unpacked extension and extension installed from official stores are stored separately. + +
+
diff --git a/extension-unpacked.zip b/extension-unpacked.zip deleted file mode 100644 index 211ce1b..0000000 Binary files a/extension-unpacked.zip and /dev/null differ diff --git a/extension/background.js b/extension/background.js index 4aa74ab..326eeec 100644 --- a/extension/background.js +++ b/extension/background.js @@ -352,55 +352,61 @@ function downloadTranscript(index, isWebhookEnabled) { content += "Transcript saved using TranscripTonic Chrome extension (https://chromewebstore.google.com/detail/ciepnfnceimjehngolkijpnbappkkiag)" content += "\n---------------" - const blob = new Blob([content], { type: "text/plain" }) + if (isFirefox()) { + // Firefox: use message passing to meetings.html for download + sendDownloadToMeetingsPage(fileName, content, resolve, reject); + } else { + // Chrome: use downloads API + const blob = new Blob([content], { type: "text/plain" }) - // Read the blob as a data URL - const reader = new FileReader() + // Read the blob as a data URL + const reader = new FileReader() - // Read the blob - reader.readAsDataURL(blob) + // Read the blob + reader.readAsDataURL(blob) - // Download as text file, once blob is read - reader.onload = function (event) { - if (event.target?.result) { - const dataUrl = event.target.result + // Download as text file, once blob is read + reader.onload = function (event) { + if (event.target?.result) { + const dataUrl = event.target.result - // Create a download with Chrome Download API - chrome.downloads.download({ - // @ts-ignore - url: dataUrl, - filename: fileName, - conflictAction: "uniquify" - }).then(() => { - console.log("Transcript downloaded") - resolve("Transcript downloaded successfully") - - // Increment anonymous transcript generated count to a Google sheet - fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, { - mode: "no-cors" - }) - }).catch((err) => { - console.error(err) + // Create a download with Chrome Download API chrome.downloads.download({ // @ts-ignore url: dataUrl, - filename: "TranscripTonic/Transcript.txt", + filename: fileName, conflictAction: "uniquify" - }) - console.log("Invalid file name. Transcript downloaded to TranscripTonic directory with simple file name.") - resolve("Transcript downloaded successfully with default file name") + }).then(() => { + console.log("Transcript downloaded") + resolve("Transcript downloaded successfully") + + // Increment anonymous transcript generated count to a Google sheet + fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, { + mode: "no-cors" + }) + }).catch((err) => { + console.error(err) + chrome.downloads.download({ + // @ts-ignore + url: dataUrl, + filename: "TranscripTonic/Transcript.txt", + conflictAction: "uniquify" + }) + console.log("Invalid file name. Transcript downloaded to TranscripTonic directory with simple file name.") + resolve("Transcript downloaded successfully with default file name") - // Logs anonymous errors to a Google sheet for swift debugging - fetch(`https://script.google.com/macros/s/AKfycbwN-bVkVv3YX4qvrEVwG9oSup0eEd3R22kgKahsQ3bCTzlXfRuaiO7sUVzH9ONfhL4wbA/exec?version=${chrome.runtime.getManifest().version}&code=009&error=${encodeURIComponent(err)}&meetingSoftware=${meeting.meetingSoftware}`, { mode: "no-cors" }) + // Logs anonymous errors to a Google sheet for swift debugging + fetch(`https://script.google.com/macros/s/AKfycbwN-bVkVv3YX4qvrEVwG9oSup0eEd3R22kgKahsQ3bCTzlXfRuaiO7sUVzH9ONfhL4wbA/exec?version=${chrome.runtime.getManifest().version}&code=009&error=${encodeURIComponent(err)}&meetingSoftware=${meeting.meetingSoftware}`, { mode: "no-cors" }) - // Increment anonymous transcript generated count to a Google sheet - fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, { - mode: "no-cors" + // Increment anonymous transcript generated count to a Google sheet + fetch(`https://script.google.com/macros/s/AKfycbxgUPDKDfreh2JIs8pIC-9AyQJxq1lx9Q1qI2SVBjJRvXQrYCPD2jjnBVQmds2mYeD5nA/exec?version=${chrome.runtime.getManifest().version}&isWebhookEnabled=${isWebhookEnabled}&meetingSoftware=${meeting.meetingSoftware}`, { + mode: "no-cors" + }) }) - }) - } - else { - reject({ errorCode: "009", errorMessage: "Failed to read blob" }) + } + else { + reject({ errorCode: "009", errorMessage: "Failed to read blob" }) + } } } } @@ -669,4 +675,72 @@ function registerContentScripts(showNotification = true) { }) }) }) +} + +function isFirefox() { + // @ts-ignore - browser is a Firefox-specific global + return typeof browser !== 'undefined' && /firefox/i.test(navigator.userAgent); +} + +/** + * Firefox-compatible download handler that sends blob to meetings.html + * @param {string} fileName + * @param {string} content + * @param {Function} resolve + * @param {Function} reject + */ +function sendDownloadToMeetingsPage(fileName, content, resolve, reject) { + // Find or open meetings.html, then send the download message + chrome.tabs.query({}, function (tabs) { + let meetingsTab = tabs.find(tab => tab.url && tab.url.includes('meetings.html')); + if (meetingsTab && meetingsTab.id) { + chrome.tabs.update(meetingsTab.id, { active: true }, function () { + if (meetingsTab.id) { + chrome.tabs.sendMessage( + meetingsTab.id, + { + type: "download_transcript_blob", + fileName: fileName, + blobContent: content + }, + function (response) { + if (response && response.success) { + resolve("Transcript downloaded successfully (Firefox)"); + } else { + reject(new Error("Failed to trigger download in Firefox (meetings.html)")); + } + } + ); + } + }); + } else { + // Open meetings.html in a new tab + chrome.tabs.create({ url: chrome.runtime.getURL('meetings.html'), active: true }, function (newTab) { + // Wait for the tab to load, then send the message + const listener = function (tabId, changeInfo) { + if (tabId === newTab.id && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + if (newTab.id) { + chrome.tabs.sendMessage( + newTab.id, + { + type: "download_transcript_blob", + fileName: fileName, + blobContent: content + }, + function (response) { + if (response && response.success) { + resolve("Transcript downloaded successfully (Firefox)"); + } else { + reject(new Error("Failed to trigger download in Firefox (new meetings.html)")); + } + } + ); + } + } + }; + chrome.tabs.onUpdated.addListener(listener); + }); + } + }); } \ No newline at end of file diff --git a/extension/manifest-firefox.json b/extension/manifest-firefox.json new file mode 100644 index 0000000..14582cb --- /dev/null +++ b/extension/manifest-firefox.json @@ -0,0 +1,40 @@ +{ + "manifest_version": 2, + "name": "TranscripTonic", + "version": "3.2.5", + "description": "Simple Google Meet transcripts. Private and open source.", + "browser_action": { + "default_icon": "icon.png", + "default_popup": "popup.html" + }, + "icons": { + "128": "icon.png" + }, + "content_scripts": [ + { + "js": [ + "content.js" + ], + "run_at": "document_end", + "matches": [ + "https://meet.google.com/*" + ], + "exclude_matches": [ + "https://meet.google.com/" + ] + } + ], + "permissions": [ + "storage", + "https://meet.google.com/*", + "https://*/*" + ], + "background": { + "scripts": [ + "background.js" + ] + }, + "web_accessible_resources": [ + "meetings.html" + ] +} diff --git a/extension/manifest.json b/extension/manifest.json index 173a3cc..37f5785 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,4 +1,5 @@ { + "_comment": "This is the Chrome/Manifest V3 version. Firefox users should use manifest-firefox.json (Manifest V2)", "manifest_version": 3, "name": "TranscripTonic", "version": "3.2.5", @@ -43,5 +44,11 @@ ], "background": { "service_worker": "background.js" - } + }, + "web_accessible_resources": [ + { + "resources": ["icon.png", "popup.html", "meetings.html"], + "matches": ["https://meet.google.com/*"] + } + ] } \ No newline at end of file diff --git a/extension/meetings.js b/extension/meetings.js index 37b3a82..52f75f5 100644 --- a/extension/meetings.js +++ b/extension/meetings.js @@ -334,4 +334,31 @@ function getDuration(meetingStartTimestamp, meetingEndTimestamp) { return durationHours > 0 ? `${durationHours}h ${remainingMinutes}m` : `${durationMinutes}m` +} + +// Add Firefox download support +if (typeof browser !== 'undefined' && /firefox/i.test(navigator.userAgent)) { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'download_transcript_blob') { + try { + const blob = new Blob([message.blobContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = message.fileName; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 100); + if (sendResponse) sendResponse({ success: true }); + } catch (e) { + if (sendResponse) sendResponse({ success: false }); + } + return true; + } + }); + // If meetings.html is opened by the background script, focus the window + window.focus(); } \ No newline at end of file diff --git a/scripts/build-cross.sh b/scripts/build-cross.sh new file mode 100755 index 0000000..4b8d63a --- /dev/null +++ b/scripts/build-cross.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Build Chrome (MV3) and Firefox (MV2) variants from unified root using separate manifests + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC_DIR="$ROOT_DIR/extension" +DIST_DIR="$ROOT_DIR/dist" +CHROME_OUT="$DIST_DIR/chrome" +FIREFOX_OUT="$DIST_DIR/amo" + +echo "[build] Root: $ROOT_DIR" + +rm -rf "$CHROME_OUT" "$FIREFOX_OUT" +mkdir -p "$CHROME_OUT/icons" "$FIREFOX_OUT/icons" "$DIST_DIR" + +read_version() { + local manifest_path="$1" + if command -v python3 >/dev/null 2>&1; then + python3 - "$manifest_path" <<'PY' +import json,sys +path=sys.argv[1] +with open(path,'r') as f: + data=json.load(f) +print(data.get('version','0.0.0')) +PY + else + grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$manifest_path" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/' + fi +} + +VERSION=$(read_version "$SRC_DIR/manifest.json") +echo "[build] Version: $VERSION" + +copy_common() { + local from="$1"; local to="$2" + local files=(background.js content.js meetings.html meetings.js popup.html popup.js icon.png) + for f in "${files[@]}"; do + if [[ -f "$from/$f" ]]; then cp "$from/$f" "$to/$f"; fi + done + if [[ -d "$from/icons" ]]; then + find "$from/icons" -maxdepth 1 -type f -name '*.svg' -exec cp {} "$to/icons/" \; + fi +} + +strip_ts_refs() { + local f="$1" + if [[ -f "$f" ]]; then + if [[ "$OSTYPE" == darwin* ]]; then + sed -i '' -E 's#^/// /dev/null 2>&1; then + echo "[lint] Running web-ext lint (Firefox)..." + web-ext lint --source-dir "$FIREFOX_OUT" || echo "[lint] web-ext reported warnings/errors (continuing)." +fi +FIREFOX_ZIP="$DIST_DIR/transcriptonic-firefox-v$VERSION.zip" +(cd "$FIREFOX_OUT" && zip -qr "$FIREFOX_ZIP" . -x '*.DS_Store') + +echo "[done] Chrome: $CHROME_ZIP" +echo "[done] Firefox: $FIREFOX_ZIP" + +