master b691eaf69bff cached
23 files
68.3 KB
16.5k tokens
44 symbols
1 requests
Download .txt
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 `<option value="THEME FILE NAME WITHOUT .scss">THEME NAME</option>`
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:
    //
    //     <html><head></head><body></body></html>
    //
    // As such, we perform a simple check as to whether the <body/> 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.
  -------------------------------------
  | <setting control>                 |
  | <setting control>                 |
  | <setting control>                 |
  | ...                               |
  |                                   |
  |-----------------------------------|
  |                           [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
================================================
<html>
    <head>
        <link rel="stylesheet" href="customize.css">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    </head>
    <body>
        <div class="dialog-grid">
            <!--Place setting controls in this div.-->
            <div id="first-row">
                <!--"Theme" dropdown-->
                <div class="form-group">
                    <label for="theme" class="font-weight-bold">Theme</label>
                    <select class="form-control" id="theme">
                        <option value="default">Default</option>
                        <option value="dracula">Dracula</option>
                        <option value="solar">Solar</option>
                        <option value="minty">Minty</option>
                        <option value="cerulean">Cerulean</option>
                        <option value="darkplus">DarkPlus</option>
                    </select>
                </div>

                <!--"Zoom" slider-->
                <div class="form-group">
                    <label class="font-weight-bold" style="margin-bottom: 0">Zoom % (25-500)</label>
                    <div class="form-row">
                        <div class="col">
                            <input type="range" class="form-control" id="zoom" min="-8" max="9" step="1" />
                        </div>
                        <div class="col-auto">
                            <button type="button" class="btn btn-secondary" id="reset-zoom">Reset</button>
                        </div>
                    </div>
                </div>

                <!--Options-->
                <div class="form-group">
                    <label class="font-weight-bold">Options</label>

                    <!--"Show menu bar" checkbox (Windows/Linux only, will get hidden for Mac)-->
                    <div class="form-check" id="show-menubar-div">
                        <input class="form-check-input" type="checkbox" id="show-menubar">
                        <label class="form-check-label" for="show-menubar">Show menu bar</label>
                    </div>

                    <!--"Start automatically" checkbox-->
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" id="start-automatically">
                        <label class="form-check-label" for="start-automatically">Start application automatically at logon</label>
                    </div>

                    <!--"Start minimized" checkbox-->
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" id="start-minimized">
                        <label class="form-check-label" for="start-minimized">Start application minimized to tray</label>
                    </div>

                    <!--"Exit on close" checkbox-->
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" id="exit-on-close">
                        <label class="form-check-label" for="exit-on-close">Exit application when main window is closed</label>
                    </div>

                    <!-- Hide dialer sidebar -->
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" id="hide-dialer-sidebar">
                        <label class="form-check-label" for="hide-dialer-sidebar">Hide dialer sidebar</label>
                    </div>
                </div>
            </div>

            <!--Place window buttons in this div.-->
            <div id="second-row">
                <!--"Close" button-->
                <button type="button" class="btn btn-primary" id="close-button" style="float: right">Close</button>
            </div>
        </div>
        <script src="customize.js" type="text/javascript"></script>
    </body>
</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;
};

Download .txt
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
Download .txt
SYMBOL INDEX (44 symbols across 4 files)

FILE: src/badge_generator.js
  method constructor (line 5) | constructor(win, opts = {}) {
  method generate (line 18) | generate(number) {
  method drawBadge (line 23) | drawBadge(number, style) {

FILE: src/constants.js
  constant APPLICATION_NAME (line 6) | const APPLICATION_NAME = 'Voice Desktop';
  constant APPLICATION_ICON_LARGE (line 9) | const APPLICATION_ICON_LARGE                = '1024px-Google_Voice_icon_...
  constant APPLICATION_ICON_MEDIUM (line 10) | const APPLICATION_ICON_MEDIUM               = '64px-Google_Voice_icon_(2...
  constant APPLICATION_ICON_SMALL (line 11) | const APPLICATION_ICON_SMALL                = 'tray-Google_Voice_icon_(2...
  constant APPLICATION_ICON_SMALL_WITH_INDICATOR (line 12) | const APPLICATION_ICON_SMALL_WITH_INDICATOR = 'tray-dirty-Google_Voice_i...
  constant URL_GOOGLE_VOICE (line 15) | const URL_GOOGLE_VOICE           = 'https://voice.google.com'
  constant URL_GITHUB_README (line 16) | const URL_GITHUB_README          = 'https://github.com/jerrod-lankford/g...
  constant URL_GITHUB_SECURITY_POLICY (line 17) | const URL_GITHUB_SECURITY_POLICY = 'https://github.com/jerrod-lankford/g...
  constant URL_GITHUB_VIEW_ISSUES (line 18) | const URL_GITHUB_VIEW_ISSUES     = 'https://github.com/jerrod-lankford/g...
  constant URL_GITHUB_REPORT_BUG (line 19) | const URL_GITHUB_REPORT_BUG      = 'https://github.com/jerrod-lankford/g...
  constant URL_GITHUB_FEATURE_REQUEST (line 20) | const URL_GITHUB_FEATURE_REQUEST = 'https://github.com/jerrod-lankford/g...
  constant URL_GITHUB_ASK_QUESTION (line 21) | const URL_GITHUB_ASK_QUESTION    = 'https://github.com/jerrod-lankford/g...
  constant URL_GITHUB_RELEASES (line 22) | const URL_GITHUB_RELEASES        = 'https://github.com/jerrod-lankford/g...
  constant DEFAULT_SETTING_SHOW_MENU_BAR (line 25) | const DEFAULT_SETTING_SHOW_MENU_BAR   = true;
  constant DEFAULT_SETTING_THEME (line 26) | const DEFAULT_SETTING_THEME           = 'default';
  constant DEFAULT_SETTING_START_MINIMIZED (line 27) | const DEFAULT_SETTING_START_MINIMIZED = false;
  constant DEFAULT_SETTING_EXIT_ON_CLOSE (line 28) | const DEFAULT_SETTING_EXIT_ON_CLOSE   = false;
  constant DEFAULT_HIDE_DIALER_SIDEBAR (line 29) | const DEFAULT_HIDE_DIALER_SIDEBAR     = false;

FILE: src/main.js
  constant REFRESH_RATE (line 15) | const REFRESH_RATE = 3000;
  constant DEFAULT_WIDTH (line 20) | const DEFAULT_WIDTH = 1200;
  constant DEFAULT_HEIGHT (line 21) | const DEFAULT_HEIGHT = 900;
  function createWindow (line 88) | function createWindow() {
  function exitApplication (line 261) | function exitApplication() {
  function loadGoogleVoice (line 270) | function loadGoogleVoice(loadExternal=false) {
  function updateNotifications (line 278) | function updateNotifications(app) {
  function processNotificationCount (line 320) | function processNotificationCount(app, count) {
  function processNotificationCount_Windows (line 347) | function processNotificationCount_Windows(oldCount, newCount) {
  function processNotificationCount_MacOS (line 370) | function processNotificationCount_MacOS(app, oldCount, newCount) {
  function createTray (line 382) | function createTray(iconPath, tipText) {
  function showMainWindow (line 405) | function showMainWindow() {
  function showSettingsWindow (line 410) | function showSettingsWindow() {
  function saveWindowSize (line 446) | function saveWindowSize() {
  function isMac (line 459) | function isMac()     {return (process.platform === 'darwin');}
  function isWindows (line 460) | function isWindows() {return (process.platform === 'win32');}

FILE: src/utils/cssInjector.js
  constant BASE (line 5) | const BASE = `base.scss`;
  constant MAPPINGS (line 6) | const MAPPINGS = `mappings.scss`;
  constant HIDE_DIALER_SIDEBAR_CSS (line 7) | const HIDE_DIALER_SIDEBAR_CSS = `gv-call-sidebar { display: none }`;
  method constructor (line 10) | constructor(app, win) {
  method showHideDialerSidebar (line 15) | showHideDialerSidebar(hide) {
  method injectTheme (line 29) | injectTheme(theme) {
  function joinImports (line 59) | function joinImports(app, file) {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (74K chars).
[
  {
    "path": ".github/workflows/build.yml",
    "chars": 869,
    "preview": "name: Build/release\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  release:\n    runs-on: ${{ matrix.os }}\n\n    strat"
  },
  {
    "path": ".gitignore",
    "chars": 45,
    "preview": "node_modules/\nout/\ndist/\n\n.DS_Store\n.vscode/\n"
  },
  {
    "path": ".npmrc",
    "chars": 36,
    "preview": "registry=https://registry.npmjs.com/"
  },
  {
    "path": "README.md",
    "chars": 2572,
    "preview": "## Why\nI'm annoyed at the lack of desktop app for voice, like hangouts had.\n\n## What does it do\nIt just lets you keep vo"
  },
  {
    "path": "SECURITY.md",
    "chars": 307,
    "preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf you need to report a vunerability with the electron wrapper itself p"
  },
  {
    "path": "THEMES.md",
    "chars": 5188,
    "preview": "# Contributing a theme\nSo you want to add a theme? Themeing google voice is fairly complex, I tried to modularize it as "
  },
  {
    "path": "package.json",
    "chars": 1361,
    "preview": "{\n \"name\": \"voice-desktop-app\",\n \"version\": \"1.3.1\",\n \"description\": \"An electron shell wrapper for the google voice app"
  },
  {
    "path": "src/badge_generator.js",
    "chars": 2838,
    "preview": "// Copied from https://github.com/viktor-shmigol/electron-windows-badge\n\n'use strict';\nmodule.exports = class BadgeGener"
  },
  {
    "path": "src/constants.js",
    "chars": 3630,
    "preview": "/**********************************************************************************************************************\r"
  },
  {
    "path": "src/main.js",
    "chars": 24490,
    "preview": "// Requires\nconst { app, nativeImage, BrowserWindow, Tray, Menu, ipcMain, BrowserView, shell, powerMonitor, systemPrefer"
  },
  {
    "path": "src/pages/customize.css",
    "chars": 1454,
    "preview": "/***************************************************************************************************\n  We use a CSS Grid"
  },
  {
    "path": "src/pages/customize.html",
    "chars": 4039,
    "preview": "<html>\n    <head>\n        <link rel=\"stylesheet\" href=\"customize.css\">\n        <link rel=\"stylesheet\" href=\"https://stac"
  },
  {
    "path": "src/pages/customize.js",
    "chars": 4922,
    "preview": "(async function() {\n    const { ipcRenderer } = require('electron');\n    const constants = require('../constants')\n\n    "
  },
  {
    "path": "src/preload.js",
    "chars": 872,
    "preview": "/**********************************************************************************************************************\n"
  },
  {
    "path": "src/themes/base.scss",
    "chars": 2115,
    "preview": "/* Dont use !important on these styles, we will inject it for you */\n\n/* Navbar */\n%navbar,\n%navbar-search {\n    backgro"
  },
  {
    "path": "src/themes/cerulean.scss",
    "chars": 882,
    "preview": "\n/* Color Palette */\n$background: white;\n$foreground: #495057;\n$primary: #2fa4e7;\n$secondary: #73a839;\n$selection: rgba("
  },
  {
    "path": "src/themes/darkplus.scss",
    "chars": 4662,
    "preview": "/* Color palette */\n$background: #131516;\n$foreground: #e3e3e3;\n$primary: #006156;\n$secondary: #003126;\n$selection: #222"
  },
  {
    "path": "src/themes/dracula.scss",
    "chars": 303,
    "preview": "/* Color palette */\n$background: #282a36;\n$foreground: #f8f8f2;\n$primary: #6272a4;\n$secondary: #bd93f9;\n$selection: #444"
  },
  {
    "path": "src/themes/mappings.scss",
    "chars": 4281,
    "preview": "/** \n * These mappings may seem ridiculous and non-sensesical, and they are... but they are for a reason\n * I tried to p"
  },
  {
    "path": "src/themes/minty.scss",
    "chars": 561,
    "preview": "\n/* Color Palette */\n$background: white;\n$foreground: #5a5a5a;\n$primary: #78c2ad;\n$secondary: #6cc3d5;\n$selection: rgba("
  },
  {
    "path": "src/themes/solar.scss",
    "chars": 249,
    "preview": "\n/* Color Palette */\n\n$background: #002b36;\n$foreground: white;\n$primary: #2aa198;\n$secondary: #b58900 ;\n$selection: #83"
  },
  {
    "path": "src/utils/cssInjector.js",
    "chars": 2321,
    "preview": "const sass = require('sass');\nconst fs = require('fs');\nconst path = require('path');\n\nconst BASE = `base.scss`;\nconst M"
  },
  {
    "path": "src/utils/notificationShim.js",
    "chars": 1947,
    "preview": "/**********************************************************************************************************************\n"
  }
]

About this extraction

This page contains the full source code of the jerrod-lankford/google-voice-desktop-app GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (68.3 KB), approximately 16.5k tokens, and a symbol index with 44 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!