├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── screenshots ├── vs-custom-color-2.jpg ├── vs-custom-color.jpg ├── vs-devtools.jpg └── vs-menu.jpg ├── src ├── App.vue ├── assets │ └── favicon-16x16.png ├── background.ts ├── electron │ └── utils.ts ├── main.ts ├── models │ └── Place.ts ├── router │ └── index.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ └── index.ts ├── styles │ └── index.scss ├── types │ ├── electron-get-location.d.ts │ ├── global.d.ts │ ├── json.d.ts │ ├── v-calendar.d.ts │ └── vue-weather-widget.d.ts ├── utils │ └── index.ts ├── views │ ├── Main.vue │ └── Settings.vue └── widgets │ ├── Activity │ ├── Activity.widget.vue │ ├── Bar.vue │ ├── CPU.vue │ ├── Disk.vue │ ├── Doughnut.vue │ ├── Network.vue │ ├── scripts │ │ └── network.sh │ └── types.ts │ ├── Calendar.widget.vue │ ├── Clock.widget.vue │ ├── HelloWorld.widget.vue │ └── Weather.widget.vue ├── tests └── unit │ ├── electron.spec.ts │ └── example.spec.ts ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/recommended', 8 | 'airbnb-base', 9 | 'eslint:recommended', 10 | '@vue/typescript/recommended', 11 | '@vue/prettier', 12 | '@vue/prettier/@typescript-eslint', 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 2020, 16 | }, 17 | rules: { 18 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 20 | 'max-classes-per-file': 'off', 21 | 'import/extensions': 'off', 22 | 'no-var': 'error', 23 | 'prefer-const': 'error', 24 | 'prettier/prettier': 'error', 25 | semi: ['error', 'never'], 26 | 'import/no-unresolved': 'off', 27 | '@typescript-eslint/no-unused-vars': 'error', 28 | 'vue/require-component-is': 'off', // This rule is broken, gives false negatives 29 | 'vue/component-name-in-template-casing': [ 30 | 'error', 31 | 'PascalCase', 32 | { 33 | registeredComponentsOnly: true, 34 | }, 35 | ], 36 | 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 37 | 'no-underscore-dangle': ['error', { allowAfterThis: true }], 38 | 'no-param-reassign': [ 39 | 'error', 40 | { 41 | props: true, 42 | ignorePropertyModificationsFor: [ 43 | 'acc', // array.reduce 44 | 'e', // error 45 | 'ctx', // koa 46 | 'req', // express 47 | 'request', // express 48 | 'res', // express 49 | 'response', // express 50 | 'state', // vuex, 51 | '$el', // dom forEach 52 | ], 53 | }, 54 | ], 55 | 'no-else-return': 'off', 56 | 'class-methods-use-this': 'off', 57 | 'prefer-destructuring': 'off', 58 | 'no-shadow': 'off', 59 | 'func-names': 'off', 60 | }, 61 | overrides: [ 62 | { 63 | files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], 64 | env: { 65 | jest: true, 66 | }, 67 | }, 68 | ], 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /dist_electon 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | #Electron-builder output 26 | /dist_electron -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/index.html 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "bracketSpacing": true, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nicholas Ford 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuebersicht 2 | 3 | Inspired by [Uebersicht](https://github.com/felixhageloh/uebersicht), built with Vue, Electron, & TypeScript. 4 | 5 | Add custom widgets to your desktop background, built as Vue single-file components, either in TypeScript or JavaScript. 6 | 7 | ![Vuebersicht](./screenshots/vs-menu.jpg) 8 | 9 | Comes with an Uebersicht-like menu 10 | 11 | ![Vuebersicht Vue Dev Tools](./screenshots/vs-devtools.jpg) 12 | 13 | ...and Chromium / Vue Dev Tools 14 | 15 | ## Widgets 16 | 17 | Widgets are automatically imported from the `./src/widgets` directory, each widget should follow the file naming convention of `.widget.vue`. You may construct widgets using multiple Vue components, and nest `.widget.vue` files in directories, just make sure the root component of the widget has a filename ending in `.widget.vue`, and child components do not. 18 | 19 | - [Vue Single File Components docs](https://vuejs.org/v2/guide/single-file-components.html) 20 | - [Vue TypeScript Class-Style Component docs](https://class-component.vuejs.org/) 21 | 22 | ## Settings 23 | 24 | There is a Settings panel in the Vuebersicht toolbar that allows you to change the primary color used by widgets. 25 | 26 | ![Vuebersicht Custom Color](./screenshots/vs-custom-color.jpg) 27 | ![Vuebersicht Custom Color](./screenshots/vs-custom-color-2.jpg) 28 | 29 | ## Utilities 30 | 31 | ### `run(command)` 32 | 33 | - Decription: Run a shell command asynchronously. 34 | - Params: `command: string` 35 | - Returns: `Promise` 36 | 37 | Note: It is also possible to use this method to run any kind of shell script, for example, in the [Network Widget](https://github.com/nickforddesign/vuebersicht/blob/master/src/widgets/Activity/Network.vue#L44). 38 | 39 | example: 40 | 41 | ```js 42 | import { run } from '@/utils' 43 | 44 | ... 45 | try { 46 | const stdout = await run('ls -la') 47 | console.log(stdout) 48 | } catch(stderr) { 49 | throw stderr 50 | } 51 | ``` 52 | 53 | ### `sleep(milliseconds)` 54 | 55 | - Description: Wait for an aribitrary amount of time asynchronously. 56 | - Params: `milliseconds: number` 57 | - Returns: `Promise` 58 | 59 | example: 60 | 61 | ```js 62 | import { sleep } from '@/utils' 63 | 64 | ... 65 | await sleep(1000) // wait for 1 sec 66 | foo() 67 | ``` 68 | 69 | ## Limitations 70 | 71 | This is still just experimental. Because of the nature of nature of the current build tooling, I haven't yet found a way to enable some important features of the original [Uebersicht](https://github.com/felixhageloh/uebersicht) application, hot-reloading in production builds, for instance. For now, it is recommended to run this experimental application in development mode. 72 | 73 | I have yet to really test for OS support (other than MacOS), though I hear that it does work in Ubuntu at least. If you have time to test in your environment please open an issue if it does not run, and I'll look into supporting it. 74 | 75 | ## Project setup 76 | 77 | ```bash 78 | yarn install # or npm install 79 | ``` 80 | 81 | ### API keys 82 | 83 | Vuebersicht currently ships with a Weather widget that uses 2 APIs, Google Geocoder API (for getting your approximate location), and OpenWeatherMap API (to get local weather data for that location). You'll need to create these API keys and add them to an untracked file called `.env.local`, in the root directory of the project: 84 | 85 | ```js 86 | GOOGLE_API_KEY = "" 87 | WEATHER_API_KEY = "" 88 | ``` 89 | 90 | - [Create a Google Geocoder API key](https://developers.google.com/maps/documentation/geocoding/get-api-key) 91 | - [Create an OpenWeatherMap API key](https://openweathermap.org/appid) 92 | 93 | ### Start the application 94 | 95 | ```bash 96 | yarn serve # or npm run serve 97 | ``` 98 | 99 | 110 | 111 | ### Lints and fixes files 112 | 113 | ```bash 114 | yarn lint # or npm run lint 115 | ``` 116 | 117 | #### Thanks 118 | 119 | Major shoutout to [@felixhageloh](https://github.com/felixhageloh) for his amazing work on [Uebersicht](https://github.com/felixhageloh/uebersicht), which is an incredible project that I find both inspiring and humbling. Vuebersicht is in no way meant to be a competitor or a replacement for Uebersicht, if anything it is intended to be a modest tribute to the original, an experimental toy project. 120 | 121 | Core libraries used to make this possible: 122 | 123 | - [TypeScript](https://github.com/microsoft/TypeScript) 124 | - [Electron](https://github.com/electron/electron) 125 | - [Vue.js](https://github.com/vuejs/vue) 126 | - [Webpack](https://github.com/webpack/webpack) 127 | 128 | And others used to build some of the widgets: 129 | 130 | - [chart.js](https://github.com/chartjs/Chart.js) 131 | - [v-calendar](https://github.com/nathanreyes/v-calendar) 132 | - [vue-weather-widget](https://github.com/dipu-bd/vue-weather-widget) 133 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | plugins: ['@babel/plugin-proposal-optional-chaining'], 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuebersicht", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service electron:serve", 7 | "build": "vue-cli-service electron:build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint", 10 | "postinstall": "electron-builder install-app-deps", 11 | "postuninstall": "electron-builder install-app-deps", 12 | "vue:build": "vue-cli-service build", 13 | "vue:serve": "vue-cli-service serve" 14 | }, 15 | "main": "background.js", 16 | "dependencies": { 17 | "axios": "^0.19.2", 18 | "chart.js": "^2.9.3", 19 | "core-js": "^3.6.5", 20 | "v-calendar": "^1.0.8", 21 | "vue": "^2.6.11", 22 | "vue-chartjs": "^3.5.0", 23 | "vue-class-component": "^7.2.3", 24 | "vue-property-decorator": "^8.4.2", 25 | "vue-router": "^3.4.3", 26 | "vue-weather-widget": "^4.2.1", 27 | "vuex": "^3.4.0" 28 | }, 29 | "devDependencies": { 30 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 31 | "@types/chart.js": "^2.9.23", 32 | "@types/electron-devtools-installer": "^2.2.0", 33 | "@types/jest": "^26.0.10", 34 | "@types/sane": "^2.0.0", 35 | "@typescript-eslint/eslint-plugin": "^2.33.0", 36 | "@typescript-eslint/parser": "^2.33.0", 37 | "@vue/cli-plugin-babel": "~4.5.0", 38 | "@vue/cli-plugin-eslint": "~4.5.0", 39 | "@vue/cli-plugin-router": "~4.5.0", 40 | "@vue/cli-plugin-typescript": "~4.5.0", 41 | "@vue/cli-plugin-unit-jest": "~4.5.0", 42 | "@vue/cli-plugin-vuex": "~4.5.0", 43 | "@vue/cli-service": "~4.5.0", 44 | "@vue/eslint-config-prettier": "^6.0.0", 45 | "@vue/eslint-config-typescript": "^5.0.2", 46 | "@vue/test-utils": "^1.0.3", 47 | "electron": "^9.0.0", 48 | "electron-devtools-installer": "^3.1.0", 49 | "eslint": "^6.7.2", 50 | "eslint-config-airbnb-base": "^14.2.0", 51 | "eslint-plugin-import": "^2.22.0", 52 | "eslint-plugin-prettier": "^3.1.3", 53 | "eslint-plugin-vue": "^6.2.2", 54 | "prettier": "^1.19.1", 55 | "raw-loader": "^4.0.1", 56 | "sane": "^4.1.0", 57 | "sass": "^1.26.5", 58 | "sass-loader": "^8.0.2", 59 | "spectron": "^11.0.0", 60 | "typescript": "~3.9.3", 61 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.4", 62 | "vue-template-compiler": "^2.6.11", 63 | "webpack": "^4.44.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickforddev/vuebersicht/5d997a60abffd6d0b9786dd263b0abc827fd4e31/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /screenshots/vs-custom-color-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickforddev/vuebersicht/5d997a60abffd6d0b9786dd263b0abc827fd4e31/screenshots/vs-custom-color-2.jpg -------------------------------------------------------------------------------- /screenshots/vs-custom-color.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickforddev/vuebersicht/5d997a60abffd6d0b9786dd263b0abc827fd4e31/screenshots/vs-custom-color.jpg -------------------------------------------------------------------------------- /screenshots/vs-devtools.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickforddev/vuebersicht/5d997a60abffd6d0b9786dd263b0abc827fd4e31/screenshots/vs-devtools.jpg -------------------------------------------------------------------------------- /screenshots/vs-menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickforddev/vuebersicht/5d997a60abffd6d0b9786dd263b0abc827fd4e31/screenshots/vs-menu.jpg -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickforddev/vuebersicht/5d997a60abffd6d0b9786dd263b0abc827fd4e31/src/assets/favicon-16x16.png -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import path from 'path' 3 | import sane, { Watcher } from 'sane' 4 | import { app, protocol, powerMonitor, BrowserWindow, Tray } from 'electron' 5 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' 6 | import { createWindow, destroyMenu, createMenu, sleep } from './electron/utils' 7 | // import pkg from '../package.json' 8 | 9 | const isDevelopment = process.env.NODE_ENV !== 'production' 10 | 11 | // Keep a global reference of the window object, if you don't, the window will 12 | // be closed automatically when the JavaScript object is garbage collected. 13 | let win: BrowserWindow | null 14 | let appIcon: Tray | null 15 | let watcher: Watcher | null 16 | 17 | // Scheme must be registered before the app is ready 18 | protocol.registerSchemesAsPrivileged([ 19 | { scheme: 'app', privileges: { secure: true, standard: true } }, 20 | ]) 21 | 22 | // Quit when all windows are closed. 23 | app.on('window-all-closed', () => { 24 | // On macOS it is common for applications and their menu bar 25 | // to stay active until the user quits explicitly with Cmd + Q 26 | if (process.platform !== 'darwin') { 27 | app.quit() 28 | } 29 | }) 30 | 31 | app.on('activate', () => { 32 | // On macOS it's common to re-create a window in the app when the 33 | // dock icon is clicked and there are no other windows open. 34 | if (win === null) { 35 | win = createWindow() 36 | } 37 | }) 38 | 39 | // This method will be called when Electron has finished 40 | // initialization and is ready to create browser windows. 41 | // Some APIs can only be used after this event occurs. 42 | app.on('ready', async () => { 43 | await sleep(200) // hack to fix transparent background in some linux desktop environments 44 | if (isDevelopment && !process.env.IS_TEST) { 45 | // Install Vue Devtools 46 | try { 47 | await installExtension(VUEJS_DEVTOOLS) 48 | } catch (e) { 49 | console.error('Vue Devtools failed to install:', e.toString()) 50 | } 51 | } 52 | win = createWindow() 53 | const widgetsFolderPath = path.resolve(__dirname, '../src/widgets') 54 | const iconPath = path.resolve(__dirname, '../src/assets/logo.png') 55 | 56 | app.setAboutPanelOptions({ 57 | applicationName: 'Vuebersicht', 58 | // applicationVersion: pkg.version, 59 | iconPath, 60 | }) 61 | app.dock.hide() 62 | 63 | appIcon = await createMenu(win) 64 | 65 | // we need to refresh the tray menu when widgets are added or removed 66 | watcher = sane(widgetsFolderPath, { glob: ['**/*.widget.vue'] }) 67 | watcher 68 | .on('add', async () => { 69 | destroyMenu(appIcon as Tray) 70 | appIcon = await createMenu(win as BrowserWindow) 71 | }) 72 | .on('delete', async () => { 73 | destroyMenu(appIcon as Tray) 74 | appIcon = await createMenu(win as BrowserWindow) 75 | }) 76 | powerMonitor.on('resume', () => { 77 | ;(win as BrowserWindow).reload() 78 | }) 79 | }) 80 | 81 | // Exit cleanly on request from parent process in development mode. 82 | if (isDevelopment) { 83 | if (process.platform === 'win32') { 84 | process.on('message', data => { 85 | if (data === 'graceful-exit') { 86 | app.quit() 87 | } 88 | }) 89 | } else { 90 | process.on('SIGTERM', () => { 91 | app.quit() 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/electron/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import path from 'path' 3 | import glob from 'glob' 4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 5 | import { 6 | app, 7 | shell, 8 | BrowserWindow, 9 | Menu, 10 | MenuItem, 11 | Tray, 12 | nativeImage, 13 | MenuItemConstructorOptions, 14 | } from 'electron' 15 | 16 | let win: BrowserWindow | null = null 17 | let settingsWin: BrowserWindow | null = null 18 | 19 | function loadWindow(browserWindow: BrowserWindow, route = '') { 20 | if (process.env.WEBPACK_DEV_SERVER_URL) { 21 | browserWindow.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}#/${route}`) 22 | // if (!process.env.IS_TEST) browserWindow.webContents.openDevTools() 23 | } else { 24 | createProtocol('app') 25 | browserWindow.loadURL(`app://./index.html#${route}`) 26 | } 27 | } 28 | 29 | function createSettingsWindow(): BrowserWindow { 30 | settingsWin = 31 | settingsWin || 32 | new BrowserWindow({ 33 | width: 600, 34 | height: 400, 35 | webPreferences: { 36 | nodeIntegration: true, 37 | }, 38 | }) 39 | 40 | loadWindow(settingsWin, 'settings') 41 | 42 | settingsWin.on('closed', () => { 43 | settingsWin = null 44 | if (win) { 45 | win.reload() 46 | } 47 | }) 48 | return settingsWin 49 | } 50 | 51 | export function createWindow(): BrowserWindow { 52 | win = new BrowserWindow({ 53 | transparent: true, 54 | backgroundColor: '#00FFFFFF', 55 | width: 800, 56 | height: 600, 57 | frame: false, 58 | hasShadow: false, 59 | type: 'desktop', 60 | webPreferences: { 61 | nodeIntegration: true, 62 | }, 63 | }) 64 | win.maximize() 65 | 66 | loadWindow(win) 67 | 68 | win.on('closed', () => { 69 | win = null 70 | }) 71 | return win 72 | } 73 | 74 | export function destroyMenu(appIcon: Tray) { 75 | appIcon.destroy() 76 | } 77 | 78 | export async function createMenu(win: BrowserWindow) { 79 | const widgetsFolderPath = path.resolve(__dirname, '../src/widgets') 80 | const faviconPath = path.resolve(__dirname, '../src/assets/favicon-16x16.png') 81 | const appIcon = new Tray(nativeImage.createFromPath(faviconPath)) 82 | const widgets = glob.sync(`${widgetsFolderPath}/**/*.widget.vue`) 83 | 84 | const template = [ 85 | { 86 | label: 'About Vuebersicht', 87 | click() { 88 | app.showAboutPanel() 89 | app.focus() 90 | }, 91 | }, 92 | { type: 'separator' }, 93 | 94 | // widgets 95 | { label: 'Widgets', enabled: false }, 96 | ...widgets.map((widgetPath: string) => { 97 | const name = widgetPath.split('/').pop() 98 | const menuItem = { 99 | label: name, 100 | submenu: [ 101 | { 102 | label: 'Edit Widget', 103 | click() { 104 | shell.openPath(widgetPath) 105 | }, 106 | }, 107 | { type: 'separator' }, 108 | { 109 | label: 'Hide widget', 110 | type: 'checkbox', 111 | checked: false, 112 | click(e: MenuItem) { 113 | win.webContents.send('data', { 114 | type: 'updateWidget', 115 | path: widgetPath, 116 | visible: !e.checked, 117 | }) 118 | }, 119 | }, 120 | ], 121 | } 122 | return menuItem 123 | }), 124 | { type: 'separator' }, 125 | 126 | // utils 127 | { 128 | label: 'Open Widgets Folder', 129 | click() { 130 | const widgetsFolderPath = path.resolve(__dirname, '../src/widgets') 131 | shell.openPath(widgetsFolderPath) 132 | }, 133 | }, 134 | { 135 | label: 'Refresh All Widgets', 136 | click() { 137 | win.reload() 138 | }, 139 | }, 140 | { 141 | label: 'Settings', 142 | click() { 143 | createSettingsWindow() 144 | }, 145 | }, 146 | { 147 | label: 'Open Dev Tools', 148 | click() { 149 | win.webContents.openDevTools({ mode: 'detach' }) 150 | win.focus() 151 | }, 152 | }, 153 | { type: 'separator' }, 154 | { 155 | label: 'Quit Vuebersicht', 156 | click() { 157 | app.quit() 158 | }, 159 | }, 160 | ] 161 | 162 | const contextMenu = Menu.buildFromTemplate(template as [MenuItemConstructorOptions]) 163 | 164 | // Call this again for Linux because we modified the context menu 165 | appIcon.setContextMenu(contextMenu) 166 | return appIcon 167 | } 168 | 169 | export function sleep(duration: number): Promise { 170 | return new Promise(resolve => setTimeout(resolve, duration)) 171 | } 172 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | 5 | import './styles/index.scss' 6 | import router from './router' 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | store, 12 | router, 13 | render: h => h(App), 14 | }).$mount('#app') 15 | -------------------------------------------------------------------------------- /src/models/Place.ts: -------------------------------------------------------------------------------- 1 | export default class Place { 2 | city: string | null = null 3 | state: string | null = null 4 | country: string | null = null 5 | latitude?: number | null = null 6 | longitude?: number | null = null 7 | } 8 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter, { RouteConfig } from 'vue-router' 3 | import Main from '../views/Main.vue' 4 | import Settings from '../views/Settings.vue' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes: Array = [ 9 | { 10 | path: '/', 11 | name: 'Main', 12 | component: Main, 13 | }, 14 | { 15 | path: '/settings', 16 | name: 'Settings', 17 | component: Settings, 18 | }, 19 | ] 20 | 21 | const router = new VueRouter({ 22 | routes, 23 | }) 24 | 25 | export default router 26 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import Vue, { VNode } from 'vue' 3 | 4 | declare global { 5 | namespace JSX { 6 | // tslint:disable no-empty-interface 7 | interface Element extends VNode {} 8 | // tslint:disable no-empty-interface 9 | interface ElementClass extends Vue {} 10 | interface IntrinsicElements { 11 | [elem: string]: any 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | 4 | export default Vue 5 | } 6 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, no-param-reassign */ 2 | import Vue from 'vue' 3 | import Vuex, { StoreOptions } from 'vuex' 4 | import { ipcRenderer } from 'electron' 5 | 6 | import { getPlace } from '@/utils' 7 | import Place from '@/models/Place' 8 | 9 | Vue.use(Vuex) 10 | 11 | interface Widget { 12 | path: string 13 | visible: boolean 14 | } 15 | 16 | interface RootState { 17 | widgets: Widget[] 18 | place: Place 19 | primaryColor: string 20 | } 21 | 22 | const storeOptions: StoreOptions = { 23 | state: { 24 | widgets: [], 25 | place: new Place(), 26 | primaryColor: localStorage.getItem('primaryColor') || '#ffffff', 27 | }, 28 | getters: { 29 | isUS(state) { 30 | return state.place.country === 'US' 31 | }, 32 | language() { 33 | return Intl.DateTimeFormat().resolvedOptions().locale 34 | }, 35 | }, 36 | mutations: { 37 | addWidget(state, widget: Widget) { 38 | state.widgets.push(widget) 39 | }, 40 | updateWidget(state, widget: Widget) { 41 | const old = state.widgets.find(oldWidget => { 42 | return oldWidget.path === widget.path 43 | }) 44 | if (old) { 45 | old.visible = widget.visible 46 | } 47 | }, 48 | resetWidgets(state) { 49 | state.widgets = [] 50 | }, 51 | updateLocation(state, place: Place) { 52 | state.place = place 53 | }, 54 | setPrimaryColor(state, color: string) { 55 | state.primaryColor = color 56 | localStorage.setItem('primaryColor', color) 57 | }, 58 | }, 59 | actions: { 60 | addWidget({ commit }, widget: Widget) { 61 | commit('addWidget', widget) 62 | }, 63 | updateWidget({ commit }, widget: Widget) { 64 | commit('updateWidget', widget) 65 | }, 66 | async getCurrentLocation({ commit }) { 67 | navigator.geolocation.getCurrentPosition( 68 | async position => { 69 | const { 70 | coords: { latitude, longitude }, 71 | } = position 72 | const { city, state, country } = await getPlace(latitude, longitude) 73 | commit('updateLocation', { 74 | city, 75 | state, 76 | country, 77 | latitude, 78 | longitude, 79 | }) 80 | }, 81 | error => { 82 | console.error(error) // eslint-disable-line 83 | }, 84 | { 85 | enableHighAccuracy: true, 86 | }, 87 | ) 88 | }, 89 | }, 90 | } 91 | 92 | const store = new Vuex.Store(storeOptions) 93 | 94 | ipcRenderer.on('data', (event, data) => { 95 | const { type, ...rest } = data 96 | store.dispatch(type, { ...rest }) 97 | }) 98 | 99 | export default store 100 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/types/electron-get-location.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'electron-get-location' { 2 | interface Location { 3 | city: string 4 | country: string 5 | latitude: string 6 | longitude: string 7 | timezone: string 8 | } 9 | export default function(): Promise 10 | } 11 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'raw-loader!*' 2 | -------------------------------------------------------------------------------- /src/types/json.d.ts: -------------------------------------------------------------------------------- 1 | // declare module '*.json' { 2 | // const value: any 3 | // export default value 4 | // } 5 | -------------------------------------------------------------------------------- /src/types/v-calendar.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'v-calendar' { 2 | import { PluginObject } from 'vue' 3 | 4 | const VCalendar: PluginObject 5 | 6 | export default VCalendar 7 | } 8 | -------------------------------------------------------------------------------- /src/types/vue-weather-widget.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-weather-widget' { 2 | import Vue from 'vue' 3 | 4 | export default Vue 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | import { exec } from 'child_process' 4 | import axios from 'axios' 5 | import Place from '@/models/Place' 6 | 7 | export function run(command: string) { 8 | return new Promise((resolve, reject) => { 9 | exec(command, (err, stdout, stderr) => { 10 | if (err) { 11 | reject(err) 12 | } else if (stderr) { 13 | reject(stderr) 14 | } else { 15 | resolve(stdout) 16 | } 17 | }) 18 | }) 19 | } 20 | 21 | export function sleep(duration: number): Promise { 22 | return new Promise(resolve => setTimeout(resolve, duration)) 23 | } 24 | 25 | interface AddressComponent { 26 | long_name: string 27 | short_name: string 28 | types: string[] 29 | } 30 | 31 | export async function getPlace(lat: string | number, lng: string | number): Promise { 32 | const apiKey = process.env.GOOGLE_API_KEY 33 | const response = await axios.get( 34 | `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${apiKey}`, 35 | ) 36 | const addressComponents: AddressComponent[] = response?.data?.results?.[0]?.address_components 37 | const city = addressComponents.find(({ types }) => types.includes('locality')) 38 | ?.long_name as string 39 | const state = addressComponents.find(({ types }) => types.includes('administrative_area_level_1')) 40 | ?.short_name as string 41 | const country = addressComponents.find(({ types }) => types.includes('country')) 42 | ?.short_name as string 43 | return { city, state, country } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 44 | 45 | 54 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 43 | 44 | 53 | -------------------------------------------------------------------------------- /src/widgets/Activity/Activity.widget.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | 70 | -------------------------------------------------------------------------------- /src/widgets/Activity/Bar.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/widgets/Activity/CPU.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | -------------------------------------------------------------------------------- /src/widgets/Activity/Disk.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/widgets/Activity/Doughnut.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/widgets/Activity/Network.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 200 | 201 | 209 | -------------------------------------------------------------------------------- /src/widgets/Activity/scripts/network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | whereAwk=$(which awk) 4 | whereCat=$(which cat) 5 | whereNetstat=$(which netstat) 6 | 7 | foundPaths="${whereCat///cat}:${whereAwk///awk}:${whereNetstat///netstat}" 8 | 9 | function getnetwork(){ 10 | export PATH="$foundPaths" && 11 | cache=$(netstat -iw 1 | head -n3 | tail -n1 | awk '{print $3 " " $6}') 12 | 13 | in=$(echo "$cache" | awk '{print $1}') 14 | out=$(echo "$cache" | awk '{print $2}') 15 | 16 | echo $in $out 17 | } 18 | 19 | # getnetwork "$PWD" 20 | 21 | if [ ! -e "$PWD/src/widgets/Activity/scripts/network.sh" ]; then 22 | getnetwork "$PWD/src/widgets/Activity/scripts" 23 | else 24 | getnetwork "$PWD" 25 | fi 26 | -------------------------------------------------------------------------------- /src/widgets/Activity/types.ts: -------------------------------------------------------------------------------- 1 | export enum color { 2 | low = 'rgb(133, 188, 86)', 3 | med = 'orange', 4 | high = 'rgb(255,44,37)', 5 | } 6 | 7 | export function getColor(percentage: number): color { 8 | if (percentage < 50) { 9 | return color.low 10 | } else if (percentage < 80) { 11 | return color.med 12 | } else { 13 | return color.high 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/widgets/Calendar.widget.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 67 | 68 | 112 | -------------------------------------------------------------------------------- /src/widgets/Clock.widget.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | 59 | 83 | -------------------------------------------------------------------------------- /src/widgets/HelloWorld.widget.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /src/widgets/Weather.widget.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 63 | 64 | 146 | -------------------------------------------------------------------------------- /tests/unit/electron.spec.ts: -------------------------------------------------------------------------------- 1 | // /** 2 | // * @jest-environment node 3 | // */ 4 | // import spectron from 'spectron' 5 | // import { testWithSpectron } from 'vue-cli-plugin-electron-builder' 6 | 7 | // jest.setTimeout(50000) 8 | 9 | // test('Window Loads Properly', async () => { 10 | // // Wait for dev server to start 11 | // const { app, stopServe } = await testWithSpectron(spectron) 12 | // const win = app.browserWindow 13 | // const client = app.client 14 | 15 | // // Window was created 16 | // expect(await client.getWindowCount()).toBe(1) 17 | // // It is not minimized 18 | // expect(await win.isMinimized()).toBe(false) 19 | // // Window is visible 20 | // expect(await win.isVisible()).toBe(true) 21 | // // Size is correct 22 | // const { width, height } = await win.getBounds() 23 | // expect(width).toBeGreaterThan(0) 24 | // expect(height).toBeGreaterThan(0) 25 | // // App is loaded properly 26 | // expect(/Welcome to Your Vue\.js (\+ TypeScript )?App/.test(await client.getHTML('#app'))).toBe( 27 | // true, 28 | // ) 29 | 30 | // await stopServe() 31 | // }) 32 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message' 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg }, 9 | }) 10 | expect(wrapper.text()).toMatch(msg) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "jest", 18 | "chart.js", 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | pluginOptions: { 6 | electronBuilder: { 7 | nodeIntegration: true, 8 | }, 9 | }, 10 | configureWebpack: { 11 | module: { 12 | rules: [ 13 | { 14 | test: /node_modules\/axios/, 15 | resolve: { aliasFields: ['axios'] }, 16 | }, 17 | ], 18 | }, 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': { 22 | GOOGLE_API_KEY: JSON.stringify(process.env.GOOGLE_API_KEY), 23 | WEATHER_API_KEY: JSON.stringify(process.env.WEATHER_API_KEY), 24 | }, 25 | }), 26 | ], 27 | }, 28 | } 29 | --------------------------------------------------------------------------------