├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .postcssrc.js ├── .travis.yml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── babel.config.js ├── package.json ├── public ├── _locales │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── sk │ │ └── messages.json │ ├── uk │ │ └── messages.json │ └── zh_cn │ │ └── messages.json ├── imgs │ ├── icon128.png │ ├── icon16.png │ ├── icon48.png │ └── weather │ │ ├── weather-200-n.png │ │ ├── weather-200.png │ │ ├── weather-201.png │ │ ├── weather-300.png │ │ ├── weather-500-n.png │ │ ├── weather-500.png │ │ ├── weather-501-n.png │ │ ├── weather-501.png │ │ ├── weather-502-n.png │ │ ├── weather-502.png │ │ ├── weather-503-n.png │ │ ├── weather-503.png │ │ ├── weather-511.png │ │ ├── weather-600-n.png │ │ ├── weather-600.png │ │ ├── weather-601.png │ │ ├── weather-602.png │ │ ├── weather-700.png │ │ ├── weather-800-n.png │ │ ├── weather-800.png │ │ ├── weather-801-n.png │ │ ├── weather-801.png │ │ ├── weather-803.png │ │ ├── weather-804.png │ │ ├── weather-900.png │ │ ├── weather-905.png │ │ └── weather-none.png └── index.html ├── screenshot-2.jpg ├── screenshot.jpg ├── src ├── App.vue ├── cards │ ├── Apps │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── Bookmarks │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── Changelog │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ └── style.scss │ ├── Downloads │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── Epitech │ │ ├── api.js │ │ ├── index.vue │ │ ├── langs │ │ │ ├── en.json │ │ │ └── tr.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── Isefac │ │ ├── index.vue │ │ ├── langs │ │ │ ├── en.json │ │ │ └── tr.json │ │ ├── main.js │ │ ├── manifest.json │ │ └── style.scss │ ├── LastFm │ │ ├── api.js │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── QuickLinks │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── QuickSettings │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ └── style.scss │ ├── RSS │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── Sessions │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── System │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ └── style.scss │ ├── TopSites │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ ├── Translate │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── languages.js │ │ ├── main.js │ │ ├── manifest.json │ │ └── style.scss │ ├── Weather │ │ ├── index.vue │ │ ├── langs │ │ │ ├── de.json │ │ │ ├── en.json │ │ │ ├── es_mx.json │ │ │ ├── fr.json │ │ │ ├── sk.json │ │ │ ├── tr.json │ │ │ ├── uk.json │ │ │ └── zh_cn.json │ │ ├── main.js │ │ ├── manifest.json │ │ ├── settings.js │ │ ├── settings.vue │ │ └── style.scss │ └── index.js ├── components │ ├── Card │ │ ├── index.vue │ │ ├── main.js │ │ └── style.scss │ ├── Dialog │ │ ├── Dialog.vue │ │ ├── index.js │ │ └── main.js │ ├── GridList │ │ ├── index.vue │ │ └── style.scss │ ├── Header │ │ ├── backgrounds.js │ │ ├── index.vue │ │ ├── main.js │ │ └── style.scss │ ├── Home │ │ ├── index.vue │ │ ├── main.js │ │ └── style.scss │ ├── List │ │ ├── index.vue │ │ └── style.scss │ ├── Onboarding │ │ ├── hello.vue │ │ ├── index.vue │ │ ├── main.js │ │ ├── settings.vue │ │ ├── style.scss │ │ └── why.vue │ ├── Settings │ │ ├── artworks.js │ │ ├── countries.js │ │ ├── index.vue │ │ ├── main.js │ │ └── style.scss │ ├── Toast │ │ ├── Toast.vue │ │ ├── index.js │ │ └── main.js │ └── Typer │ │ ├── index.vue │ │ ├── main.js │ │ └── style.scss ├── i18n │ ├── de.json │ ├── en.json │ ├── es_mx.json │ ├── fr.json │ ├── index.js │ ├── sk.json │ ├── tr.json │ ├── uk.json │ └── zh_cn.json ├── langs │ └── .gitkeep ├── main.js ├── manifest-chrome.json ├── manifest-firefox.json ├── mixins │ ├── dark.js │ ├── date.js │ ├── permissions.js │ └── utils.js ├── router │ └── index.js ├── store │ ├── cache.js │ ├── cards.js │ ├── cards_settings.js │ ├── index.js │ └── settings.js └── style.scss ├── vue.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /src/langs/ 3 | babel.config.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | globals: { 7 | browser: true, 8 | CardsObj: true, 9 | Langs: true, 10 | browserName: true, 11 | }, 12 | extends: [ 13 | 'plugin:vue/recommended', 14 | '@vue/airbnb', 15 | ], 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 19 | 'vue/max-attributes-per-line': 0, 20 | 'no-bitwise': 0, 21 | 'no-underscore-dangle': 0, 22 | // don't require .vue extension when importing 23 | 'import/extensions': ['error', 'always', { 24 | js: 'never', 25 | vue: 'never', 26 | }], 27 | // disallow reassignment of function parameters 28 | // disallow parameter object manipulation except for specific exclusions 29 | 'no-param-reassign': ['error', { 30 | props: true, 31 | ignorePropertyModificationsFor: [ 32 | 'state', // for vuex state 33 | 'acc', // for reduce accumulators 34 | 'e', // for e.returnvalue 35 | 'f', 36 | ], 37 | }], 38 | }, 39 | parserOptions: { 40 | parser: 'babel-eslint', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://paypal.me/ARouillard 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | epiboard-*.zip 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | dist/ 36 | src/langs/*.js 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | package-lock.json 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "9" 5 | - "8" 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | install: 12 | - npm install 13 | 14 | before_script: 15 | - npm install -g bundlesize 16 | 17 | script: 18 | - npm run build 19 | - npm run lint 20 | - bundlesize 21 | 22 | branches: 23 | only: 24 | - master 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 2 | 3 | - Reporting a bug 4 | - Discussing the current state of the code 5 | - Submitting a fix 6 | - Proposing new features 7 | - Becoming a maintainer 8 | 9 | ## We Develop with Github 10 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 11 | 12 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 13 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 14 | 15 | 1. Fork the repo and create your branch from `master`. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Ensure the test suite passes. 19 | 5. Make sure your code lints. 20 | 6. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 24 | 25 | ## Report bugs using Github's [issues](https://github.com/Alexays/Epiboard/issues) 26 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/Alexays/Epiboard/issues/new); it's that easy! 27 | 28 | **Great Bug Reports** tend to have: 29 | 30 | - A quick summary and/or background 31 | - Steps to reproduce 32 | - Be specific! 33 | - What you expected would happen 34 | - What actually happens 35 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 36 | 37 | People *love* thorough bug reports. I'm not even kidding. 38 | 39 | ## Use a Consistent Coding Style 40 | I'm again borrowing these from [Airbnb's Guidelines](https://github.com/airbnb/javascript) 41 | 42 | * 2 spaces for indentation rather than tabs 43 | * You can try running `npm run lint` for style unification 44 | 45 | ## License 46 | By contributing, you agree that your contributions will be licensed under its MIT License. 47 | 48 | ## References 49 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Your issue may already be reported! 2 | Please search on the [issue track](../) before creating one. 3 | 4 | ## Expected Behavior 5 | 6 | 7 | 8 | ## Current Behavior 9 | 10 | 11 | 12 | ## Possible Solution 13 | 14 | 15 | 16 | ## Steps to Reproduce (for bugs) 17 | 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 23 | ## Context 24 | 25 | 26 | 27 | ## Your Environment 28 | 29 | * Version used: 30 | * Browser Name and version: 31 | * Operating System and version: 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexis Rouillard 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@vue/app', 5 | { 6 | useBuiltIns: 'entry', 7 | }, 8 | ], 9 | ], 10 | plugins: [ 11 | [ 12 | 'transform-imports', 13 | { 14 | vuetify: { 15 | transform: 'vuetify/es5/components/${member}', 16 | preventFullImport: true, 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epiboard", 3 | "version": "1.7.3", 4 | "description": "A new tab page adding a touch of WOW whith an interface following the lines of material design, One card for each need, that's your dashboard!", 5 | "author": "Alexis Rouillard ", 6 | "private": true, 7 | "scripts": { 8 | "build": "vue-cli-service build", 9 | "build:chrome": "cross-env BUILD_TARGET=chrome vue-cli-service build", 10 | "build:firefox": "cross-env BUILD_TARGET=firefox vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.1", 15 | "hammerjs": "^2.0.8", 16 | "marked": "^0.7.0", 17 | "material-design-icons-iconfont": "^5.0.1", 18 | "muuri": "^0.8.0", 19 | "resize-observer-polyfill": "^1.5.1", 20 | "vue": "^2.6.10", 21 | "vue-analytics": "^5.17.2", 22 | "vue-axios": "^2.1.5", 23 | "vue-i18n": "^8.15.0", 24 | "vue-router": "^3.1.3", 25 | "vuetify": "^1.5.19", 26 | "vuex": "^3.1.1", 27 | "vuex-persist": "^2.1.0", 28 | "webextension-polyfill": "^0.5.0" 29 | }, 30 | "devDependencies": { 31 | "@vue/cli": "^3.12.0", 32 | "@vue/cli-plugin-babel": "^3.12.0", 33 | "@vue/cli-plugin-eslint": "^3.12.0", 34 | "@vue/cli-service": "^3.12.0", 35 | "@vue/eslint-config-airbnb": "^4.0.1", 36 | "babel-plugin-transform-imports": "^2.0.0", 37 | "copy-webpack-plugin": "^5.0.4", 38 | "cross-env": "^6.0.3", 39 | "glob": "^7.1.4", 40 | "node-sass": "^4.13.1", 41 | "optimize-css-assets-webpack-plugin": "^5.0.3", 42 | "prerender-spa-plugin": "^3.4.0", 43 | "sass-loader": "^7.3.1", 44 | "stylus": "^0.54.7", 45 | "stylus-loader": "^3.0.2", 46 | "vue-cli-plugin-vuetify": "^0.5.0", 47 | "vue-template-compiler": "^2.6.10", 48 | "vuetify-loader": "^1.2.2", 49 | "zip-a-folder": "0.0.9" 50 | }, 51 | "browserslist": [ 52 | "Firefox >= 54", 53 | "Chrome >= 54" 54 | ], 55 | "bundlesize": [ 56 | { 57 | "path": "./dist/js/*.js", 58 | "maxSize": "90 kB" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /public/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - Neuer Tab" 4 | }, 5 | "appDesc": { 6 | "message": "Eine neue Tab-Startseite die einen Hauch von WOW erzeugt, mit einer Oberfläche, die den Richtlinien von Material Design folgt. Eine Karte für jeden Anspruch" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - New Tab" 4 | }, 5 | "appDesc": { 6 | "message": "A new tab page adding a touch of WOW whith an interface following the lines of material design, One card for each need" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - Nueva pestaña" 4 | }, 5 | "appDesc": { 6 | "message": "Una página de nueva pestaña con un toque de WOW, con una interfaz que sigue las líneas del Material Design, una tarjeta para cada necesidad" 7 | } 8 | } -------------------------------------------------------------------------------- /public/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - Nouvel onglet" 4 | }, 5 | "appDesc": { 6 | "message": "Une page nouvel onglet ajoutant une touche de WOW avec une interface suivant les lignes du material design, un besoin une carte." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/sk/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - Nová karta" 4 | }, 5 | "appDesc": { 6 | "message": "Nová karta ktorá prináša dotyk WOW efektu s interface-om ktorý dodržiava línie materiál dizajnu, Jedna karta pre všetky potreby" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/uk/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - Нова вкладка" 4 | }, 5 | "appDesc": { 6 | "message": "Нова вкладка з ВАУ ефектом та інтерфейсом за принципами material design. Картка для кожної потреби." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/zh_cn/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Epiboard - 新标签页" 4 | }, 5 | "appDesc": { 6 | "message": "一个新的标签页,在材质设计的基础上增加了令人惊叹的视觉,一卡多用。" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/imgs/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/icon128.png -------------------------------------------------------------------------------- /public/imgs/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/icon16.png -------------------------------------------------------------------------------- /public/imgs/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/icon48.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-200-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-200-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-200.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-201.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-300.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-500-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-500-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-500.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-501-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-501-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-501.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-501.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-502-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-502-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-502.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-503-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-503-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-503.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-503.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-511.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-511.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-600-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-600-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-600.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-601.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-602.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-602.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-700.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-700.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-800-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-800-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-800.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-801-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-801-n.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-801.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-801.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-803.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-803.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-804.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-804.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-900.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-905.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-905.png -------------------------------------------------------------------------------- /public/imgs/weather/weather-none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/public/imgs/weather/weather-none.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | New tab 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/screenshot-2.jpg -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/screenshot.jpg -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 76 | -------------------------------------------------------------------------------- /src/cards/Apps/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Um Ihre Chrome-Apps anzuzeigen und zu öffnen", 3 | "empty": "Sie haben keine Apps installiert." 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Apps", 3 | "description": "To view and use your chrome apps", 4 | "empty": "You have no apps.", 5 | "settings": { 6 | "size": "Block size" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Aplicaciónes", 3 | "description": "Para ver y usar sus Aplicaciónes de Chrome", 4 | "empty": "No tienes ningúna app." 5 | } 6 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Pour afficher et utiliser vos applications chrome", 3 | "empty": "Vous n'avez pas d'applications." 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zobrazenie a používanie aplikácií chromu", 3 | "empty": "Nemáte žiadne aplikácie." 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Uygulamalar", 3 | "description": "Chrome uygulamarınızı kullanmak ve görmek için", 4 | "empty": "Hiçbir uygulamanız yok.", 5 | "settings": { 6 | "size": "Eleman boyutu" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Додатки", 3 | "description": "Перегляд та використання ваших додатків chrome", 4 | "empty": "У вас немає додатків.", 5 | "settings": { 6 | "size": "Розмір іконок" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Apps/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "查看和使用您的Chrome应用", 3 | "empty": "您没有应用。" 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Apps/main.js: -------------------------------------------------------------------------------- 1 | // DEPRECATED: Chrome apps will be removed soon 2 | import Toast from '@/components/Toast'; 3 | import GridList from '@/components/GridList'; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'Apps', 8 | components: { 9 | GridList, 10 | }, 11 | props: { 12 | settings: { 13 | type: Object, 14 | required: true, 15 | }, 16 | }, 17 | data() { 18 | return { 19 | apps: [], 20 | extensions: [], 21 | themes: [], 22 | }; 23 | }, 24 | computed: { 25 | size() { 26 | return [32, 64, 96, 128][this.settings.size]; 27 | }, 28 | }, 29 | created() { 30 | this.getAll() 31 | .catch(err => this.$emit('init', err)); 32 | }, 33 | methods: { 34 | getType(info) { 35 | if (info.type === 'extension') { 36 | const idx = this.extensions.findIndex(f => f.id === info.id); 37 | if (idx === -1) this.extensions.push(info); 38 | else this.$set(this.extensions, idx, info); 39 | } else if (info.type === 'theme') { 40 | const idx = this.themes.findIndex(f => f.id === info.id); 41 | if (idx === -1) this.themes.push(info); 42 | else this.$set(this.themes, idx, info); 43 | } else { 44 | const app = info; 45 | // TODO: get closest size 46 | app.icon = app.icons 47 | ? app.icons[app.icons.length - 1].url 48 | : 'chrome://extension-icon/khopmbdjffemhegeeobelklnbglcdgfh/256/1'; 49 | if (!app.enabled) { 50 | app.icon += '?grayscale=true'; 51 | } 52 | const idx = this.apps.findIndex(f => f.id === info.id); 53 | if (idx === -1) this.apps.push(app); 54 | else this.$set(this.apps, idx, app); 55 | } 56 | }, 57 | getAll() { 58 | return browser.management.getAll().then((all) => { 59 | for (let i = 0; i < all.length; i += 1) { 60 | this.getType(all[i]); 61 | } 62 | }); 63 | }, 64 | get(id) { 65 | return browser.management.get(id) 66 | .then(this.getType); 67 | }, 68 | launch(app) { 69 | if (app.launchType && app.enabled) { 70 | browser.management.launchApp(app.id); 71 | } else if (!app.enabled) { 72 | browser.management.setEnabled(app.id, true) 73 | .then(() => this.get(app.id)) 74 | .then(() => Toast.show({ title: `${app.name} is now enabled.` })) 75 | .catch(() => Toast.show({ title: `${app.name} is disabled.`, color: 'warning' })) 76 | .finally(() => browser.management.launchApp(app.id)); 77 | } 78 | }, 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/cards/Apps/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "more": "chrome://apps", 3 | "browsers": ["chrome"], 4 | "settings": { 5 | "size": 3 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Apps/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'Apps', 4 | tickLabels: [32, 64, 96, 128], 5 | data() { 6 | return { 7 | settings: {}, 8 | }; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/cards/Apps/settings.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/cards/Apps/style.scss: -------------------------------------------------------------------------------- 1 | #apps { 2 | max-height: 274px; 3 | overflow: auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Lesezeichen", 3 | "description": "Um Ihre Lesezeichen anzuzeigen und zu verwenden", 4 | "recents": "Aktuell", 5 | "all": "Alle", 6 | "empty": "Sie haben hier keine Lesezeichen.", 7 | "back_parent": "Zurück zum übergeordneten Ordner", 8 | "add_folder": "Diesen Ordner als Tab kennzeichnen", 9 | "remove_folder": "Tab entfernen", 10 | "settings": { 11 | "maxRecents": "Maximale aktuelle Lesezeichen" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Bookmarks", 3 | "description": "To view and use your bookmarks", 4 | "recents": "Recents", 5 | "all": "All", 6 | "empty": "You have no bookmarks here.", 7 | "back_parent": "Back to parent folder", 8 | "add_folder": "Pin this folder as tab", 9 | "remove_folder": "Remove tab", 10 | "settings": { 11 | "maxRecents": "Max recents bookmarks" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Marcadores", 3 | "description": "Para ver y abrir sus marcadores", 4 | "recents": "Recientes", 5 | "all": "Todo", 6 | "empty": "No tienes marcadores aquí", 7 | "back_parent": "Volver a la carpeta anterior", 8 | "add_folder": "Marcar esta carpeta como una pestaña", 9 | "remove_folder": "Eliminar pestaña", 10 | "settings": { 11 | "maxRecents": "Límite de marcadores recientes" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Marques pages", 3 | "description": "Pour voir et utiliser vos marques pages", 4 | "recents": "Récents", 5 | "all": "Tous", 6 | "empty": "Vous n'avez pas de marques pages ici.", 7 | "back_parent": "Retour au dossier parent", 8 | "add_folder": "Épingler ce dossier comme onglet", 9 | "remove_folder": "Enlever l'onglet", 10 | "settings": { 11 | "maxRecents": "Nombres maximum de marque-pages récents" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zobrazenie a používanie záložiek", 3 | "recents": "Nedávne", 4 | "all": "Všetky", 5 | "empty": "Nemáte žiadne záložky.", 6 | "back_parent": "Späť do nadradenej zložky", 7 | "add_folder": "Priradiť tento priečinok ako kartu", 8 | "remove_folder": "Odstrániť kartu", 9 | "settings": { 10 | "maxRecents": "Maximálny počet nedávnych záložiek" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Yer İmleri", 3 | "description": "Yer imlerinizi görüntülemek ve kullanmak için", 4 | "recents": "En Yeniler", 5 | "all": "Tümü", 6 | "empty": "Burada herhangi bir yer iminiz yok.", 7 | "back_parent": "Önceki klasöre geri dön", 8 | "add_folder": "Bu klasörü sekme olarak sabitle", 9 | "remove_folder": "Sekmeyi kaldır", 10 | "settings": { 11 | "maxRecents": "En yenilerde görüntülenecek yer imi sayısı" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Закладки", 3 | "description": "Перегляд та використання ваших закладок", 4 | "recents": "Нещодавні", 5 | "all": "Всі", 6 | "empty": "У вас немає закладок.", 7 | "back_parent": "Повернутись до батьківської папки", 8 | "add_folder": "Закріпити цю папку, як вкладку", 9 | "remove_folder": "Прибрати вкладку", 10 | "settings": { 11 | "maxRecents": "Максимальна кількість нещодавних закладок" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "查看和使用您的书签", 3 | "recents": "最近的", 4 | "all": "全部", 5 | "empty": "您在这里没有书签。", 6 | "back_parent": "返回上级文件夹", 7 | "add_folder": "将此文件夹固定为卡片", 8 | "remove_folder": "移除卡片", 9 | "settings": { 10 | "maxRecents": "最近的书签最多数量" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/main.js: -------------------------------------------------------------------------------- 1 | import List from '@/components/List'; 2 | import date from '@/mixins/date'; 3 | import utils from '@/mixins/utils'; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'Boomarks', 8 | components: { 9 | List, 10 | }, 11 | mixins: [date, utils], 12 | props: { 13 | settings: { 14 | type: Object, 15 | required: true, 16 | }, 17 | }, 18 | data() { 19 | return { 20 | recents: { 21 | id: 'recents', 22 | data: [], 23 | }, 24 | all: { 25 | id: 'all', 26 | data: [], 27 | }, 28 | folders: [], 29 | foldersId: [], 30 | rootId: '0', 31 | active: 0, 32 | }; 33 | }, 34 | computed: { 35 | tabs() { 36 | return [ 37 | this.recents, 38 | this.all, 39 | ...this.folders, 40 | ]; 41 | }, 42 | }, 43 | created() { 44 | Promise.all([this.getRecent(), this.getAll()]) 45 | .then(() => this.$emit('init', ['foldersId', 'active'])) 46 | .catch(err => this.$emit('init', err)); 47 | }, 48 | mounted() { 49 | // getFolders is run in 'mounted' because it needs cache. 50 | this.getFolders(); 51 | }, 52 | methods: { 53 | addTab(item) { 54 | this.foldersId.push(item.id); 55 | this.getFolders(); 56 | }, 57 | removeTab(item) { 58 | this.foldersId = this.foldersId.filter(f => f !== item.id); 59 | this.folders = this.folders.filter(f => f.id !== item.id); 60 | }, 61 | backParent(tab) { 62 | if (tab.parentNode && tab.parentNode[0].parentId === this.rootId) { 63 | this.$set(tab, 'data', tab.parentNode); 64 | this.$set(tab, 'parentNode', null); 65 | return; 66 | } 67 | browser.bookmarks.get(tab.data[0].parentId) 68 | .then((parent) => { 69 | this.$set(tab, 'data', tab.parentNode); 70 | this.$set(tab, 'parentNode', parent); 71 | }); 72 | }, 73 | getSubFolder(tab, item) { 74 | if (item.url) return; 75 | browser.bookmarks.getChildren(item.id) 76 | .then((children) => { 77 | this.$set(tab, 'parentNode', tab.data); 78 | this.$set(tab, 'data', children); 79 | }); 80 | }, 81 | getRecent() { 82 | return browser.bookmarks.getRecent(this.settings.maxRecents) 83 | .then((recents) => { 84 | this.recents.data = recents; 85 | }); 86 | }, 87 | getAll() { 88 | return browser.bookmarks.getTree() 89 | .then((tree) => { 90 | this.rootId = tree[0].id; 91 | return browser.bookmarks.getChildren(tree[0].id); 92 | }) 93 | .then((all) => { 94 | this.all.data = all; 95 | }); 96 | }, 97 | getFolders() { 98 | if (!this.foldersId.length) return Promise.resolve(); 99 | return Promise.all(this.foldersId 100 | .map(f => browser.bookmarks.get(f) 101 | .then(folder => browser.bookmarks.getChildren(f) 102 | .then(children => ({ 103 | name: folder[0].title, folder: true, id: f, data: children, 104 | }))))) 105 | .then((folders) => { 106 | this.folders = folders; 107 | }); 108 | }, 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "maxRecents": 7 4 | }, 5 | "more": { 6 | "chrome": "chrome://bookmarks/", 7 | "firefox": "chrome://browser/content/places/places.xul" 8 | }, 9 | "permissions": ["bookmarks"] 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'Bookmarks', 4 | data() { 5 | return { 6 | settings: {}, 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/settings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/cards/Bookmarks/style.scss: -------------------------------------------------------------------------------- 1 | #bookmarks { 2 | .v-window-item { 3 | overflow-y: auto; 4 | max-height: 274px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/Changelog/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/cards/Changelog/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "Was gibt es neues in {version}?", 3 | "empty": "Keine Änderungen, komisch.", 4 | "missed": "Update verpasst?", 5 | "previous": "Vergangene Änderungen anzeigen" 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/Changelog/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "What's new in {version}?", 3 | "empty": "No changelog, that's weird.", 4 | "missed": "Missed an update?", 5 | "previous": "Look at the previous changelogs" 6 | } -------------------------------------------------------------------------------- /src/cards/Changelog/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "¿Qué hay de nuevo en {version}?", 3 | "empty": "No hay registro de modificaciones, es raro.", 4 | "missed": "¿Te perdiste una actualización?", 5 | "previous": "Ver los últimos cambios" 6 | } -------------------------------------------------------------------------------- /src/cards/Changelog/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "Quoi de neuf dans la {version} ?", 3 | "empty": "Pas de journal des modifications, c'est bizarre.", 4 | "missed": "Vous avez manqué une mise à jour ?", 5 | "previous": "Voir les derniers changements" 6 | } -------------------------------------------------------------------------------- /src/cards/Changelog/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "Čo je nové v {version}?", 3 | "empty": "Žiadny changelog, to je čudné.", 4 | "missed": "Zmeškaly ste aktualizáciu?", 5 | "previous": "Pozrite sa na predošlé changelogy" 6 | } -------------------------------------------------------------------------------- /src/cards/Changelog/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "{version} sürümünde neler yeni?", 3 | "empty": "Bir değişiklik kaydı yok, bu ilginç.", 4 | "missed": "Bir güncellemeyi mi kaçırdınız?", 5 | "previous": "Önceki değişiklik kayıtlarına bakın" 6 | } -------------------------------------------------------------------------------- /src/cards/Changelog/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Журнал змін", 3 | "whatsnew": "Що нового у версії {version}?", 4 | "empty": "Немає журналу змін, це дивно.", 5 | "missed": "Пропустили оновлення?", 6 | "previous": "Переглянути попередні зміни" 7 | } -------------------------------------------------------------------------------- /src/cards/Changelog/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "whatsnew": "{version} 的新增功能有哪些?", 3 | "empty": "没有更改日志,这很奇怪。", 4 | "missed": "错过更新了吗?", 5 | "previous": "查看以前的变更日志" 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/Changelog/main.js: -------------------------------------------------------------------------------- 1 | import Marked from 'marked'; 2 | 3 | const { version } = browser.runtime.getManifest(); 4 | const API = 'https://api.github.com/repos/alexays/epiboard/releases/tags/'; 5 | 6 | // @vue/component 7 | export default { 8 | name: 'Changelog', 9 | data() { 10 | return { 11 | version: null, 12 | body: null, 13 | loading: true, 14 | }; 15 | }, 16 | beforeCreate() { 17 | this.$emit('update:cardtitle', this.$t('Changelog.whatsnew', { version })); 18 | }, 19 | mounted() { 20 | if (this.version === version && this.VALID_CACHE) { 21 | this.$emit('init', false); 22 | return; 23 | } 24 | this.axios.get(`${API}${version}`) 25 | .then((res) => { 26 | this.version = version; 27 | this.body = Marked(res.data.body); 28 | }) 29 | .then(() => this.$emit('init', true)) 30 | .catch(err => this.$emit('init', err)) 31 | .finally(() => { 32 | this.loading = false; 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/cards/Changelog/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "cacheValidity": 86400, 3 | "externalsRequests": true 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Changelog/style.scss: -------------------------------------------------------------------------------- 1 | #changelog { 2 | padding: 0 16px 16px; 3 | .previous { 4 | padding-top: 10px; 5 | a { 6 | text-decoration: underline; 7 | } 8 | } 9 | /deep/ .markdown { 10 | h3 { 11 | font-size: 10px; 12 | border-radius: 2px; 13 | margin: 4px; 14 | align-items: center; 15 | display: inline-flex; 16 | vertical-align: middle; 17 | padding: 5px 10px; 18 | text-transform: uppercase; 19 | &#changed { 20 | color: #ffffff; 21 | background-color: #00a7f7; 22 | } 23 | &#added { 24 | color: #ffffff; 25 | background-color: #FF9900; 26 | } 27 | &#removed { 28 | color: #ffffff; 29 | background-color: #47B04B; 30 | } 31 | &#credits { 32 | color: #000000; 33 | background-color: #ffffff; 34 | } 35 | } 36 | ul { 37 | list-style-type: none; 38 | margin: 10px 15px; 39 | font-size: 13px; 40 | > li:before { 41 | content: "–"; 42 | position: absolute; 43 | margin-left: -1.1em; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cards/Downloads/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Verwalten und Anzeigen Ihrer Downloads", 3 | "no_downloads": "Keine Downloads", 4 | "open": "Öffnen", 5 | "delete": "Löschen", 6 | "remove_list": "Von der Liste entfernen", 7 | "clear": "Alle Downloads entfernen", 8 | "error": { 9 | "moved": "Datei verschoben oder gelöscht", 10 | "unable": "Kann nicht entfernt werden" 11 | }, 12 | "settings": { 13 | "max": "Maximale Anzahl Downloads" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Downloads", 3 | "description": "Manage and see your downloads easly", 4 | "no_downloads": "You have no downloads", 5 | "open": "Open", 6 | "delete": "Delete", 7 | "remove_list": "Remove from list", 8 | "clear": "Clear all downloads", 9 | "error": { 10 | "moved": "File moved or deleted", 11 | "unable": "Unable to remove" 12 | }, 13 | "settings": { 14 | "max": "Max downloads" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Descargas", 3 | "description": "Manejar y ver sus descargas fácilmente", 4 | "no_downloads": "No tienes descargas", 5 | "open": "Abrir", 6 | "delete": "Eliminar", 7 | "remove_list": "Quitar de la lísta", 8 | "clear": "Limpiar todas las descargas", 9 | "error": { 10 | "moved": "Archivo movido o eliminado", 11 | "unable": "Imposible de eliminar" 12 | }, 13 | "settings": { 14 | "max": "Límite de descargas" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Téléchargements", 3 | "description": "Gérez et visualisez facilement vos téléchargements", 4 | "no_downloads": "Vous n'avez pas de téléchargements", 5 | "open": "Ouvrir", 6 | "delete": "Supprimer", 7 | "remove_list": "Retirer de la liste", 8 | "clear": "Effacer tout les téléchargements", 9 | "error": { 10 | "moved": "Fichier déplacé ou supprimé", 11 | "unable": "Impossible à retirer" 12 | }, 13 | "settings": { 14 | "max": "Nombre maximum de téléchargements" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Pozrite si a zľahka spravujte vaše stiahnuté súbory", 3 | "no_downloads": "Nemáte žiadne stiahnuté súbory", 4 | "open": "Otvoriť", 5 | "delete": "Vymazať", 6 | "remove_list": "Odstrániť zo zoznamu", 7 | "clear": "Odstrániť všetky stiahnuté súbory zo zoznamu", 8 | "error": { 9 | "moved": "Súbor bol premiestnený alebo zmazaný", 10 | "unable": "Nemožno odstrániť" 11 | }, 12 | "settings": { 13 | "max": "Maximálny počet stiahnutých" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "İndirilenler", 3 | "description": "İndirmelerinizi kolayca görüntüleyin ve yönetin", 4 | "no_downloads": "Herhangi bir indirmeniz yok", 5 | "open": "Aç", 6 | "delete": "Sil", 7 | "remove_list": "Listeden kaldır", 8 | "clear": "Tüm indirmeleri temizle", 9 | "error": { 10 | "moved": "Dosya taşınmış veya silinmiş", 11 | "unable": "Silinemedi" 12 | }, 13 | "settings": { 14 | "max": "Gösterilecek indirilenler sayısı" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Завантаження", 3 | "description": "Легкий перегляд та керування вашими завантаженнями", 4 | "no_downloads": "Немає завантажень", 5 | "open": "Відкрити", 6 | "delete": "Видалити", 7 | "remove_list": "Прибрати зі списку", 8 | "clear": "Очистити всі завантаження", 9 | "error": { 10 | "moved": "Файл переміщено або видалено", 11 | "unable": "Неможливо видалити" 12 | }, 13 | "settings": { 14 | "max": "Максимум останніх завантажень" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/cards/Downloads/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "轻松管理和查看您的下载", 3 | "no_downloads": "您没有下载内容", 4 | "open": "打开", 5 | "delete": "删除", 6 | "remove_list": "从列表中删除", 7 | "clear": "清除所有下载", 8 | "error": { 9 | "moved": "文件已移动或删除", 10 | "unable": "无法删除" 11 | }, 12 | "settings": { 13 | "max": "显示下载的最多数量" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cards/Downloads/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "limitDownloads": 5 4 | }, 5 | "more": { 6 | "chrome": "chrome://downloads", 7 | "firefox": "chrome://browser/content/places/places.xul" 8 | }, 9 | "permissions": ["downloads", "downloads.open"] 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/Downloads/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'Donwloads', 4 | data() { 5 | return { 6 | settings: {}, 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/cards/Downloads/settings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/cards/Downloads/style.scss: -------------------------------------------------------------------------------- 1 | #downloads { 2 | max-height: 302px; 3 | overflow-y: auto; 4 | .v-menu { 5 | width: 100%; 6 | } 7 | .download { 8 | padding: 5px 0px; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | width: 100%; 13 | &:hover { 14 | background-color: rgba(0, 0, 0, .1); 15 | } 16 | .icon { 17 | flex-shrink: 0; 18 | cursor: pointer; 19 | width: 36px; 20 | height: 36px; 21 | margin-left: 8px; 22 | overflow: hidden; 23 | i { 24 | display: table-cell; 25 | vertical-align: middle; 26 | text-align: center; 27 | } 28 | } 29 | .fileicon { 30 | height: inherit; 31 | width: inherit; 32 | background-repeat: no-repeat; 33 | background-size: contain; 34 | } 35 | .d-info { 36 | flex-grow: 1; 37 | overflow: hidden; 38 | white-space: nowrap; 39 | text-overflow: ellipsis; 40 | padding: 5px; 41 | .name { 42 | cursor: pointer; 43 | overflow: hidden; 44 | white-space: nowrap; 45 | text-overflow: ellipsis; 46 | font-size: 13px; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/cards/Epitech/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API = 'https://intra.epitech.eu'; 4 | 5 | export default { 6 | parseCalendarDate(epiDate) { 7 | const date = epiDate.replace(' ', '-').replace(/:/g, '-').split('-'); 8 | return new Date(date[0], date[1] - 1, date[2], date[3], date[4]); 9 | }, 10 | parseDate(epiDate) { 11 | const date = epiDate.replace(', ', '/').replace(':', '/').replace('h', '/').split('/'); 12 | return new Date(date[2], date[1] - 1, date[0], date[3], date[4]); 13 | }, 14 | getLink(url) { 15 | return `${API}${url}`; 16 | }, 17 | getUser() { 18 | return axios.get(`${API}/user/?format=json`) 19 | .then(res => res.data); 20 | }, 21 | isRegistered(project) { 22 | return axios.get(`${API}${project.title_link}project?format=json`) 23 | .then(res => !!res.data.user_project_code); 24 | }, 25 | getProjects() { 26 | return axios.get(`${API}/?format=json`) 27 | .then((res) => { 28 | if (!res.data) return Promise.resolve([]); 29 | return res.data.board.projets.map((f) => { 30 | f.link = this.getLink(f.title_link); 31 | return f; 32 | }); 33 | }); 34 | }, 35 | getCurrentProjects() { 36 | return this.getProjects() 37 | .then((projects) => { 38 | const data = projects.filter(f => f.timeline_barre < 100 39 | && !f.date_inscription && this.parseDate(f.timeline_start) <= new Date() 40 | && this.parseDate(f.timeline_end) > new Date()); 41 | const check = data.map(f => this.isRegistered(f) 42 | .then((isRegistered) => { 43 | f.isRegistered = isRegistered; 44 | return f; 45 | })); 46 | return Promise.all(check); 47 | }) 48 | .then(projects => projects 49 | .filter(f => f.isRegistered) 50 | .sort((a, b) => this.parseDate(a.timeline_end) - this.parseDate(b.timeline_end))); 51 | }, 52 | getPlanning(user) { 53 | const d = new Date(); 54 | const dString = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`; 55 | return axios.get(`${API}/planning/load?format=json&start=${dString}&end=${dString}`) 56 | .then(res => (Array.isArray(res.data) ? res.data : []) 57 | .filter(f => f.instance_location === user.location)); 58 | }, 59 | getRoom(planning) { 60 | const d = new Date(); 61 | return planning.filter(f => f.room && f.room.code) 62 | .map((f) => { 63 | f.start = this.parseCalendarDate(f.start); 64 | f.end = this.parseCalendarDate(f.end); 65 | f.startString = `${f.start.getHours()}h${(`0${f.start.getMinutes()}`).substr(-2)}`; 66 | f.endString = `${f.end.getHours()}h${(`0${f.end.getMinutes()}`).substr(-2)}`; 67 | return f; 68 | }).filter(f => f.end > d) 69 | .sort((a, b) => a.start - b.start); 70 | }, 71 | getModule(f) { 72 | return axios.get(`${API}/module/${f.scolaryear}/${f.code}/${f.codeinstance}/?format=json`) 73 | .then(res => res.data); 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/cards/Epitech/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "alldone": "No on going projects, well done!", 3 | "noupcoming": "No upcoming activities, go get some rest!", 4 | "noneocuupied": "No occupied rooms, have fun!", 5 | "error": { 6 | "login": "You must be logged to Epitech to use this card" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Epitech/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "alldone": "Devam eden bir proje yok, tebrikler!", 3 | "noupcoming": "Yapılacak bir şey yok, dinlenmene bak!", 4 | "noneocuupied": "Oda yok, eğlenmene bak!", 5 | "error": { 6 | "login": "Bu kartı kullanabilmek için Epitech'e giriş yapmanız gerekmektedir" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Epitech/main.js: -------------------------------------------------------------------------------- 1 | import utils from '@/mixins/utils'; 2 | import API from './api'; 3 | 4 | // @vue/component 5 | export default { 6 | name: 'Epitech', 7 | mixins: [utils], 8 | props: { 9 | settings: { 10 | type: Object, 11 | required: true, 12 | }, 13 | }, 14 | data() { 15 | return { 16 | is_logged: false, 17 | loading: true, 18 | user: null, 19 | projects: [], 20 | rooms: [], 21 | upcoming: [], 22 | }; 23 | }, 24 | computed: { 25 | ordinal() { 26 | const n = this.user.studentyear; 27 | return ['st', 'nd', 'rd'][(n % (100 >> 3) ^ 1 && n % 10) - 1] || 'th'; 28 | }, 29 | }, 30 | mounted() { 31 | if (this.VALID_CACHE && !this.loading) { 32 | this.$emit('init', false); 33 | return; 34 | } 35 | Promise.all([this.getUser(), this.getProjects()]) 36 | .then(() => API.getPlanning(this.user)) 37 | .then((planning) => { 38 | this.getRoom(planning); 39 | this.getUpcoming(planning); 40 | }) 41 | .then(() => this.$emit('init', true)) 42 | .catch(err => this.$emit('init', err)) 43 | .finally(() => { 44 | this.loading = false; 45 | }); 46 | }, 47 | methods: { 48 | getUser() { 49 | return API.getUser().then((user) => { 50 | this.is_logged = true; 51 | this.user = user; 52 | }).catch((err) => { 53 | if (err.response && err.response.status === 401) { 54 | this.is_logged = false; 55 | } 56 | throw err; 57 | }); 58 | }, 59 | getProjects() { 60 | return API.getCurrentProjects() 61 | .then((projects) => { 62 | this.projects = projects; 63 | }); 64 | }, 65 | getRoom(planning) { 66 | this.rooms = API.getRoom(planning); 67 | }, 68 | getUpcoming(planning) { 69 | this.upcoming = planning 70 | .filter(f => f.event_registered && f.start > new Date()) 71 | .sort((a, b) => a.start - b.start); 72 | }, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/cards/Epitech/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "origins": ["https://intra.epitech.eu/"], 3 | "more": "https://intra.epitech.eu/", 4 | "cacheValidity": 1800, 5 | "externalsRequests": true, 6 | "settings": { 7 | "hideInfo": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/Epitech/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'Epitech', 4 | data() { 5 | return { 6 | settings: {}, 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/cards/Epitech/settings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/cards/Epitech/style.scss: -------------------------------------------------------------------------------- 1 | #epitech { 2 | .p-no-bottom { 3 | padding-bottom: 0; 4 | } 5 | .projects { 6 | max-height: 200px; 7 | overflow: auto; 8 | .project { 9 | margin: 10px 0; 10 | .end_date { 11 | float: right; 12 | } 13 | } 14 | } 15 | .rooms { 16 | max-height: 325px; 17 | overflow: auto; 18 | } 19 | .upcoming { 20 | max-height: 302px; 21 | overflow-y: auto; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/cards/Isefac/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/cards/Isefac/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "nomore": "No more activities today, go get some rest!", 3 | "error": { 4 | "login": "You must be logged to Isefac to use this card" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/Isefac/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "nomore": "Bugünlük bu kadar, git biraz dinlen!", 3 | "error": { 4 | "login": "Bu kartı kullanabilmek için Isefac'a giriş yapmanız gerekmektedir" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/Isefac/main.js: -------------------------------------------------------------------------------- 1 | import date from '@/mixins/date'; 2 | 3 | const API = 'https://nantes.campus-isefac.fr/bachelor/'; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'Isefac', 8 | mixins: [date], 9 | data() { 10 | return { 11 | is_logged: true, 12 | loading: true, 13 | dates: [], 14 | user: { 15 | name: '', 16 | }, 17 | }; 18 | }, 19 | mounted() { 20 | if (this.VALID_CACHE && !this.loading) { 21 | this.$emit('init', false); 22 | return; 23 | } 24 | this.getCalendar() 25 | .then(() => this.$emit('init', true)) 26 | .catch(err => this.$emit('init', err)) 27 | .finally(() => { 28 | this.loading = false; 29 | }); 30 | }, 31 | methods: { 32 | getCalendar() { 33 | return this.axios.get(`${API}index.php/apps/planning/`) 34 | .then((res) => { 35 | if (res.data.indexOf('name="login"') > -1) { 36 | this.is_logged = false; 37 | return Promise.reject(new Error('You must be logged to Isefac to use this card.')); 38 | } 39 | this.is_logged = true; 40 | const name = res.data.substring(res.data.indexOf('expandDisplayName">') + 19); 41 | this.user.name = name.substring(0, name.indexOf('<')); 42 | const part = res.data.substring(res.data.indexOf('events:') + 7); 43 | const dates = part.substring(0, part.indexOf('],') + 1); 44 | return Promise.resolve(JSON.parse(dates)); 45 | }) 46 | .then((dates) => { 47 | this.dates = dates 48 | .map((f) => { 49 | f.start = new Date(f.start); 50 | f.end = new Date(f.end); 51 | return f; 52 | }) 53 | .filter(f => f.end > new Date()) 54 | .map((f) => { 55 | f.startString = `${f.start.getHours()}h${(`0${f.start.getMinutes()}`).substr(-2)}`; 56 | f.endString = `${f.end.getHours()}h${(`0${f.end.getMinutes()}`).substr(-2)}`; 57 | f.header = `${f.start.toLocaleDateString('en-Us', this.timeOption)} ${f.start.getDate()}/${f.start.getMonth() + 1}`; 58 | return f; 59 | }) 60 | .sort((a, b) => a.start - b.start); 61 | }); 62 | }, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/cards/Isefac/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "origins": ["https://nantes.campus-isefac.fr/bachelor/"], 3 | "cacheValidity": 1800, 4 | "externalsRequests": true 5 | } 6 | -------------------------------------------------------------------------------- /src/cards/Isefac/style.scss: -------------------------------------------------------------------------------- 1 | #isefac { 2 | .dates { 3 | max-height: 300px; 4 | padding-right: 10px; 5 | overflow: auto; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/LastFm/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API = 'https://ws.audioscrobbler.com/2.0/?format=json'; 4 | const fallback = 'https://lastfm-img2.akamaized.net/i/u/300x300/c6f59c1e5e7240a4c0d427abd71f3dbb'; 5 | 6 | export const Periods = [ 7 | { title: 'LastFm.overall', value: 'overall' }, 8 | { title: 'LastFm.years', nb: 1, value: '12month' }, 9 | { title: 'LastFm.months', nb: 6, value: '6month' }, 10 | { title: 'LastFm.months', nb: 3, value: '3month' }, 11 | { title: 'LastFm.months', nb: 1, value: '1month' }, 12 | { title: 'LastFm.days', nb: 7, value: '7day' }, 13 | ]; 14 | 15 | export const Api = { 16 | parseRes(res) { 17 | return res.map((f) => { 18 | f.image = { 19 | small: f.image[0]['#text'] !== '' ? f.image[0]['#text'] : fallback, 20 | medium: f.image[1]['#text'] !== '' ? f.image[1]['#text'] : fallback, 21 | large: f.image[2]['#text'] !== '' ? f.image[2]['#text'] : fallback, 22 | extralarge: f.image[3]['#text'] !== '' ? f.image[3]['#text'] : fallback, 23 | }; 24 | return f; 25 | }); 26 | }, 27 | fetchTopUser(type, apiKey, user, limit, period) { 28 | return axios.get(`${API}&method=user.get${type}&api_key=${apiKey}&user=${user}&limit=${limit}&period=${period}`) 29 | .then(res => res.data[type] || {}); 30 | }, 31 | getTopArtists(apiKey, user, limit, period) { 32 | return this.fetchTopUser('topartists', apiKey, user, limit, period) 33 | .then(topartists => this.parseRes(topartists.artist || [])); 34 | }, 35 | getTopAlbums(apiKey, user, limit, period) { 36 | return this.fetchTopUser('topalbums', apiKey, user, limit, period) 37 | .then(topalbums => this.parseRes(topalbums.album || [])); 38 | }, 39 | getTopTracks(apiKey, user, limit, period) { 40 | return this.fetchTopUser('toptracks', apiKey, user, limit, period) 41 | .then(toptracks => this.parseRes(toptracks.track || [])); 42 | }, 43 | getRecentTracks(apiKey, user, limit) { 44 | return axios.get(`${API}&method=user.getrecenttracks&api_key=${apiKey}&user=${user}&limit=${limit}`) 45 | .then(res => (res.data.recenttracks || {}).track || []); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/cards/LastFm/index.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Sehen Sie Ihre neuesten LastFm-Statistiken.", 3 | "plays": "{nb} spielt ab | {nb} abspielen", 4 | "overall": "Gesamter Zeitraum", 5 | "years": "{nb} Jahr | {nb} Jahre", 6 | "months": "{nb} Monat | {nb} Monate", 7 | "days": "{nb} Tag | {nb} Tage", 8 | "need_user": "Bitte geben Sie Ihren LastFm-Nutzernamen in den Karteneinstellungen ein.", 9 | "empty": "Offenbar haben Sie keine Top-Einträge.", 10 | "now_playing": "läuft jetzt:", 11 | "top": { 12 | "artists": "Top-Küstler", 13 | "albums": "Top-Alben", 14 | "tracks": "Top-Lieder" 15 | }, 16 | "tip": "Kicken Sie den Rand der Karte an, um die Top-Einträge zu ändern", 17 | "settings": { 18 | "username": "LastFm Nutzername" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "See your latest LastFm statistics easily", 3 | "plays": "{nb} play | {nb} plays", 4 | "overall": "All times", 5 | "years": "{nb} year | {nb} years", 6 | "months": "{nb} month | {nb} months", 7 | "days": "{nb} day | {nb} days", 8 | "need_user": "Please enter your LastFm username in the card settings.", 9 | "empty": "Obviously you don't have any top.", 10 | "now_playing": "Now playing:", 11 | "top": { 12 | "artists": "Top Artists", 13 | "albums": "Top Albums", 14 | "tracks": "Top Tracks" 15 | }, 16 | "tip": "Click on the edges of the card to change top", 17 | "settings": { 18 | "username": "LastFm Username" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "plays": "Reproducír {nb} | Está sonando {nb}", 3 | "overall": "Todos los tiempos", 4 | "years": "Año {nb} | {nb} años", 5 | "months": "Mes {nb} | {nb} meses", 6 | "days": "Día {nb} | {nb} días", 7 | "need_user": "Por favor ingrese su usuario de LastFm en los ajustes de tarjeta.", 8 | "empty": "Obviamente no tienes ningún top.", 9 | "now_playing": "Reproduciendo:", 10 | "top": { 11 | "artists": "Top Artistas", 12 | "albums": "Top Álbums", 13 | "tracks": "Top Pistas" 14 | }, 15 | "tip": "Haga clic en los bordes de la tarjeta para cambiar el top", 16 | "settings": { 17 | "username": "Usuario de LastFm" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Visualisez facilement vos dernières statistiques LastFm", 3 | "plays": "{nb} lecture | {nb} lectures", 4 | "overall": "Tout le temps", 5 | "years": "{nb} an | {nb} ans", 6 | "months": "{nb} mois", 7 | "days": "{nb} jour | {nb} jours", 8 | "need_user": "Veuillez entrer votre nom d'utilisateur LastFm dans les paramètres de la carte.", 9 | "empty": "Manifestement, vous n'avez pas de top.", 10 | "now_playing": "En lecture:", 11 | "top": { 12 | "artists": "Top Artistes", 13 | "albums": "Top Albums", 14 | "tracks": "Top Titres" 15 | }, 16 | "tip": "Cliquez sur les bords de la carte pour changer de top", 17 | "settings": { 18 | "username": "Nom d'utilisateur LastFm" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "En güncel LastFm istatistiklerinizi görüntüleyin", 3 | "plays": "{nb} oynatma | {nb} oynatma", 4 | "overall": "Tüm zamanlar", 5 | "years": "{nb} yıl | {nb} yıl", 6 | "months": "{nb} ay | {nb} ay", 7 | "days": "{nb} gün | {nb} gün", 8 | "need_user": "Lütfen kart ayarlarından LastFm kullanıcı adınızı girin.", 9 | "empty": "Elbette, hiçbir şeyi beğenmiyorsunuz.", 10 | "now_playing": "Oynatılıyor:", 11 | "top": { 12 | "artists": "Sanatçılar", 13 | "albums": "Albümler", 14 | "tracks": "Şarkılar" 15 | }, 16 | "tip": "Kenarlara tıklayarak listeyi değiştirebilirsiniz", 17 | "settings": { 18 | "username": "LastFm Kullanıcı Adı" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Легкий перегляд вашої останньої статистики LastFm", 3 | "plays": "{nb} програвання | {nb} програвань", 4 | "overall": "За весь час", 5 | "years": "{nb} рік | {nb} років", 6 | "months": "{nb} місяць | {nb} місяців", 7 | "days": "{nb} день | {nb} днів", 8 | "need_user": "Будь ласка, введіть ваше ім'я користувача LastFm в налаштуваннях картки.", 9 | "empty": "Вочевидь, у вас немає жодного чарту.", 10 | "now_playing": "Зараз грає:", 11 | "top": { 12 | "artists": "Топ Артистів", 13 | "albums": "Топ Альбомів", 14 | "tracks": "Топ Треків" 15 | }, 16 | "tip": "Натисніть на краї картки, щоб змінити чарт", 17 | "settings": { 18 | "username": "Ім'я у LastFm" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/LastFm/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "轻松查看最新的 LastFm 统计信息", 3 | "plays": "{nb} play | {nb} plays", 4 | "overall": "所有时间", 5 | "years": "{nb} 年 | {nb} 年", 6 | "months": "{nb} 月 | {nb} 月", 7 | "days": "{nb} 日 | {nb} 日", 8 | "need_user": "请在卡片设置中输入您的 LastFm 用户名。", 9 | "empty": "显然你的 LastFm 还没有个人榜单", 10 | "now_playing": "正在播放:", 11 | "top": { 12 | "artists": "艺术家榜单", 13 | "albums": "专辑榜单", 14 | "tracks": "单曲榜单" 15 | }, 16 | "tip": "单击卡的左右边缘以更改榜单类型", 17 | "settings": { 18 | "username": "LastFm 用户名" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/LastFm/main.js: -------------------------------------------------------------------------------- 1 | import { Api, Periods } from './api'; 2 | 3 | // @vue/component 4 | export default { 5 | name: 'LastFm', 6 | props: { 7 | settings: { 8 | type: Object, 9 | required: true, 10 | }, 11 | }, 12 | data() { 13 | return { 14 | loading: true, 15 | active: null, 16 | period: 'overall', 17 | showTip: true, 18 | items: {}, 19 | }; 20 | }, 21 | computed: { 22 | user() { 23 | return this.settings.user; 24 | }, 25 | itemsLength() { 26 | return Object.keys(this.items).length; 27 | }, 28 | }, 29 | watch: { 30 | active(val, old) { 31 | if (val !== old) { 32 | const keys = Object.keys(this.items); 33 | if (this.items[keys[val]]) { 34 | this.$emit('update:cardtitle', this.items[keys[val]].title); 35 | } 36 | } 37 | }, 38 | }, 39 | created() { 40 | this.getNowPlaying(); 41 | this.updateActions(); 42 | }, 43 | mounted() { 44 | if (this.VALID_CACHE && !this.loading) { 45 | this.$emit('init', false); 46 | return; 47 | } 48 | this.getAll() 49 | .then(() => { 50 | const keys = Object.keys(this.items); 51 | if (this.items[keys[this.active]]) { 52 | this.$emit('update:cardtitle', this.items[keys[this.active]].title); 53 | } 54 | this.$emit('init', true); 55 | }) 56 | .catch(err => this.$emit('init', err)) 57 | .finally(() => { 58 | this.loading = false; 59 | }); 60 | }, 61 | methods: { 62 | updateActions() { 63 | this.$emit('update:actions', Periods.map(f => ({ 64 | title: this.$tc(f.title, f.nb, { nb: f.nb }), 65 | active: f.value === this.period, 66 | func: () => this.changePeriod(f.value), 67 | }))); 68 | }, 69 | changePeriod(period) { 70 | this.period = period; 71 | this.getAll().then(() => { 72 | this.updateActions(); 73 | }); 74 | }, 75 | getAll() { 76 | return Promise.all([ 77 | this.getTopArtists(), 78 | this.getTopAlbums(), 79 | this.getTopTracks(), 80 | ]); 81 | }, 82 | getTopArtists() { 83 | return Api.getTopArtists(this.settings.apiKey, this.user, 5, this.period) 84 | .then((artists) => { 85 | if (!artists || !artists.length) return; 86 | this.$set(this.items, 'artists', { 87 | title: this.$t('LastFm.top.artists'), 88 | data: artists, 89 | }); 90 | }); 91 | }, 92 | getTopAlbums() { 93 | return Api.getTopAlbums(this.settings.apiKey, this.user, 5, this.period) 94 | .then((albums) => { 95 | if (!albums || !albums.length) return; 96 | this.$set(this.items, 'albums', { 97 | title: this.$t('LastFm.top.albums'), 98 | data: albums, 99 | }); 100 | }); 101 | }, 102 | getTopTracks() { 103 | return Api.getTopTracks(this.settings.apiKey, this.user, 5, this.period) 104 | .then((tracks) => { 105 | if (!tracks || !tracks.length) return; 106 | this.$set(this.items, 'tracks', { 107 | title: this.$t('LastFm.top.tracks'), 108 | data: tracks, 109 | }); 110 | }); 111 | }, 112 | getNowPlaying() { 113 | return Api.getRecentTracks(this.settings.apiKey, this.user, 1) 114 | .then((tracks) => { 115 | if (tracks.length && tracks[0]['@attr'] && tracks[0]['@attr'].nowplaying) { 116 | this.$emit('update:subtitle', `${this.$t('LastFm.now_playing')} ${tracks[0].name} / ${tracks[0].artist['#text']}`); 117 | } 118 | }); 119 | }, 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /src/cards/LastFm/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "more": "https://www.last.fm", 3 | "settings": { 4 | "apiKey": "10ca053a6585f6de17bde3736500de8b", 5 | "user": "" 6 | }, 7 | "cacheValidity": 21600, 8 | "externalsRequests": true, 9 | "allowMultiple": true 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/LastFm/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'LastFm', 4 | data() { 5 | return { 6 | settings: {}, 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/cards/LastFm/settings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/cards/LastFm/style.scss: -------------------------------------------------------------------------------- 1 | #lastfm { 2 | overflow: hidden; 3 | /deep/ .v-tabs__bar { 4 | display: none; 5 | } 6 | .prev, .next { 7 | position: absolute; 8 | height: 100%; 9 | width: 15px; 10 | opacity: 0.5; 11 | top: 0; 12 | z-index: 1; 13 | transition: opacity .25s ease; 14 | &:hover { 15 | opacity: 0.85; 16 | } 17 | } 18 | .prev { 19 | left: 0; 20 | background: linear-gradient(-90deg, transparent 0%, #000000 100%); 21 | } 22 | .next { 23 | right: 0; 24 | background: linear-gradient(90deg, transparent 0%, #000000 100%); 25 | } 26 | .top-grid { 27 | height: 200px; 28 | overflow: hidden; 29 | position: relative; 30 | .cover { 31 | position: relative; 32 | width: 100%; 33 | height: 100%; 34 | &:hover .overlay { 35 | opacity: 1; 36 | background: linear-gradient(0deg, transparent 0%, #000 100%); 37 | padding-top: 10px 38 | } 39 | .overlay { 40 | position: absolute; 41 | opacity: 0; 42 | bottom: 0; 43 | left: 0; 44 | padding-left: 10px; 45 | width: 100%; 46 | height: 100%; 47 | color: white; 48 | transition: .25s ease; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Schnellzugriff", 3 | "empty": "Sie haben keine Schnellzugriff-Links, fügen Sie welche in den Karteneinstellungen hinzu", 4 | "settings": { 5 | "links": "Links", 6 | "grid": "Grid-Layout verwenden", 7 | "grid_size": "Blockgrösse im Grid" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Quick Links", 3 | "empty": "You don't have quick links, add some in the card settings", 4 | "settings": { 5 | "label": "e.g. https://example.com", 6 | "links": "Links", 7 | "grid": "Use a grid layout", 8 | "grid_size": "Grid block size" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Enlaces rápidos", 3 | "empty": "No tienes ningún enlace rápido, agrega algúnos en los ajustes de tarjeta", 4 | "settings": { 5 | "links": "Enlaces", 6 | "grid": "Usar diseño de cuadrícula", 7 | "grid_size": "Tamaño de la cuadrícula" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Liens rapides", 3 | "empty": "Vous n'avez pas de liens rapides, ajoutez-en dans les paramètres de la carte", 4 | "settings": { 5 | "links": "Liens", 6 | "grid": "Utiliser une disposition en grille", 7 | "grid_size": "Taille des blocs de la grille" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Rýchle odkazy", 3 | "empty": "Nemáte žiadne rýchle odkazy, pridajte nejaké v nastaveniach karty", 4 | "settings": { 5 | "links": "Odkazy", 6 | "grid": "Použiť usporiadanie mriežky", 7 | "grid_size": "Veľkosť bloku siete" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Hızlı Bağlantılar", 3 | "empty": "Herhangi bir hızlı bağlantınız yok, hemen kart ayarlarından birkaç tane ekleyin", 4 | "settings": { 5 | "label": "örn. https://ornek.com", 6 | "links": "Linkler", 7 | "grid": "Izgara görünümü kullan", 8 | "grid_size": "Izgara eleman boyutu" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Швидкі посилання", 3 | "empty": "У вас немає жодного посилання. Додайте щось у налаштуваннях картки.", 4 | "settings": { 5 | "links": "Посилання", 6 | "grid": "Використовувати сітку", 7 | "grid_size": "Розмір блоку сітки" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "快速链接", 3 | "empty": "您没有快速链接,请在卡片设置中添加一些链接", 4 | "settings": { 5 | "links": "链接", 6 | "grid": "使用网格布局", 7 | "grid_size": "网格块大小" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/main.js: -------------------------------------------------------------------------------- 1 | import GridList from '@/components/GridList'; 2 | import List from '@/components/List'; 3 | import utils from '@/mixins/utils'; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'QuickLinks', 8 | mixins: [utils], 9 | props: { 10 | settings: { 11 | type: Object, 12 | required: true, 13 | }, 14 | }, 15 | computed: { 16 | size() { 17 | return [32, 64, 96, 128][this.settings.size]; 18 | }, 19 | cmp() { 20 | if (this.settings.grid) { 21 | return GridList; 22 | } 23 | return List; 24 | }, 25 | links() { 26 | return this.settings.links 27 | .map((f) => { 28 | const Url = new URL(f); 29 | const icon = this.settings.grid ? this.getFavicon(Url.hostname, this.size) 30 | : this.getFavicon(f); 31 | return { name: Url.hostname, url: f, icon }; 32 | }); 33 | }, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "links": [], 4 | "grid": true, 5 | "size": 2 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'QuickLinks', 4 | tickLabels: [32, 64, 96, 128], 5 | data() { 6 | return { 7 | settings: {}, 8 | newLink: '', 9 | }; 10 | }, 11 | methods: { 12 | addLink(url) { 13 | if (url.trim().length === 0) return; 14 | this.newFeed = ''; 15 | this.settings.links.push(url); 16 | }, 17 | removeLink(idx) { 18 | this.settings.links.splice(idx, 1); 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/settings.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /src/cards/QuickLinks/style.scss: -------------------------------------------------------------------------------- 1 | #quick-links { 2 | max-height: 278px; 3 | overflow-y: auto; 4 | .v-list__tile__avatar { 5 | min-width: 29px; 6 | } 7 | /deep/ .v-list__tile { 8 | height: 34px; 9 | } 10 | .v-list__tile__action { 11 | flex-shrink: 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Schnelleinstellungen", 3 | "description": "Leeren Sie Cache, Cookies, Verlauf und lokalen Speicher", 4 | "cache": "Cache", 5 | "cookies": "Cookies", 6 | "history": "Verlauf", 7 | "local_storage": "Lokaler Speicher", 8 | "clear": "Leeren" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Quick settings", 3 | "description": "Remove your cache, cookies, history and local storage quickly", 4 | "cache": "Cache", 5 | "cookies": "Cookies", 6 | "history": "History", 7 | "local_storage": "Local storage", 8 | "clear": "Clear" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Ajustes rápidos", 3 | "description": "Elimina tu caché, cookies, historia y almacenamiento local rápidamente", 4 | "cache": "Caché", 5 | "cookies": "Cookies", 6 | "history": "Historial", 7 | "local_storage": "Almancenamiento local", 8 | "clear": "Limpiar" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Réglages rapides", 3 | "description": "Supprimez rapidement votre cache, vos cookies, votre historique et votre stockage local", 4 | "cache": "Cache", 5 | "cookies": "Cookies", 6 | "history": "Historique", 7 | "local_storage": "Stockage local", 8 | "clear": "Nettoyer" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Rýchle nastavenia", 3 | "description": "Zmažte vaše cache, cookies, históriu a lokálne úložisku rýchlo", 4 | "cache": "Cache", 5 | "cookies": "Cookies", 6 | "history": "História", 7 | "local_storage": "Lokálne úložisko", 8 | "clear": "Zmazať" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Hızlı ayarlar", 3 | "description": "Önbellek, çerez, geçmiş ve yerel depolama bilgilerinizi hızlıca temizleyin", 4 | "cache": "Önbellek", 5 | "cookies": "Çerezler", 6 | "history": "Geçmiş", 7 | "local_storage": "Yerel depolama", 8 | "clear": "Temizle" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Швидкі налаштування", 3 | "description": "Швидке очищення кеша, cookies, історії та локального сховища", 4 | "cache": "Кеш", 5 | "cookies": "Cookies", 6 | "history": "Історія", 7 | "local_storage": "Локальне сховище", 8 | "clear": "Очистити" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "快速设置", 3 | "description": "快速清除缓存,Cookie,历史记录和本地存储", 4 | "cache": "缓存", 5 | "cookies": "Cookies", 6 | "history": "历史", 7 | "local_storage": "本地存储", 8 | "clear": "清除" 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/main.js: -------------------------------------------------------------------------------- 1 | import Toast from '@/components/Toast'; 2 | 3 | // @vue/component 4 | export default { 5 | name: 'QuickSettings', 6 | data() { 7 | return { 8 | loading: false, 9 | types: { 10 | cache: false, 11 | cookies: false, 12 | history: false, 13 | localStorage: false, 14 | }, 15 | }; 16 | }, 17 | computed: { 18 | isFalse() { 19 | const keys = Object.keys(this.types); 20 | for (let i = 0; i < keys.length; i += 1) { 21 | if (this.types[keys[i]] !== false) return false; 22 | } 23 | return true; 24 | }, 25 | }, 26 | methods: { 27 | clear() { 28 | this.loading = true; 29 | browser.browsingData.remove({}, Object.assign({}, this.types)).then(() => { 30 | Toast.show({ title: 'Cleared !' }); 31 | this.types = { 32 | cache: false, 33 | cookies: false, 34 | history: false, 35 | localStorage: false, 36 | }; 37 | this.loading = false; 38 | }); 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "more": { 3 | "chrome": "chrome://settings/clearBrowserData", 4 | "firefox": "chrome://browser/content/sanitize.xul" 5 | }, 6 | "permissions": ["browsingData"] 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/QuickSettings/style.scss: -------------------------------------------------------------------------------- 1 | #quick-settings { 2 | padding: 20px 25px; 3 | .v-input { 4 | margin-top: 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/RSS/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Alle Ihre Feeds an einem Ort", 3 | "empty": "Keine Feeds", 4 | "settings": { 5 | "feeds": "Feeds" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "All your feeds in one place", 3 | "empty": "Empty feeds", 4 | "settings": { 5 | "label": "e.g. https://example.com/rss", 6 | "feeds": "Feeds" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Tódos sus clasificados en un lugar", 3 | "empty": "Clasificados vacíos", 4 | "settings": { 5 | "feeds": "Clasificados" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Tous vos flux en un seul endroit", 3 | "empty": "Flux vides", 4 | "settings": { 5 | "feeds": "Flux" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Všetky vaše novinky na jednom mieste", 3 | "empty": "Vymazať novinky", 4 | "settings": { 5 | "feeds": "Novinky" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Tüm kaynaklarınızı bir arada", 3 | "empty": "Hiçbir kaynak yok", 4 | "settings": { 5 | "label": "örn. https://ornek.com/rss", 6 | "feeds": "Kaynaklar" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Всі ваші канали в одному місці", 3 | "empty": "Немає каналів", 4 | "settings": { 5 | "feeds": "Канали" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/RSS/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "所有的 feed 都聚合在一个地方", 3 | "empty": "无 feed", 4 | "settings": { 5 | "feeds": "Feeds" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/RSS/main.js: -------------------------------------------------------------------------------- 1 | import date from '@/mixins/date'; 2 | 3 | const API = 'https://api.rss2json.com/v1/api.json?rss_url='; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'RSS', 8 | mixins: [date], 9 | props: { 10 | settings: { 11 | type: Object, 12 | required: true, 13 | }, 14 | }, 15 | data() { 16 | return { 17 | items: [], 18 | loading: true, 19 | }; 20 | }, 21 | mounted() { 22 | if (this.VALID_CACHE && !this.loading) { 23 | this.$emit('init', false); 24 | return; 25 | } 26 | Promise.all(this.settings.feeds.map(this.fetch)) 27 | .then((feeds) => { 28 | if (!feeds) throw new Error('Unexpected response from API'); 29 | const items = feeds.map(f => f.items.map((d) => { 30 | d.feed = f.feed; // eslint-disable-line 31 | return d; 32 | })); 33 | this.items = [].concat(...items).sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)); 34 | }) 35 | .then(() => this.$emit('init', true)) 36 | .catch(err => this.$emit('init', err)) 37 | .finally(() => { 38 | this.loading = false; 39 | }); 40 | }, 41 | methods: { 42 | fetch(url) { 43 | let endpoint = `${API}${encodeURIComponent(url)}`; 44 | if (this.settings.apiKey.length) { 45 | endpoint += `&api_key=${this.settings.apiKey}`; 46 | } 47 | return this.axios.get(endpoint).then(res => res.data); 48 | }, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/cards/RSS/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "cacheValidity": 300, 3 | "externalsRequests": true, 4 | "settings": { 5 | "feeds": ["https://news.google.com/news/rss/"], 6 | "apiKey": "" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/RSS/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'RSS', 4 | data() { 5 | return { 6 | settings: {}, 7 | newFeed: '', 8 | }; 9 | }, 10 | methods: { 11 | addFeed(url) { 12 | if (url.trim().length === 0) return; 13 | this.newFeed = ''; 14 | this.settings.feeds.push(url); 15 | }, 16 | removeFeed(idx) { 17 | this.settings.feeds.splice(idx, 1); 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/cards/RSS/settings.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /src/cards/RSS/style.scss: -------------------------------------------------------------------------------- 1 | #rss { 2 | max-height: 350px; 3 | overflow-y: auto; 4 | /deep/ .v-list__tile { 5 | height: auto; 6 | padding: 10px 16px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Sessions/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Sehen Sie zuletzt geschlossene Seiten und Seiten von anderen Geräten", 3 | "recents": "Zuletzt geschlossen", 4 | "empty": "Sie haben keine zuletzt geschlossenen Seiten.", 5 | "settings": { 6 | "maxRecents": "Maximale Anzahl geschlossener Seiten", 7 | "maxSessions": "Maximale Anzahl Sitzungen", 8 | "maxDevices": "Maximale Anzahl Geräte" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sessions", 3 | "description": "View your recently closed pages and pages opened on your other devices", 4 | "recents": "Recently closed", 5 | "empty": "You have no recently closed page.", 6 | "settings": { 7 | "maxRecents": "Max recently closed", 8 | "maxSessions": "Max sessions", 9 | "maxDevices": "Max devices" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sesiónes", 3 | "description": "Vea sus páginas cerradas recientemente y páginas abiertas en sus otros dispositivos", 4 | "recents": "Cerradas recientemente", 5 | "empty": "No has cerrado páginas recientemente", 6 | "settings": { 7 | "maxRecents": "Límite de recientes", 8 | "maxSessions": "Límite de sesiones", 9 | "maxDevices": "Límite de dispositivos" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Visualisez vos pages récemment fermées et les pages ouvertes sur vos autres périphériques", 3 | "recents": "Récemment fermé", 4 | "empty": "Vous n'avez pas de page récemment fermée.", 5 | "settings": { 6 | "maxRecents": "Nombre maximum de page récemment fermée", 7 | "maxSessions": "Nombre maximum de sessions", 8 | "maxDevices": "Nombre maximum d'appareils" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Oturumlar", 3 | "description": "En son kapattığınız sayfaları ve diğer cihazlarınızda açık olan sayfaları görüntüleyin", 4 | "recents": "En son kapatılanlar", 5 | "empty": "Son kapatılan hiçbir sayfanız yok.", 6 | "settings": { 7 | "maxRecents": "Gösterilecek kapatılan sayısı", 8 | "maxSessions": "Gösterilecek oturum sayısı", 9 | "maxDevices": "Gösterilecek cihaz sayısı" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Сесії", 3 | "description": "Перегляд нещодавно закритих сторінок та відкритих сторінок на інших пристроях", 4 | "recents": "Нещодавно закриті", 5 | "empty": "У вас немає нещодавно закритих сторінок.", 6 | "settings": { 7 | "maxRecents": "Максимум нещодавно закритих сторінок", 8 | "maxSessions": "Максимум сесій", 9 | "maxDevices": "Максимум пристроїв" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/Sessions/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "查看您最近关闭的页面和在其他设备上打开的页面", 3 | "recents": "最近关闭", 4 | "empty": "您没有最近关闭的页面。", 5 | "settings": { 6 | "maxRecents": "最近关闭数量 最多数量", 7 | "maxSessions": "Max sessions", 8 | "maxDevices": "显示设备最多数量" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/Sessions/main.js: -------------------------------------------------------------------------------- 1 | import List from '@/components/List'; 2 | import utils from '@/mixins/utils'; 3 | 4 | // @vue/component 5 | export default { 6 | name: 'Sessions', 7 | components: { 8 | List, 9 | }, 10 | mixins: [utils], 11 | props: { 12 | settings: { 13 | type: Object, 14 | required: true, 15 | }, 16 | }, 17 | data() { 18 | return { 19 | recents: [], 20 | devices: [], 21 | }; 22 | }, 23 | computed: { 24 | tabs() { 25 | return [ 26 | { id: 'recents', data: this.recents }, 27 | ...this.devices.map(f => ({ 28 | id: `${f.deviceName}${f.sessions.length}${f.data.length}`, data: f.data, 29 | })), 30 | ]; 31 | }, 32 | }, 33 | created() { 34 | Promise.all([this.getRecentlyClosed(), this.getDevices()]) 35 | .then(() => { 36 | browser.sessions.onChanged.addListener(() => { 37 | Promise.all([this.getRecentlyClosed(), this.getDevices()]); 38 | }); 39 | }) 40 | .catch(err => this.$emit('init', err)); 41 | }, 42 | methods: { 43 | mergeTabsAndWindows(sessionItem) { 44 | const tabs = []; 45 | const keys = Object.keys(sessionItem); 46 | for (let i = 0; i < keys.length && tabs.length < this.settings.maxDeviceTabs; i += 1) { 47 | const item = sessionItem[keys[i]]; 48 | // If it's a tab we push it with lastModified value 49 | if (item.tab) { 50 | const { tab } = item; 51 | tab.date = new Date(item.lastModified * 1e3); 52 | tab.icon = tab.favIconUrl || this.getFavicon(tab.url); 53 | tabs.push(tab); 54 | // If it's a window we gather each tab and add them to the others 55 | // e.g: we don't care about the difference between tabs and windows 56 | } else if (item.window) { 57 | const subKeys = Object.keys(item.window.tabs); 58 | for (let j = 0; j < subKeys.length; j += 1) { 59 | const tab = item.window.tabs[subKeys[j]]; 60 | tab.date = new Date(item.lastModified * 1e3); 61 | tab.icon = tab.favIconUrl || this.getFavicon(tab.url); 62 | tabs.push(tab); 63 | } 64 | } 65 | } 66 | return tabs; 67 | }, 68 | getDevices() { 69 | // Firefox doesn't support getDevices 70 | if (browserName !== 'chrome') { 71 | return Promise.resolve([]); 72 | } 73 | return browser.sessions.getDevices({ maxResults: this.settings.maxDevices }) 74 | .then((devices) => { 75 | this.devices = devices.map((f) => { 76 | f.data = this.mergeTabsAndWindows(f.sessions); 77 | return f; 78 | }); 79 | }); 80 | }, 81 | getRecentlyClosed() { 82 | return browser.sessions.getRecentlyClosed({ maxResults: this.settings.maxRecentlyClosed }) 83 | .then((recentlyClosed) => { 84 | this.recents = this.mergeTabsAndWindows(recentlyClosed); 85 | }); 86 | }, 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /src/cards/Sessions/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "maxRecentlyClosed": 7, 4 | "maxDevices": 9, 5 | "maxDeviceTabs": 7 6 | }, 7 | "permissions": ["sessions", "tabs"] 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Sessions/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'Sessions', 4 | data() { 5 | return { 6 | settings: {}, 7 | }; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/cards/Sessions/settings.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/cards/Sessions/style.scss: -------------------------------------------------------------------------------- 1 | #sessions { 2 | .v-window-item { 3 | overflow-y: auto; 4 | max-height: 274px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/System/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/cards/System/langs/de.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "description": "Überwachen Sie Systeminformationen", 4 | "cpu": "CPU", 5 | "memory": "Arbeitsspeicher", 6 | "network": "Netzwerk", 7 | "storage": "Speicher", 8 | "core": "Kern | Kerne", 9 | "estimation": "Geschätzte Download-Geschwindigkeit", 10 | "rtt": "Geschätzte Paketumlaufzeit" 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/System/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "System", 3 | "description": "View and monitor your system information", 4 | "cpu": "CPU", 5 | "memory": "Memory", 6 | "network": "Network", 7 | "storage": "Storage", 8 | "core": "core | cores", 9 | "estimation": "Downlink estimation with a max of 10 Mb/s", 10 | "rtt": "Round-trip time estimation" 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/System/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sistema", 3 | "description": "Vea y monitoree información del sistema", 4 | "cpu": "CPU", 5 | "memory": "Memoria", 6 | "network": "Red", 7 | "storage": "Almacenamiento", 8 | "core": "núcleo | núcleos", 9 | "estimation": "La estimación de la velocidad de descarga tiene un máximo de 10 Mb/s", 10 | "rtt": "Estimación del tiempo de ída y vuelta" 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/System/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Système", 3 | "description": "Visualisez et surveillez les informations de votre système", 4 | "cpu": "CPU", 5 | "memory": "Mémoire", 6 | "network": "Réseau", 7 | "storage": "Stockage", 8 | "core": "cœur | cœurs", 9 | "estimation": "Estimation de la liaison descendante avec un maximum de 10 Mb/s", 10 | "rtt": "Estimation du temps de trajet aller-retour" 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/System/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sistem", 3 | "description": "Sistem bilgilerinizi görüntüleyin", 4 | "cpu": "İşlemci", 5 | "memory": "Hafıza", 6 | "network": "Ağ", 7 | "storage": "Depolama", 8 | "core": "çekirdek | çekirdek", 9 | "estimation": "Maksimum 10 Mb/sn ile aşağı bağlantı tahmini", 10 | "rtt": "Gidiş dönüş süresi tahmini " 11 | } 12 | -------------------------------------------------------------------------------- /src/cards/System/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Перегляд та відстеження системної інформації", 3 | "cpu": "CPU", 4 | "memory": "Пам'ять", 5 | "network": "Мережа", 6 | "storage": "Сховище", 7 | "core": "ядро | ядра", 8 | "estimation": "Оцінка швидкості з максимумом у 10 Mb/s", 9 | "rtt": "Оцінка часу 'туди й назад'" 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/System/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "查看和监视您的系统信息", 3 | "cpu": "CPU", 4 | "memory": "内存", 5 | "network": "网络", 6 | "storage": "硬盘", 7 | "core": "核心 | 核心", 8 | "estimation": "网络下载速度最高估计为 10 Mb/s", 9 | "rtt": "往返时间估计" 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/System/main.js: -------------------------------------------------------------------------------- 1 | import utils from '@/mixins/utils'; 2 | 3 | // @vue/component 4 | export default { 5 | name: 'System', 6 | mixins: [utils], 7 | data() { 8 | return { 9 | cpu: null, 10 | memory: null, 11 | connection: null, 12 | storage: [], 13 | }; 14 | }, 15 | created() { 16 | return Promise.all([ 17 | this.getCpu(), 18 | this.getMemory(), 19 | this.getStorage(), 20 | ]).then(() => { 21 | if (navigator.connection) { 22 | this.getConnection(); 23 | navigator.connection.onchange = this.getConnection; 24 | } 25 | browser.system.storage.onAttached.addListener(this.addStorage); 26 | browser.system.storage.onDetached.addListener(this.removeStorage); 27 | setInterval(this.getCpu, 3000); 28 | setInterval(this.getMemory, 20000); 29 | }) 30 | .catch(err => this.$emit('init', err)); 31 | }, 32 | methods: { 33 | getConnection() { 34 | this.connection = navigator.connection; 35 | }, 36 | getCpu() { 37 | return new Promise((resolve, reject) => { 38 | browser.system.cpu.getInfo((cpu) => { 39 | if (browser.runtime.lastError) return reject(browser.runtime.lastError); 40 | const { loads } = this.cpu || {}; 41 | this.cpu = cpu; 42 | this.$set(this.cpu, 'loads', this.cpu.processors.map((core, idx) => { 43 | const { total, kernel, user } = core.usage; 44 | const progress = kernel + user; 45 | const newLoad = loads && loads[idx] 46 | ? (progress - loads[idx].progress) / (total - loads[idx].total) : progress / total; 47 | return { value: Math.floor(newLoad * 100), progress, total }; 48 | })); 49 | return resolve(); 50 | }); 51 | }); 52 | }, 53 | getMemory() { 54 | return new Promise((resolve, reject) => { 55 | browser.system.memory.getInfo((memory) => { 56 | if (browser.runtime.lastError) return reject(browser.runtime.lastError); 57 | this.memory = memory; 58 | const { capacity } = memory; 59 | const progress = capacity - memory.availableCapacity; 60 | this.$set(this.memory, 'load', { value: Math.floor((progress / capacity) * 100), progress }); 61 | return resolve(); 62 | }); 63 | }); 64 | }, 65 | addStorage(f) { 66 | if (f.capacity <= 0) return; 67 | f.name = f.name.replace(/[^ -~]+/g, ''); 68 | this.storage.push(f); 69 | }, 70 | removeStorage(storageId) { 71 | this.storage = this.storage.filter(f => f.id !== storageId); 72 | }, 73 | getAvailableCapacity(storage) { 74 | return new Promise((resolve, reject) => { 75 | browser.system.storage.getAvailableCapacity(storage.id, (res) => { 76 | if (browser.runtime.lastError) return reject(browser.runtime.lastError); 77 | return resolve({ 78 | ...storage, 79 | ...{ 80 | available: res.availableCapacity, 81 | percent: 100 - ((res.availableCapacity / storage.capacity) * 100), 82 | used: storage.capacity - res.availableCapacity, 83 | }, 84 | }); 85 | }); 86 | }); 87 | }, 88 | getStorage() { 89 | return new Promise((resolve, reject) => { 90 | browser.system.storage.getInfo((storage) => { 91 | if (browser.runtime.lastError) return reject(browser.runtime.lastError); 92 | return resolve(Promise.all(storage 93 | .filter(f => f.capacity > 0) 94 | .map((f) => { 95 | f.name = f.name.replace(/[^ -~]+/g, ''); 96 | if (!browser.system.storage.getAvailableCapacity) return f; 97 | return this.getAvailableCapacity(f); 98 | }))); 99 | }); 100 | }).then((storage) => { 101 | this.storage = storage; 102 | }); 103 | }, 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /src/cards/System/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": ["chrome"], 3 | "permissions": ["system.cpu", "system.memory", "system.storage"] 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/System/style.scss: -------------------------------------------------------------------------------- 1 | #system { 2 | .wrapper { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | margin-bottom: 10px; 7 | .v-progress-linear { 8 | margin: .5rem 0; 9 | } 10 | .wrapper-name { 11 | margin-right: 20px; 12 | text-align: center; 13 | width: 50px; 14 | } 15 | .wrapper-info { 16 | width: 100%; 17 | .storage-unit { 18 | width: 100%; 19 | list-style: none; 20 | } 21 | i { 22 | vertical-align: middle; 23 | } 24 | .disk-name { 25 | text-overflow: ellipsis; 26 | vertical-align: middle; 27 | } 28 | .disk-capacity { 29 | float: right; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/cards/TopSites/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Zeigt Ihre meistbesuchten Seiten an", 3 | "empty": "Sie haben keine meistbesuchten Seiten.", 4 | "settings": { 5 | "maxTopSites": "Maximale Anzahl Seiten", 6 | "grid": "Ein Grid-Layout verwenden", 7 | "grid_size": "Blockgrösse im Grid" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Top Sites", 3 | "description": "See your most visited sites", 4 | "empty": "You have no top sites.", 5 | "settings": { 6 | "maxTopSites": "Max top sites", 7 | "grid": "Use a grid layout", 8 | "grid_size": "Grid block size" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sitios recientes", 3 | "description": "Vea sus sitios más visitados", 4 | "empty": "No tienes sítios recientes.", 5 | "settings": { 6 | "maxTopSites": "Límite de sitios recientes", 7 | "grid": "Usar diseño de cuadrícula", 8 | "grid_size": "Tamaño de la cuadrícula" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Top sites", 3 | "description": "Voir vos sites les plus visités", 4 | "empty": "Vous n'avez pas de top sites.", 5 | "settings": { 6 | "maxTopSites": "Nombre maximum de top sites", 7 | "grid": "Utiliser une disposition en grille", 8 | "grid_size": "Taille des blocs de la grille" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sık Ziyaret Edilenler", 3 | "description": "En çok ziyaret ettiğiniz siteleri görün", 4 | "empty": "Hiç en çok ziyaret ettiğiniz site yok.", 5 | "settings": { 6 | "maxTopSites": "Görüntülenecek site sayısı", 7 | "grid": "Izgara görünümü kullan", 8 | "grid_size": "Izgara eleman boyutu" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Найпопулярніші сайти", 3 | "description": "Перегляд найбільш відвідуваних сайтів", 4 | "empty": "Ще немає найбільш відвідуваних сайтів.", 5 | "settings": { 6 | "maxTopSites": "Максимум сайтів", 7 | "grid": "Використовувати сітку", 8 | "grid_size": "Розмір блока сітки" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/cards/TopSites/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "查看您最常用的网站", 3 | "empty": "您没有常用网站。", 4 | "settings": { 5 | "maxTopSites": "最常用的网站", 6 | "grid": "使用网格布局", 7 | "grid_size": "网格块大小" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/TopSites/main.js: -------------------------------------------------------------------------------- 1 | import GridList from '@/components/GridList'; 2 | import List from '@/components/List'; 3 | import utils from '@/mixins/utils'; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'TopSites', 8 | components: { 9 | List, 10 | }, 11 | mixins: [utils], 12 | props: { 13 | settings: { 14 | type: Object, 15 | required: true, 16 | }, 17 | }, 18 | data() { 19 | return { 20 | topSites: [], 21 | }; 22 | }, 23 | computed: { 24 | cmp() { 25 | if (this.settings.grid) { 26 | return GridList; 27 | } 28 | return List; 29 | }, 30 | size() { 31 | return [32, 64, 96, 128][this.settings.size]; 32 | }, 33 | }, 34 | created() { 35 | this.getTopSites() 36 | .then(() => this.$emit('init', true)) 37 | .catch(err => this.$emit('init', err)); 38 | }, 39 | methods: { 40 | getTopSites() { 41 | return browser.topSites.get().then((topSites) => { 42 | this.topSites = topSites.slice(0, this.settings.maxSites).map((f) => { 43 | f.icon = this.settings.grid 44 | ? this.getFavicon(new URL(f.url).hostname, this.size) 45 | : this.getFavicon(f.url); 46 | return f; 47 | }); 48 | }); 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/cards/TopSites/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "maxSites": 5, 4 | "grid": false, 5 | "size": 2 6 | }, 7 | "externalsRequests": true, 8 | "permissions": ["topSites"] 9 | } 10 | -------------------------------------------------------------------------------- /src/cards/TopSites/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'TopSites', 4 | tickLabels: [32, 64, 96, 128], 5 | data() { 6 | return { 7 | settings: {}, 8 | }; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/cards/TopSites/settings.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/cards/TopSites/style.scss: -------------------------------------------------------------------------------- 1 | #top-sites { 2 | overflow-y: auto; 3 | max-height: 274px; 4 | } 5 | -------------------------------------------------------------------------------- /src/cards/Translate/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Übersetzen", 3 | "auto": "Automatisch", 4 | "from_ph": "Text eingeben", 5 | "to_ph": "Übersetzung", 6 | "copied": "Kopiert!" 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Translate", 3 | "auto": "Auto", 4 | "from_ph": "Enter text", 5 | "to_ph": "Translation", 6 | "copied": "Copied" 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Traducir", 3 | "auto": "Automático", 4 | "from_ph": "Introducir texto", 5 | "to_ph": "Traducción", 6 | "copied": "Copiado" 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Traduire", 3 | "auto": "Auto", 4 | "from_ph": "Entrez du texte", 5 | "to_ph": "Traduction", 6 | "copied": "Copié" 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Çeviri", 3 | "auto": "Otomatik", 4 | "from_ph": "Bir şeyler girin", 5 | "to_ph": "Çeviri", 6 | "copied": "Kopyalandı" 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "переводити", 3 | "auto": "автоматичний", 4 | "from_ph": "Введіть текст", 5 | "to_ph": "переклад", 6 | "copied": "скопійований" 7 | } 8 | -------------------------------------------------------------------------------- /src/cards/Translate/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "auto": "自动", 3 | "from_ph": "输入文字", 4 | "to_ph": "翻译", 5 | "copied": "已复制" 6 | } 7 | -------------------------------------------------------------------------------- /src/cards/Translate/languages.js: -------------------------------------------------------------------------------- 1 | export default { 2 | af: 'Afrikaans', 3 | sq: 'Albanian', 4 | ar: 'Arabic', 5 | hy: 'Armenian', 6 | az: 'Azerbaijani', 7 | eu: 'Basque', 8 | be: 'Belarusian', 9 | bn: 'Bengali', 10 | bs: 'Bosnian', 11 | bg: 'Bulgarian', 12 | my: 'Burmese', 13 | ca: 'Catalan', 14 | ceb: 'Cebuano', 15 | 'zh-CN': 'Chinese (Simplified)', 16 | 'zh-TW': 'Chinese (Traditional)', 17 | hr: 'Croatian', 18 | cs: 'Czech', 19 | da: 'Danish', 20 | nl: 'Dutch', 21 | en: 'English', 22 | eo: 'Esperanto', 23 | et: 'Estonian', 24 | tl: 'Filipino', 25 | fi: 'Finnish', 26 | fr: 'French', 27 | gl: 'Galician', 28 | ka: 'Georgian', 29 | de: 'German', 30 | el: 'Greek', 31 | gu: 'Gujarati', 32 | ht: 'Haitian', 33 | ha: 'Hausa', 34 | iw: 'Hebrew', 35 | hi: 'Hindi', 36 | hmn: 'Hmong', 37 | hu: 'Hungarian', 38 | is: 'Icelandic', 39 | ig: 'Igbo', 40 | id: 'Indonesian', 41 | ga: 'Irish', 42 | it: 'Italian', 43 | ja: 'Japanese', 44 | jv: 'Javanese', 45 | kn: 'Kannada', 46 | kk: 'Kazakh', 47 | km: 'Khmer', 48 | ko: 'Korean', 49 | lo: 'Lao', 50 | la: 'Latin', 51 | lv: 'Latvian', 52 | lt: 'Lithuanian', 53 | mk: 'Macedonian', 54 | mg: 'Malagasy', 55 | ms: 'Malay', 56 | ml: 'Malayalam', 57 | mt: 'Maltese', 58 | mi: 'Maori', 59 | mr: 'Marathi', 60 | mn: 'Mongolian', 61 | no: 'Norwegian', 62 | ny: 'Nyanja', 63 | fa: 'Persian', 64 | pl: 'Polish', 65 | pt: 'Portuguese', 66 | pa: 'Punjabi', 67 | ro: 'Romanian', 68 | ru: 'Russian', 69 | sr: 'Serbian', 70 | si: 'Sinhala', 71 | sk: 'Slovak', 72 | sl: 'Slovenian', 73 | so: 'Somali', 74 | es: 'Spanish', 75 | su: 'Sundanese', 76 | sw: 'Swahili', 77 | sv: 'Swedish', 78 | tg: 'Tajik', 79 | ta: 'Tamil', 80 | te: 'Telugu', 81 | th: 'Thai', 82 | tr: 'Turkish', 83 | uk: 'Ukrainian', 84 | ur: 'Urdu', 85 | uz: 'Uzbek', 86 | vi: 'Vietnamese', 87 | cy: 'Welsh', 88 | yi: 'Yiddish', 89 | yo: 'Yoruba', 90 | zu: 'Zulu', 91 | }; 92 | -------------------------------------------------------------------------------- /src/cards/Translate/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "title": "#ffffff", 4 | "actions": "auto" 5 | }, 6 | "more": "https://translate.google.com/", 7 | "origins": ["https://www.google.com/async/translate"] 8 | } 9 | -------------------------------------------------------------------------------- /src/cards/Translate/style.scss: -------------------------------------------------------------------------------- 1 | #translate { 2 | .left, .right { 3 | padding: 50px 16px 16px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | button { 8 | margin: 0; 9 | } 10 | } 11 | .left { 12 | position: relative; 13 | background-color: #2787f4; 14 | } 15 | /deep/ .right textarea { 16 | color: #2787F4; 17 | } 18 | .v-btn--floating { 19 | right: -20px; 20 | top: 43%; 21 | } 22 | .languages { 23 | max-height: 200px; 24 | overflow-y: auto; 25 | display: flex; 26 | flex-direction: row; 27 | flex-wrap: wrap; 28 | justify-content: space-between; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cards/Weather/index.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Sehen Sie sich das heutige Wetter und die Vorhersagen einfach an.", 3 | "humidity": "Luftfeuchtigkeit", 4 | "wind_speed": "Windgeschwindigkeit", 5 | "sunrise": "Sonnenaufgang", 6 | "sunset": "Sonnenuntergang", 7 | "cloudiness": "Bewölktheit", 8 | "error": { 9 | "title": "Standortfehler", 10 | "unsupported": "Standort wird nicht unterstützt, bitte geben Sie ihn in den Karteneinstellungen an.", 11 | "sample": "Versuchen Sie es später noch einmal, oder geben Sie einen Standort in den Karteneinstellungen an" 12 | }, 13 | "settings": { 14 | "location": "Standort", 15 | "forecast": "Vorhersage", 16 | "city": "Stadt, z.B. `Nantes, FR` oder den ZIP-Code", 17 | "choose_units": "Wählen Sie die Masseinheit" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Weather", 3 | "description": "See today's weather and forecasts simply", 4 | "humidity": "Humidity", 5 | "wind_speed": "Wind speed", 6 | "sunrise": "Sunrise", 7 | "sunset": "Sunset", 8 | "cloudiness": "Cloudiness", 9 | "error": { 10 | "title": "Geolocation error", 11 | "unsupported": "Geolocation is not supported please enter city name in card settings.", 12 | "sample": "Try later or enter a city manually in card settings." 13 | }, 14 | "settings": { 15 | "location": "Location", 16 | "forecast": "Forecast", 17 | "city": "City, e.g. `Nantes, FR` or its id", 18 | "choose_units": "Choose your prefered units" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/es_mx.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Clíma", 3 | "description": "Vea el clíma de hoy y pronósticos", 4 | "humidity": "Humedad", 5 | "wind_speed": "Velocidad del viento", 6 | "sunrise": "Amanecer", 7 | "sunset": "Atardecer", 8 | "cloudiness": "Abundancia de nubes", 9 | "error": { 10 | "title": "Error de geo-localización", 11 | "unsupported": "La geo-localización no está disponible, favor de ingresar ciudad en los ajustes de tarjeta.", 12 | "sample": "Intente de nuevo más tarde o entre una ciudad manualmente en los ajustes de tarjeta." 13 | }, 14 | "settings": { 15 | "location": "Localidad", 16 | "forecast": "Pronóstico", 17 | "city": "Ciudad, ejemplo: `Nantes, FR` o su ID", 18 | "choose_units": "Escoja sus medidas preferidas" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Voir la météo et les prévisions d'aujourd'hui simplement", 3 | "humidity": "Humidité", 4 | "wind_speed": "Vitesse du vent", 5 | "sunrise": "Lever du soleil", 6 | "sunset": "Coucher de soleil", 7 | "cloudiness": "Taux de nuages", 8 | "error": { 9 | "title": "Erreur de géolocalisation", 10 | "unsupported": "La géolocalisation n'est pas prise en charge, veuillez entrer le nom de la ville dans les paramètres de la carte.", 11 | "sample": "Essayez plus tard ou entrez une ville manuellement dans les paramètres de la carte." 12 | }, 13 | "settings": { 14 | "location": "Emplacement", 15 | "forecast": "Prévisions", 16 | "city": "Ville, e.g. `Nantes, FR` ou son id", 17 | "choose_units": "Choisissez votre unité préférée" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "See today's weather and forecasts simply", 3 | "humidity": "Vlhkosť", 4 | "wind_speed": "Rýchlosť vetra", 5 | "sunrise": "Východ slnka", 6 | "sunset": "Západ slnka", 7 | "cloudiness": "Oblačnosť", 8 | "error": { 9 | "title": "Chyba Geolokácie", 10 | "unsupported": "Geolokácia nieje podporovaná, napíšte prosím názov mesta v nastaveniach karty.", 11 | "sample": "Pokúste sa neskôr alebo vpíšte názov mesta manuálne v nastaveniach karty." 12 | }, 13 | "settings": { 14 | "location": "Lokácia", 15 | "forecast": "Predpoveď", 16 | "city": "Mesto, napr. `Bratislava, SK` alebo id mesta", 17 | "choose_units": "Vyberte si preferovanú jednotku." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Hava Durumu", 3 | "description": "Bugüne ait hava bilgisini görüntüleyin", 4 | "humidity": "Basınç", 5 | "wind_speed": "Rüzgar hızı", 6 | "sunrise": "Güneş doğuşu", 7 | "sunset": "Güneş batışı", 8 | "cloudiness": "Bulut oranı", 9 | "error": { 10 | "title": "Geolokasyon hatası", 11 | "unsupported": "Geolokasyon desteklenmiyor, lütfen kart ayarlarıandan şehrinizin ismini girin.", 12 | "sample": "Daha sonra tekrar deneyin veya kart ayarlarından şehrinizi el ile ekleyin." 13 | }, 14 | "settings": { 15 | "location": "Bölge", 16 | "forecast": "Tahminler", 17 | "city": "Şehir, örn. `İstanbul` veya şehrin ID'si", 18 | "choose_units": "Tercih ettiğiniz birimi seçin" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Погода", 3 | "description": "Простий перегляд погоди на сьогодні та прогноз", 4 | "humidity": "Вологість", 5 | "wind_speed": "Швидкість вітру", 6 | "sunrise": "Cхід сонця", 7 | "sunset": "Захід сонця", 8 | "cloudiness": "Хмарність", 9 | "error": { 10 | "title": "Помилка геолокації", 11 | "unsupported": "Геолокація не підтримується. Введіть, будь ласка, назву міста у налаштуваннях картки.", 12 | "sample": "Спробуйте пізніше або введіть місто у налаштуваннях картки." 13 | }, 14 | "settings": { 15 | "location": "Розташування", 16 | "forecast": "Прогноз", 17 | "city": "Місто, наприклад `Nantes, FR` або його ID", 18 | "choose_units": "Оберіть бажані одиниці виміру" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cards/Weather/langs/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "只需查看今天的天气和天气预报", 3 | "humidity": "湿度", 4 | "wind_speed": "风速", 5 | "sunrise": "日出", 6 | "sunset": "日落", 7 | "cloudiness": "云量", 8 | "error": { 9 | "title": "地理位置错误", 10 | "unsupported": "不支持自动获取地理位置,请在卡设置中输入城市名称。", 11 | "sample": "请稍后再试或在卡片设置中手动输入城市。" 12 | }, 13 | "settings": { 14 | "location": "位置", 15 | "forecast": "预报", 16 | "city": "城市,如 `Nantes, FR` or its id", 17 | "choose_units": "选择您喜欢的单位" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cards/Weather/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "title": "auto" 4 | }, 5 | "settings": { 6 | "auto": true, 7 | "city": "", 8 | "forecast": true, 9 | "appId": "0c9042777e3128fab0244da248184801", 10 | "units": "metric" 11 | }, 12 | "externalsRequests": true, 13 | "cacheValidity": 3600, 14 | "allowMultiple": true 15 | } 16 | -------------------------------------------------------------------------------- /src/cards/Weather/settings.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | name: 'Weather', 4 | data() { 5 | return { 6 | settings: {}, 7 | allUnits: [ 8 | { text: 'Metric', value: 'metric' }, 9 | { text: 'Imperial', value: 'imperial' }, 10 | { text: 'Kelvin', value: 'kelvin' }, 11 | ], 12 | }; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/cards/Weather/settings.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /src/cards/Weather/style.scss: -------------------------------------------------------------------------------- 1 | #weather { 2 | .left { 3 | padding: 16px 16px 12px; 4 | .today .v-image { 5 | width: 100px; 6 | } 7 | .forecast .v-image { 8 | width: 24px; 9 | height: 24px; 10 | } 11 | } 12 | .weather-info { 13 | padding: 50px 16px 16px; 14 | .detail { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | .small-font { 19 | font-size: 70%; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/cards/index.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze(CardsObj); 2 | -------------------------------------------------------------------------------- /src/components/Card/index.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/components/Card/style.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | min-width: 400px; 3 | min-height: 100px; 4 | margin: 15px; 5 | position: absolute; 6 | z-index: 1; 7 | .v-card { 8 | overflow: hidden; 9 | .v-toolbar__title { 10 | font-weight: normal; 11 | text-transform: capitalize; 12 | } 13 | .v-toolbar--absolute { 14 | background-color: transparent!important; 15 | } 16 | .subheading { 17 | overflow: hidden; 18 | white-space: nowrap; 19 | text-overflow: ellipsis; 20 | max-width: 100%; 21 | } 22 | .head-drag { 23 | cursor: grab; 24 | } 25 | } 26 | } 27 | 28 | .muuri-item-dragging { 29 | z-index: 3 !important; 30 | .head-drag { 31 | cursor: grabbing!important; 32 | } 33 | } 34 | .muuri-item-releasing { 35 | z-index: 2 !important; 36 | } 37 | .muuri-item-hidden { 38 | z-index: 0 !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/Dialog/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Dialog from './Dialog'; 3 | 4 | const defaultOptions = { 5 | title: '', 6 | text: '', 7 | ok: 'Ok', 8 | cancel: 'Cancel', 9 | }; 10 | 11 | let dialogCmp = null; 12 | 13 | function createDialogCmp() { 14 | const cmp = new Vue(Dialog); 15 | document.body.appendChild(cmp.$mount().$el); 16 | return cmp; 17 | } 18 | 19 | function getDialogCmp() { 20 | if (!dialogCmp) { 21 | dialogCmp = createDialogCmp(); 22 | } 23 | return dialogCmp; 24 | } 25 | 26 | function show(options = {}) { 27 | return createDialogCmp().show({ ...defaultOptions, ...options }); 28 | } 29 | 30 | function close() { 31 | getDialogCmp().close(); 32 | } 33 | 34 | export default { 35 | show, 36 | close, 37 | getDialogCmp, 38 | defaultOptions, 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/Dialog/main.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | data() { 4 | return { 5 | active: false, 6 | title: '', 7 | text: '', 8 | ok: 'Ok', 9 | cancel: 'Cancel', 10 | resolve: null, 11 | }; 12 | }, 13 | methods: { 14 | show(options = {}) { 15 | return new Promise((resolve) => { 16 | if (this.active) { 17 | this.close(); 18 | this.$nextTick(() => this.show(options)); 19 | return; 20 | } 21 | this.resolve = resolve; 22 | const keys = Object.keys(options); 23 | for (let i = 0; i < keys.length; i += 1) { 24 | this[keys[i]] = options[keys[i]]; 25 | } 26 | this.active = true; 27 | }); 28 | }, 29 | valid() { 30 | this.active = false; 31 | this.resolve(true); 32 | }, 33 | close() { 34 | this.active = false; 35 | this.resolve(false); 36 | }, 37 | dismiss() { 38 | if (this.dismissible) { 39 | this.active = false; 40 | } 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/GridList/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/GridList/style.scss: -------------------------------------------------------------------------------- 1 | .grid-list { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | justify-content: flex-start; 6 | overflow-y: hidden; 7 | .node { 8 | list-style: none; 9 | :hover { 10 | background-color: rgba(0, 0, 0, .1); 11 | } 12 | a { 13 | display: block; 14 | width: 121px; 15 | height: 121px; 16 | background-size: 80%; 17 | background-repeat: no-repeat; 18 | background-position: center; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Header/backgrounds.js: -------------------------------------------------------------------------------- 1 | // imgur album: https://imgur.com/a/NAaUE 2 | export default { 3 | unsplash: ['?dawn', '', '?dusk', '?night'] 4 | .map(f => `https://source.unsplash.com/random/${window.innerWidth}x${window.innerHeight}/${f}`), 5 | austin: [ 6 | 'https://i.imgur.com/7ndeJog.jpg', 7 | 'https://i.imgur.com/FsJ8mCW.jpg', 8 | 'https://i.imgur.com/Mmwv5GQ.jpg', 9 | 'https://i.imgur.com/brJBKA3.jpg', 10 | ], 11 | beach: [ 12 | 'https://i.imgur.com/Q5Tn8u9.jpg', 13 | 'https://i.imgur.com/dTFXUxt.jpg', 14 | 'https://i.imgur.com/vdO9Ote.jpg', 15 | 'https://i.imgur.com/YaoPX9P.jpg', 16 | ], 17 | berlin: [ 18 | 'https://i.imgur.com/jG1OdPc.jpg', 19 | 'https://i.imgur.com/lnILrRU.jpg', 20 | 'https://i.imgur.com/ZCJVfSn.jpg', 21 | 'https://i.imgur.com/5mN7Iau.jpg', 22 | ], 23 | chicago: [ 24 | 'https://i.imgur.com/f4HUPlZ.jpg', 25 | 'https://i.imgur.com/t5wzT8j.jpg', 26 | 'https://i.imgur.com/XrJi3O1.jpg', 27 | 'https://i.imgur.com/xDWHJ45.jpg', 28 | ], 29 | mountains: [ 30 | 'https://i.imgur.com/kJFNQLr.jpg', 31 | 'https://i.imgur.com/foVYQ6T.jpg', 32 | 'https://i.imgur.com/dW217U5.jpg', 33 | 'https://i.imgur.com/87UObPk.jpg', 34 | ], 35 | greatPlains: [ 36 | 'https://i.imgur.com/dWzcGbr.jpg', 37 | 'https://i.imgur.com/huGlyp2.jpg', 38 | 'https://i.imgur.com/XNUMKAT.jpg', 39 | 'https://i.imgur.com/d7KaqQ1.jpg', 40 | ], 41 | london: [ 42 | 'https://i.imgur.com/ZD0XBoz.jpg', 43 | 'https://i.imgur.com/C2Sg6JG.jpg', 44 | 'https://i.imgur.com/Qb8PHnA.jpg', 45 | 'https://i.imgur.com/k0idCJG.jpg', 46 | ], 47 | newYork: [ 48 | 'https://i.imgur.com/JVK8ID7.jpg', 49 | 'https://i.imgur.com/yB93g10.jpg', 50 | 'https://i.imgur.com/z4elpiG.jpg', 51 | 'https://i.imgur.com/lh0LV5L.jpg', 52 | ], 53 | paris: [ 54 | `https://lh6.ggpht.com/F9QFQqIVAPulSHCiMuobs4m0fYReFB55NiOaY8RUbyzqb1rOSqeu0Q4cVVRsOSA=w${window.innerWidth}-h${window.innerHeight}`, 55 | `https://lh4.ggpht.com/wHsOCpRMA92KGQ3pHl6tBWuVY-ruvOKxYrjIZM-1mZIX7VUfLYAkIli4u1XMFTg=w${window.innerWidth}-h${window.innerHeight}`, 56 | `https://lh4.ggpht.com/n3FKbvIraVzxbxvq3B3btqg5Kx4dSC935cDuNRYZ7BomnyumsqA4Mr46KmY5N5Q=w${window.innerWidth}-h${window.innerHeight}`, 57 | `https://lh4.ggpht.com/4tlgfGMLRz4OpfhJWRXLEFAZeudAGozJgq3ak45DVzcQVHJeTYaw5knl7ce17Q=w${window.innerWidth}-h${window.innerHeight}`, 58 | ], 59 | sanFrancisco: [ 60 | 'https://i.imgur.com/fqewVsW.jpg', 61 | 'https://i.imgur.com/lUZp177.jpg', 62 | 'https://i.imgur.com/XP6Omxa.jpg', 63 | 'https://i.imgur.com/NATsgio.jpg', 64 | ], 65 | seattle: [ 66 | 'https://i.imgur.com/7nsrzRK.jpg', 67 | 'https://i.imgur.com/0E2xXb0.jpg', 68 | 'https://i.imgur.com/wYytDhF.jpg', 69 | 'https://i.imgur.com/ddI0eBh.jpg', 70 | ], 71 | tahoe: [ 72 | 'https://i.imgur.com/ZSXPIkL.jpg', 73 | 'https://i.imgur.com/xeVYGPU.jpg', 74 | 'https://i.imgur.com/Buxx2Cs.jpg', 75 | 'https://i.imgur.com/g761v2t.jpg', 76 | ], 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/Header/style.scss: -------------------------------------------------------------------------------- 1 | header { 2 | height: 233px; 3 | padding: 15px 50px; 4 | position: relative; 5 | display: flex; 6 | align-items: flex-end; 7 | .background { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | &[lazy=loading] { 12 | filter: blur(5px); 13 | } 14 | } 15 | .v-toolbar { 16 | width: 66%; 17 | margin: 0 auto; 18 | top: 46px; 19 | z-index: 1; 20 | } 21 | #settings { 22 | position: absolute; 23 | top: 15px; 24 | right: 15px; 25 | z-index: 1; 26 | } 27 | .doodle { 28 | position: absolute; 29 | margin: auto; 30 | max-height: 100%; 31 | width: 100%; 32 | left: 50%; 33 | transform: translateY(-50%) translateX(-50%); 34 | top: 50%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Home/index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Home/style.scss: -------------------------------------------------------------------------------- 1 | #home { 2 | position: relative; 3 | flex: 1; 4 | .v-speed-dial { 5 | position: absolute; 6 | top: -35px; 7 | z-index: 2; 8 | /deep/ .v-speed-dial__list { 9 | align-items: flex-end !important; 10 | } 11 | } 12 | #card-container { 13 | position: relative; 14 | margin: 0 auto; 15 | &.has-toolbar { 16 | margin: 20px auto; 17 | } 18 | &.placeholder { 19 | padding: 15px 0; 20 | column-gap: 15px; 21 | } 22 | .placeholder-item { 23 | width: 400px; 24 | margin-bottom: 30px; 25 | page-break-inside: avoid; 26 | will-change: transform; 27 | break-inside: avoid; 28 | background-color: rgba(164, 163, 164, .35); 29 | } 30 | /deep/ .muuri-item-placeholder { 31 | background-color: rgba(0,0,0,0.23); 32 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 33 | z-index: 0; 34 | } 35 | } 36 | // While waiting for a muuri update this is the easiest way to center the cards. 37 | // 1 column 38 | @media (max-width: 859px) { 39 | #card-container { 40 | width: 430px; 41 | &.placeholder { 42 | column-count: 1; 43 | } 44 | } 45 | } 46 | // 2 columns 47 | @media (min-width: 860px) and (max-width: 1289px) { 48 | #card-container { 49 | width: 860px; 50 | &.placeholder { 51 | column-count: 2; 52 | } 53 | } 54 | } 55 | // 3 columns 56 | @media (min-width: 1290px) and (max-width: 1719px) { 57 | #card-container { 58 | width: 1290px; 59 | &.placeholder { 60 | column-count: 3; 61 | } 62 | } 63 | } 64 | // 4 columns 65 | @media (min-width: 1720px) and (max-width: 2149px) { 66 | #card-container { 67 | width: 1720px; 68 | &.placeholder { 69 | column-count: 4; 70 | } 71 | } 72 | } 73 | // 5 columns 74 | @media (min-width: 2150px) { 75 | #card-container { 76 | width: 2150px; 77 | &.placeholder { 78 | column-count: 5; 79 | } 80 | } 81 | } 82 | } 83 | 84 | .fade-enter-active, .fade-leave-active { 85 | transition: opacity .2s cubic-bezier(.4,0,.2,1) 86 | } 87 | .fade-enter, .fade-leave-to { 88 | opacity: 0 89 | } 90 | -------------------------------------------------------------------------------- /src/components/List/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 54 | 55 | -------------------------------------------------------------------------------- /src/components/List/style.scss: -------------------------------------------------------------------------------- 1 | .v-list__tile__avatar { 2 | min-width: 29px!important; 3 | } 4 | /deep/ .v-list__tile { 5 | height: 34px!important; 6 | } 7 | .v-list__tile__action { 8 | flex-shrink: 0!important; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Onboarding/hello.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/components/Onboarding/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Onboarding/main.js: -------------------------------------------------------------------------------- 1 | import Hello from './hello'; 2 | import Why from './why'; 3 | import Settings from './settings'; 4 | 5 | // @vue/component 6 | export default { 7 | name: 'Onboarding', 8 | components: { 9 | Hello, 10 | Why, 11 | Settings, 12 | }, 13 | board: ['hello', 'why', 'settings'], 14 | data() { 15 | return { 16 | index: 0, 17 | }; 18 | }, 19 | methods: { 20 | prev() { 21 | this.index -= this.index > 0; 22 | }, 23 | next() { 24 | if (this.index + 1 < this.$options.board.length) { 25 | this.index += 1; 26 | } else this.finish(); 27 | }, 28 | finish() { 29 | this.$store.commit('SET_TUTORIAL', true); 30 | this.$router.replace('/'); 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/Onboarding/settings.vue: -------------------------------------------------------------------------------- 1 | 45 | 79 | -------------------------------------------------------------------------------- /src/components/Onboarding/style.scss: -------------------------------------------------------------------------------- 1 | #onboarding { 2 | .has-toolbar { 3 | margin: 20px auto; 4 | } 5 | .slide-fade-transition-enter-active { 6 | transition: all .3s ease; 7 | } 8 | .slide-fade-transition-leave-active { 9 | transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0); 10 | } 11 | .slide-fade-transition-enter, .slide-fade-transition-leave-to { 12 | transform: translateX(50px); 13 | opacity: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Onboarding/why.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/components/Settings/artworks.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { text: 'settings.artworks.random', value: 'random' }, 3 | { text: 'settings.artworks.unsplash', value: 'unsplash' }, 4 | { text: 'settings.artworks.url', value: 'url' }, 5 | { text: 'settings.artworks.local', value: 'local' }, 6 | { text: 'settings.artworks.mountains', value: 'mountains' }, 7 | { text: 'Austin', value: 'austin' }, 8 | { text: 'Beach', value: 'beach' }, 9 | { text: 'Berlin', value: 'berlin' }, 10 | { text: 'Chicago', value: 'chicago' }, 11 | { text: 'Great Plains', value: 'greatPlains' }, 12 | { text: 'London', value: 'london' }, 13 | { text: 'New York', value: 'newYork' }, 14 | { text: 'Paris', value: 'paris' }, 15 | { text: 'San Francisco', value: 'sanFrancisco' }, 16 | { text: 'Seattle', value: 'seattle' }, 17 | { text: 'Tahoe', value: 'tahoe' }, 18 | ]; 19 | -------------------------------------------------------------------------------- /src/components/Settings/countries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { value: 'argentina', text: 'Argentina' }, 3 | { value: 'austria', text: 'Austria' }, 4 | { value: 'australia', text: 'Australia' }, 5 | { value: 'belgium', text: 'Belgium' }, 6 | { value: 'brazil', text: 'Brazil' }, 7 | { value: 'canada', text: 'Canada' }, 8 | { value: 'chile', text: 'Chile' }, 9 | { value: 'colombia', text: 'Colombia' }, 10 | { value: 'czech_republic', text: 'Czech Republic' }, 11 | { value: 'denmark', text: 'Denmark' }, 12 | { value: 'egypt', text: 'Egypt' }, 13 | { value: 'finland', text: 'Finland' }, 14 | { value: 'france', text: 'France' }, 15 | { value: 'germany', text: 'Germany' }, 16 | { value: 'greece', text: 'Greece' }, 17 | { value: 'hong_kong', text: 'Hong Kong' }, 18 | { value: 'hungary', text: 'Hungary' }, 19 | { value: 'india', text: 'India' }, 20 | { value: 'indonesia', text: 'Indonesia' }, 21 | { value: 'ireland', text: 'Ireland' }, 22 | { value: 'israel', text: 'Israel' }, 23 | { value: 'italy', text: 'Italy' }, 24 | { value: 'japan', text: 'Japan' }, 25 | { value: 'kenya', text: 'Kenya' }, 26 | { value: 'malaysia', text: 'Malaysia' }, 27 | { value: 'mexico', text: 'Mexico' }, 28 | { value: 'netherlands', text: 'Netherlands' }, 29 | { value: 'new zealand', text: 'New Zealand' }, 30 | { value: 'nigeria', text: 'Nigeria' }, 31 | { value: 'norway', text: 'Norway' }, 32 | { value: 'philippines', text: 'Philippines' }, 33 | { value: 'poland', text: 'Poland' }, 34 | { value: 'portugal', text: 'Portugal' }, 35 | { value: 'romania', text: 'Romania' }, 36 | { value: 'russia', text: 'Russia' }, 37 | { value: 'saudi_arabia', text: 'Saudi Arabia' }, 38 | { value: 'singapore', text: 'Singapore' }, 39 | { value: 'south_africa', text: 'South Africa' }, 40 | { value: 'south_korea', text: 'South Korea' }, 41 | { value: 'spain', text: 'Spain' }, 42 | { value: 'sweden', text: 'Sweden' }, 43 | { value: 'switzerland', text: 'Switzerland' }, 44 | { value: 'taiwan', text: 'Taiwan' }, 45 | { value: 'thailand', text: 'Thailand' }, 46 | { value: 'turkey', text: 'Turkey' }, 47 | { value: 'ukraine', text: 'Ukraine' }, 48 | { value: 'united_kingdom', text: 'United Kingdom' }, 49 | { value: 'united_states', text: 'United States' }, 50 | { value: 'vietnam', text: 'Vietnam' }, 51 | ]; 52 | -------------------------------------------------------------------------------- /src/components/Settings/style.scss: -------------------------------------------------------------------------------- 1 | #settings { 2 | .picker { 3 | display: flex; 4 | margin: auto; 5 | li { 6 | list-style: none; 7 | width: 25px; 8 | height: 25px; 9 | display: flex; 10 | cursor: pointer; 11 | .color-dot { 12 | width: 6px; 13 | height: 6px; 14 | background: #fff; 15 | margin: auto; 16 | } 17 | } 18 | } 19 | .file-btn { 20 | display: inline-block; 21 | } 22 | .file-btn input[type=file] { 23 | position: absolute; 24 | opacity: 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Toast/Toast.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/Toast/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Toast from './Toast'; 3 | 4 | const defaultOptions = { 5 | title: '', 6 | desc: null, 7 | icon: '', 8 | color: 'info', 9 | timeout: 4000, 10 | dismissible: true, 11 | callback: null, 12 | dismissCb: null, 13 | }; 14 | 15 | let toastCmp = null; 16 | 17 | function createToastCmp() { 18 | const cmp = new Vue(Toast); 19 | document.body.appendChild(cmp.$mount().$el); 20 | return cmp; 21 | } 22 | 23 | function getToastCmp() { 24 | if (!toastCmp) { 25 | toastCmp = createToastCmp(); 26 | } 27 | return toastCmp; 28 | } 29 | 30 | function show(options = {}) { 31 | getToastCmp().show({ ...defaultOptions, ...options }); 32 | } 33 | 34 | function close() { 35 | getToastCmp().close(); 36 | } 37 | 38 | export default { 39 | show, 40 | close, 41 | getToastCmp, 42 | defaultOptions, 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Toast/main.js: -------------------------------------------------------------------------------- 1 | // @vue/component 2 | export default { 3 | data() { 4 | return { 5 | active: false, 6 | title: '', 7 | desc: null, 8 | icon: '', 9 | color: 'info', 10 | timeout: 3000, 11 | dismissible: true, 12 | callback: null, 13 | dismissCb: null, 14 | }; 15 | }, 16 | methods: { 17 | show(options = {}) { 18 | if (this.active) { 19 | this.close(); 20 | this.$nextTick(() => this.show(options)); 21 | return; 22 | } 23 | const keys = Object.keys(options); 24 | for (let i = 0; i < keys.length; i += 1) { 25 | this[keys[i]] = options[keys[i]]; 26 | } 27 | this.active = true; 28 | }, 29 | close() { 30 | this.active = false; 31 | }, 32 | dismiss() { 33 | if (this.dismissible) { 34 | this.active = false; 35 | if (this.dismissCb) { 36 | this.dismissCb(); 37 | } 38 | } 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/Typer/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/Typer/style.scss: -------------------------------------------------------------------------------- 1 | #typewriter { 2 | z-index: 1; 3 | width: 100%; 4 | h1 { 5 | font-weight: 400; 6 | font-size: 6vw; 7 | text-shadow: 0 2px 1px rgba(0, 0, 0, 0.35); 8 | } 9 | .typewriter-selected { 10 | background-color: rgba(0, 0, 0, 0.2); 11 | } 12 | .typewriter-cursor { 13 | opacity: 1; 14 | animation: blink 0.7s infinite; 15 | position: relative; 16 | top: -3px; 17 | vertical-align: middle; 18 | } 19 | .typewriter-msg { 20 | color: white; 21 | text-overflow: ellipsis; 22 | overflow: hidden; 23 | display: inline-block; 24 | max-width: calc(100% - 40px); 25 | vertical-align: middle; 26 | white-space: nowrap; 27 | &::first-letter { 28 | text-transform: capitalize; 29 | } 30 | } 31 | @keyframes blink { 32 | 0% { 33 | opacity: 1; 34 | } 35 | 50% { 36 | opacity: 0; 37 | } 38 | 100% { 39 | opacity: 1; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueI18n from 'vue-i18n'; 3 | import en from '@/langs/en'; 4 | 5 | Vue.use(VueI18n); 6 | 7 | const defaultState = { 8 | locale: 'en', 9 | fallbackLocale: 'en', 10 | messages: { 11 | en, 12 | }, 13 | }; 14 | 15 | const i18n = new VueI18n(defaultState); 16 | 17 | const setLang = (vm, lang) => { 18 | i18n.locale = lang; 19 | vm.axios.defaults.headers.common['Accept-Language'] = lang; // eslint-disable-line 20 | }; 21 | 22 | const loadLang = (vm, lang) => { 23 | if (!lang || lang === i18n.locale) return Promise.resolve(); 24 | if (lang === defaultState.locale) { 25 | setLang(vm, lang); 26 | return Promise.resolve(); 27 | } 28 | return import(/* webpackChunkName: "lang-[request]" */`@/langs/${lang}.js`) 29 | .then((msgs) => { 30 | i18n.setLocaleMessage(lang, msgs.default); 31 | setLang(vm, lang); 32 | }); 33 | }; 34 | 35 | export { i18n, loadLang }; 36 | -------------------------------------------------------------------------------- /src/i18n/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chinese (Simplified)", 3 | "locale": "zh-CN", 4 | "onboarding": { 5 | "hello": { 6 | "title": "你好 !", 7 | "content": "这是你的崭新的标签页!" 8 | }, 9 | "why": { 10 | "title": "为什么 Epiboard 这么棒", 11 | "prefs": "首选项已跨浏览器同步(如果已登录)", 12 | "needs": "一卡多用", 13 | "privacy": "Epiboard 只在需要时请求权限" 14 | }, 15 | "settings": { 16 | "title": "开始之前的一些设置" 17 | }, 18 | "welcome": "欢迎!", 19 | "begin": "开始", 20 | "next": "下一步", 21 | "previous": "上一步" 22 | }, 23 | "card": { 24 | "loading": "{id} 正在加载...", 25 | "reload": "刷新", 26 | "remove": "移除", 27 | "permissions_failed": "{id} 需要它没有的权限,请稍后重试", 28 | "error": "{id} 发生错误,请稍后再试", 29 | "error_reload": "{id} 发生错误,请点击以重新加载", 30 | "more": "查看更多" 31 | }, 32 | "add": "Add", 33 | "greetMessages": [ 34 | "嘿,你今天好吗?", 35 | "希望你一切都好", 36 | "有人在想你 ;)", 37 | "你好!", 38 | "没有不可能", 39 | "你知道你摇滚吗?", 40 | "人之所以能,是相信能", 41 | "加油!", 42 | "您可以随心所欲!" 43 | ], 44 | "permissions": { 45 | "required": "权限请求", 46 | "message": "'{name}' 要求它正常工作所必需的权限,可以吗?", 47 | "allow": "允许", 48 | "deny": "拒绝" 49 | }, 50 | "auth": { 51 | "connect_to": "连接到 {service}", 52 | "disconnect_from": "与 {service} 断开连接" 53 | }, 54 | "home": { 55 | "no_cards": "没有卡片", 56 | "add_cards": "您可以通过单击右上方的+号按钮以添加卡片。" 57 | }, 58 | "settings": { 59 | "title": "设置", 60 | "langs": "语言", 61 | "header": "搜索框", 62 | "browse": "Browse", 63 | "theme": "主题", 64 | "analytics_desc": "只是看你如何使用本新标签页", 65 | "custom_css": "自定义CSS", 66 | "custom_css_desc": "插入您的CSS文件的URL,比如 https://example.com/style.css", 67 | "custom_css_warning": "警告:CSS定制是针对高级用户的,恢复默认主题留空即可", 68 | "debug_desc": "如果你愿意的话", 69 | "apply_change": "为了保存您所修改的设置,必须使用页面顶部附近的后退按钮或单击页面底部的保存。", 70 | "reset": "重置设置", 71 | "save": "保存", 72 | "saved": "已保存!", 73 | "cancel": "Cancel", 74 | "onOff": "开启 | 关闭", 75 | "apiKey": "{name} API Key", 76 | "custom_message": "自定义搜索框消息", 77 | "whatsnew": "请随时关注最新更新", 78 | "donate": { 79 | "title": "你喜欢我的工作吗?", 80 | "desc": "点击这里为我买啤酒 <3" 81 | }, 82 | "dark": { 83 | "title": "黑暗模式", 84 | "desc": "让一切变黑", 85 | "auto": "自动", 86 | "from": "从", 87 | "to": "到" 88 | }, 89 | "artworks": { 90 | "random": "随机", 91 | "unsplash": "从照片库供应商 Unsplash 获取", 92 | "url": "从 Url 获取", 93 | "local": "从本地获取", 94 | "mountains": "山脉" 95 | }, 96 | "choose": { 97 | "lang": "选择你的语言", 98 | "24h": "强制使用24小时制", 99 | "design": "选择你喜欢的搜索框主题设计", 100 | "background": "选择你的背景图", 101 | "trends": "选择您的Google趋势国家/地区(指搜索框里会出现热门搜索词)", 102 | "color": "选择强调色", 103 | "custom_font": "使用自定义字体" 104 | }, 105 | "design": { 106 | "full": "充满顶部空间", 107 | "toolbar": "工具栏" 108 | }, 109 | "auth": { 110 | "title": "关联服务", 111 | "desc": "使卡能够访问这些服务的功能" 112 | }, 113 | "placeholder": { 114 | "background": "从 Url 获取,比如 https://i.imgur.com/foVYQ6T.jpg", 115 | "color": "Main color,比如 #607D8B", 116 | "custom_message": "比如励志名言?", 117 | "custom_font": "比如 Source Han Sans SC", 118 | "custom_desactived": "Google趋势启用时不能使用此功能" 119 | }, 120 | "error": { 121 | "color": "这是无效的十六进制码,请使用正确颜色代码,比如 #607D8B" 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/langs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alexays/Epiboard/4fe5f0f0865f489db37be01616e0e390490bd359/src/langs/.gitkeep -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import 'material-design-icons-iconfont/dist/material-design-icons.css'; 3 | import axios from 'axios'; 4 | import VueAxios from 'vue-axios'; 5 | import VueAnalytics from 'vue-analytics'; 6 | import Vuetify from 'vuetify/lib'; 7 | import App from '@/App'; 8 | import { i18n } from '@/i18n'; 9 | import router from '@/router'; 10 | import store from '@/store'; 11 | import 'vuetify/src/stylus/app.styl'; 12 | import '@/style.scss'; 13 | 14 | Vue.config.productionTip = false; 15 | 16 | // TODO: Firefox doesnt allow to load external script 17 | if (browserName === 'chrome' && !window.__PRERENDER_INJECTED) { 18 | Vue.use(VueAnalytics, { 19 | id: 'UA-78514802-2', 20 | // In Chrome extension, must close checking protocol. 21 | set: [{ field: 'checkProtocolTask', value: null }], 22 | router, 23 | debug: { 24 | sendHitTask: localStorage.getItem('analytics') !== 'false', 25 | }, 26 | }); 27 | } 28 | 29 | Vue.use(Vuetify, { 30 | iconfont: 'md', 31 | theme: { 32 | primary: '#607D8B', 33 | secondary: '#546E7A', 34 | accent: '#2196F3', 35 | foreground: '#ffffff', 36 | }, 37 | }); 38 | Vue.use(VueAxios, axios); 39 | // eslint-disable-next-line no-new 40 | new Vue({ 41 | el: '#app', 42 | i18n, 43 | router, 44 | store, 45 | render: h => h(App), 46 | }); 47 | -------------------------------------------------------------------------------- /src/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_appName__", 4 | "short_name": "Epiboard", 5 | "description": "__MSG_appDesc__", 6 | "author": "Alexays", 7 | "version": "$version", 8 | "icons": { 9 | "16": "imgs/icon16.png", 10 | "48": "imgs/icon48.png", 11 | "128": "imgs/icon128.png" 12 | }, 13 | "default_locale": "en", 14 | "permissions": [ 15 | "storage", 16 | "geolocation", 17 | "management", 18 | "https://trends.google.com/trends/hottrends/visualize/internal/data" 19 | ], 20 | "content_security_policy": "script-src 'self' https://www.google-analytics.com/; object-src 'self'", 21 | "optional_permissions": [ 22 | "https://*/", 23 | "http://*/", 24 | "system.cpu", 25 | "system.memory", 26 | "system.storage", 27 | "sessions", 28 | "tabs", 29 | "browsingData", 30 | "downloads", 31 | "downloads.open", 32 | "topSites", 33 | "identity", 34 | "bookmarks" 35 | ], 36 | "chrome_url_overrides": { 37 | "newtab": "index.html" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_appName__", 4 | "short_name": "Epiboard", 5 | "description": "__MSG_appDesc__", 6 | "author": "Alexays", 7 | "version": "$version", 8 | "icons": { 9 | "16": "imgs/icon16.png", 10 | "48": "imgs/icon48.png", 11 | "128": "imgs/icon128.png" 12 | }, 13 | "default_locale": "en", 14 | "permissions": [ 15 | "storage", 16 | "geolocation", 17 | "sessions", 18 | "browsingData", 19 | "identity", 20 | "https://trends.google.com/trends/hottrends/visualize/internal/data" 21 | ], 22 | "optional_permissions": [ 23 | "https://*/", 24 | "http://*/", 25 | "tabs", 26 | "downloads", 27 | "downloads.open", 28 | "topSites", 29 | "bookmarks" 30 | ], 31 | "chrome_settings_overrides": { 32 | "homepage": "index.html" 33 | }, 34 | "chrome_url_overrides": { 35 | "newtab": "index.html" 36 | }, 37 | "applications": { 38 | "gecko": { 39 | "id": "contact@arouillard.fr", 40 | "strict_min_version": "60.0" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/mixins/dark.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | isDark() { 4 | const { dark } = this.$store.state.settings; 5 | if (window.__PRERENDER_INJECTED || !dark || !dark.enabled) return false; 6 | if (!dark.auto) return true; 7 | const from = (dark.from || '22:00').split(':').map(Number); 8 | const to = (dark.to || '9:00').split(':').map(Number); 9 | const date = new Date(); 10 | const year = date.getFullYear(); 11 | const month = date.getMonth(); 12 | const day = date.getDate(); 13 | const fromDate = new Date(year, month, day, from[0], from[1]).getTime(); 14 | const toDate = new Date(year, month, day, to[0], to[1]).getTime(); 15 | const time = date.getTime(); 16 | if (fromDate > toDate) { 17 | return (!(time > toDate && time < fromDate)); 18 | } 19 | return (time > fromDate && time < toDate); 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/mixins/date.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | timeOptions() { 4 | const options = { hour: '2-digit', minute: '2-digit' }; 5 | if (this.$store.state.settings.hour24) { 6 | options.hour12 = false; 7 | } 8 | return options; 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/mixins/permissions.js: -------------------------------------------------------------------------------- 1 | import Dialog from '@/components/Dialog'; 2 | 3 | export default { 4 | methods: { 5 | // TODO: avoid useless dialog 6 | checkPermissions(payload, name) { 7 | return browser.permissions.contains(payload) 8 | .then(res => res || browser.permissions.request(payload)) 9 | .catch(() => Dialog.show({ 10 | title: this.$t('permissions.required'), 11 | text: this.$t('permissions.message', { name }), 12 | ok: this.$t('permissions.allow'), 13 | cancel: this.$t('permissions.deny'), 14 | }).then((res) => { 15 | if (res) { 16 | return browser.permissions.request(payload).then((granted) => { 17 | if (!granted) throw new Error('User has refused'); 18 | return granted; 19 | }); 20 | } 21 | throw new Error('User has refused the dialog'); 22 | })); 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/mixins/utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | filters: { 3 | bytes(nb) { 4 | if (!nb || Number.isNaN(parseFloat(nb)) || !Number.isFinite(nb)) return '-'; 5 | const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; 6 | const idx = Math.floor(Math.log(nb) / Math.log(1024)); 7 | return `${(nb / (1024 ** Math.floor(idx))).toFixed(1)} ${units[idx]}`; 8 | }, 9 | truncate(string, nb) { 10 | if (!string) return ''; 11 | const trimmed = string.trim(); 12 | if (trimmed.length < nb) { 13 | return trimmed; 14 | } 15 | return `${trimmed.substring(0, nb)}...`; 16 | }, 17 | filename(path) { 18 | if (path) { 19 | return path.substring(path.lastIndexOf(path.indexOf('/') > -1 ? '/' : '\\') + 1); 20 | } 21 | return ''; 22 | }, 23 | }, 24 | methods: { 25 | getFavicon(url, size) { 26 | if (!size && (url.indexOf('https://') === 0 || url.indexOf('http://') === 0)) { 27 | return `https://www.google.com/s2/favicons?domain_url=${encodeURI(url)}`; 28 | } 29 | if (size) { 30 | return `https://api.faviconkit.com/${encodeURI(url)}/${size}`; 31 | } 32 | return null; 33 | }, 34 | shuffle(f) { 35 | return f.map(a => [Math.random(), a]).sort((a, b) => a[0] - b[0]).map(a => a[1]); 36 | }, 37 | gotTo(url) { 38 | const payload = { permissions: ['tabs'] }; 39 | browser.permissions.contains(payload) 40 | .then(res => res || browser.permissions.request(payload)) 41 | .then(() => { 42 | browser.tabs.update({ url }); 43 | }); 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import { loadLang } from '@/i18n'; 4 | import store from '@/store'; 5 | 6 | const Header = () => import(/* webpackChunkName: "main" */ '@/components/Header'); 7 | const Home = () => import(/* webpackChunkName: "main" */ '@/components/Home'); 8 | const Settings = () => import(/* webpackChunkName: "settings" */ '@/components/Settings'); 9 | const OnBoarding = () => import(/* webpackChunkName: "onboarding" */ '@/components/Onboarding'); 10 | 11 | Vue.use(Router); 12 | 13 | const router = new Router({ 14 | mode: 'abstract', 15 | routes: [ 16 | { 17 | path: '/', 18 | name: 'home', 19 | components: { 20 | default: Home, 21 | header: Header, 22 | }, 23 | }, 24 | { 25 | path: '/settings', 26 | name: 'settings', 27 | components: { 28 | default: Settings, 29 | header: Header, 30 | }, 31 | }, 32 | { 33 | path: '/onboarding', 34 | name: 'onboarding', 35 | components: { 36 | default: OnBoarding, 37 | header: Header, 38 | }, 39 | }, 40 | ], 41 | }); 42 | 43 | const checkTutorial = (to, next) => { 44 | const { tutorial } = store.state.settings; 45 | if (!tutorial && to.path !== '/onboarding') next('/onboarding'); 46 | else next(); 47 | }; 48 | 49 | router.beforeEach((to, from, next) => { 50 | // Hold the request, until storage is ready if necessary. 51 | store.restored 52 | // Load lang if necessary. 53 | .then(() => loadLang(store._vm, store.state.settings.lang)) 54 | .then(() => checkTutorial(to, next)); 55 | }); 56 | 57 | router.replace('/'); 58 | 59 | export default router; 60 | -------------------------------------------------------------------------------- /src/store/cache.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | version: null, 3 | cards: {}, 4 | validCards: [], 5 | trends: { 6 | data: [], 7 | dt: null, 8 | }, 9 | doodle: { 10 | data: {}, 11 | dt: null, 12 | }, 13 | backgroundLocal: { 14 | filename: null, 15 | dataUrl: '', 16 | }, 17 | google: { 18 | accessToken: null, 19 | refreshToken: null, 20 | exp: 0, 21 | }, 22 | }; 23 | 24 | export default { 25 | state: defaultState, 26 | mutations: { 27 | SET_VERSION(state, version) { 28 | state.version = version; 29 | }, 30 | SET_CARD_CACHE(state, { key, data }) { 31 | if (!data || !Object.keys(data).length) return; 32 | state.cards[key] = Object.assign({}, data, { CACHE_DT: Date.now() }); 33 | }, 34 | DEL_CARD_CACHE(state, key) { 35 | if (state.cards[key]) delete state.cards[key]; 36 | }, 37 | DEL_CARDS_CACHE(state) { 38 | state.cards = {}; 39 | }, 40 | SET_TRENDS_CACHE(state, trends) { 41 | state.trends.dt = trends && trends.length ? Date.now() : null; 42 | state.trends.data = trends; 43 | }, 44 | SET_DOODLE_CACHE(state, doodle) { 45 | state.doodle.dt = doodle && doodle.url ? Date.now() : null; 46 | state.doodle.data = doodle; 47 | }, 48 | ADD_VALID_CARD(state, key) { 49 | if (state.validCards.indexOf(key) > -1) return; 50 | state.validCards.push(key); 51 | }, 52 | DEL_VALID_CARD(state, key) { 53 | state.validCards = state.validCards.filter(f => f !== key); 54 | }, 55 | SET_BACKGROUND_LOCAL(state, data) { 56 | state.backgroundLocal = data; 57 | }, 58 | SET_GOOGLE(state, data) { 59 | state.google = { ...state.google, ...data }; 60 | }, 61 | DEL_GOOGLE(state) { 62 | state.google = defaultState.google; 63 | }, 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /src/store/cards.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: [], 3 | mutations: { 4 | SET_CARDS(state, cards) { 5 | state.length = 0; 6 | for (let i = 0; i < cards.length; i += 1) state.push(cards[i]); 7 | }, 8 | ADD_CARD(state, card) { 9 | state.push(card); 10 | }, 11 | ADD_CARD_FIRST(state, card) { 12 | state.unshift(card); 13 | }, 14 | DEL_CARD(state, card) { 15 | const idx = state.indexOf(card); 16 | if (idx > -1) { 17 | state.splice(idx, 1); 18 | } 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/cards_settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: { 3 | cards: {}, 4 | }, 5 | mutations: { 6 | SET_CARD_SETTINGS(state, { key, data }) { 7 | state.cards[key] = data; 8 | }, 9 | DEL_CARD_SETTINGS(state, key) { 10 | if (state.cards[key]) delete state.cards[key]; 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import VuexPersistence from 'vuex-persist'; 4 | import cards from './cards'; 5 | import settings from './settings'; 6 | import cache from './cache'; 7 | import cardsSettings from './cards_settings'; 8 | 9 | if (window.__PRERENDER_INJECTED) { 10 | window.browser = { 11 | storage: { 12 | sync: { 13 | get: () => Promise.resolve({ vuex: '{"settings":{"tutorial":true}}' }), 14 | }, 15 | local: { 16 | get: () => Promise.resolve({}), 17 | }, 18 | }, 19 | identity: { 20 | getRedirectURL: () => '', 21 | }, 22 | }; 23 | } else { 24 | window.browser = require('webextension-polyfill'); // eslint-disable-line 25 | } 26 | 27 | Vue.use(Vuex); 28 | 29 | const vuexSync = new VuexPersistence({ 30 | asyncStorage: true, 31 | modules: ['settings', 'cards', 'cardsSettings'], 32 | storage: { 33 | getItem: key => browser.storage.sync.get(key).then(data => data[key]), 34 | setItem: (key, value) => browser.storage.sync.set({ [key]: value }), 35 | removeItem: key => browser.storage.sync.remove(key), 36 | clear: () => browser.storage.sync.clear(), 37 | }, 38 | }); 39 | 40 | const vuexLocal = new VuexPersistence({ 41 | asyncStorage: true, 42 | modules: ['cache'], 43 | storage: { 44 | getItem: key => browser.storage.local.get(key).then(data => data[key]), 45 | setItem: (key, value) => browser.storage.local.set({ [key]: value }), 46 | removeItem: key => browser.storage.local.remove(key), 47 | clear: () => browser.storage.local.clear(), 48 | }, 49 | }); 50 | 51 | export default new Vuex.Store({ 52 | modules: { 53 | cards, 54 | settings, 55 | cardsSettings, 56 | cache, 57 | }, 58 | plugins: [ 59 | vuexSync.plugin, 60 | vuexLocal.plugin, 61 | ], 62 | }); 63 | -------------------------------------------------------------------------------- /src/store/settings.js: -------------------------------------------------------------------------------- 1 | const locales = Langs.map(e => e.locale); 2 | const langs = navigator.languages.filter(f => locales.indexOf(f) > -1); 3 | 4 | const initialState = { 5 | dark: { 6 | enabled: true, 7 | auto: true, 8 | from: '21:00', 9 | to: '9:00', 10 | }, 11 | trends: { 12 | enabled: true, 13 | country: 'france', 14 | }, 15 | doodle: { 16 | enabled: false, 17 | }, 18 | header: { 19 | design: 'full', 20 | background: 'random', 21 | backgroundUrl: '', 22 | customMessage: false, 23 | message: '', 24 | }, 25 | theme: { 26 | primary: '#607D8B', 27 | secondary: '#546E7A', 28 | light: false, 29 | customFont: false, 30 | font: '', 31 | customCssUrl: '', 32 | }, 33 | lang: langs[0] || 'en', 34 | hour24: false, 35 | tutorial: false, 36 | debug: false, 37 | whatsnew: true, 38 | donate: 500, 39 | }; 40 | 41 | export default { 42 | state: initialState, 43 | mutations: { 44 | SET_SETTINGS(state, data) { 45 | const keys = Object.keys(data); 46 | for (let i = 0; i < keys.length; i += 1) { 47 | if (state[keys[i]] !== undefined) state[keys[i]] = data[keys[i]]; 48 | } 49 | }, 50 | RESET_SETTINGS(state) { 51 | const keys = Object.keys(initialState); 52 | for (let i = 0; i < keys.length; i += 1) { 53 | state[keys[i]] = initialState[keys[i]]; 54 | } 55 | }, 56 | RESET_SETTING(state, key) { 57 | state[key] = initialState[key]; 58 | }, 59 | SET_TUTORIAL(state, tutorial) { 60 | state.tutorial = tutorial; 61 | }, 62 | SET_WHATSNEW(state, whatsnew) { 63 | state.whatsnew = whatsnew; 64 | }, 65 | DECREASE_DONATE(state) { 66 | state.donate -= 1; 67 | }, 68 | RESET_DONATE(state) { 69 | state.donate = initialState.donate; 70 | }, 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Roboto, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | color: #2c3e50; 6 | } 7 | 8 | a { 9 | text-decoration: none; 10 | } 11 | 12 | p { 13 | margin: 0; 14 | } 15 | 16 | .theme--dark { 17 | ::-webkit-scrollbar-thumb { 18 | background: #ddd; 19 | } 20 | ::-webkit-scrollbar-track { 21 | background: #666; 22 | } 23 | } 24 | ::-webkit-scrollbar { 25 | width: 5px; 26 | } 27 | ::-webkit-scrollbar-track { 28 | background: #ddd; 29 | } 30 | ::-webkit-scrollbar-thumb { 31 | background: #666; 32 | } 33 | --------------------------------------------------------------------------------