Repository: jerrod-lankford/google-voice-desktop-app Branch: master Commit: b691eaf69bff Files: 23 Total size: 68.3 KB Directory structure: gitextract_8gu62_hp/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .npmrc ├── README.md ├── SECURITY.md ├── THEMES.md ├── package.json └── src/ ├── badge_generator.js ├── constants.js ├── main.js ├── pages/ │ ├── customize.css │ ├── customize.html │ └── customize.js ├── preload.js ├── themes/ │ ├── base.scss │ ├── cerulean.scss │ ├── darkplus.scss │ ├── dracula.scss │ ├── mappings.scss │ ├── minty.scss │ └── solar.scss └── utils/ ├── cssInjector.js └── notificationShim.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build/release on: push: branches: - master jobs: release: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] steps: - name: Check out Git repository uses: actions/checkout@v1 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 14 - name: Build/release Electron app uses: samuelmeuli/action-electron-builder@v1 with: # GitHub token, automatically provided to the action # (No need to define this secret in the repo settings) github_token: ${{ secrets.github_token }} # If the commit is tagged with a version (e.g. "v1.0.0"), # release the app after building release: ${{ startsWith(github.ref, 'refs/tags/v') }} ================================================ FILE: .gitignore ================================================ node_modules/ out/ dist/ .DS_Store .vscode/ ================================================ FILE: .npmrc ================================================ registry=https://registry.npmjs.com/ ================================================ FILE: README.md ================================================ ## Why I'm annoyed at the lack of desktop app for voice, like hangouts had. ## What does it do It just lets you keep voice open without a chrome browser. It will also check the dom for notifications and display a badge in the task bar and closing the app will send it to a tray instead of closing. ## Supported Operating Systems Currently supports both OSX, Windows 10 & Linux. Questions? Ideas? Join us in discord https://discord.gg/3SSS6vkKET ## Installation Go to the [Releases Page](https://github.com/Jerrkawz/google-voice-desktop-app/releases) and download the release for your OS. Simply uzip and drag into the applications folder (mac) or run the executable (windows) or run the app image (ubuntu) **Mac Note: The mac version is unsigned, so you will have to click "Open Anyway" after running, or go to Settings > Security & Privacy > General > Open Anyway. Sorry not paying for a dev license just for this** **Linux Note: You will have to make the AppImage executable in order to run it. Right Click > Properties > Permissions > Allow Executing file as a program** ## Customize You can change settings, apply themes and other things in the customization page. Go to `Electron > Settings` on Mac in the global menu bar, or `File > Settings` on windows. If you've hidden the menu bar you can also access the settings from the system tray. **Windows Settings** ![Windows Settings](/screenshots/windowsSettings.png?raw=true) **Mac Settings** ![Mac Settings](/screenshots/macSettings.png?raw=true) **System Tray Settings** ![System Tray Settings](/screenshots/systemTraySettings.png?raw=true) ## Themes The latest version now supports custom themes, which can be set in the Settings dialog. Not only themes but also a system for themeing! If you want to create your own theme and contribute back to the project you can do that [here](THEMES.md). ## Run From Source `git clone git@github.com:Jerrkawz/google-voice-desktop-app.git` `npm install -g yarn` `yarn install` `yarn start` To build yourself you can run `yarn run build:windows` or `yarn run build:mac` or `yarn run build:linux` ## Screenshots **Main window:** ![Windows](/screenshots/windows.png?raw=true) **Dracula theme:** ![Dracula](/screenshots/dracula.png?raw=true) **Solar theme:** ![Solar](/screenshots/solar.png?raw=true) **Minty theme:** ![Minty](/screenshots/minty.png?raw=true) **Cerulean theme:** ![Cerulean](/screenshots/cerulean.png?raw=true) ## Attributions - Dracula: https://github.com/dracula/dracula-theme - Solar / Minty / Cerulean: https://bootswatch.com/ ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability If you need to report a vunerability with the electron wrapper itself please email me at jllankfo -at- ncsu (dot) edu. If theres a specific issue with the google voice web app you can see here for more information: https://support.google.com/voice/community ================================================ FILE: THEMES.md ================================================ # Contributing a theme So you want to add a theme? Themeing google voice is fairly complex, I tried to modularize it as much as possible so that other people could add themes. Theres several different levels of creating a theme. If all you want to do is create a theme that looks like one of the existing themes then: 1. Setup a fork of this repository 2. Run `npm install` to fetch dependencies 3. Then just copy that themes `src/themes/.scss` file and create a new theme with a new name 4. Modify the color palette with the colors that you want. 5. Then go into `src/pages/customize.html` and add a new option tag like so `` 6. Run `npm run start` to start the app and test your theme ## Building a custom theme If you really hate yourself and you want to build a custom theme or theme some specific part of the application a different way then you certainly can do that. I tried to break down the page into somewhat logical pieces and allow you to style those specific pieces. There are 5 main components that you can style, and there are base styles applied to these components in the `base.scss` file. This file does basic things like set background / foreground colors. Fair warning, since I was attemping to simiplify it greatly, some of the generated selectors can be very complex and match many things that might not be intended. If you are having trouble please reach out to me on discord. The page is broken down into 5 main components, each with sub components underneath. You can reference these components by using scss placeholder selectors (%navbar for example) %navbar - The navigation bar at the top of the page - %navbar-title - The title that says "Voice" - %navbar-search - The search bar **Note: I frequently style svgs as well but I didnt give them a placeholder name, you can style them like so `%navbar-search svg` would allow you to style or change the color of the search inputs icon** %side-nav - The navigation bar on the left side of the page - %side-nav-item - Each item in the bar - %side-nav-item-active - The currently selected item - %side-nav-item-badge - The badge when you have an unread message %conversation - %conversation-header - The header container at the top of the conversation - %conversation-title - Title at the top of the conversation in the header - %conversatpon-footer - Footer / send message box at the bottom of the conversation - %conversation-summary - Summary when no thread is selected (1 new message etc) - %conversation-message-recieved - Message recieved bubble - %conversation-message-sent - Message sent bubble - %conversation-timestamp - Message timestamp - %conversation-link - Link inside of a message %lists - All of the lists inside voice. Messages, voicemails, search results etc. - %list-item - An item inside the list, is also sometimes the cards at the top of lists (contact card etc) - %list-item-active - Currently active item, only used in message threads and voicemails afaik - %list-item-heading - Primary text field inside an item (eg. Name) - %list-item-detail - Secondary text field inside an item (eg. Phone number) %dial-pad - Dial pad on the right side when on phone tab - %dial-pad-button - A button inside the dial pad - %dial-pad-toggle - The toggle to show / hide the dial pad The easiest way to visualize these components is to create a new theme with background colors for each component. Create a new theme as mentioned in the first section or overwrite one of the existing ones. For example doing this: ```@use 'base'; /** Hacky border color fix for conversation list */ %conversation { background-color: yellow; } /** Note: I used %list-item instead of %lists because seting bg on %lists wont do anything */ %list-item { background-color: pink; } %navbar { background-color: red; } %side-nav { background-color: green; } %dial-pad { background-color: blue; } ``` Gives us the follow output so we can easily tell which component is which. ![Components](screenshots/components.png) ## More complex themeing and modifying mappings.scss You are really asking for it huh... If you continue you should have at least a decent understanding of css and css selectors, and possibly scss as well. Alright fine. The way this crazy shit works is that all of the components map to a selector or a group of selectors within the app. If you want to see what a certain component maps to (%navbar for example) you can look at the `src/themes/mappings.scss` file. For example %conversation-link maps to the css selector `gv-message-item gv-annotation a`. Most of them are not this simple. Some of them map to many different things, for example list-item has like 10 different mappings because we have to style all the components that can possibly show up inside our lists. If you need to target a component specifically for your theme then you can always include a one-off css selector in your theme, its going to work fine but I would recommend talking to me and trying to understand my thought process when creating these components and perhaps it makes sense to pull out another component by itself. ================================================ FILE: package.json ================================================ { "name": "voice-desktop-app", "version": "1.3.1", "description": "An electron shell wrapper for the google voice app", "main": "src/main.js", "build": { "appId": "com.jerrkawz.voiceDesktop", "productName": "Voice Desktop", "mac": { "category": "public.app-category.social-networking", "extendInfo": { "NSMicrophoneUsageDescription": "This app requires microphone access to capture your voice while on calls." } }, "dmg": { "icon": false }, "linux": { "target": [ "AppImage" ], "category": "Office" } }, "scripts": { "start": "electron .", "postinstall": "electron-builder install-app-deps", "build:windows": "electron-builder --windows", "build:mac": "electron-builder --mac", "build:linux": "electron-builder --linux", "build:x64": "electron-builder --mac --windows --linux --x64", "build:arm64": "electron-builder --mac --windows --linux --arm64", "release": "electron-builder --mac --windows --linux --x64 --arm64 --publish always" }, "keywords": [ "electron", "voice", "wrapper", "desktop", "google" ], "author": "jllankfo@ncsu.edu", "license": "ISC", "dependencies": { "auto-launch": "^5.0.5", "electron-context-menu": "^3.1.1", "electron-store": "^8.0.0", "sass": "^1.32.8" }, "devDependencies": { "electron": "^14.2.4", "electron-builder": "^23.0.2" } } ================================================ FILE: src/badge_generator.js ================================================ // Copied from https://github.com/viktor-shmigol/electron-windows-badge 'use strict'; module.exports = class BadgeGenerator { constructor(win, opts = {}) { const defaultStyle = { fontColor : 'white', font : '24px arial', color : 'red', fit : true, decimals : 0, radius: 8 }; this.win = win; this.style = Object.assign(defaultStyle, opts); } generate(number) { const opts = JSON.stringify(this.style); return this.win.webContents.executeJavaScript(`window.drawBadge = function ${this.drawBadge}; window.drawBadge(${number}, ${opts});`); } drawBadge(number, style) { var radius = style.radius; var img = document.createElement('canvas'); img.width = Math.ceil(radius * 2); img.height = Math.ceil(radius * 2); img.ctx = img.getContext('2d'); img.radius = radius; img.number = number; img.displayStyle = style; style.color = style.color ? style.color : 'red'; style.font = style.font ? style.font : '18px arial'; style.fontColor = style.fontColor ? style.fontColor : 'white'; style.fit = style.fit === undefined ? true : style.fit; style.decimals = style.decimals === undefined || isNaN(style.decimals) ? 0 : style.decimals; img.draw = function() { var fontScale, fontWidth, fontSize, number; this.width = Math.ceil(this.radius * 2); this.height = Math.ceil(this.radius * 2); this.ctx.clearRect(0,0,this.width,this.height); this.ctx.fillStyle = this.displayStyle.color; this.ctx.beginPath(); this.ctx.arc(radius,radius,radius,0,Math.PI * 2); this.ctx.fill(); this.ctx.font = this.displayStyle.font; this.ctx.textAlign = "center"; this.ctx.textBaseline = "middle"; this.ctx.fillStyle = this.displayStyle.fontColor; number = this.number.toFixed(this.displayStyle.decimals); fontSize = Number(/[0-9\.]+/.exec(this.ctx.font)[0]); if (!this.displayStyle.fit || isNaN(fontSize)) { this.ctx.fillText(number,radius,radius); } else { fontWidth = this.ctx.measureText(number).width; fontScale = Math.cos(Math.atan(fontSize/fontWidth)) * this.radius * 2 / fontWidth; this.ctx.setTransform(fontScale,0,0,fontScale,this.radius,this.radius); this.ctx.fillText(number,0,0); this.ctx.setTransform(1,0,0,1,0,0); } if (!this.displayStyle.fit || isNaN(fontSize)) { this.ctx.fillText(number,radius,radius); } else { fontScale = Math.cos(Math.atan(fontSize/fontWidth)) * this.radius * 2 / fontWidth; this.ctx.setTransform(fontScale,0,0,fontScale,this.radius,this.radius); this.ctx.fillText(number,0,0); this.ctx.setTransform(1,0,0,1,0,0); } return this; } img.draw(); return img.toDataURL(); } } ================================================ FILE: src/constants.js ================================================ /********************************************************************************************************************** * This module contains constants that are meant to be used throughout the entire application. **********************************************************************************************************************/ // Strings const APPLICATION_NAME = 'Voice Desktop'; // Images const APPLICATION_ICON_LARGE = '1024px-Google_Voice_icon_(2020).png'; const APPLICATION_ICON_MEDIUM = '64px-Google_Voice_icon_(2020).png'; const APPLICATION_ICON_SMALL = 'tray-Google_Voice_icon_(2020).png'; const APPLICATION_ICON_SMALL_WITH_INDICATOR = 'tray-dirty-Google_Voice_icon_(2020).png'; // URLs const URL_GOOGLE_VOICE = 'https://voice.google.com' const URL_GITHUB_README = 'https://github.com/jerrod-lankford/google-voice-desktop-app/blob/master/README.md' const URL_GITHUB_SECURITY_POLICY = 'https://github.com/jerrod-lankford/google-voice-desktop-app/blob/master/SECURITY.md' const URL_GITHUB_VIEW_ISSUES = 'https://github.com/jerrod-lankford/google-voice-desktop-app/issues'; const URL_GITHUB_REPORT_BUG = 'https://github.com/jerrod-lankford/google-voice-desktop-app/issues/new?labels=bug'; const URL_GITHUB_FEATURE_REQUEST = 'https://github.com/jerrod-lankford/google-voice-desktop-app/issues/new?labels=enhancement'; const URL_GITHUB_ASK_QUESTION = 'https://github.com/jerrod-lankford/google-voice-desktop-app/issues/new?labels=question'; const URL_GITHUB_RELEASES = 'https://github.com/jerrod-lankford/google-voice-desktop-app/releases'; // Default Settings const DEFAULT_SETTING_SHOW_MENU_BAR = true; const DEFAULT_SETTING_THEME = 'default'; const DEFAULT_SETTING_START_MINIMIZED = false; const DEFAULT_SETTING_EXIT_ON_CLOSE = false; const DEFAULT_HIDE_DIALER_SIDEBAR = false; module.exports = { // Strings APPLICATION_NAME, // Application name (displayed in various places) // Images APPLICATION_ICON_LARGE, // Main application icon (large) --sufficient size for MacOS Doc APPLICATION_ICON_MEDIUM, // Main application icon (medium) --sufficient size for Windows Taskbar APPLICATION_ICON_SMALL, // Main application icon (small) --sufficient size for system notification area APPLICATION_ICON_SMALL_WITH_INDICATOR, // Main application icon (small, with "notifications" indicator) // URLs URL_GOOGLE_VOICE, // Google Voice homepage URL_GITHUB_README, // The "README.md" file on GitHub URL_GITHUB_SECURITY_POLICY, // The "SECURITY.md" file on GitHub URL_GITHUB_VIEW_ISSUES, // List of currently logged issues on GitHub URL_GITHUB_REPORT_BUG, // Link to open a new bug on GitHub URL_GITHUB_FEATURE_REQUEST, // Link to request a feature on GitHub URL_GITHUB_ASK_QUESTION, // Link to ask a question on GitHub URL_GITHUB_RELEASES, // Link to published releases on GitHub // Default Settings DEFAULT_SETTING_SHOW_MENU_BAR, // Whether the MenuBar of the main application window should be visible DEFAULT_SETTING_THEME, // Default theme to apply DEFAULT_SETTING_START_MINIMIZED, // Whether the application should start minimized to the system notification area DEFAULT_SETTING_EXIT_ON_CLOSE, // Whether the application should terminate when the user closes the main application window DEFAULT_HIDE_DIALER_SIDEBAR // Whether the dialer sidebar should be hidden or not }; ================================================ FILE: src/main.js ================================================ // Requires const { app, nativeImage, BrowserWindow, Tray, Menu, ipcMain, BrowserView, shell, powerMonitor, systemPreferences } = require('electron'); const constants = require('./constants'); const AutoLaunch = require('auto-launch') const contextMenu = require('electron-context-menu'); const BadgeGenerator = require('./badge_generator'); const path = require('path'); const CSSInjector = require('./utils/cssInjector'); const Store = require('electron-store'); const Url = require('url'); // Constants const store = new Store(); const appPath = app.getAppPath(); const REFRESH_RATE = 3000; // 3 seconds const icon = path.join(appPath, 'images', constants.APPLICATION_ICON_MEDIUM); const iconTray = path.join(appPath, 'images', constants.APPLICATION_ICON_SMALL); const iconTrayDirty = path.join(appPath, 'images', constants.APPLICATION_ICON_SMALL_WITH_INDICATOR); const dockIcon = nativeImage.createFromPath(path.join(appPath, 'images', constants.APPLICATION_ICON_LARGE)); const DEFAULT_WIDTH = 1200; const DEFAULT_HEIGHT = 900; // Globals let lastNotification = 0; let badgeGenerator; let cssInjector; let tray; let win; // The main application window let settingsWindow; // When not null, the "Settings" window, which is currently open // Only one instance of the app should run if (!app.requestSingleInstanceLock()) { exitApplication(); } app.on('second-instance', () => { showMainWindow(); }); // If the computer is shutting down or restarting then close powerMonitor.on('shutdown', () => { exitApplication(); }); // Setup context menu contextMenu({ showSaveImage: true, showInspectElement: true }); // If we're running on Windows, set our Application User Model ID to our application name. // This will be displayed in all system Toasts that get generated to display notifications // to the user. If we don't do this, "electron.app.Electron" will be displayed instead. if (isWindows()){ app.setAppUserModelId(constants.APPLICATION_NAME); } // Setup notification shim to focus window ipcMain.on('notification-clicked', () => { showMainWindow(); }); // Ask for permission to use the microphone if the OS requires it if (isMac()) { console.log('asking for microphone access'); systemPreferences.askForMediaAccess("microphone"); } // Show window when clicking on macosx dock icon app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } // Unhide on mac if dock icon is clicked if (win && !win.isVisible()) { win.show(); } }); // Setup timer to keep dock notifications up to date setInterval(updateNotifications.bind(this, app), REFRESH_RATE); app.dock && app.dock.setIcon(dockIcon); app.whenReady().then(createWindow); // Creates and returns this application's main BrowserWindow, navigated to Google Voice. function createWindow() { // Create the window, making it hidden initially. If we have // it on record, re-apply the window size last set by the user. const prefs = store.get('prefs') || {}; win = new BrowserWindow({ width: prefs.windowWidth || DEFAULT_WIDTH, height: prefs.windowHeight || DEFAULT_HEIGHT, icon, show: false, webPreferences: { spellcheck: true, preload: path.join(__dirname, 'preload.js'), nodeIntegration: true, contextIsolation: false } }) //win.webContents.openDevTools(); // Create the window's menu bar. let menuBar = Menu.buildFromTemplate([ { label: '&File', submenu: [ {label: '&Reload', click: () => {loadGoogleVoice();}}, // Reload Google Voice within our main window {label: 'Go to &website', click: () => {loadGoogleVoice(true);}}, // Open Google Voice externally in the user's browser {type: 'separator'}, {label: '&Settings', click: () => {showSettingsWindow()}}, // Open/display our Settings window {type: 'separator'}, { label: '&Close', // Close the window accelerator: isMac() ? 'Command+W' : 'Ctrl+W', click: () => {win.close();} }, { label: '&Exit', // Exit the application accelerator: isMac() ? 'Command+Q' : 'Ctrl+Shift+W', click: () => {exitApplication();} } ] }, { label: "Edit", submenu: [ { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, { type: "separator" }, { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" } ] }, { label: '&View', submenu: [ {role: 'zoomIn', visible: false}, // Zoom in (Ctrl+Shift++) {role: 'zoomIn', accelerator: 'CommandOrControl+='}, // Zoom in (Ctrl+=) {role: 'zoomOut'}, // Zoom out (Ctrl+-) {role: 'zoomOut', visible: false, accelerator: 'CommandOrControl+Shift+_'}, // Zoom out (Ctrl+Shift+_) {role: 'resetZoom'}, // Reset zoom (Ctrl+0) {type: 'separator'}, {role: 'toggleFullScreen'}, // Toggle full screen (F11) {type: 'separator'}, {label: '&Hide menu bar', visible: !isMac(), click: () => {win.setMenuBarVisibility(false);}} // Hide the menu bar (not supported for Mac) ] }, { label: '&Help', submenu: [ {label: 'Report a &bug', click: () => {shell.openExternal(constants.URL_GITHUB_REPORT_BUG);}}, {label: 'Request a &feature', click: () => {shell.openExternal(constants.URL_GITHUB_FEATURE_REQUEST);}}, {label: 'Ask a &question', click: () => {shell.openExternal(constants.URL_GITHUB_ASK_QUESTION);}}, {label: 'View &issues', click: () => {shell.openExternal(constants.URL_GITHUB_VIEW_ISSUES);}}, {type: 'separator'}, {label: '&Security Policy', click: () => {shell.openExternal(constants.URL_GITHUB_SECURITY_POLICY);}}, {type: 'separator'}, {label: 'View &releases', click: () => {shell.openExternal(constants.URL_GITHUB_RELEASES);}}, {label: `&About (v${app.getVersion()})`, click: () => {shell.openExternal(constants.URL_GITHUB_README);}} ] } ]); // Set the menu bar's visibility. if (isMac()) { Menu.setApplicationMenu(menuBar) // On Mac, we always show the menu bar } else { // On Windows/Linux, we give the user a setting for hiding the menu bar. Add the menu bar to // the window (which ensures that its keyboard shortcuts will work regardless of the menu bar's // visibility), but make the menu bar visible only if the user hasn't asked us to hide it. win.setMenu(menuBar); if (((prefs.showMenuBar != undefined) && !prefs.showMenuBar) || !constants.DEFAULT_SETTING_SHOW_MENU_BAR) { win.setMenuBarVisibility(false); } } // Navigate the window to Google Voice. When it finishes loading, modify Google's markup as needed // to support user customizations that we allow the user to make from within our application UI. loadGoogleVoice(); win.webContents.on('did-finish-load', () => { // Re-apply the theme last selected by the user. const theme = store.get('prefs.theme') || constants.DEFAULT_SETTING_THEME; const hideDialerSidebar = store.get('prefs.hideDialerSidebar') || constants.DEFAULT_HIDE_DIALER_SIDEBAR; cssInjector = new CSSInjector(app, win); cssInjector.injectTheme(theme); cssInjector.showHideDialerSidebar(hideDialerSidebar); }); // Create our system notification area icon. if (tray) { tray.destroy; } tray = createTray(iconTray, constants.APPLICATION_NAME); badgeGenerator = new BadgeGenerator(win); win.webContents.on('new-window', function(e, url) { e.preventDefault(); // Cancel the request to open the target URL in a new window // If the target URL is a Google Voice URL, have our main window navigate to it instead of opening // it in a new window. This supports the ability to add additional accounts and switch between // them on-demand. Otherwise, for all other URLs, have the system open them using the default type // handler. This is done to force URLs to open in the user's browser, where they are likely already // signed into services that need authentication (e.g. Spotify). Note that if the user ever gets // stuck navigated somewhere that isn't the main Google Voice page, they can always use the "Reload" // item in the notification area icon context menu to get back to the Google Voice home page. const hostName = Url.parse(url).hostname; if ( hostName === 'voice.google.com' || hostName === 'accounts.google.com') { win && win.loadURL(url); } else { shell.openExternal(url); } }); // Whenever a request is made for the window to be closed, determine whether we should allow the close to // happen and terminate the application, or just hide the window and keep running in the notification area. win.on('close', function (event) { // Proceed based on the reason why the window is being closed. if (app.isQuiting) { // The window is being closed as a result of us calling app.quit() due to the // user's invocation of one of our "Exit" menu items. In this case, we'll // allow the close to happen. This will lead to termination of the application. } else { // The window is being closed as a result of the user explicitly trying to close // it. If the user has enabled the "exit on close" setting, allow the close and // subsequent termination of the application to proceed. Otherwise, cancel the // close and hide the window instead; we'll keep running in the notification area. const exitOnClose = store.get('prefs.exitOnClose') || constants.DEFAULT_SETTING_EXIT_ON_CLOSE; if (!exitOnClose) { event.preventDefault(); win.hide(); } } }); win.on('restore', function (event) { win.show(); }); win.on('resize', saveWindowSize); // Now that we've finished creating and initializing the window, show // it (unless the user has enabled the "start minimized" setting). if (!prefs.startMinimized) { win.show(); } return win; } // Terminates this application. function exitApplication() { app.isQuiting = true; app.quit(); } // Loads Google Voice. The "loadExternal" parameter specifies whether the load should // take place inside this application's main browser window. If set to false, Google // Voice will be opened in the user's default external browser instead. During the // load, Google Voice itself takes care of asking the user to log in when necessary. function loadGoogleVoice(loadExternal=false) { if (loadExternal) {shell.openExternal(constants.URL_GOOGLE_VOICE);} else {win && win.loadURL(constants.URL_GOOGLE_VOICE);} } // Invoked every "REFRESH_RATE" seconds. Parses the current notification count from Google // Voice's markup and then has this application display it to the user in an appropriate way. // Also implements a workaround for Electron's "blank white screen" bug that many users encounter. function updateNotifications(app) { if (!win || BrowserWindow.getAllWindows().length === 0) { return; } let sum = 0; // Query the dom for the notification badges win.webContents.executeJavaScript(`Array.from(document.querySelectorAll('.gv_root .navListItem .navItemBadge')).map(n => n.textContent && n.textContent.trim());`).then(counts => { if (counts && counts.length > 0) { sum = counts.reduce((accum, count) => { try { accum += parseInt(count, 10); } catch (e) { } return accum; }, 0); } processNotificationCount(app, sum); }); // The following is a workaround for the Electron bug where after an indeterminate // period of inactivity, the main application window turns into a blank white screen. // When this happens, inspection shows that the loaded page consists of the following // empty HTML markup: // // // // As such, we perform a simple check as to whether the of our loaded page // has become empty. If it has, then we automatically reload Google Voice for the // user. This seems to eliminate the problem entirely, without any adverse effects, // as once we detect an empty body, the application is already in a non-working state. win.webContents.executeJavaScript("document.querySelector('body').childNodes.length").then( (result) => { if (result === 0){ loadGoogleVoice(); } }) } // Displays a specified notification count to the user (if it isn't already // being displayed), in a way that is appropriate for their Operating System. function processNotificationCount(app, count) { if (count !== lastNotification) { // Update our record of what the new count is. We update our record *before* proceeding to ensure we don't // enter a loop of continuously trying to update UI in the event that we experience some failure down below. let oldCount = lastNotification; lastNotification = count; // Perform OS-specific operations. if (isMac()) { processNotificationCount_MacOS(app, oldCount, count); } else if (isWindows()) { processNotificationCount_Windows(oldCount, count); } // Update our notification area icon based on the count. If it's greater than 0, // display the icon with a red dot, otherwise display the icon without a red dot. if (count > 0) { tray && tray.setImage(iconTrayDirty); } else { tray && tray.setImage(iconTray); } } } // Updates this application's UI on Windows in a way that is appropriate for a specified notification count. function processNotificationCount_Windows(oldCount, newCount) { if (win) { // If the specified new count is non-0, use our Badge Generator to dynamically generate an image representing it, // and then apply the image as an overlay icon on our main window's Taskbar button. Note that if the user has // the "Use small Taskbar buttons" setting turned on, the overlay won't actually be rendered due to lack of space. if (newCount) { badgeGenerator.generate(newCount).then((base64) => { const image = nativeImage.createFromDataURL(base64); win.setOverlayIcon(image, 'You have new messages and/or calls'); }); } else { win.setOverlayIcon(null, ''); } // If the notification count has gone up and our main window isn't currently // focused, also flash the window's Taskbar button to catch the user's attention. if ((newCount > oldCount) && !win.isFocused()) { win.flashFrame(true); } } } // Updates this application's UI on Mac OS in a way that is appropriate for a specified notification count. function processNotificationCount_MacOS(app, oldCount, newCount) { // Overlay the specified new count on our Dock icon. If the count has // increased, also bouce the icon the catch the user's attention. if (app.dock) { app.dock.setBadge(`${newCount || ''}`); if (newCount > oldCount) { app.dock.bounce(); } } } // Creates this application's notification area icon. function createTray(iconPath, tipText) { // Create the icon, assigning it our application icon and name. let appIcon = new Tray(iconPath); appIcon.setToolTip(tipText); // Construct the icon's context menu. This is done using an array of MenuItem objects. appIcon.setContextMenu(Menu.buildFromTemplate([ {label: '&Open', click: () => {showMainWindow();}}, {label: '&Reload', click: () => {loadGoogleVoice();}}, {type: 'separator'}, {label: '&Settings', click: () => {showSettingsWindow();}}, {type: 'separator'}, {label: '&Exit', click: () => {exitApplication();}} ])); appIcon.on('click', function (event) { showMainWindow(); }); return appIcon; } // Displays this application's main window to the user. function showMainWindow() { win && win.show(); } // Creates (if it doesn't already exist) this application's "Settings" window, and then displays it to the user. function showSettingsWindow() { if (!settingsWindow) { // Create our Settings window, keeping a global reference to it. This reference allows // us to know when the window is open, preventing the user from opening it a second time. settingsWindow = new BrowserWindow({ width: 600, height: 600, title: 'Settings', parent: win, modal: true, resizable: false, minimizable: false, webPreferences: { nodeIntegration: true, contextIsolation: false }}); settingsWindow.removeMenu(); // Stash the user's settings store on the window so that it can be accessed by the window's renderer process. settingsWindow.prefs = store.get('prefs') || {}; // Load our settings page into the window. settingsWindow.loadFile(path.join(appPath, 'src', 'pages', 'customize.html')); //settingsWindow.webContents.openDevTools(); // When the window gets closed, release its global reference. settingsWindow.on('close', function() { settingsWindow = null; }); } else { // Our Settings window is already open, just bring it back to the foreground. settingsWindow.show(); } } function saveWindowSize() { const bounds = win.getBounds(); const prefs = store.get('prefs') || {}; prefs.windowWidth = bounds.width; prefs.windowHeight = bounds.height; store.set('prefs', prefs); } // ==================================================================================================================== // Helper Functions // ==================================================================================================================== function isMac() {return (process.platform === 'darwin');} function isWindows() {return (process.platform === 'win32');} // ==================================================================================================================== // Invokable IPC Handlers // ==================================================================================================================== // Returns the execution path of this application. ipcMain.handle('get-appPath', () => { return app.getAppPath(); }); // Returns the platform that this application is running on. ipcMain.handle('get-platform', () => { return process.platform; }); // Returns an object representing the user's current settings store. ipcMain.handle('get-user-prefs', () => { return store.get('prefs') || {}; }); // Returns a bool indicating whether this application is registered to start automatically at logon. ipcMain.handle('get-start-automatically', async () => { let autoLaunch = new AutoLaunch({name: constants.APPLICATION_NAME, path: app.getPath('exe')}) return await autoLaunch.isEnabled(); }); // Returns the current zoom level of this this application's main window. ipcMain.handle('get-zoom-level', () => { return win.webContents.getZoomLevel(); }); // ==================================================================================================================== // Settings Window Event Handlers // ==================================================================================================================== // Called when the theme has been changed. ipcMain.on('pref-change-theme', (event, theme) => { console.log(`Theme changed to: ${theme}`); // Apply the selected them and then save the selection to the user's settings store. cssInjector.injectTheme(theme); const prefs = store.get('prefs') || {}; prefs.theme = theme; store.set('prefs', prefs); }); // Called when the zoom level has been changed. ipcMain.on('pref-change-zoom', (event, zoomLevel) => { console.log(`Zoom level changed to: ${zoomLevel}`); // Apply the newly selected zoom level. Note that there is no need to save this setting to // the user's settings store. Electron handles remembering our main window's zoom level by // default, so it will automatically be restored the next time the application is launched. win.webContents.setZoomLevel(parseInt(zoomLevel)); }); // Called when the "show menu bar" checkbox has been checked/unchecked. ipcMain.on('pref-change-show-menubar', (e, showMenuBar) => { console.log(`"Show menu bar changed to: ${showMenuBar}`); // Apply the new value and then save it to the user's settings store. win.setMenuBarVisibility(showMenuBar); const prefs = store.get('prefs') || {}; prefs.showMenuBar = showMenuBar; store.set('prefs', prefs); }); // Called when the "start automatically" checkbox has been checked/unchecked. ipcMain.on('pref-change-start-automatically', (e, startAutomatically) => { console.log(`"Start Automatically" changed to: ${startAutomatically}`); // Register/unregister this application to be automatically started at logon. let autoLaunch = new AutoLaunch({name: constants.APPLICATION_NAME, path: app.getPath('exe')}) if (startAutomatically) { autoLaunch.enable(); } else { autoLaunch.disable(); } }); // Called when the "start minimized" checkbox has been checked/unchecked. ipcMain.on('pref-change-start-minimized', (e, startMinimized) => { console.log(`"Start Minimized" changed to: ${startMinimized}`); // Apply the new value and then save it to the user's settings store. const prefs = store.get('prefs') || {}; prefs.startMinimized = startMinimized; store.set('prefs', prefs); }); // Called when the "exit on close" checkbox has been checked/unchecked. ipcMain.on('pref-change-exit-on-close', (e, exitOnClose) => { console.log(`"Exit on close" changed to: ${exitOnClose}`); // Apply the new value and then save it to the user's settings store. const prefs = store.get('prefs') || {}; prefs.exitOnClose = exitOnClose; store.set('prefs', prefs); }); // Called when the "hide dialer sidebar" checkbox has been checked/unchecked. ipcMain.on('pref-change-hide-dialer-sidebar', (e, hideDialerSidebar) => { console.log(`Hide dialer sidebar changed to: ${hideDialerSidebar}`); // Apply the new value and then save it to the user's settings store. const prefs = store.get('prefs') || {}; prefs.hideDialerSidebar = hideDialerSidebar; cssInjector.showHideDialerSidebar(hideDialerSidebar); store.set('prefs', prefs); }); ================================================ FILE: src/pages/customize.css ================================================ /*************************************************************************************************** We use a CSS Grid to lay out the controls in the Settings window. The grid consists of a single column and two rows. The second row is meant to contain a single row of any and all buttons that the window needs to have (e.g. "Close"). As such, this row is assigned a height of "min-content" so that it is only ever as tall as it needs to be to fit the buttons placed inside it. The first row is then given a height of "auto", so that it takes up the remainder of the window's vertical space. ------------------------------------- | | | | | | | ... | | | |-----------------------------------| | [Close] | ------------------------------------- ***************************************************************************************************/ .dialog-grid { display: grid; grid-template-rows: auto min-content; grid-template-columns: auto; height: 100vh; padding: 20px; } /*Give checkboxes a slightly larger size that normal.*/ input[type=checkbox] { width: 20px; height: 20px; display: inline-block; position: relative; top: 5px; } ================================================ FILE: src/pages/customize.html ================================================
================================================ FILE: src/pages/customize.js ================================================ (async function() { const { ipcRenderer } = require('electron'); const constants = require('../constants') // Allow the user to hit the Escape key to close the window. window.addEventListener('keyup', (event) => { if (event.code === 'Escape') { window.close(); } }, true); // Retrieve the user's settings store from the main process. const prefs = await ipcRenderer.invoke('get-user-prefs'); // Populate the "theme" dropdown with the user's currently selected theme. // Notify the main process whenever the user selects a different theme. const themePicker = document.getElementById('theme'); themePicker.value = (prefs.theme || constants.DEFAULT_SETTING_THEME); themePicker.addEventListener('change', (e) => { const theme = e.target.value; ipcRenderer.send('pref-change-theme', theme); }); // Set the "zoom" slider to the main window's current zoom level. // Notify the main process whenever the user selects a new level. const zoomSlider = document.getElementById('zoom'); zoomSlider.value = await ipcRenderer.invoke('get-zoom-level'); zoomSlider.addEventListener('change', (e) => { const zoomLevel = e.target.value; ipcRenderer.send('pref-change-zoom', zoomLevel); }); // Whenever the user clicks the "reset zoom" button, set the "zoom" // slider back to 0 and notify the main process of this new value. const zoomResetButton = document.getElementById('reset-zoom'); zoomResetButton.addEventListener('click', (e) => { zoomSlider.value = 0; ipcRenderer.send('pref-change-zoom', 0); }); // Set the "show menu bar" checkbox based on the user's currently selected preference. // Notify the main process whenever the user changes their preference. If we're running // on Mac, just hide this setting instead since we don't support it for that platform. const menubarSettingDiv = document.getElementById('show-menubar-div'); const isMac = (await ipcRenderer.invoke('get-platform')) === "darwin"; if (isMac) { menubarSettingDiv.remove(); } else { const menubarSetting = document.getElementById('show-menubar'); menubarSetting.checked = (prefs.showMenuBar != undefined) ? prefs.showMenuBar : constants.DEFAULT_SETTING_SHOW_MENU_BAR; menubarSetting.addEventListener('change', (e) => { const checked = e.target.checked; ipcRenderer.send('pref-change-show-menubar', checked); }); } // Set the "start automatically" checkbox based on the user's currently selected // preference. Notify the main process whenever the preference changes. const startAutomatically = document.getElementById('start-automatically'); startAutomatically.checked = await ipcRenderer.invoke('get-start-automatically'); startAutomatically.addEventListener('change', (e) => { const checked = e.target.checked; ipcRenderer.send('pref-change-start-automatically', checked); }); // Set the "start minimized" checkbox based on the user's currently selected // startup mode. Notify the main process whenever the user changes the mode. const minimizedSetting = document.getElementById('start-minimized'); minimizedSetting.checked = (prefs.startMinimized != undefined) ? prefs.startMinimized : constants.DEFAULT_SETTING_START_MINIMIZED; minimizedSetting.addEventListener('change', (e) => { const checked = e.target.checked; ipcRenderer.send('pref-change-start-minimized', checked); }); // Set the "exit on close" checkbox based on the user's currently selected // preference. Notify the main process whenever the preference changes. const exitOnCloseSetting = document.getElementById('exit-on-close'); exitOnCloseSetting.checked = (prefs.exitOnClose != undefined) ? prefs.exitOnClose : constants.DEFAULT_SETTING_EXIT_ON_CLOSE; exitOnCloseSetting.addEventListener('change', (e) => { const checked = e.target.checked; ipcRenderer.send('pref-change-exit-on-close', checked); }); // Set the "hide dialer sidebar" checkbox based on the user's currently selected // preference. Notify the main process whenever the preference changes. const hideDialerSidebar = document.getElementById('hide-dialer-sidebar'); hideDialerSidebar.checked = (prefs.hideDialerSidebar != undefined) ? prefs.hideDialerSidebar : constants.DEFAULT_HIDE_DIALER_SIDEBAR; hideDialerSidebar.addEventListener('change', (e) => { const checked = e.target.checked; ipcRenderer.send('pref-change-hide-dialer-sidebar', checked); }); // Close the window if the user clicks the "Close" button. const closeButton = document.getElementById('close-button'); closeButton.addEventListener('click', (e) => { window.close(); }); })(); ================================================ FILE: src/preload.js ================================================ /********************************************************************************************************************** * Preload script for the main application window. Contains all code * that needs to execute before the window's web content begins loading. **********************************************************************************************************************/ (async() => { // Get this application's execution path. const {ipcRenderer} = require('electron'); const appPath = await ipcRenderer.invoke('get-appPath'); // Modify JavaScript's "Notification" class such that all notifications that get generated will have a // click handler attached to them which fires a "notification-clicked" event back at the main process. const {notificationShim} = require('./utils/notificationShim'); notificationShim(appPath); })(); ================================================ FILE: src/themes/base.scss ================================================ /* Dont use !important on these styles, we will inject it for you */ /* Navbar */ %navbar, %navbar-search { background-color: $background; } %navbar-title, %navbar input, %navbar input::placeholder, %navbar svg, %navbar-search svg /* Check if needed */{ color: $foreground; } /** Side Nav */ %side-nav { background-color: $background; } %side-nav-item, %side-nav-item svg { color: $foreground; } %side-nav-item-active { background-color: $selection; } %side-nav-item-active svg { color: $foreground; } %side-nav-item-badge { background-color: $primary; } /* Conversation */ %conversation, %conversation-header, %conversation-footer { color: $foreground; background-color: $background; } %conversation-title, %conversation-footer textarea, %conversation-footer textarea::placeholder, %conversation-footer svg, %conversation-header svg { color: $foreground; } %conversation-link { background: inherit; color: inherit; } %conversation-footer textarea::placeholder { opacity: .5; } %conversation-message-recieved, %conversation-message-sent { color: $foreground; border-radius: 2rem; } %conversation-message-recieved { background-color: $secondary; } %conversation-message-sent { background-color: $primary; } %conversation-summary, %conversation-timestamp { color: $foreground; } /* Lists */ %list { background-color: $background; } %list-item { background-color: $background; color: $foreground; } %list-item svg { color: $foreground; } %list-item-active, %list-item:hover { background-color: $selection; } %list-item-heading, %list-item-heading input, %list-item-heading input::placeholder { color: $foreground; } %list-item-detail { color: $foreground; opacity: .6; } /* Dial pad */ %dial-pad { background-color: $background; } %dial-pad-button { color: $foreground; } %dial-pad-button:hover { background-color: $selection; } %dial-pad-toggle { background-color: $background; color: $foreground; } %dial-pad-toggle svg { color: $foreground; } @use 'mappings'; ================================================ FILE: src/themes/cerulean.scss ================================================ /* Color Palette */ $background: white; $foreground: #495057; $primary: #2fa4e7; $secondary: #73a839; $selection: rgba(73, 80, 87, 0.6); @use 'base'; /* Override some of the default values of base.css. Thats why they come after the include */ %navbar { background-image: linear-gradient(#04519b, #033c73 60%, #02325f); background-repeat: no-repeat; } %navbar-title, %navbar svg { color: $background; } %navbar input, %navbar input::placeholder, %navbar-search svg { color: $foreground; } %navbar input::placeholder { opacity: .7; } %conversation-message-recieved { background-image: linear-gradient(#88c149, #73a839 60%, #699934); background-repeat: no-repeat; color: $background; } %conversation-message-sent { background-image: linear-gradient(#54b4eb, #2fa4e7 60%, #1d9ce5); background-repeat: no-repeat; color: $background; } ================================================ FILE: src/themes/darkplus.scss ================================================ /* Color palette */ $background: #131516; $foreground: #e3e3e3; $primary: #006156; $secondary: #003126; $selection: #222425; $border: #252525; $active: #30a99b; $button: #212425; @use 'base'; %side-nav { border-right: 1px solid $border; } div { border-color: $border; } mat-divider { border-color: $border; } ::-webkit-scrollbar { width: 0px; } %side-nav-item-active svg { color: $active } gv-thread-item-detail :first-child { opacity: 1; color: #999999; } .gvHighContrast.gvPageRoot [_nghost-oqk-c303] .thread-info.read[_ngcontent-oqk-c303] .latest-item-details[_ngcontent-oqk-c303] { opacity: 1; color: #999999; } %conversation { background-color: transparent; border-color: #505050; } div .caption { color: #82898e; } .md-button.md-raised[disabled] { background-color: $secondary; } .md-button.md-raised[disabled] { opacity: 0.5; } div.call-as-label { color: #858d92; } path[d="M19.59 7L12 14.59 6.41 9H11V7H3v8h2v-4.59l7 7 9-9L19.59 7z"] { color: #d93025; opacity: 1; } path[d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z"] { color: #1a73e8; opacity: 1; } path[d="M15 19v-2H8.41L20 5.41 18.59 4 7 15.59V9H5v10h10z"] { color: #1e8e3e; opacity: 1; } gv-contact-list.YYoZ8c-npMLoc { background-color: $background; } .rkljfb-biJjHb.gmat-caption.flex-noshrink { opacity: 1; color: #999999; } md-menu-content { background-color: #181A1B; } p.gmat-body-2 { color: #FFF; } .gvHighContrast .mat-icon-button.mat-accent { color: #d5d5d5; } .YYoZ8c-VeKiLd .YYoZ8c-NkdnBe:not([disabled]) { background-color: $primary; } /* settings view fixes start here */ .layout-row > md-content { background-color: $background; } .gvPageRoot .W3hWSd span[role=heading] { color: rgb(227, 227, 227); } div.block-container { background-color: #181A1B; } h3.sectionTitle { color: rgb(227,227,227); } .header { color: rgb(220, 220, 220); } span.phone-number { color: rgb(200, 200, 200); } .internalSubheader { color: rgb(180,180,180); } .no-devices.ng-star-inserted { color: rgb(180, 180, 180); } .subheader.ng-star-inserted { color: rgb(180,180,180); } .mat-slide-toggle-bar.mat-slide-toggle-bar-no-side-margin { background-color: rgb(95 95 95); } .gvHighContrast .mat-slide-toggle.mat-primary.mat-checked .mat-slide-toggle-bar { background-color: rgba(0,121,107,0.54); } .device-label { color: rgb(220, 220, 220); } div#LABEL_IDS\.ANONYMOUS_HEADER { color: rgb(220, 220, 220); } .deviceLabel { color: rgb(220, 220, 220); } .internalHeader { color: rgb(220, 220, 220); } .mat-form-field-appearance-outline[_ngcontent-egk-c24] .mat-form-field-infix { padding-top: 11px; } .mat-select-arrow { border-color: transparent; border-top-color: initial; } path[d="M10 17l5-5-5-5v10z"] { color: rgb(213, 213, 213); } .notificationText.ng-star-inserted { color: rgb(200, 200, 200); } .name { color: rgb(220, 220, 220); } button.mat-focus-indicator.mat-stroked-button.mat-button-base.ng-star-inserted { color: #00E1D6; } .active-title { color: #009186; } span.metadata { color: #009186; } .creditSubheader { color: #009186; } mat-icon.icon.mat-icon.notranslate.mat-icon-no-color { background-color: transparent; } .icon { background-color: #063331; } .mat-list-item-content { color: rgb(220, 220, 220); } button.mat-focus-indicator.mat-stroked-button.mat-button-base { border-color: $border; } a.mat-list-item.gmat-nav-list-item.gmat-subtitle-2.gmat-list-item-active { background-color: #1A1D1E; } div#mat-select-value-1 { color: rgb(220, 220, 220); } .mat-form-field-infix { padding-top: 11px; border-top: initial; } div[ng-class="ctrl.dialpadVisible ? '' : ctrl.CSS.DIALPAD_COLLAPSED"] { background-color: #131516; } /* Fixes for light-colored dial pad */ .dialpad-container { background-color: $background; } .input-row { background-color: $background; color: $foreground; } .mat-ripple.button { background-color: $button; color: $foreground; } .call-button { background-color: $active; } .call-button.mat-button-disabled { background-color: $secondary; } .active-call { background-color: $background; } .active-call-wrapper { background-color: $background; } .remote-display-name { color: $foreground; } .gmat-button { background-color: $button; color: $active; } .dtmf-input { background-color: $selection; color: $foreground; } /* Make unread texts a bit more obvious */ .gmat-subtitle-2.gvThreadItem-unread { font-weight: bold; background-color: $secondary; } ================================================ FILE: src/themes/dracula.scss ================================================ /* Color palette */ $background: #282a36; $foreground: #f8f8f2; $primary: #6272a4; $secondary: #bd93f9; $selection: #44475a; @use 'base'; %side-nav { border-right: 1px solid $foreground; } /** Hacky border color fix for conversation list */ %conversation > div { border-color: $foreground; } ================================================ FILE: src/themes/mappings.scss ================================================ /** * These mappings may seem ridiculous and non-sensesical, and they are... but they are for a reason * I tried to pick identifiers in google voice's cryptic ass css classes that are not likely to change * They roughly correlate to components on the page. See THEMES.md for more details on these components */ /* Navbar */ header { @extend %navbar; } header [title='Google Voice'] > span { @extend %navbar-title; } header form[role='search'] { @extend %navbar-search; } /* Side Nav */ gv-side-nav { @extend %side-nav; } gmat-nav-list .navListItem { @extend %side-nav-item; } gv-side-nav a.navListItem.gmat-list-item-active { @extend %side-nav-item-active; } [role='tablist']:not(.expanded) .navItemBadge { @extend %side-nav-item-badge; } /* Conversation */ gv-message-list, gv-voicemail-player md-content, gv-inbox-summary-ng2, gv-thread-details, gv-messaging-view md-content { // Message view when clicking someones name after search @extend %conversation; } gv-message-list-header > div { @extend %conversation-header; } gv-message-list-header [gv-test-id='conversation-title'], gv-message-list-header [gv-test-id='conversation-subtitle'], [aria-label='Group message'] { @extend %conversation-title; } gv-inbox-summary-ng2 .gv-inbox-summary .greeting, gv-inbox-summary-ng2 .gv-inbox-summary .status { @extend %conversation-summary; } gv-message-entry > div { @extend %conversation-footer; } gv-message-item [layout-align='start start'] [gv-test-id='bubble'] { @extend %conversation-message-recieved; } gv-message-item [layout-align='start end'] [gv-test-id='bubble'] { @extend %conversation-message-sent; } [gv-test-id='sms-sender-time-stamp'] { @extend %conversation-timestamp; } gv-message-item gv-annotation a { @extend %conversation-link; } /** Lists */ gv-conversation-list, // messages list #contact-list, // All contacts list on right body > [role='listbox'] { // List box for the contact search @extend %list; } gv-thread-item > div, [gv-test-id='send-new-message'], // send new message button gv-recipient-picker md-content, // Recipient picker wrapper gv-recipient-picker > div, // Select recipient picker on new call screen gv-call-as-banner, // Call as banner on phone number right panel gv-thread > div, // Item in the phone call list gv-make-call-panel-ng2 > div > div:nth-child(2), // Call panel enter a name or number gv-contact-item > div, gv-contact-card, gv-frequent-contact-card > div { @extend %list-item; } gv-thread-item > div.layout-row[aria-selected='true'] { @extend %list-item-active; } gv-thread-item div.layout-row gv-annotation[gv-test-id='item-contact'], gv-contact-list .gmat-overline, .gmat-subhead-2, // send new message text gv-recipient-picker md-content, // recipient picker wrapper gv-recipient-picker md-content md-input-container label, // recipient picker placeholder label gv-call-as-banner .call-as-label, // Call as banner label gv-call-as-banner .phone-number-details, // Call as banner phone number gv-make-call-panel-ng2 > div > div, gv-frequent-contact-card div > div > div:first-child, // Search result heading gv-frequent-contact-card div > div:first-child span, // frequent contact name gv-contact-item div gv-annotation, // All contacts heading on right gv-contact-card .gmat-body-2 div:first-child div:nth-child(2), // Contact card name after clicking on someones name in the search bar gv-recent-search-item gv-annotation, gv-call-log-item .primaryText { @extend %list-item-heading; } gv-thread-item-detail :first-child, gv-call-log-item .secondaryText, gv-frequent-contact-card div > div:nth-child(2) span, // frequent contact phone number gv-contact-card .gmat-body-2 > div > div:nth-child(3), // Contact card phone number after clicking on someones name in the search bar gv-contact-item > button > div > div:nth-child(2),// Phone number under all contacts on right gv-contact-item > div > div > div:nth-child(2) // Phone number under search { @extend %list-item-detail; } /* Dial pad */ gv-dialpad > div { @extend %dial-pad; } gv-dialpad [role='gridcell'] > div, gv-dialpad [role='gridcell'] .gmat-caption { @extend %dial-pad-button; } gv-dialpad-toggle button { @extend %dial-pad-toggle; } ================================================ FILE: src/themes/minty.scss ================================================ /* Color Palette */ $background: white; $foreground: #5a5a5a; $primary: #78c2ad; $secondary: #6cc3d5; $selection: rgba(0, 0, 0, 0.075); @use 'base'; /* Override some of the default values of base.css. Thats why they come after the include */ %navbar, %navbar-search { background-color: $primary; } %navbar-title, %navbar input, %navbar svg, %navbar input::placeholder, %navbar-search svg { color: $background; } %navbar input::placeholder { opacity: .7; } %conversation-message-recieved, %conversation-message-sent { color: $background; } ================================================ FILE: src/themes/solar.scss ================================================ /* Color Palette */ $background: #002b36; $foreground: white; $primary: #2aa198; $secondary: #b58900 ; $selection: #839496; @use 'base'; /** Hacky border color fix for conversation list */ %conversation > div { border-color: $foreground; } ================================================ FILE: src/utils/cssInjector.js ================================================ const sass = require('sass'); const fs = require('fs'); const path = require('path'); const BASE = `base.scss`; const MAPPINGS = `mappings.scss`; const HIDE_DIALER_SIDEBAR_CSS = `gv-call-sidebar { display: none }`; module.exports = class Injector { constructor(app, win) { this.win = win; this.app = app; } showHideDialerSidebar(hide) { if (!this.win) return; if (hide) { this.win.webContents.insertCSS(HIDE_DIALER_SIDEBAR_CSS).then(key => { this.sidebarStyleKey = key; }); } else { if (this.sidebarStyleKey) { this.win.webContents.removeInsertedCSS(this.sidebarStyleKey); } } } injectTheme(theme) { if (this.styleKey) { this.win.webContents.removeInsertedCSS(this.styleKey); this.styleKey = null; } if (theme !== 'default') { try { const file = fs.readFileSync(path.join(this.app.getAppPath(), 'src', 'themes', `${theme}.scss`), 'utf-8'); const data = joinImports(this.app, file); const result = sass.renderSync({data}); const styles = result.css.toString().replace(/;/g, ' !important;'); if (this.win) { this.win.webContents.insertCSS(styles).then(key => { this.styleKey = key; }); } } catch (e) { console.log(e); console.error(`Could not find theme ${theme}`); } } } } /** * The way sass processes use functions just isn't good enough, we need variables that can scope across files and we also * need to be able to split our selectors and placeholder selectors into different files for neatness. Anyway this is just a * simple function to recombine multiple files and then let sass process that */ function joinImports(app, file) { const base = fs.readFileSync(path.join(app.getAppPath(), 'src', 'themes', BASE), 'utf-8'); const mappings = fs.readFileSync(path.join(app.getAppPath(), 'src', 'themes', MAPPINGS), 'utf-8'); let contents = file.replace("@use 'base';", base); contents = contents.replace("@use 'mappings';", mappings); return contents; } ================================================ FILE: src/utils/notificationShim.js ================================================ /********************************************************************************************************************** * This module's main purpose is to automatically attach a "click" handler onto all Notifications that get * created using JavaScript's "Notification" class. When the attached handler is invoked (i.e. when any * Notification gets clicked by the user), a "notification-clicked" event gets sent to the main application * process, letting it know that the user has activated a notification. Secondary to that, Notifications * that don't have an icon assigned to them are also modified to display this application's icon. **********************************************************************************************************************/ const { ipcRenderer } = require('electron'); const constants = require('../constants.js'); const path = require('path'); module.exports.notificationShim = function(appPath) { const OldNotification = Notification; Notification = function (title, options) { // If the specified options don't include an icon for the notification, set the icon to our application icon. if (!options) { options = {}; } if (!options.icon) { options.icon = path.join(appPath, 'images', constants.APPLICATION_ICON_MEDIUM); } // Create a normal Notification instance using the specified parameters. const oldNotification = new OldNotification(title, options); // Automatically add a click handler, which notifies the main process when a click occurs. oldNotification.addEventListener('click', () => { ipcRenderer.send('notification-clicked', {}); }); return oldNotification; }; Notification.prototype = OldNotification.prototype; Notification.permission = OldNotification.permission; Notification.requestPermission = OldNotification.requestPermission; };