├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── Design ├── icons.sketch ├── info.svg └── power.svg ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── screenshot.png ├── sentry-symbols.js ├── src ├── electron │ ├── auto-updater.js │ ├── icons │ │ ├── app │ │ │ ├── AppIcon.icns │ │ │ ├── AppIcon.ico │ │ │ └── AppIcon.png │ │ └── tray │ │ │ ├── BarnacalIcon10Template.png │ │ │ ├── BarnacalIcon10Template@2x.png │ │ │ ├── BarnacalIcon11Template.png │ │ │ ├── BarnacalIcon11Template@2x.png │ │ │ ├── BarnacalIcon12Template.png │ │ │ ├── BarnacalIcon12Template@2x.png │ │ │ ├── BarnacalIcon13Template.png │ │ │ ├── BarnacalIcon13Template@2x.png │ │ │ ├── BarnacalIcon14Template.png │ │ │ ├── BarnacalIcon14Template@2x.png │ │ │ ├── BarnacalIcon15Template.png │ │ │ ├── BarnacalIcon15Template@2x.png │ │ │ ├── BarnacalIcon16Template.png │ │ │ ├── BarnacalIcon16Template@2x.png │ │ │ ├── BarnacalIcon17Template.png │ │ │ ├── BarnacalIcon17Template@2x.png │ │ │ ├── BarnacalIcon18Template.png │ │ │ ├── BarnacalIcon18Template@2x.png │ │ │ ├── BarnacalIcon19Template.png │ │ │ ├── BarnacalIcon19Template@2x.png │ │ │ ├── BarnacalIcon1Template.png │ │ │ ├── BarnacalIcon1Template@2x.png │ │ │ ├── BarnacalIcon20Template.png │ │ │ ├── BarnacalIcon20Template@2x.png │ │ │ ├── BarnacalIcon21Template.png │ │ │ ├── BarnacalIcon21Template@2x.png │ │ │ ├── BarnacalIcon22Template.png │ │ │ ├── BarnacalIcon22Template@2x.png │ │ │ ├── BarnacalIcon23Template.png │ │ │ ├── BarnacalIcon23Template@2x.png │ │ │ ├── BarnacalIcon24Template.png │ │ │ ├── BarnacalIcon24Template@2x.png │ │ │ ├── BarnacalIcon25Template.png │ │ │ ├── BarnacalIcon25Template@2x.png │ │ │ ├── BarnacalIcon26Template.png │ │ │ ├── BarnacalIcon26Template@2x.png │ │ │ ├── BarnacalIcon27Template.png │ │ │ ├── BarnacalIcon27Template@2x.png │ │ │ ├── BarnacalIcon28Template.png │ │ │ ├── BarnacalIcon28Template@2x.png │ │ │ ├── BarnacalIcon29Template.png │ │ │ ├── BarnacalIcon29Template@2x.png │ │ │ ├── BarnacalIcon2Template.png │ │ │ ├── BarnacalIcon2Template@2x.png │ │ │ ├── BarnacalIcon30Template.png │ │ │ ├── BarnacalIcon30Template@2x.png │ │ │ ├── BarnacalIcon31Template.png │ │ │ ├── BarnacalIcon31Template@2x.png │ │ │ ├── BarnacalIcon3Template.png │ │ │ ├── BarnacalIcon3Template@2x.png │ │ │ ├── BarnacalIcon4Template.png │ │ │ ├── BarnacalIcon4Template@2x.png │ │ │ ├── BarnacalIcon5Template.png │ │ │ ├── BarnacalIcon5Template@2x.png │ │ │ ├── BarnacalIcon6Template.png │ │ │ ├── BarnacalIcon6Template@2x.png │ │ │ ├── BarnacalIcon7Template.png │ │ │ ├── BarnacalIcon7Template@2x.png │ │ │ ├── BarnacalIcon8Template.png │ │ │ ├── BarnacalIcon8Template@2x.png │ │ │ ├── BarnacalIcon9Template.png │ │ │ └── BarnacalIcon9Template@2x.png │ └── index.js └── ui │ ├── about.html │ ├── calendar.tsx │ ├── calendar_body.tsx │ ├── calendar_header.tsx │ ├── header.tsx │ ├── index.html │ ├── index.tsx │ ├── menu.tsx │ └── types.ts ├── test ├── auto-updater.test.js └── index.test.js └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Test the project on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Install node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12.x 18 | - name: Install dependencies 19 | run: npm install 20 | - name: Run tests 21 | env: 22 | NODE_ENV: test 23 | run: npm run test:coverage 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | *.orig 5 | 6 | .cache/ 7 | .electron-symbols/ 8 | .nyc_output/ 9 | node_modules/ 10 | dist/ 11 | coverage/ 12 | public/ 13 | 14 | sentry.properties 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | 4 | .cache/ 5 | .electron-symbols/ 6 | .nyc_output/ 7 | coverage/ 8 | design/ 9 | dist/ 10 | public/ 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mike@mike-engel.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Design/icons.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/Design/icons.sketch -------------------------------------------------------------------------------- /Design/info.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Design/power.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mike Engel 2 | 3 | This software is released under the MIT license: 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Barnacal 2 | 3 | > A simple menu bar app for viewing a calendar 4 | 5 | 6 | Screenshot of Barnacal 7 | 8 | 9 | # Installation 10 | 11 | To install Barnacal on your system, visit the [Releases](https://github.com/mike-engel/barnacal/releases) page. From there you can download the binary for your system. If your system isn't supported, consider opening an issue or pull request to add support for it. 12 | 13 | # Developing 14 | 15 | This a Node.js project at heart, so begin by installing the npm dependencies. 16 | 17 | ```sh 18 | npm install 19 | ``` 20 | 21 | To start the front end compiler, run `npm run dev` in a terminal window. This will automatically watch and compile changes to your files. 22 | 23 | ```sh 24 | npm run dev 25 | ``` 26 | 27 | Finally, open a new terminal window/tab and start the electron process. This will launch the app and you can begin using it. 28 | 29 | ```sh 30 | npm run electron:start 31 | ``` 32 | 33 | If you want the electron app to be reloaded when you edit the html or JS files, then you'll want to install `watchexec` first, then run the watch command. 34 | 35 | ```sh 36 | # install watchexec if you don't have it already 37 | brew install watchexec 38 | 39 | npm run watch:electron 40 | ``` 41 | 42 | # Debugging 43 | 44 | You can open Chrome Dev Tools while developing 45 | Put this in `index.js` inside `configureWindow` function. It will open Dev Tools in the calendar. 46 | 47 | ```javascript 48 | window.webContents.openDevTools(); 49 | ``` 50 | 51 | # Contributing 52 | 53 | Issues and Pull requests are both welcomed! Prettier is enabled by default as a pre-commit hook, but feel free to add it to your editor. 54 | 55 | # [Code of Conduct](CODE_OF_CONDUCT.md) 56 | 57 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. 58 | 59 | # [Changelog](https://github.com/mike-engel/barnacal/releases) 60 | 61 | # [License](LICENSE.md) 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barnacal", 3 | "version": "0.9.4", 4 | "description": "A simple menu bar app for viewing a calendar", 5 | "main": "src/electron/index.js", 6 | "scripts": { 7 | "dev": "parcel --target electron --public-url ./ -d public/ src/ui/index.html", 8 | "build": "parcel build --target electron --public-url ./ -d public/ src/ui/index.html", 9 | "dist": "rm -rf dist; npm run build; electron-builder -mwl", 10 | "electron:start": "electron ./src/electron/index", 11 | "electron:watch": "watchexec -r -s SIGKILL -e js,html -i '*/lib/**' npm run electron:start", 12 | "pack": "npm run build; electron-builder -mwl --dir", 13 | "start": "node scripts/startDev.js", 14 | "test": "cross-env NODE_ENV=test npx mocha --exit --recursive test/**/*.test.js", 15 | "test:coverage": "nyc --reporter=text --reporter=html npm run test" 16 | }, 17 | "author": "Mike Engel ", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@sentry/browser": "^5.25.0", 21 | "@sentry/electron": "^1.5.2", 22 | "date-fns": "^2.16.1", 23 | "electron-is-dev": "^1.2.0", 24 | "first-run": "^2.0.0", 25 | "is-online": "^8.4.0", 26 | "react": "^16.13.1", 27 | "react-dom": "^16.13.1", 28 | "styled-components": "^4.4.1", 29 | "styled-typography": "^1.0.3" 30 | }, 31 | "devDependencies": { 32 | "@sentry/cli": "^1.58.0", 33 | "@types/react": "^16.9.51", 34 | "@types/react-dom": "^16.9.8", 35 | "@types/styled-components": "^4.4.3", 36 | "chai": "^4.2.0", 37 | "cross-env": "^6.0.3", 38 | "electron": "^7.3.3", 39 | "electron-builder": "^22.8.1", 40 | "electron-builder-squirrel-windows": "^22.9.1", 41 | "electron-download": "^4.1.1", 42 | "husky": "^3.1.0", 43 | "lint-staged": "^9.5.0", 44 | "mocha": "^6.2.3", 45 | "nyc": "^15.1.0", 46 | "parcel-bundler": "^1.12.4", 47 | "prettier": "^1.19.1", 48 | "proxyquire": "^2.1.3", 49 | "sinon": "^7.5.0", 50 | "typescript": "^3.9.7" 51 | }, 52 | "build": { 53 | "appId": "com.mike-engel.barnacal", 54 | "productName": "Barnacal", 55 | "compression": "maximum", 56 | "files": [ 57 | "src/electron/icons/**/*", 58 | "public/*", 59 | "src/electron/index.js", 60 | "src/electron/auto-updater.js", 61 | "src/ui/about.html" 62 | ], 63 | "mac": { 64 | "icon": "src/electron/icons/app/AppIcon.icns" 65 | }, 66 | "win": { 67 | "icon": "src/electron/icons/app/AppIcon.ico" 68 | }, 69 | "linux": { 70 | "category": "Utility", 71 | "target": [ 72 | { 73 | "target": "deb", 74 | "arch": [ 75 | "ia32", 76 | "x64" 77 | ] 78 | }, 79 | { 80 | "target": "AppImage", 81 | "arch": [ 82 | "ia32", 83 | "x64" 84 | ] 85 | } 86 | ] 87 | } 88 | }, 89 | "nyc": { 90 | "check-coverage": true 91 | }, 92 | "prettier": { 93 | "useTabs": true 94 | }, 95 | "husky": { 96 | "hooks": { 97 | "precommit": "lint-staged" 98 | } 99 | }, 100 | "lint-staged": { 101 | "*.{ts,tsx,js,jsx,md,json}": [ 102 | "prettier --write", 103 | "git add" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/screenshot.png -------------------------------------------------------------------------------- /sentry-symbols.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let SentryCli; 4 | let download; 5 | 6 | try { 7 | SentryCli = require('@sentry/cli'); 8 | download = require('electron-download'); 9 | } catch (e) { 10 | console.error('ERROR: Missing required packages, please run:'); 11 | console.error('npm install --save-dev @sentry/cli electron-download'); 12 | process.exit(1); 13 | } 14 | 15 | const VERSION = /\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/i; 16 | const SYMBOL_CACHE_FOLDER = '.electron-symbols'; 17 | const package = require('./package.json'); 18 | const sentryCli = new SentryCli('./sentry.properties'); 19 | 20 | async function main() { 21 | let version = getElectronVersion(); 22 | if (!version) { 23 | console.error('Cannot detect electron version, check package.json'); 24 | return; 25 | } 26 | 27 | console.log('We are starting to download all possible electron symbols'); 28 | console.log('We need it in order to symbolicate native crashes'); 29 | console.log( 30 | 'This step is only needed once whenever you update your electron version', 31 | ); 32 | console.log('Just call this script again it should do everything for you.'); 33 | 34 | let zipPath = await downloadSymbols({ 35 | version, 36 | platform: 'darwin', 37 | arch: 'x64', 38 | dsym: true, 39 | }); 40 | await sentryCli.execute(['upload-dif', '-t', 'dsym', zipPath], true); 41 | 42 | zipPath = await downloadSymbols({ 43 | version, 44 | platform: 'win32', 45 | arch: 'ia32', 46 | symbols: true, 47 | }); 48 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); 49 | 50 | zipPath = await downloadSymbols({ 51 | version, 52 | platform: 'win32', 53 | arch: 'x64', 54 | symbols: true, 55 | }); 56 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); 57 | 58 | zipPath = await downloadSymbols({ 59 | version, 60 | platform: 'linux', 61 | arch: 'x64', 62 | symbols: true, 63 | }); 64 | await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); 65 | 66 | console.log('Finished downloading and uploading to Sentry'); 67 | console.log(`Feel free to delete the ${SYMBOL_CACHE_FOLDER}`); 68 | } 69 | 70 | function getElectronVersion() { 71 | if (!package) { 72 | return false; 73 | } 74 | 75 | let electronVersion = 76 | (package.dependencies && package.dependencies.electron) || 77 | (package.devDependencies && package.devDependencies.electron); 78 | 79 | if (!electronVersion) { 80 | return false; 81 | } 82 | 83 | const matches = VERSION.exec(electronVersion); 84 | return matches ? matches[0] : false; 85 | } 86 | 87 | async function downloadSymbols(options) { 88 | return new Promise((resolve, reject) => { 89 | download( 90 | { 91 | ...options, 92 | cache: SYMBOL_CACHE_FOLDER, 93 | }, 94 | (err, zipPath) => { 95 | if (err) { 96 | reject(err); 97 | } else { 98 | resolve(zipPath); 99 | } 100 | }, 101 | ); 102 | }); 103 | } 104 | 105 | main().catch(e => console.error(e)); 106 | -------------------------------------------------------------------------------- /src/electron/auto-updater.js: -------------------------------------------------------------------------------- 1 | const { autoUpdater, ipcMain, Notification } = require("electron"); 2 | const isDev = require("electron-is-dev"); 3 | const isOnline = require("is-online"); 4 | const Sentry = require("@sentry/electron"); 5 | const { version } = require("../../package"); 6 | const { platform } = require("os"); 7 | 8 | const updateHost = "https://barnacal-updates.now.sh"; 9 | 10 | let isInitialized = false; 11 | 12 | const init = () => { 13 | autoUpdater.on("error", (err, msg) => { 14 | console.error("Error checking for updates: ", msg, err.stack); 15 | 16 | if (!isDev) 17 | isOnline() 18 | .then(() => Sentry.captureException(err)) 19 | .catch(() => {}); 20 | }); 21 | 22 | autoUpdater.setFeedURL(`${updateHost}/update/${platform()}/${version}`); 23 | 24 | // Don't check immediately on start for windows 25 | setTimeout(() => { 26 | autoUpdater.checkForUpdates(); 27 | }, 10000); 28 | 29 | setInterval(() => { 30 | autoUpdater.checkForUpdates(); 31 | }, 3600000); 32 | 33 | isInitialized = true; 34 | }; 35 | 36 | const onUpdate = (window, releaseNotes, releaseName) => { 37 | if (Notification.isSupported()) { 38 | const notification = new Notification({ 39 | title: "Barnacal update available", 40 | body: `Barnacal ${releaseName} is ready to install. Click to apply the update.` 41 | }); 42 | 43 | notification.show(); 44 | 45 | notification.on("click", autoUpdater.quitAndInstall); 46 | } 47 | 48 | window.webContents.send("update-ready", { releaseNotes, releaseName }); 49 | }; 50 | 51 | module.exports = window => { 52 | if (!isInitialized) init(); 53 | 54 | autoUpdater.on("update-downloaded", (evt, releaseNotes, releaseName) => { 55 | window.webContents.send("update-downloaded"); 56 | onUpdate(window, releaseNotes, releaseName); 57 | }); 58 | 59 | ipcMain.on("install-update", autoUpdater.quitAndInstall); 60 | 61 | window.on("close", autoUpdater.removeAllListeners); 62 | }; 63 | 64 | module.exports.isInitialized = isInitialized; 65 | module.exports.init = init; 66 | module.exports.onUpdate = onUpdate; 67 | -------------------------------------------------------------------------------- /src/electron/icons/app/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/app/AppIcon.icns -------------------------------------------------------------------------------- /src/electron/icons/app/AppIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/app/AppIcon.ico -------------------------------------------------------------------------------- /src/electron/icons/app/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/app/AppIcon.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon10Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon10Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon10Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon10Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon11Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon11Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon11Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon11Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon12Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon12Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon12Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon12Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon13Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon13Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon13Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon13Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon14Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon14Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon14Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon14Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon15Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon15Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon15Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon15Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon16Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon16Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon16Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon16Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon17Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon17Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon17Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon17Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon18Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon18Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon18Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon18Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon19Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon19Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon19Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon19Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon1Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon1Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon1Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon1Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon20Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon20Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon20Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon20Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon21Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon21Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon21Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon21Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon22Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon22Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon22Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon22Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon23Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon23Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon23Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon23Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon24Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon24Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon24Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon24Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon25Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon25Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon25Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon25Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon26Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon26Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon26Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon26Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon27Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon27Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon27Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon27Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon28Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon28Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon28Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon28Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon29Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon29Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon29Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon29Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon2Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon2Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon2Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon2Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon30Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon30Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon30Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon30Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon31Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon31Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon31Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon31Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon3Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon3Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon3Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon3Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon4Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon4Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon4Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon4Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon5Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon5Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon5Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon5Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon6Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon6Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon6Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon6Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon7Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon7Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon7Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon7Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon8Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon8Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon8Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon8Template@2x.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon9Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon9Template.png -------------------------------------------------------------------------------- /src/electron/icons/tray/BarnacalIcon9Template@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-engel/Barnacal/ac9058c353cfb602c85bcb807b98f00dbfa0c985/src/electron/icons/tray/BarnacalIcon9Template@2x.png -------------------------------------------------------------------------------- /src/electron/index.js: -------------------------------------------------------------------------------- 1 | const electron = require("electron"); 2 | const firstRun = require("first-run"); 3 | const { getDate } = require("date-fns"); 4 | const isDev = require("electron-is-dev"); 5 | const isOnline = require("is-online"); 6 | const path = require("path"); 7 | const { platform } = require("os"); 8 | const Sentry = require("@sentry/electron"); 9 | const autoUpdater = require("./auto-updater"); 10 | const { version } = require("../../package.json"); 11 | 12 | const WINDOW_WIDTH = 300; 13 | const WINDOW_HEIGHT = 290; 14 | const HORIZ_PADDING = 15; 15 | const VERT_PADDING = 15; 16 | 17 | const { 18 | app, 19 | ipcMain, 20 | Menu, 21 | MenuItem, 22 | Tray, 23 | BrowserWindow, 24 | systemPreferences 25 | } = electron; 26 | const isWin = platform() === "win32"; 27 | 28 | // prevent garbage collection & icon from dissapearing 29 | let trayIcon = null; 30 | let window = null; 31 | let aboutWindow = null; 32 | /* istanbul ignore next */ 33 | let readyToShow = process.env.NODE_ENV === "test" ? true : false; 34 | 35 | const getTrayIconName = () => 36 | `./icons/tray/BarnacalIcon${getDate(new Date())}Template@2x.png`; 37 | 38 | const quitApp = (app, interval) => () => { 39 | clearInterval(interval); 40 | app.exit(); 41 | }; 42 | 43 | /* istanbul ignore next */ 44 | const reportToRaven = err => { 45 | if (!isDev) 46 | isOnline() 47 | .then(() => Raven.captureException(err)) 48 | .catch(() => {}); 49 | }; 50 | 51 | const getWindowPosition = (window, tray) => { 52 | const { screen } = electron; 53 | const trayBounds = tray.getBounds(); 54 | const windowSize = window.getSize(); 55 | const cursorPosition = screen.getCursorScreenPoint(); 56 | const display = screen.getDisplayNearestPoint(cursorPosition); 57 | const displayArea = display.workArea; 58 | 59 | if (isWin) { 60 | const horizontalPosition = 61 | displayArea.x + displayArea.width - windowSize[0]; 62 | const verticalPosition = displayArea.y + displayArea.height - windowSize[1]; 63 | 64 | return [horizontalPosition, verticalPosition]; 65 | } else { 66 | const trayCenter = trayBounds.x + trayBounds.width / 2; 67 | const horizontalPosition = trayCenter - windowSize[0] / 2; 68 | // The macOS implementation of Electron. Tray ceils trayBounds.y to zero 69 | // making it unreliable for vertically positioning the window. 70 | // Use the display's work area instead. 71 | const verticalPosition = displayArea.y + 5; 72 | const left = horizontalPosition + windowSize[0]; 73 | const maxLeft = displayArea.width - 15; 74 | 75 | // Check if window would be outside screen 76 | // If yes, make sure it isn't 77 | if (left > maxLeft) { 78 | return [horizontalPosition - left - maxLeft, verticalPosition]; 79 | } 80 | 81 | return [horizontalPosition, verticalPosition]; 82 | } 83 | }; 84 | 85 | function getUserFirstWeekday() { 86 | // By default Sunday is first day of the week 87 | let day = 1; 88 | 89 | if (platform() === "darwin") { 90 | const firstWeekdayPref = systemPreferences.getUserDefault( 91 | "AppleFirstWeekday", 92 | "dictionary" 93 | ); 94 | 95 | if (!Object.keys(firstWeekdayPref).length) return day; 96 | 97 | // key in this example is `gregorian` and day is `2` 98 | // { gregorian = 2; } 99 | const key = Object.keys(firstWeekdayPref)[0]; 100 | 101 | day = firstWeekdayPref[key] || day; 102 | } 103 | 104 | return day; 105 | } 106 | 107 | const toggleTray = (window, tray) => () => { 108 | /* istanbul ignore next */ 109 | if (!readyToShow) return; 110 | 111 | const [horizontalPosition, verticalPosition] = getWindowPosition( 112 | window, 113 | tray 114 | ); 115 | 116 | window.setPosition(horizontalPosition, verticalPosition); 117 | 118 | if (window.isVisible()) { 119 | window.hide(); 120 | } else { 121 | window.show(); 122 | } 123 | }; 124 | 125 | const toggleAbout = aboutWindow => () => { 126 | if (aboutWindow.isVisible()) { 127 | aboutWindow.hide(); 128 | } else { 129 | aboutWindow.show(); 130 | } 131 | }; 132 | 133 | const configureTrayIcon = (window, trayIcon, menu) => { 134 | const menuIconPath = path.join(__dirname, getTrayIconName()); 135 | 136 | trayIcon = new Tray(menuIconPath); 137 | 138 | const toggleTrayWithContext = toggleTray(window, trayIcon); 139 | 140 | trayIcon.setToolTip("Barnacal"); 141 | 142 | trayIcon.on("click", toggleTrayWithContext); 143 | trayIcon.on("double-click", toggleTrayWithContext); 144 | trayIcon.on("right-click", () => { 145 | menu.popup(window); 146 | }); 147 | 148 | // update the icon every day 149 | const iconUpdateInterval = setInterval(() => { 150 | if (!window) return; 151 | 152 | trayIcon.setImage(path.join(__dirname, getTrayIconName())); 153 | if (!window.isVisible()) window.webContents.send("background-update"); 154 | }, 60000); 155 | 156 | return iconUpdateInterval; 157 | }; 158 | 159 | const configureAboutWindow = () => { 160 | const htmlPath = `file://${path.resolve(__dirname, "../ui/about.html")}`; 161 | 162 | aboutWindow = new BrowserWindow({ 163 | width: WINDOW_WIDTH, 164 | height: WINDOW_HEIGHT, 165 | resizable: false, 166 | frame: false, 167 | transparent: false, 168 | show: false, 169 | title: "About Barnacal", 170 | center: true, 171 | fullscreenable: false, 172 | maximizable: false, 173 | minimizable: false, 174 | titleBarStyle: "hidden", 175 | backgroundColor: "#000", 176 | // parent: window, 177 | webPreferences: { 178 | backgroundThrottling: false 179 | } 180 | }); 181 | 182 | aboutWindow.on("close", evt => { 183 | evt.preventDefault(); 184 | aboutWindow.hide(); 185 | }); 186 | 187 | aboutWindow.loadURL(htmlPath); 188 | 189 | return aboutWindow; 190 | }; 191 | 192 | const configureWindow = () => { 193 | const htmlPath = `file://${path.resolve( 194 | __dirname, 195 | "../../public/index.html" 196 | )}`; 197 | 198 | window = new BrowserWindow({ 199 | width: WINDOW_WIDTH, 200 | height: WINDOW_HEIGHT, 201 | resizable: false, 202 | hasShadow: false, 203 | frame: false, 204 | transparent: true, 205 | show: false, 206 | webPreferences: { 207 | backgroundThrottling: false, 208 | nodeIntegration: true, 209 | devTools: true 210 | } 211 | }); 212 | 213 | window.loadURL(htmlPath); 214 | 215 | window.webContents.on("crashed", reportToRaven); 216 | window.on("ready-to-show", () => (readyToShow = true)); 217 | window.on("unresponsive", reportToRaven); 218 | /* istanbul ignore next */ 219 | window.on("close", () => (window = null)); 220 | window.on("blur", () => { 221 | window.hide(); 222 | window.webContents.send("background-update"); 223 | }); 224 | 225 | // configure the auto updater, only in prod 226 | if (!isDev) autoUpdater(window); 227 | 228 | return window; 229 | }; 230 | 231 | const configureBarnacal = () => { 232 | configureWindow(); 233 | configureAboutWindow(); 234 | 235 | const menu = new Menu(); 236 | const iconUpdateInterval = configureTrayIcon(window, trayIcon, menu); 237 | const quitAppWithContext = quitApp(app, iconUpdateInterval); 238 | 239 | if (platform() === "darwin") app.dock.hide(); 240 | 241 | menu.append( 242 | new MenuItem({ 243 | label: "About", 244 | click: toggleAbout(aboutWindow) 245 | }) 246 | ); 247 | menu.append( 248 | new MenuItem({ 249 | label: "Quit", 250 | click: quitAppWithContext 251 | }) 252 | ); 253 | 254 | ipcMain.on("show-config-menu", () => menu.popup(window)); 255 | ipcMain.on("show-about", toggleAbout(aboutWindow)); 256 | ipcMain.on("get-first-weekday", event => { 257 | /* istanbul ignore next */ 258 | event.reply("set-first-weekday", getUserFirstWeekday()); 259 | }); 260 | ipcMain.on("quit-app", quitAppWithContext); 261 | }; 262 | 263 | /* istanbul ignore next */ 264 | if (!isDev && process.env.NODE_ENV !== "test") { 265 | Sentry.init({ 266 | dsn: "https://f98d2418699d4fe9acac2e08621e31d0@sentry.io/204280" 267 | }); 268 | } 269 | 270 | // set the app to open on login 271 | if (!isDev && firstRun()) { 272 | app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true }); 273 | } 274 | 275 | process.on("beforeExit", app.quit); 276 | 277 | app.on("ready", configureBarnacal); 278 | 279 | module.exports = { 280 | getTrayIconName, 281 | quitApp, 282 | reportToRaven, 283 | getWindowPosition, 284 | getUserFirstWeekday, 285 | toggleTray, 286 | configureAboutWindow, 287 | showAbout: toggleAbout, 288 | configureWindow, 289 | configureTrayIcon, 290 | configureBarnacal, 291 | WINDOW_HEIGHT, 292 | WINDOW_WIDTH, 293 | HORIZ_PADDING, 294 | VERT_PADDING 295 | }; 296 | -------------------------------------------------------------------------------- /src/ui/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 86 | 87 | 88 | 89 | Barnacal logo 90 |

Barnacal

91 |

v0.9.3

92 | 106 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { ipcRenderer } from "electron"; 4 | import { Stylable } from "./types"; 5 | import { CalendarHeader } from "./calendar_header"; 6 | import { CalendarBody } from "./calendar_body"; 7 | 8 | type Props = Stylable & { 9 | date: Date; 10 | }; 11 | 12 | export const RawCalendar = ({ date, className }: Props) => { 13 | const [firstWeekday, setFirstWeekday] = useState(1); 14 | 15 | useEffect(() => { 16 | const cb = (_: any, weekday: number) => { 17 | setFirstWeekday(weekday); 18 | }; 19 | 20 | ipcRenderer.on("set-first-weekday", cb); 21 | 22 | ipcRenderer.send("get-first-weekday"); 23 | 24 | return () => { 25 | ipcRenderer.removeListener("set-first-weekday", cb); 26 | }; 27 | }, []); 28 | 29 | return ( 30 | 31 | 32 | 33 |
34 | ); 35 | }; 36 | 37 | export const Calendar = styled(RawCalendar)` 38 | width: 100%; 39 | padding: 0 1em; 40 | 41 | th, 42 | td { 43 | text-align: center; 44 | width: calc(100% / 7); 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/ui/calendar_body.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | getDate, 5 | getDaysInMonth, 6 | startOfMonth, 7 | getDay, 8 | setDate, 9 | getISODay, 10 | differenceInCalendarWeeks, 11 | differenceInCalendarISOWeeks, 12 | isSameMonth, 13 | format 14 | } from "date-fns"; 15 | import { Stylable } from "./types"; 16 | import { Span, FontWeight } from "styled-typography"; 17 | 18 | type Props = Stylable & { 19 | date: Date; 20 | firstWeekday: number; 21 | }; 22 | 23 | export const weekDayNumber = (date: Date, firstWeekday: number) => { 24 | return firstWeekday === 1 ? getDay(date) : getISODay(date); 25 | }; 26 | 27 | export const weekOfMonth = (date: Date, firstWeekday: number) => { 28 | const firstOfMonth = startOfMonth(date); 29 | 30 | return firstWeekday === 1 31 | ? differenceInCalendarWeeks(date, firstOfMonth) 32 | : differenceInCalendarISOWeeks(date, firstOfMonth); 33 | }; 34 | 35 | export const weeks = (date: Date, firstWeekday: number) => { 36 | const totalDays = Array.from({ length: getDaysInMonth(date) }, () => null); 37 | const weeks = Array.from({ length: 6 }, () => 38 | new Array(7).fill(null) 39 | ); 40 | 41 | return totalDays.reduce((acc, _, dayOfMonth) => { 42 | const currentDay = setDate(date, dayOfMonth + 1); 43 | const week = weekOfMonth(currentDay, firstWeekday); 44 | const dayOfWeek = 45 | weekDayNumber(currentDay, firstWeekday) - (firstWeekday - 1); 46 | 47 | acc[week][dayOfWeek] = dayOfMonth + 1; 48 | 49 | return acc; 50 | }, weeks); 51 | }; 52 | 53 | export const isToday = (date: Date, dayOfMonth: number) => { 54 | const now = new Date(); 55 | 56 | return isSameMonth(now, date) && getDate(now) === dayOfMonth; 57 | }; 58 | 59 | export const RawCalendarBody = ({ date, firstWeekday, className }: Props) => { 60 | const weeksOfMonth = weeks(date, firstWeekday); 61 | 62 | return ( 63 | 64 | {weeksOfMonth.map((week, weekIdx) => ( 65 | 66 | {week.map((day, dayIdx) => ( 67 | 73 | 77 | {day} 78 | 79 | 80 | ))} 81 | 82 | ))} 83 | 84 | ); 85 | }; 86 | 87 | export const CalendarBody = styled(RawCalendarBody)``; 88 | -------------------------------------------------------------------------------- /src/ui/calendar_header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Stylable } from "./types"; 4 | import { Span } from "styled-typography"; 5 | 6 | type Props = Stylable & { 7 | firstWeekday: number; 8 | }; 9 | 10 | export const weekdays = (firstWeekday: number) => { 11 | const index = firstWeekday - 1; 12 | 13 | const list = ["S", "M", "T", "W", "T", "F", "S"]; 14 | 15 | if (index === 0) return list; 16 | 17 | return [...list.slice(1), list[0]]; 18 | }; 19 | 20 | export const RawCalendarHeader = ({ firstWeekday, className }: Props) => ( 21 | 22 | 23 | {weekdays(firstWeekday).map((weekday, idx) => ( 24 | 25 | {weekday} 26 | 27 | ))} 28 | 29 | 30 | ); 31 | 32 | export const CalendarHeader = styled(RawCalendarHeader)` 33 | th { 34 | font-size: 0.725rem; 35 | opacity: 0.5; 36 | padding: 0.3em 0; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /src/ui/header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { format } from "date-fns"; 4 | import { Heading } from "styled-typography"; 5 | import { Stylable } from "./types"; 6 | 7 | type Props = Stylable & { 8 | onNextMonth: () => void; 9 | onPreviousMonth: () => void; 10 | onResetMonth: () => void; 11 | date: Date; 12 | }; 13 | 14 | export const RawHeader = ({ 15 | onNextMonth: nextMonth, 16 | onPreviousMonth: previousMonth, 17 | onResetMonth: resetMonth, 18 | date, 19 | className 20 | }: Props) => ( 21 |
22 | 23 | ← 24 | 25 | 26 | {format(date, "LLLL yyyy")} 27 | 28 | 29 | → 30 | 31 |
32 | ); 33 | 34 | export const Header = styled(RawHeader)` 35 | margin-bottom: 10px; 36 | text-align: center; 37 | background-color: #000; 38 | border-top-right-radius: 4px; 39 | border-top-left-radius: 4px; 40 | padding-top: 0px; 41 | display: flex; 42 | align-items: center; 43 | 44 | ${Heading} { 45 | line-hight: 40px; 46 | font-size: 22px; 47 | padding: 0.5em 0; 48 | flex-grow: 2; 49 | cursor: pointer; 50 | } 51 | 52 | a { 53 | text-decoration: none; 54 | color: #fff; 55 | font-size: 1.2rem; 56 | width: 50px; 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /src/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 56 | 57 | 58 | 59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { ipcRenderer } from "electron"; 4 | import styled, { ThemeProvider } from "styled-components"; 5 | import { GlobalTypeStyles } from "styled-typography"; 6 | import { addMonths, subMonths } from "date-fns"; 7 | import * as Sentry from "@sentry/browser"; 8 | import { Header } from "./header"; 9 | import { Menu } from "./menu"; 10 | import { Calendar } from "./calendar"; 11 | 12 | if (process.env.NODE_ENV !== "development") { 13 | Sentry.init({ 14 | dsn: "https://970be29a9988418ebe1215c7f12223ef@sentry.io/204281" 15 | }); 16 | } 17 | 18 | enum DateAction { 19 | Next = "NEXT", 20 | Previous = "PREVIOUS", 21 | Reset = "RESET" 22 | } 23 | 24 | export const RawApp = ({ className }: any) => { 25 | const [date, setDate] = useState(new Date()); 26 | const [updateAvailable, setUpdateAvailable] = useState(false); 27 | 28 | useEffect(() => { 29 | ipcRenderer.on("update-ready", () => setUpdateAvailable(true)); 30 | ipcRenderer.on("background-update", () => setDate(new Date())); 31 | 32 | return () => { 33 | ipcRenderer.removeAllListeners("update-ready"); 34 | ipcRenderer.removeAllListeners("background-update"); 35 | }; 36 | }, []); 37 | 38 | const updateDate = (action: DateAction) => 39 | useCallback(() => { 40 | switch (action) { 41 | case DateAction.Next: 42 | return setDate(addMonths(date, 1)); 43 | case DateAction.Previous: 44 | return setDate(subMonths(date, 1)); 45 | case DateAction.Reset: 46 | return setDate(new Date()); 47 | } 48 | }, [date]); 49 | 50 | return ( 51 | 52 |
53 | 54 |
60 | 61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export const App = styled(RawApp)` 68 | height: 100%; 69 | border-radius: 5px; 70 | height: 100vh; 71 | width: 300px; 72 | padding: 0; 73 | background-color: #000; 74 | margin: 0 auto; 75 | position: relative; 76 | cursor: default; 77 | padding-bottom: 1px; 78 | `; 79 | 80 | ReactDOM.render(, document.querySelector("#app")); 81 | -------------------------------------------------------------------------------- /src/ui/menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import styled from "styled-components"; 3 | import { ipcRenderer } from "electron"; 4 | import { Stylable } from "./types"; 5 | 6 | type Props = Stylable & { 7 | updateAvailable: boolean; 8 | }; 9 | 10 | export const UpdateAvailable = ({ updateAvailable }: Pick) => { 11 | if (!updateAvailable) return null; 12 | 13 | const updateApp = useCallback(() => { 14 | ipcRenderer.send("install-update"); 15 | }, []); 16 | 17 | return ( 18 | <> 19 | An update is available.{" "} 20 | 21 | Install now 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const RawMenu = ({ updateAvailable, className }: Props) => { 28 | const quitApp = useCallback(() => { 29 | ipcRenderer.send("quit-app"); 30 | }, []); 31 | const showAbout = useCallback(() => { 32 | ipcRenderer.send("show-about"); 33 | }, []); 34 | 35 | return ( 36 |
37 | 38 | 39 | 40 | 69 |
70 | ); 71 | }; 72 | 73 | export const Menu = styled(RawMenu)` 74 | position: absolute; 75 | display: flex; 76 | align-items: center; 77 | justify-content: space-between; 78 | bottom: 5px; 79 | height: 26px; 80 | width: 100%; 81 | border-bottom-right-radius: 5px; 82 | border-bottom-left-radius: 5px; 83 | text-align: right; 84 | padding: 0 1em; 85 | margin-bottom: 0.5em; 86 | 87 | svg { 88 | display: inline-block; 89 | height: 15px; 90 | width: 15px; 91 | text-decoration: none; 92 | cursor: pointe; 93 | margin-left: 10px; 94 | } 95 | 96 | span { 97 | color: #fff; 98 | font-size: 12px; 99 | } 100 | 101 | a { 102 | color: #f012be; 103 | font-size: 12px; 104 | text-decoration: none; 105 | } 106 | `; 107 | -------------------------------------------------------------------------------- /src/ui/types.ts: -------------------------------------------------------------------------------- 1 | export type Stylable = { className?: string }; 2 | -------------------------------------------------------------------------------- /test/auto-updater.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | const proxyquire = require("proxyquire") 4 | .noCallThru() 5 | .noPreserveCache(); 6 | const { version } = require("../package.json"); 7 | const ms = require("ms"); 8 | 9 | const notificationReturnStub = { 10 | show: sinon.stub(), 11 | on: sinon.stub() 12 | }; 13 | 14 | const electronStub = { 15 | autoUpdater: { 16 | on: sinon.stub(), 17 | setFeedURL: sinon.stub(), 18 | checkForUpdates: sinon.stub(), 19 | quitAndInstall: sinon.stub(), 20 | removeAllListeners: sinon.stub() 21 | }, 22 | ipcMain: { 23 | on: sinon.stub() 24 | }, 25 | Notification: sinon.stub().returns(notificationReturnStub) 26 | }; 27 | 28 | const windowStub = { 29 | webContents: { 30 | send: sinon.stub() 31 | }, 32 | on: sinon.stub() 33 | }; 34 | 35 | const ravenStub = { 36 | captureException: sinon.stub() 37 | }; 38 | 39 | const autoUpdater = proxyquire("../src/electron/auto-updater", { 40 | electron: electronStub, 41 | "@sentry/electron": ravenStub, 42 | "electron-is-dev": false, 43 | "is-online": sinon.stub().resolves(), 44 | os: { platform: () => "darwin" } 45 | }); 46 | 47 | electronStub.Notification.isSupported = sinon.stub().returns(true); 48 | 49 | describe("autoUpdater", () => { 50 | beforeEach(() => { 51 | electronStub.Notification.returns(notificationReturnStub); 52 | electronStub.Notification.isSupported.returns(true); 53 | }); 54 | 55 | afterEach(() => { 56 | electronStub.autoUpdater.on.reset(); 57 | electronStub.autoUpdater.setFeedURL.reset(); 58 | electronStub.autoUpdater.checkForUpdates.reset(); 59 | electronStub.autoUpdater.quitAndInstall.reset(); 60 | electronStub.autoUpdater.removeAllListeners.reset(); 61 | electronStub.ipcMain.on.reset(); 62 | electronStub.Notification.reset(); 63 | electronStub.Notification.isSupported.reset(); 64 | 65 | windowStub.webContents.send.reset(); 66 | windowStub.on.reset(); 67 | 68 | notificationReturnStub.on.reset(); 69 | notificationReturnStub.show.reset(); 70 | 71 | ravenStub.captureException.reset(); 72 | }); 73 | 74 | describe("main", () => { 75 | it("should call init on first load", () => { 76 | autoUpdater(windowStub); 77 | 78 | expect(electronStub.autoUpdater.setFeedURL.calledOnce).to.be.true; 79 | }); 80 | 81 | it("should not call init on later loads", () => { 82 | autoUpdater(windowStub); 83 | 84 | expect(electronStub.autoUpdater.setFeedURL.called).to.be.false; 85 | }); 86 | 87 | it("should notify the user when the update has been downloaded", () => { 88 | autoUpdater(windowStub); 89 | 90 | electronStub.autoUpdater.on.firstCall.args[1](null, "yolo", "v1.0.0"); 91 | 92 | expect(electronStub.autoUpdater.on.calledOnce).to.be.true; 93 | expect(electronStub.autoUpdater.on.firstCall.args[0]).to.equal( 94 | "update-downloaded" 95 | ); 96 | expect(windowStub.webContents.send.calledTwice).to.be.true; 97 | expect(windowStub.webContents.send.firstCall.args).to.deep.equal([ 98 | "update-downloaded" 99 | ]); 100 | expect(windowStub.webContents.send.secondCall.args[0]).to.deep.equal( 101 | "update-ready" 102 | ); 103 | }); 104 | 105 | it("should listen for the event to install the update", () => { 106 | autoUpdater(windowStub); 107 | 108 | expect(electronStub.ipcMain.on.calledOnce).to.be.true; 109 | expect(electronStub.ipcMain.on.firstCall.args).to.deep.equal([ 110 | "install-update", 111 | electronStub.autoUpdater.quitAndInstall 112 | ]); 113 | }); 114 | 115 | it("should remove all autoUpdater listeners on exit", () => { 116 | autoUpdater(windowStub); 117 | 118 | expect(windowStub.on.calledOnce).to.be.true; 119 | expect(windowStub.on.firstCall.args).to.deep.equal([ 120 | "close", 121 | electronStub.autoUpdater.removeAllListeners 122 | ]); 123 | }); 124 | }); 125 | 126 | describe("init", () => { 127 | it("should handle autoupdater errors", done => { 128 | autoUpdater.init(); 129 | 130 | electronStub.autoUpdater.on.firstCall.args[1]("yolo", "test"); 131 | 132 | setTimeout(() => { 133 | expect(electronStub.autoUpdater.on.calledOnce).to.be.true; 134 | expect(electronStub.autoUpdater.on.firstCall.args[0]).to.equal("error"); 135 | expect(ravenStub.captureException.calledOnce).to.be.true; 136 | expect(ravenStub.captureException.firstCall.args).to.deep.equal([ 137 | "yolo" 138 | ]); 139 | done(); 140 | }); 141 | }); 142 | 143 | it("should not report autoupdater errors to sentry in non-prod", done => { 144 | const autoUpdaterDev = proxyquire("../src/electron/auto-updater", { 145 | electron: electronStub, 146 | "@sentry/electron": ravenStub, 147 | "electron-is-dev": true, 148 | "is-online": sinon.stub().resolves(), 149 | os: { platform: () => "darwin" } 150 | }); 151 | 152 | autoUpdaterDev.init(); 153 | 154 | electronStub.autoUpdater.on.firstCall.args[1]("yolo", "test"); 155 | 156 | setTimeout(() => { 157 | expect(ravenStub.captureException.called).to.be.false; 158 | done(); 159 | }); 160 | }); 161 | 162 | it("should not report autoupdater errors to sentry if offline", done => { 163 | const autoUpdaterDev = proxyquire("../src/electron/auto-updater", { 164 | electron: electronStub, 165 | "@sentry/electron": ravenStub, 166 | "electron-is-dev": false, 167 | "is-online": sinon.stub().rejects(), 168 | os: { platform: () => "darwin" } 169 | }); 170 | 171 | autoUpdaterDev.init(); 172 | 173 | electronStub.autoUpdater.on.firstCall.args[1]("yolo", "test"); 174 | 175 | setTimeout(() => { 176 | expect(ravenStub.captureException.called).to.be.false; 177 | done(); 178 | }); 179 | }); 180 | 181 | it("should set the right feed url", () => { 182 | autoUpdater.init(); 183 | 184 | expect(electronStub.autoUpdater.setFeedURL.calledOnce).to.be.true; 185 | expect(electronStub.autoUpdater.setFeedURL.firstCall.args).to.deep.equal([ 186 | `https://barnacal-updates.now.sh/update/darwin/${version}` 187 | ]); 188 | }); 189 | 190 | it("should check for updates periodically", () => { 191 | sinon.spy(global, "setInterval"); 192 | sinon.spy(global, "setTimeout"); 193 | 194 | autoUpdater.init(); 195 | 196 | setTimeout.firstCall.args[0](); 197 | setInterval.firstCall.args[0](); 198 | 199 | expect(setTimeout.calledOnce).to.be.true; 200 | expect(setInterval.calledOnce).to.be.true; 201 | expect(electronStub.autoUpdater.checkForUpdates.calledTwice).to.be.true; 202 | expect(setTimeout.firstCall.args[1]).to.equal(ms("10s")); 203 | expect(setInterval.firstCall.args[1]).to.equal(ms("60m")); 204 | 205 | setTimeout.restore(); 206 | setInterval.restore(); 207 | }); 208 | }); 209 | 210 | describe("onUpdate", () => { 211 | it("should display a notification to the user", () => { 212 | autoUpdater.onUpdate(windowStub, "yolo", "v1.0.0"); 213 | 214 | expect(electronStub.Notification.isSupported.calledOnce).to.be.true; 215 | expect(electronStub.Notification.calledOnce).to.be.true; 216 | expect(electronStub.Notification.firstCall.args).to.deep.equal([ 217 | { 218 | title: "Barnacal update available", 219 | body: 220 | "Barnacal v1.0.0 is ready to install. Click to apply the update." 221 | } 222 | ]); 223 | expect(notificationReturnStub.show.calledOnce).to.be.true; 224 | expect(notificationReturnStub.on.calledOnce).to.be.true; 225 | expect(notificationReturnStub.on.firstCall.args).to.deep.equal([ 226 | "click", 227 | electronStub.autoUpdater.quitAndInstall 228 | ]); 229 | }); 230 | 231 | it("should not display a notification to the user if they are not supported", () => { 232 | electronStub.Notification.isSupported.returns(false); 233 | 234 | autoUpdater.onUpdate(windowStub, "yolo", "v1.0.0"); 235 | 236 | expect(electronStub.Notification.isSupported.calledOnce).to.be.true; 237 | expect(electronStub.Notification.called).to.be.false; 238 | expect(notificationReturnStub.show.called).to.be.false; 239 | expect(notificationReturnStub.on.called).to.be.false; 240 | }); 241 | 242 | it("should sent a message to the app", () => { 243 | autoUpdater.onUpdate(windowStub, "yolo", "v1.0.0"); 244 | 245 | expect(windowStub.webContents.send.calledOnce).to.be.true; 246 | expect(windowStub.webContents.send.firstCall.args).to.deep.equal([ 247 | "update-ready", 248 | { releaseNotes: "yolo", releaseName: "v1.0.0" } 249 | ]); 250 | }); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const { getDate } = require("date-fns"); 2 | const { expect } = require("chai"); 3 | const sinon = require("sinon"); 4 | const proxyquire = require("proxyquire") 5 | .noCallThru() 6 | .noPreserveCache(); 7 | const { version } = require("../package.json"); 8 | const { resolve } = require("path"); 9 | 10 | const testSandbox = sinon.createSandbox(); 11 | 12 | sinon.spy(process, "on"); 13 | 14 | const electronStub = { 15 | app: { 16 | quit: testSandbox.stub(), 17 | on: testSandbox.stub(), 18 | exit: testSandbox.stub(), 19 | setLoginItemSettings: sinon.stub(), 20 | dock: { hide: testSandbox.stub() }, 21 | getAppPath: () => resolve(__dirname, "../src/electron") 22 | }, 23 | ipcMain: { 24 | on: testSandbox.stub() 25 | }, 26 | Menu: testSandbox.stub(), 27 | MenuItem: testSandbox.stub(), 28 | Tray: testSandbox.stub(), 29 | Notification: testSandbox.stub(), 30 | BrowserWindow: testSandbox.stub(), 31 | screen: { 32 | getCursorScreenPoint: testSandbox.stub(), 33 | getDisplayNearestPoint: testSandbox.stub() 34 | }, 35 | systemPreferences: { 36 | getUserDefault: () => ({ gregorian: 2 }) 37 | } 38 | }; 39 | 40 | const ravenStub = { 41 | config: testSandbox.stub().returnsThis(), 42 | install: testSandbox.stub(), 43 | captureException: testSandbox.stub() 44 | }; 45 | 46 | const autoUpdaterStub = testSandbox.stub(); 47 | 48 | const firstRunStub = testSandbox.stub().returns(true); 49 | 50 | const { 51 | getTrayIconName, 52 | quitApp, 53 | reportToRaven, 54 | getWindowPosition, 55 | toggleTray, 56 | configureAboutWindow, 57 | showAbout, 58 | configureWindow, 59 | configureTrayIcon, 60 | configureBarnacal, 61 | WINDOW_WIDTH, 62 | WINDOW_HEIGHT 63 | } = proxyquire("../src/electron/index", { 64 | electron: electronStub, 65 | "@sentry/electron": ravenStub, 66 | "first-run": firstRunStub, 67 | "electron-is-dev": false, 68 | os: { platform: () => "darwin" }, 69 | "is-online": sinon.stub().resolves(true), 70 | "./auto-updater": autoUpdaterStub 71 | }); 72 | 73 | describe("index", () => { 74 | beforeEach(() => { 75 | electronStub.screen.getDisplayNearestPoint.returns({ 76 | workArea: { x: 500, y: 500, width: 1024, height: 768 } 77 | }); 78 | 79 | ravenStub.config.returnsThis(); 80 | }); 81 | 82 | afterEach(() => { 83 | testSandbox.reset(); 84 | }); 85 | 86 | context("startup", () => { 87 | it("should add a listener for `beforeExit`", () => { 88 | expect(process.on.getCall(0).args).to.deep.equal([ 89 | "beforeExit", 90 | electronStub.app.quit 91 | ]); 92 | }); 93 | 94 | it("should set the login settings on first run", () => { 95 | expect(electronStub.app.setLoginItemSettings.callCount).to.equal(1); 96 | expect( 97 | electronStub.app.setLoginItemSettings.getCall(0).args 98 | ).to.deep.equal([{ openAtLogin: true, openAsHidden: true }]); 99 | }); 100 | 101 | it("should not set the login settings in non-prod", () => { 102 | electronStub.app.setLoginItemSettings.reset(); 103 | 104 | proxyquire("../src/electron/index", { 105 | electron: electronStub, 106 | "@sentry/electron": ravenStub, 107 | "first-run": firstRunStub, 108 | "electron-is-dev": true, 109 | os: { platform: () => "darwin" }, 110 | "is-online": testSandbox.stub().resolves(true), 111 | "./auto-updater": autoUpdaterStub 112 | }); 113 | 114 | expect(electronStub.app.setLoginItemSettings.callCount).to.equal(0); 115 | }); 116 | 117 | it("should not set the login settings if barnacal has already been run", () => { 118 | electronStub.app.setLoginItemSettings.reset(); 119 | 120 | proxyquire("../src/electron/index", { 121 | electron: electronStub, 122 | "@sentry/electron": ravenStub, 123 | "first-run": firstRunStub.returns(false), 124 | "electron-is-dev": false, 125 | os: { platform: () => "darwin" }, 126 | "is-online": testSandbox.stub().resolves(true), 127 | "./auto-updater": autoUpdaterStub 128 | }); 129 | 130 | expect(electronStub.app.setLoginItemSettings.callCount).to.equal(0); 131 | }); 132 | }); 133 | 134 | context("getTrayIconName", () => { 135 | const now = new Date(); 136 | const date = getDate(now); 137 | 138 | it("should return the relative path to the icon for today", () => { 139 | expect(getTrayIconName()).to.equal( 140 | `./icons/tray/BarnacalIcon${date}Template@2x.png` 141 | ); 142 | }); 143 | }); 144 | 145 | context("quitApp", () => { 146 | const interval = 1; 147 | 148 | testSandbox.stub(global, "clearInterval"); 149 | 150 | afterEach(() => clearInterval.reset()); 151 | after(() => clearInterval.restore()); 152 | 153 | it("should clear the interval and exit the app on quit", () => { 154 | quitApp(electronStub.app, interval)(); 155 | 156 | expect(electronStub.app.exit.callCount).to.equal(1); 157 | expect(clearInterval.callCount).to.equal(1); 158 | expect(clearInterval.getCall(0).args).to.deep.equal([interval]); 159 | }); 160 | }); 161 | 162 | context("reportToRaven", () => { 163 | const error = "YOLOERR"; 164 | 165 | it("should not capture the exception in non-prod", done => { 166 | const { reportToRaven: reportToRavenDev } = proxyquire( 167 | "../src/electron/index", 168 | { 169 | electron: electronStub, 170 | "@sentry/electron": ravenStub, 171 | "first-run": firstRunStub, 172 | "electron-is-dev": true, 173 | os: { platform: () => "darwin" }, 174 | "is-online": testSandbox.stub().resolves(true), 175 | "./auto-updater": autoUpdaterStub 176 | } 177 | ); 178 | 179 | reportToRavenDev(error); 180 | 181 | setTimeout(() => { 182 | expect(ravenStub.captureException.called).to.be.false; 183 | done(); 184 | }); 185 | }); 186 | 187 | it("should not capture the exception if offline", done => { 188 | const { reportToRaven: reportToRavenDev } = proxyquire( 189 | "../src/electron/index", 190 | { 191 | electron: electronStub, 192 | "@sentry/electron": ravenStub, 193 | "first-run": firstRunStub, 194 | "electron-is-dev": false, 195 | os: { platform: () => "darwin" }, 196 | "is-online": testSandbox.stub().rejects(false), 197 | "./auto-updater": autoUpdaterStub 198 | } 199 | ); 200 | 201 | reportToRavenDev(error); 202 | 203 | setTimeout(() => { 204 | expect(ravenStub.captureException.called).to.be.false; 205 | done(); 206 | }); 207 | }); 208 | }); 209 | 210 | context("getUserFirstWeekday", () => { 211 | it("should return 1 for non-darwin platforms", () => { 212 | const { getUserFirstWeekday } = proxyquire("../src/electron/index", { 213 | electron: electronStub, 214 | "@sentry/electron": ravenStub, 215 | "first-run": firstRunStub.returns(false), 216 | "electron-is-dev": false, 217 | os: { platform: () => "win32" }, 218 | "is-online": testSandbox.stub().resolves(true), 219 | "./auto-updater": autoUpdaterStub 220 | }); 221 | 222 | const day = getUserFirstWeekday(); 223 | 224 | expect(day).to.equal(1); 225 | }); 226 | 227 | it("should return the preference for darwin platforms", () => { 228 | const { getUserFirstWeekday } = proxyquire("../src/electron/index", { 229 | electron: electronStub, 230 | "@sentry/electron": ravenStub, 231 | "first-run": firstRunStub.returns(false), 232 | "electron-is-dev": false, 233 | os: { platform: () => "darwin" }, 234 | "is-online": testSandbox.stub().resolves(true), 235 | "./auto-updater": autoUpdaterStub 236 | }); 237 | 238 | const day = getUserFirstWeekday(); 239 | 240 | expect(day).to.equal(2); 241 | }); 242 | 243 | it("should return the default for darwin platforms if there is no system preference", () => { 244 | const { getUserFirstWeekday } = proxyquire("../src/electron/index", { 245 | electron: { 246 | ...electronStub, 247 | systemPreferences: { 248 | getUserDefault: () => ({}) 249 | } 250 | }, 251 | "@sentry/electron": ravenStub, 252 | "first-run": firstRunStub.returns(false), 253 | "electron-is-dev": false, 254 | os: { platform: () => "darwin" }, 255 | "is-online": testSandbox.stub().resolves(true), 256 | "./auto-updater": autoUpdaterStub 257 | }); 258 | 259 | const day = getUserFirstWeekday(); 260 | 261 | expect(day).to.equal(1); 262 | }); 263 | 264 | it("should return the default for darwin platforms if the preference is corrupt", () => { 265 | const { getUserFirstWeekday } = proxyquire("../src/electron/index", { 266 | electron: { 267 | ...electronStub, 268 | systemPreferences: { 269 | getUserDefault: () => ({ gregorian: null }) 270 | } 271 | }, 272 | "@sentry/electron": ravenStub, 273 | "first-run": firstRunStub.returns(false), 274 | "electron-is-dev": false, 275 | os: { platform: () => "darwin" }, 276 | "is-online": testSandbox.stub().resolves(true), 277 | "./auto-updater": autoUpdaterStub 278 | }); 279 | 280 | const day = getUserFirstWeekday(); 281 | 282 | expect(day).to.equal(1); 283 | }); 284 | }); 285 | 286 | context("getWindowPosition", () => { 287 | const trayStub = { 288 | getBounds: testSandbox.stub() 289 | }; 290 | const windowStub = { 291 | getSize: testSandbox.stub() 292 | }; 293 | 294 | beforeEach(() => { 295 | trayStub.getBounds.returns({ x: 500, width: 30 }); 296 | windowStub.getSize.returns([WINDOW_WIDTH, WINDOW_HEIGHT]); 297 | }); 298 | 299 | it("should return the x and y position", () => { 300 | const [x, y] = getWindowPosition(windowStub, trayStub); 301 | 302 | expect(x).to.equal(365); 303 | expect(y).to.equal(505); 304 | }); 305 | 306 | it("should return the x and y position for windows machines", () => { 307 | const { getWindowPosition: getWindowPositionWin } = proxyquire( 308 | "../src/electron/index", 309 | { 310 | electron: electronStub, 311 | "@sentry/electron": ravenStub, 312 | "first-run": firstRunStub.returns(false), 313 | "electron-is-dev": false, 314 | os: { platform: () => "win32" }, 315 | "is-online": testSandbox.stub().resolves(true), 316 | "./auto-updater": autoUpdaterStub 317 | } 318 | ); 319 | 320 | const [x, y] = getWindowPositionWin(windowStub, trayStub); 321 | 322 | expect(x).to.equal(1224); 323 | expect(y).to.equal(978); 324 | }); 325 | 326 | it("should avoid throwing the app off the left side of the screen", () => { 327 | electronStub.screen.getDisplayNearestPoint.returns({ 328 | workArea: { x: 500, y: 500, width: 10, height: 768 } 329 | }); 330 | 331 | const [x, y] = getWindowPosition(windowStub, trayStub); 332 | 333 | expect(x).to.equal(-295); 334 | expect(y).to.equal(505); 335 | }); 336 | }); 337 | 338 | context("toggleTray", () => { 339 | const windowStub = { 340 | setPosition: testSandbox.stub(), 341 | isVisible: testSandbox.stub().returns(true), 342 | getSize: testSandbox.stub().returns([1024, 768]), 343 | hide: testSandbox.stub(), 344 | show: testSandbox.stub() 345 | }; 346 | const trayStub = { 347 | getBounds: testSandbox.stub().returns({ x: 500, width: 30 }) 348 | }; 349 | 350 | beforeEach(() => { 351 | windowStub.isVisible.returns(true); 352 | windowStub.getSize.returns([1024, 768]); 353 | trayStub.getBounds.returns({ x: 500, width: 30 }); 354 | }); 355 | 356 | it("should set the window position", () => { 357 | const [x, y] = getWindowPosition(windowStub, trayStub); 358 | 359 | toggleTray(windowStub, trayStub)(); 360 | 361 | expect(windowStub.setPosition.callCount).to.equal(1); 362 | expect(windowStub.setPosition.getCall(0).args).to.deep.equal([x, y]); 363 | }); 364 | 365 | it("should hide the window if it is already visible", () => { 366 | toggleTray(windowStub, trayStub)(); 367 | 368 | expect(windowStub.hide.callCount).to.equal(1); 369 | expect(windowStub.show.called).to.be.false; 370 | }); 371 | 372 | it("should show the window if it is already hidden", () => { 373 | windowStub.isVisible.returns(false); 374 | 375 | toggleTray(windowStub, trayStub)(); 376 | 377 | expect(windowStub.hide.called).to.be.false; 378 | expect(windowStub.show.callCount).to.equal(1); 379 | }); 380 | }); 381 | 382 | context("configureAboutWindow", () => { 383 | const windowStub = { 384 | loadURL: testSandbox.stub(), 385 | on: testSandbox.stub(), 386 | hide: testSandbox.stub() 387 | }; 388 | 389 | beforeEach(() => { 390 | electronStub.BrowserWindow.returns(windowStub); 391 | }); 392 | 393 | it("should create the browser window with the correct options", () => { 394 | configureAboutWindow(); 395 | 396 | expect(electronStub.BrowserWindow.callCount).to.equal(1); 397 | expect(electronStub.BrowserWindow.getCall(0).args).to.deep.equal([ 398 | { 399 | width: WINDOW_WIDTH, 400 | height: WINDOW_HEIGHT, 401 | resizable: false, 402 | frame: false, 403 | transparent: false, 404 | show: false, 405 | title: "About Barnacal", 406 | center: true, 407 | fullscreenable: false, 408 | maximizable: false, 409 | minimizable: false, 410 | titleBarStyle: "hidden", 411 | backgroundColor: "#000", 412 | webPreferences: { 413 | backgroundThrottling: false 414 | } 415 | } 416 | ]); 417 | }); 418 | 419 | it("should bind events to the about window", () => { 420 | const result = configureAboutWindow(); 421 | const dir = resolve(__dirname, ".."); 422 | const evtStub = { preventDefault: testSandbox.stub() }; 423 | 424 | windowStub.on.getCall(0).args[1](evtStub); 425 | 426 | expect(windowStub.on.callCount).to.equal(1); 427 | expect(windowStub.on.getCall(0).args[0]).to.equal("close"); 428 | expect(evtStub.preventDefault.callCount).to.equal(1); 429 | expect(windowStub.hide.callCount).to.equal(1); 430 | expect(result).to.deep.equal(windowStub); 431 | }); 432 | 433 | it("should load the url and return the window object", () => { 434 | const result = configureAboutWindow(); 435 | const file = resolve(__dirname, "..", "src", "ui", "about.html"); 436 | 437 | expect(windowStub.loadURL.callCount).to.equal(1); 438 | expect(windowStub.loadURL.getCall(0).args).to.deep.equal([ 439 | `file://${file}` 440 | ]); 441 | expect(result).to.deep.equal(windowStub); 442 | }); 443 | }); 444 | 445 | context("showAbout", () => { 446 | it("should hide the window if it's already visible", () => { 447 | const aboutWindowStub = { 448 | isVisible: testSandbox.stub().returns(true), 449 | hide: testSandbox.stub() 450 | }; 451 | 452 | showAbout(aboutWindowStub)(); 453 | 454 | expect(aboutWindowStub.isVisible.callCount).to.equal(1); 455 | expect(aboutWindowStub.hide.callCount).to.equal(1); 456 | }); 457 | 458 | it("should show the window if it's already hidden", () => { 459 | const aboutWindowStub = { 460 | isVisible: testSandbox.stub().returns(false), 461 | show: testSandbox.stub() 462 | }; 463 | 464 | showAbout(aboutWindowStub)(); 465 | 466 | expect(aboutWindowStub.isVisible.callCount).to.equal(1); 467 | expect(aboutWindowStub.show.callCount).to.equal(1); 468 | }); 469 | }); 470 | 471 | context("configureWindow", () => { 472 | const windowStub = { 473 | on: testSandbox.stub(), 474 | webContents: { on: testSandbox.stub(), send: testSandbox.stub() }, 475 | loadURL: testSandbox.stub(), 476 | hide: testSandbox.stub() 477 | }; 478 | 479 | beforeEach(() => { 480 | electronStub.BrowserWindow.returns(windowStub); 481 | }); 482 | 483 | it("should create the browser window with the correct options", () => { 484 | configureWindow(); 485 | 486 | expect(electronStub.BrowserWindow.callCount).to.equal(1); 487 | expect(electronStub.BrowserWindow.getCall(0).args).to.deep.equal([ 488 | { 489 | width: WINDOW_WIDTH, 490 | height: WINDOW_HEIGHT, 491 | hasShadow: false, 492 | resizable: false, 493 | frame: false, 494 | transparent: true, 495 | show: false, 496 | webPreferences: { 497 | backgroundThrottling: false, 498 | devTools: true, 499 | nodeIntegration: true 500 | } 501 | } 502 | ]); 503 | }); 504 | 505 | it("should set up error listeners", () => { 506 | configureWindow(); 507 | 508 | expect(windowStub.webContents.on.callCount).to.equal(1); 509 | expect(windowStub.on.callCount).to.equal(4); 510 | expect(windowStub.webContents.on.getCall(0).args).to.deep.equal([ 511 | "crashed", 512 | reportToRaven 513 | ]); 514 | expect(windowStub.on.getCall(1).args).to.deep.equal([ 515 | "unresponsive", 516 | reportToRaven 517 | ]); 518 | }); 519 | 520 | it("should hide and update the window on blur", () => { 521 | configureWindow(); 522 | 523 | windowStub.on.getCall(3).args[1](); 524 | 525 | expect(windowStub.on.getCall(3).args[0]).to.equal("blur"); 526 | expect(windowStub.hide.callCount).to.equal(1); 527 | expect(windowStub.webContents.send.callCount).to.equal(1); 528 | expect(windowStub.webContents.send.getCall(0).args).to.be.deep.equal([ 529 | "background-update" 530 | ]); 531 | }); 532 | 533 | it("should configure the autoupdater", () => { 534 | configureWindow(); 535 | 536 | expect(autoUpdaterStub.callCount).to.equal(1); 537 | expect(autoUpdaterStub.getCall(0).args).to.deep.equal([windowStub]); 538 | }); 539 | 540 | it("should not configure the autoupdater in non-prod", () => { 541 | const { configureWindow: configureWindowDev } = proxyquire( 542 | "../src/electron/index", 543 | { 544 | electron: electronStub, 545 | "@sentry/electron": ravenStub, 546 | "first-run": firstRunStub, 547 | "electron-is-dev": true, 548 | os: { platform: () => "win32" }, 549 | "is-online": testSandbox.stub().resolves(true), 550 | "./auto-updater": autoUpdaterStub 551 | } 552 | ); 553 | 554 | configureWindowDev(); 555 | 556 | expect(autoUpdaterStub.called).to.be.false; 557 | }); 558 | 559 | it("should load the url and return the window object", () => { 560 | const result = configureWindow(); 561 | const file = resolve(__dirname, "..", "public", "index.html"); 562 | 563 | expect(windowStub.loadURL.callCount).to.equal(1); 564 | expect(windowStub.loadURL.getCall(0).args).to.deep.equal([ 565 | `file://${file}` 566 | ]); 567 | expect(result).to.deep.equal(windowStub); 568 | }); 569 | }); 570 | 571 | context("configureTrayIcon", () => { 572 | const windowStub = { 573 | on: testSandbox.stub(), 574 | webContents: { on: testSandbox.stub(), send: testSandbox.stub() }, 575 | loadURL: testSandbox.stub(), 576 | hide: testSandbox.stub(), 577 | isVisible: testSandbox.stub().returns(false) 578 | }; 579 | const trayStub = { 580 | on: testSandbox.stub(), 581 | setToolTip: testSandbox.stub(), 582 | setImage: testSandbox.stub() 583 | }; 584 | const menuStub = { 585 | popup: testSandbox.stub() 586 | }; 587 | 588 | beforeEach(() => { 589 | electronStub.Tray.returns(trayStub); 590 | }); 591 | 592 | it("should create the browser window with the correct options", () => { 593 | const iconPath = resolve(__dirname, "../src/electron", getTrayIconName()); 594 | 595 | configureTrayIcon(windowStub, null, menuStub); 596 | 597 | expect(electronStub.Tray.callCount).to.equal(1); 598 | expect(electronStub.Tray.getCall(0).args).to.deep.equal([iconPath]); 599 | }); 600 | 601 | it("should set up error listeners", () => { 602 | configureTrayIcon(windowStub, null, menuStub); 603 | 604 | expect(trayStub.on.callCount).to.equal(3); 605 | expect(trayStub.on.getCall(0).args[0]).to.deep.equal("click"); 606 | expect(trayStub.on.getCall(1).args[0]).to.deep.equal("double-click"); 607 | expect(trayStub.on.getCall(0).args[1]).to.be.a("function"); 608 | expect(trayStub.on.getCall(1).args[1]).to.be.a("function"); 609 | }); 610 | 611 | it("should open the context menu on right click", () => { 612 | configureTrayIcon(windowStub, null, menuStub); 613 | 614 | trayStub.on.getCall(2).args[1](); 615 | 616 | expect(trayStub.on.getCall(2).args[0]).to.equal("right-click"); 617 | expect(menuStub.popup.callCount).to.equal(1); 618 | }); 619 | 620 | it("should return the interval ID", () => { 621 | const result = configureTrayIcon(windowStub, null, menuStub); 622 | 623 | expect(result).to.be.ok; 624 | }); 625 | 626 | it("should check refresh the icon every minute", () => { 627 | const iconPath = resolve(__dirname, "../src/electron", getTrayIconName()); 628 | 629 | testSandbox.spy(global, "setInterval"); 630 | 631 | configureTrayIcon(windowStub, null, menuStub); 632 | 633 | setInterval.getCall(0).args[0](); 634 | 635 | expect(setInterval.callCount).to.equal(1); 636 | expect(trayStub.setImage.callCount).to.equal(1); 637 | expect(trayStub.setImage.getCall(0).args).to.deep.equal([iconPath]); 638 | expect(windowStub.webContents.send.callCount).to.equal(1); 639 | expect(windowStub.webContents.send.getCall(0).args).to.deep.equal([ 640 | "background-update" 641 | ]); 642 | 643 | setInterval.restore(); 644 | }); 645 | 646 | it("should not send a background update message if the window is visible", () => { 647 | testSandbox.spy(global, "setInterval"); 648 | 649 | windowStub.isVisible.returns(true); 650 | 651 | configureTrayIcon(windowStub, null, menuStub); 652 | 653 | setInterval.getCall(0).args[0](); 654 | 655 | expect(windowStub.webContents.send.called).to.be.false; 656 | 657 | setInterval.restore(); 658 | }); 659 | }); 660 | 661 | context("configure app", () => { 662 | const windowStub = { 663 | on: testSandbox.stub(), 664 | webContents: { on: testSandbox.stub(), send: testSandbox.stub() }, 665 | loadURL: testSandbox.stub(), 666 | hide: testSandbox.stub(), 667 | show: testSandbox.stub(), 668 | isVisible: testSandbox.stub() 669 | }; 670 | const trayStub = { 671 | on: testSandbox.stub(), 672 | setToolTip: testSandbox.stub(), 673 | setImage: testSandbox.stub() 674 | }; 675 | const menuReturnStub = { 676 | popup: testSandbox.stub(), 677 | append: testSandbox.stub() 678 | }; 679 | 680 | beforeEach(() => { 681 | electronStub.BrowserWindow.returns(windowStub); 682 | electronStub.Tray.returns(trayStub); 683 | electronStub.Menu.returns(menuReturnStub); 684 | }); 685 | 686 | it("should append a menu item to the menu", () => { 687 | configureBarnacal(); 688 | 689 | expect(electronStub.Menu.callCount).to.equal(1); 690 | expect(menuReturnStub.append.callCount).to.equal(2); 691 | expect(electronStub.MenuItem.callCount).to.equal(2); 692 | expect(electronStub.MenuItem.getCall(0).args[0].label).to.equal("About"); 693 | expect(electronStub.MenuItem.getCall(0).args[0].click).to.be.a( 694 | "function" 695 | ); 696 | expect(electronStub.MenuItem.getCall(1).args[0].label).to.equal("Quit"); 697 | expect(electronStub.MenuItem.getCall(1).args[0].click).to.be.a( 698 | "function" 699 | ); 700 | }); 701 | 702 | it("should add listeners to ipcMain", () => { 703 | configureBarnacal(); 704 | 705 | electronStub.ipcMain.on.getCall(0).args[1](); 706 | electronStub.ipcMain.on.getCall(1).args[1](); 707 | 708 | expect(electronStub.ipcMain.on.callCount).to.equal(4); 709 | expect(electronStub.ipcMain.on.getCall(0).args[0]).to.equal( 710 | "show-config-menu" 711 | ); 712 | expect(menuReturnStub.popup.callCount).to.equal(1); 713 | expect(electronStub.ipcMain.on.getCall(1).args[0]).to.equal("show-about"); 714 | expect(electronStub.ipcMain.on.getCall(1).args[1]).to.be.a("function"); 715 | expect(windowStub.isVisible.callCount).to.equal(1); 716 | expect(electronStub.ipcMain.on.getCall(2).args[0]).to.equal( 717 | "get-first-weekday" 718 | ); 719 | expect(electronStub.ipcMain.on.getCall(2).args[1]).to.be.a("function"); 720 | expect(electronStub.ipcMain.on.getCall(3).args[0]).to.equal("quit-app"); 721 | expect(electronStub.ipcMain.on.getCall(3).args[1]).to.be.a("function"); 722 | }); 723 | 724 | it("should hide the dock icon on macOS", () => { 725 | configureBarnacal(); 726 | 727 | expect(electronStub.app.dock.hide.callCount).to.equal(1); 728 | }); 729 | 730 | it("should not hide the dock on non-macOS machines", () => { 731 | const { configureBarnacal: configureBarnacalWin } = proxyquire( 732 | "../src/electron/index", 733 | { 734 | electron: electronStub, 735 | "@sentry/electron": ravenStub, 736 | "first-run": firstRunStub, 737 | "electron-is-dev": false, 738 | os: { platform: () => "win32" }, 739 | "is-online": testSandbox.stub().resolves(true), 740 | "./auto-updater": autoUpdaterStub 741 | } 742 | ); 743 | 744 | configureBarnacalWin(); 745 | 746 | expect(electronStub.app.dock.hide.called).to.be.false; 747 | }); 748 | }); 749 | }); 750 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "inlineSources": true, 5 | "jsx": "preserve", 6 | "lib": [ 7 | "es2017", 8 | "esnext.asynciterable", 9 | "dom", 10 | "dom.iterable", 11 | "scripthost" 12 | ], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noImplicitAny": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "pretty": true, 19 | "removeComments": true, 20 | "resolveJsonModule": true, 21 | "outDir": "dist", 22 | "skipLibCheck": true, 23 | "sourceMap": true, 24 | "strictNullChecks": true, 25 | "target": "es2016", 26 | "allowJs": true, 27 | "strict": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "noEmit": true, 30 | "isolatedModules": true 31 | }, 32 | "exclude": ["node_modules"], 33 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 34 | } 35 | --------------------------------------------------------------------------------