├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── frontend ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .postcssrc.js ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── quasar.conf.js └── src │ ├── App.vue │ ├── assets │ ├── flipper-screen-updating.png │ └── flipper.svg │ ├── components │ ├── ProgressBar.vue │ └── Updater.vue │ ├── css │ ├── app.sass │ └── quasar.variables.sass │ ├── flipper │ ├── core.js │ ├── protobuf │ │ ├── commands │ │ │ ├── core.js │ │ │ ├── gui.js │ │ │ ├── storage.js │ │ │ └── system.js │ │ ├── proto-compiled.js │ │ ├── rpc.js │ │ └── xbms.js │ ├── serial.js │ ├── util.js │ └── workers │ │ └── webSerial.js │ ├── index.template.html │ ├── layouts │ ├── PacksLayout.vue │ └── UpdateLayout.vue │ ├── pages │ ├── Packs.vue │ └── Update.vue │ ├── quasar.d.ts │ ├── router │ ├── index.js │ └── routes.js │ ├── untar │ ├── ProgressivePromise.js │ ├── untar-worker.js │ └── untar.js │ └── util │ └── util.js └── public ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-touch-icon.png ├── asset-packs-frame └── index.html ├── asset-packs └── index.html ├── browserconfig.xml ├── css ├── animate.css ├── app.d4a8c1ed.css ├── bootstrap.min.css ├── ionicons.min.css ├── responsive.css └── vendor.9b29db89.css ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts ├── KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff ├── KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff ├── KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff ├── KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff ├── KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff ├── KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.0383092b.woff2 ├── ionicons.eot ├── ionicons.svg ├── ionicons.ttf ├── ionicons.woff ├── materialdesignicons-webfont.d8e8e0f7.woff └── materialdesignicons-webfont.e9db4005.woff2 ├── icon.png ├── img ├── hero-background.png └── hero-image.png ├── index.html ├── js ├── 144.36e4dd28.js ├── 254.3e4a3252.js ├── 665.e7cdfcdd.js ├── 842.c344dabf.js ├── 85.1db80424.js ├── 890.065c7545.js ├── active.js ├── app.76009145.js ├── bootstrap.min.js ├── chunk-common.4e59ed1c.js ├── jquery-3.3.1.min.js ├── plugins.js ├── popper.min.js └── vendor.52055a46.js ├── logo-collapsed.png ├── logo.png ├── mstile-150x150.png ├── robots.txt ├── safari-pinned-tab.svg ├── site.webmanifest ├── style.css ├── update-frame └── index.html └── update └── index.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: willyjl 2 | custom: ["https://paypal.me/willyjl1"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | dist 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | 21 | # Log files of my broken-af editor 22 | log.txt 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend/flipperzero-protobuf"] 2 | path = frontend/flipperzero-protobuf 3 | url = https://github.com/flipperdevices/flipperzero-protobuf.git 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | 5 | "editorconfig.editorconfig", 6 | "johnsoncodehk.volar", 7 | "wayou.vscode-todo-highlight" 8 | ], 9 | "unwantedRecommendations": [ 10 | "octref.vetur", 11 | "hookyqr.beautify", 12 | "dbaeumer.jshint", 13 | "ms-vscode.vscode-typescript-tslint-plugin" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.guides.bracketPairs": true, 4 | "editor.formatOnSave": false, 5 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 6 | "editor.codeActionsOnSave": [ 7 | "source.fixAll.eslint" 8 | ], 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "vue" 14 | ], 15 | "[json]": { 16 | "editor.defaultFormatter": "vscode.json-language-features" 17 | }, 18 | "[xml]": { 19 | "editor.defaultFormatter": "redhat.vscode-xml" 20 | }, 21 | 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Source-code for our website momentum-fw.dev

2 | 3 | ### Structure: 4 | 5 | This project is structured into two folders: 6 | 7 | 1. [Public](https://github.com/Next-Flip/Momentum-Website/tree/main/public): 8 | 9 | The `public` folder is what you see when visiting the website. It's the compiled files for the webupdater & asset packs, and all the other static pages. 10 | 11 | 12 | 2. [frontend](https://github.com/Next-Flip/Momentum-Website/tree/main/frontend): 13 | 14 | The `frontend` folder contains the sourcecode for the webupdater & asset pack page. This will be compiled, then put into the public static assets. 15 | 16 | ### Development workflow: 17 | 18 | The webupdater and asset-packs page use [Vue](https://github.com/vuejs/) to handle state (like handling Flipper via serial) and are integrated into the rest of the static pages. 19 | 20 | 1. Install dependencies: 21 | 22 | - For Fedora: 23 | ```console 24 | dnf install tzdata npm 25 | ``` 26 | - For Ubuntu: 27 | ```console 28 | apt install tzdata npm 29 | ``` 30 | - For alpine: 31 | ```console 32 | apk install tzdata npm 33 | ``` 34 | 35 | 2. Build with these commands in the `frontend` directory: 36 | ```console 37 | npm i 38 | # If there is any vulnerabilities ALWAYS run "npm audit fix". Dont be lazy! 39 | quasar build -m spa 40 | ``` 41 | 42 | 3. Finally, copy the built files over into the `public` folder. You can distinguish generated files by the random filenames. Also you'll need to copy the main index page into the frame directories. Now you can serve it with any webserver software. 43 | 44 | ### Credits: 45 | 46 | The core backend for the webupdater and the asset packs (as in, the Vue part) is largely built on the base of [lab.flipper.net](https://github.com/flipperdevices/lab.flipper.net). 47 | 48 |

49 | 50 | ----- 51 | 52 | ## ❤️ Support 53 | If you enjoy the firmware please __**spread the word!**__ And if you really love it, maybe consider donating to the team? :D 54 | 55 | > **[Ko-fi](https://ko-fi.com/willyjl)**: One-off or Recurring, No signup required 56 | 57 | > **[PayPal](https://paypal.me/willyjl1)**: One-off, Signup required 58 | 59 | > **BTC**: `1EnCi1HF8Jw6m2dWSUwHLbCRbVBCQSyDKm` 60 | 61 | **Thank you <3** 62 | -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /src-bex/www 3 | /src-capacitor 4 | /src-cordova 5 | /.quasar 6 | /node_modules 7 | .eslintrc.js 8 | babel.config.js 9 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy 3 | // This option interrupts the configuration hierarchy at this file 4 | // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos) 5 | root: true, 6 | 7 | parserOptions: { 8 | parser: '@babel/eslint-parser', 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module' // Allows for the use of imports 11 | }, 12 | 13 | env: { 14 | browser: true 15 | }, 16 | 17 | // Rules order is important, please avoid shuffling them 18 | extends: [ 19 | // Base ESLint recommended rules 20 | // 'eslint:recommended', 21 | 22 | 23 | // Uncomment any of the lines below to choose desired strictness, 24 | // but leave only one uncommented! 25 | // See https://eslint.vuejs.org/rules/#available-rules 26 | 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention) 27 | // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability) 28 | // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 29 | 30 | 'standard' 31 | 32 | ], 33 | 34 | plugins: [ 35 | // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files 36 | // required to lint *.vue files 37 | 'vue', 38 | 39 | ], 40 | 41 | globals: { 42 | ga: 'readonly', // Google Analytics 43 | cordova: 'readonly', 44 | __statics: 'readonly', 45 | __QUASAR_SSR__: 'readonly', 46 | __QUASAR_SSR_SERVER__: 'readonly', 47 | __QUASAR_SSR_CLIENT__: 'readonly', 48 | __QUASAR_SSR_PWA__: 'readonly', 49 | process: 'readonly', 50 | Capacitor: 'readonly', 51 | chrome: 'readonly' 52 | }, 53 | 54 | // add your custom rules here 55 | rules: { 56 | // allow async-await 57 | 'generator-star-spacing': 'off', 58 | // allow paren-less arrow functions 59 | 'arrow-parens': 'off', 60 | 'one-var': 'off', 61 | 'no-void': 'off', 62 | 'multiline-ternary': 'off', 63 | 64 | 'import/first': 'off', 65 | 'import/named': 'error', 66 | 'import/namespace': 'error', 67 | 'import/default': 'error', 68 | 'import/export': 'error', 69 | 'import/extensions': 'off', 70 | 'import/no-unresolved': 'off', 71 | 'import/no-extraneous-dependencies': 'off', 72 | 'prefer-promise-reject-errors': 'off', 73 | 74 | 75 | // allow debugger during development only 76 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = api => { 4 | return { 5 | presets: [ 6 | [ 7 | '@quasar/babel-preset-app', 8 | api.caller(caller => caller && caller.target === 'node') 9 | ? { targets: { node: 'current' } } 10 | : {} 11 | ] 12 | ] 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": [ 6 | "src/*" 7 | ], 8 | "app/*": [ 9 | "*" 10 | ], 11 | "components/*": [ 12 | "src/components/*" 13 | ], 14 | "layouts/*": [ 15 | "src/layouts/*" 16 | ], 17 | "pages/*": [ 18 | "src/pages/*" 19 | ], 20 | "assets/*": [ 21 | "src/assets/*" 22 | ], 23 | "vue$": [ 24 | "node_modules/vue/dist/vue.runtime.esm-bundler.js" 25 | ] 26 | } 27 | }, 28 | "exclude": [ 29 | "dist", 30 | ".quasar", 31 | "node_modules" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "momentum-fw.dev", 3 | "version": "1.0", 4 | "description": "Web-updater and asset pack downloader for momentum-fw.dev", 5 | "productName": "momentum-fw.dev", 6 | "author": "Next-Flip", 7 | "private": true, 8 | "scripts": { 9 | "lint": "eslint --ext .js,.vue ./", 10 | "dev/web": "quasar dev", 11 | "dev/electron": "quasar dev -m electron", 12 | "build/web": "quasar build", 13 | "build/electron": "quasar build -m electron", 14 | "compile-protofiles": "npx pbjs -t static-module -w es6 --no-comments --lint \"eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars, camelcase, default-case-last, no-mixed-operators\" -o src/flipper/protobuf/proto-compiled.js ./flipperzero-protobuf/*.proto && eslint --fix src/flipper/protobuf/proto-compiled.js" 15 | }, 16 | "dependencies": { 17 | "@quasar/cli": "^2.1.1", 18 | "@quasar/extras": "^1.14.0", 19 | "core-js": "^3.6.5", 20 | "loglevel": "^1.8.0", 21 | "nanoevents": "^6.0.2", 22 | "pako": "^2.0.4", 23 | "protobufjs": "~6.11.2", 24 | "quasar": "^2.7.1", 25 | "semver": "^7.5.2", 26 | "serialport": "^10.3.0", 27 | "sha.js": "^2.4.11", 28 | "simple-async-sleep": "^1.0.3", 29 | "socket.io-client": "^4.5.1", 30 | "vue": "^3.0.0", 31 | "vue-router": "^4.0.0", 32 | "xterm": "^5.0.0", 33 | "xterm-addon-fit": "^0.6.0", 34 | "xterm-addon-serialize": "^0.8.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/eslint-parser": "^7.13.14", 38 | "@quasar/app-webpack": "^3.5.3", 39 | "eslint": "^7.14.0", 40 | "eslint-config-standard": "^16.0.2", 41 | "eslint-plugin-import": "^2.19.1", 42 | "eslint-plugin-node": "^11.0.0", 43 | "eslint-plugin-promise": "^5.1.0", 44 | "eslint-plugin-vue": "^7.0.0", 45 | "eslint-webpack-plugin": "^2.4.0" 46 | }, 47 | "browserslist": [ 48 | "last 10 Chrome versions", 49 | "last 10 Firefox versions", 50 | "last 4 Edge versions", 51 | "last 7 Safari versions", 52 | "last 8 Android versions", 53 | "last 8 ChromeAndroid versions", 54 | "last 8 FirefoxAndroid versions", 55 | "last 10 iOS versions", 56 | "last 5 Opera versions" 57 | ], 58 | "engines": { 59 | "node": ">= 12.22.1", 60 | "npm": ">= 6.13.4", 61 | "yarn": ">= 1.21.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | */ 5 | 6 | // Configuration for your app 7 | // https://quasar.dev/quasar-cli/quasar-conf-js 8 | 9 | /* eslint-env node */ 10 | const ESLintPlugin = require('eslint-webpack-plugin') 11 | const { configure } = require('quasar/wrappers') 12 | 13 | module.exports = configure(function (ctx) { 14 | return { 15 | // https://quasar.dev/quasar-cli/supporting-ts 16 | supportTS: false, 17 | 18 | // https://quasar.dev/quasar-cli/prefetch-feature 19 | // preFetch: true, 20 | 21 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 22 | css: [ 23 | 'app.sass' 24 | ], 25 | 26 | // https://github.com/quasarframework/quasar/tree/dev/extras 27 | extras: [ 28 | // 'ionicons-v4', 29 | // 'mdi-v5', 30 | // 'fontawesome-v5', 31 | // 'eva-icons', 32 | // 'themify', 33 | // 'line-awesome', 34 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 35 | 36 | 'roboto-font', // optional, you are not bound to it 37 | 'material-icons', // optional, you are not bound to it 38 | 'mdi-v5' 39 | ], 40 | 41 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 42 | build: { 43 | vueRouterMode: 'history', // available values: 'hash', 'history' 44 | 45 | // transpile: false, 46 | // publicPath: '/', 47 | 48 | // Add dependencies for transpiling with Babel (Array of string/regex) 49 | // (from node_modules, which are by default not transpiled). 50 | // Applies only if "transpile" is set to true. 51 | // transpileDependencies: [], 52 | 53 | // rtl: true, // https://quasar.dev/options/rtl-support 54 | // preloadChunks: true, 55 | // showProgress: false, 56 | // gzip: true, 57 | // analyze: true, 58 | 59 | // Options below are automatically set depending on the env, set them if you want to override 60 | // extractCSS: false, 61 | 62 | // https://quasar.dev/quasar-cli/handling-webpack 63 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 64 | chainWebpack (chain) { 65 | chain.plugin('eslint-webpack-plugin') 66 | .use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]) 67 | } 68 | }, 69 | 70 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 71 | devServer: { 72 | server: { 73 | type: 'http' 74 | }, 75 | port: 8080, 76 | open: false // opens browser window automatically 77 | }, 78 | 79 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 80 | framework: { 81 | config: {}, 82 | 83 | // iconSet: 'material-icons', // Quasar icon set 84 | // lang: 'en-US', // Quasar language pack 85 | 86 | // For special cases outside of where the auto-import strategy can have an impact 87 | // (like functional components as one of the examples), 88 | // you can manually specify Quasar components/directives to be available everywhere: 89 | // 90 | // components: [], 91 | // directives: [], 92 | 93 | // Quasar plugins 94 | plugins: [ 95 | 'Notify' 96 | ] 97 | }, 98 | 99 | // animations: 'all', // --- includes all animations 100 | // https://quasar.dev/options/animations 101 | animations: [], 102 | 103 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 104 | ssr: { 105 | pwa: false, 106 | 107 | // manualStoreHydration: true, 108 | // manualPostHydrationTrigger: true, 109 | 110 | prodPort: 3000, // The default port that the production server should use 111 | // (gets superseded if process.env.PORT is specified at runtime) 112 | 113 | maxAge: 1000 * 60 * 60 * 24 * 30, 114 | // Tell browser when a file from the server should expire from cache (in ms) 115 | 116 | chainWebpackWebserver (chain) { 117 | chain.plugin('eslint-webpack-plugin') 118 | .use(ESLintPlugin, [{ extensions: ['js'] }]) 119 | }, 120 | 121 | middlewares: [ 122 | ctx.prod ? 'compression' : '', 123 | 'render' // keep this as last one 124 | ] 125 | }, 126 | 127 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 128 | cordova: { 129 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 130 | }, 131 | 132 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 133 | capacitor: { 134 | hideSplashscreen: true 135 | }, 136 | 137 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 138 | electron: { 139 | bundler: 'packager', // 'packager' or 'builder' 140 | 141 | packager: { 142 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 143 | 144 | // OS X / Mac App Store 145 | // appBundleId: '', 146 | // appCategoryType: '', 147 | // osxSign: '', 148 | // protocol: 'myapp://path', 149 | 150 | // Windows only 151 | // win32metadata: { ... } 152 | }, 153 | 154 | builder: { 155 | // https://www.electron.build/configuration/configuration 156 | 157 | appId: 'momentum-fw.dev' 158 | }, 159 | 160 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 161 | chainWebpackMain (chain) { 162 | chain.plugin('eslint-webpack-plugin') 163 | .use(ESLintPlugin, [{ extensions: ['js'] }]) 164 | }, 165 | 166 | // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain 167 | chainWebpackPreload (chain) { 168 | chain.plugin('eslint-webpack-plugin') 169 | .use(ESLintPlugin, [{ extensions: ['js'] }]) 170 | } 171 | } 172 | } 173 | }) 174 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 11 | -------------------------------------------------------------------------------- /frontend/src/assets/flipper-screen-updating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/frontend/src/assets/flipper-screen-updating.png -------------------------------------------------------------------------------- /frontend/src/assets/flipper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 21 | 23 | 30 | 34 | 38 | 42 | 43 | 47 | 49 | 53 | 54 | 56 | 57 | 59 | 63 | 64 | 66 | 70 | 71 | 73 | 77 | 78 | 80 | 84 | 85 | 87 | 91 | 92 | 94 | 98 | 99 | 101 | 105 | 106 | 108 | 112 | 113 | 115 | 119 | 120 | 122 | 126 | 127 | 129 | 133 | 134 | 136 | 140 | 141 | 143 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /frontend/src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/Updater.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 371 | -------------------------------------------------------------------------------- /frontend/src/css/app.sass: -------------------------------------------------------------------------------- 1 | body 2 | color: white!important 3 | background-color: #000000 !important 4 | 5 | ::-webkit-scrollbar 6 | width: 16px 7 | background: transparent 8 | 9 | ::-webkit-scrollbar-track 10 | background: transparent 11 | border: solid 3px transparent 12 | overflow: auto 13 | margin: 14px 0px 14 | 15 | ::-webkit-scrollbar-thumb 16 | background: #666 17 | border-radius: 16px 18 | min-height: 24px 19 | overflow: auto 20 | border: 6px solid transparent 21 | background-clip: padding-box 22 | transition: height 0.2s ease-in-out 23 | padding: 50px 24 | &:hover 25 | background-color: #bbb 26 | 27 | .device-screen 28 | .info 29 | margin: 16px 3rem 0 0 30 | p 31 | display: flex 32 | flex-wrap: nowrap 33 | justify-content: space-between 34 | font-family: monospace 35 | margin: 0 0 12px 0 36 | font-family: monospace 37 | font-size: 16px 38 | span:first-of-type 39 | margin-right: 1.5rem 40 | font-weight: 600 41 | .flipper 42 | width: 368px 43 | height: 165px 44 | padding: 31px 0 0 97px 45 | background-size: 360px 46 | background-repeat: no-repeat 47 | background-position: center 48 | background-image: url(../assets/flipper.svg) 49 | h5 50 | text-align: center 51 | margin: 25px 0px 0px 0px 52 | width: 71% 53 | color: #f58235 54 | font-weight: 600 55 | transform: skew(-9deg, 0deg) scale(1, 0.9) 56 | canvas 57 | transform: scale(1) 58 | .updater 59 | margin: 1.5rem 0 60 | p 61 | margin: 0 0 8px 62 | div.flex 63 | flex-direction: row 64 | 65 | @media (max-width: 750px) 66 | margin-top: 2rem 67 | & > .flex 68 | flex-direction: column-reverse 69 | .info 70 | width: 275px 71 | margin: 2rem auto 0 auto 72 | @media (max-width: 599px) 73 | .updater div.flex 74 | flex-direction: column 75 | @media (max-width: 380px) 76 | .flipper 77 | width: 300px 78 | height: 133px 79 | padding: 18px 0 0 67px 80 | background-size: 300px 81 | canvas 82 | transform: scale(0.85) 83 | 84 | .fw-option-label 85 | width: fit-content 86 | padding: 0.2rem 0.4rem 87 | border-radius: 5px 88 | background-color: $primary !important 89 | 90 | .q-btn.main-btn 91 | color: white 92 | border: 2px solid $primary 93 | border-radius: 18px 94 | transition-duration: 500ms 95 | overflow: hidden 96 | padding: 10px 20px 97 | &:not(.disabled):hover 98 | background-color: $primary 99 | &.attention 100 | border-color: $attention 101 | color: $attention 102 | &:not(.disabled):hover 103 | color: white 104 | background-color: $attention 105 | &.negative 106 | border-color: $negative 107 | color: $negative 108 | &:not(.disabled):hover 109 | color: white 110 | background-color: $negative 111 | 112 | .q-btn.flat-btn 113 | color: white 114 | transition-duration: 500ms 115 | min-height: unset 116 | &:hover 117 | color: $primary 118 | .q-focus-helper 119 | display: none 120 | 121 | .q-select 122 | color: white 123 | border: 2px solid white 124 | border-radius: 24px 125 | transition-duration: 500ms 126 | &:hover, &:active, &:focus-within 127 | border-color: $primary 128 | >* 129 | >* 130 | padding: 0px 7px 0px 14px 131 | >:last-of-type 132 | padding-left: 0px 133 | .q-field__native 134 | >* 135 | padding: 0.2rem 0.4rem 136 | &#fw-select 137 | >* 138 | width: fit-content 139 | border-radius: 5px 140 | background-color: $primary !important 141 | 142 | .q-input 143 | color: white 144 | border: 2px solid white 145 | border-radius: 14px 146 | transition-duration: 500ms 147 | padding: 0 8px 0 8px 148 | &:hover, &:active, &:focus-within 149 | border-color: $primary 150 | 151 | #changelog 152 | font-size: 16px 153 | background-color: #111 154 | padding: 0px 30px 155 | border-radius: 24px 156 | border: 2px solid #bbb 157 | width: 690px 158 | min-height: 320px 159 | height: calc(100vh - 400px) 160 | overflow-x: hidden 161 | overflow-y: scroll 162 | h1, h2, h3, h4, h5 163 | font-size: 24px 164 | margin: 0 165 | text-decoration: underline 166 | text-decoration-color: $primary !important 167 | a 168 | display: contents 169 | color: $primary 170 | code 171 | background: #333 172 | border-radius: 5px 173 | padding: 0.1em 0.3em 174 | ::marker 175 | color: $primary 176 | >*:first-child 177 | padding-top: 20px 178 | >*:last-child 179 | padding-bottom: 20px 180 | 181 | .packs-grid 182 | display: flex 183 | flex-wrap: wrap 184 | justify-content: center 185 | max-width: 1000px 186 | .q-card 187 | width: 300px 188 | margin: 12px 189 | border-radius: 24px 190 | border: 2px solid #bbb 191 | cursor: pointer 192 | .q-carousel 193 | aspect-ratio: 128/64 194 | width: 100% 195 | height: auto 196 | .q-carousel__prev-arrow 197 | left: 6px 198 | .q-carousel__next-arrow 199 | right: 6px 200 | .q-carousel__navigation 201 | bottom: 0px 202 | .q-carousel__control 203 | opacity: 0 204 | transition: opacity 0.25s ease-out 205 | &:hover 206 | .q-carousel__control 207 | opacity: 0.69 208 | -------------------------------------------------------------------------------- /frontend/src/css/quasar.variables.sass: -------------------------------------------------------------------------------- 1 | $primary : #a883e9 2 | $attention : #fd923f 3 | 4 | $negative : #f05145 5 | $info : #31ccEc 6 | -------------------------------------------------------------------------------- /frontend/src/flipper/core.js: -------------------------------------------------------------------------------- 1 | import { createNanoEvents } from 'nanoevents' 2 | const emitter = createNanoEvents() 3 | import { connect, disconnect, write, read, closeReader } from './serial' 4 | import * as commands from './protobuf/commands/core' 5 | 6 | export { 7 | emitter, 8 | connect, 9 | disconnect, 10 | write, 11 | read, 12 | closeReader, 13 | commands 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/flipper/protobuf/commands/core.js: -------------------------------------------------------------------------------- 1 | import * as rpc from '../rpc' 2 | import asyncSleep from 'simple-async-sleep' 3 | import { emitter } from '../../core' 4 | 5 | import * as system from './system' 6 | import * as storage from './storage' 7 | import * as gui from './gui' 8 | 9 | let flipper, rpcIdle = true 10 | const commandQueue = [] 11 | 12 | function enqueue (c) { 13 | commandQueue.push(c) 14 | if (rpcIdle) { 15 | sendRpcRequest() 16 | } 17 | } 18 | 19 | async function sendRpcRequest () { 20 | rpcIdle = false 21 | 22 | while (commandQueue.length) { 23 | const c = commandQueue[0] 24 | 25 | const req = rpc.createRequest(c.requestType, c.args, c.hasNext, c.commandId) 26 | await flipper.write('raw', req.data) 27 | 28 | let res = { commandId: req.commandId } 29 | if (!c.hasNext && c.requestType !== 'stopSession') { 30 | let buffer = new Uint8Array(0) 31 | const unbind = emitter.on('raw output', data => { 32 | const newBuffer = new Uint8Array(buffer.length + data.length) 33 | newBuffer.set(buffer) 34 | newBuffer.set(data, buffer.length) 35 | buffer = newBuffer 36 | try { 37 | res = rpc.parseResponse(buffer) 38 | if (res) { 39 | buffer = new Uint8Array(0) 40 | if (res.commandId === 0) { 41 | emitter.emit('screen frame', res.data) 42 | } else { 43 | emitter.emit('response', res) 44 | } 45 | } 46 | } catch (error) { 47 | if (!(error.toString().includes('index out of range'))) { 48 | if (error.toString().includes('invalid wire type')) { 49 | emitter.emit('restart session') 50 | unbind() 51 | } else { 52 | throw error 53 | } 54 | } 55 | } 56 | }) 57 | const unbindStop = emitter.on('stop screen streaming', () => { 58 | unbind() 59 | unbindStop() 60 | }) 61 | } else { 62 | const unbind = emitter.on('write/end', () => { 63 | emitter.emit('response', res) 64 | unbind() 65 | }) 66 | } 67 | commandQueue.shift() 68 | } 69 | 70 | rpcIdle = true 71 | } 72 | 73 | async function startRpcSession (f) { 74 | flipper = f 75 | await asyncSleep(500) 76 | await flipper.write('cli', 'start_rpc_session\r') 77 | flipper.read('raw') 78 | await asyncSleep(500) 79 | return system.ping() 80 | } 81 | 82 | function stopRpcSession () { 83 | return new Promise((resolve) => { 84 | enqueue({ 85 | requestType: 'stopSession', 86 | args: {} 87 | }) 88 | const unbind = emitter.on('response', async () => { 89 | await asyncSleep(300) 90 | await flipper.closeReader() 91 | rpc.flushCommandQueue() 92 | resolve() 93 | unbind() 94 | }) 95 | }) 96 | } 97 | 98 | export { 99 | emitter, 100 | enqueue, 101 | startRpcSession, 102 | stopRpcSession, 103 | system, 104 | storage, 105 | gui 106 | } 107 | -------------------------------------------------------------------------------- /frontend/src/flipper/protobuf/commands/gui.js: -------------------------------------------------------------------------------- 1 | import { enqueue, emitter } from './core' 2 | 3 | function startVirtualDisplay (firstFrame) { 4 | return new Promise((resolve, reject) => { 5 | enqueue({ 6 | requestType: 'guiStartVirtualDisplayRequest', 7 | args: { 8 | firstFrame: firstFrame 9 | } 10 | }) 11 | const unbind = emitter.on('response', res => { 12 | if (res && res.error) { 13 | reject(res.error, res) 14 | } else { 15 | resolve(res) 16 | } 17 | unbind() 18 | }) 19 | }) 20 | } 21 | 22 | function stopVirtualDisplay () { 23 | return new Promise((resolve, reject) => { 24 | enqueue({ 25 | requestType: 'guiStopVirtualDisplayRequest', 26 | args: {} 27 | }) 28 | const unbind = emitter.on('response', res => { 29 | if (res && res.error) { 30 | reject(res.error, res) 31 | } else { 32 | resolve(res) 33 | } 34 | unbind() 35 | }) 36 | }) 37 | } 38 | 39 | function startScreenStreamRequest () { 40 | return new Promise((resolve, reject) => { 41 | enqueue({ 42 | requestType: 'guiStartScreenStreamRequest', 43 | args: {} 44 | }) 45 | const unbind = emitter.on('response', res => { 46 | if (res && res.error) { 47 | reject(res.error, res) 48 | } else { 49 | resolve(res) 50 | } 51 | unbind() 52 | }) 53 | }) 54 | } 55 | 56 | function stopScreenStreamRequest () { 57 | emitter.emit('stop screen streaming') 58 | return new Promise((resolve, reject) => { 59 | enqueue({ 60 | requestType: 'guiStopScreenStreamRequest', 61 | args: {} 62 | }) 63 | const unbind = emitter.on('response', res => { 64 | if (res && res.error) { 65 | reject(res.error, res) 66 | } else { 67 | resolve(res) 68 | } 69 | unbind() 70 | }) 71 | }) 72 | } 73 | 74 | function screenFrame (data) { 75 | return new Promise((resolve, reject) => { 76 | enqueue({ 77 | requestType: 'guiScreenFrame', 78 | args: { 79 | data: data 80 | } 81 | }) 82 | const unbind = emitter.on('response', res => { 83 | if (res && res.error) { 84 | reject(res.error, res) 85 | } else { 86 | resolve(res) 87 | } 88 | unbind() 89 | }) 90 | }) 91 | } 92 | 93 | function sendInputEvent (key, type) { 94 | return new Promise((resolve, reject) => { 95 | enqueue({ 96 | requestType: 'guiSendInputEventRequest', 97 | args: { 98 | key: key, 99 | type: type 100 | } 101 | }) 102 | const unbind = emitter.on('response', res => { 103 | if (res && res.error) { 104 | reject(res.error, res) 105 | } else { 106 | resolve(res) 107 | } 108 | unbind() 109 | }) 110 | }) 111 | } 112 | 113 | export { 114 | startVirtualDisplay, 115 | stopVirtualDisplay, 116 | startScreenStreamRequest, 117 | stopScreenStreamRequest, 118 | screenFrame, 119 | sendInputEvent 120 | } 121 | -------------------------------------------------------------------------------- /frontend/src/flipper/protobuf/commands/storage.js: -------------------------------------------------------------------------------- 1 | import { enqueue, emitter } from './core' 2 | 3 | function info (path) { 4 | return new Promise((resolve, reject) => { 5 | enqueue({ 6 | requestType: 'storageInfoRequest', 7 | args: { path: path } 8 | }) 9 | const unbind = emitter.on('response', res => { 10 | if (res && res.error) { 11 | reject(res.error, res) 12 | } else { 13 | resolve(res.chunks[0]) 14 | } 15 | unbind() 16 | }) 17 | }) 18 | } 19 | 20 | function list (path) { 21 | return new Promise((resolve, reject) => { 22 | enqueue({ 23 | requestType: 'storageListRequest', 24 | args: { path: path } 25 | }) 26 | const unbind = emitter.on('response', res => { 27 | if (res && res.error) { 28 | reject(res.error, res) 29 | } else { 30 | if (res.chunks && res.chunks.length) { 31 | let buffer = [] 32 | res.chunks.forEach(c => { 33 | buffer = buffer.concat(c.file) 34 | }) 35 | resolve(buffer) 36 | } 37 | resolve('empty response') 38 | } 39 | unbind() 40 | }) 41 | }) 42 | } 43 | 44 | function read (path) { 45 | return new Promise((resolve, reject) => { 46 | enqueue({ 47 | requestType: 'storageReadRequest', 48 | args: { path: path } 49 | }) 50 | const unbind = emitter.on('response', res => { 51 | if (res && res.error) { 52 | reject(res.error, res) 53 | } else { 54 | if (res.chunks.length) { 55 | let buffer = new Uint8Array(0) 56 | res.chunks.forEach(c => { 57 | const newBuffer = new Uint8Array(buffer.length + c.file.data.length) 58 | newBuffer.set(buffer) 59 | newBuffer.set(c.file.data, buffer.length) 60 | buffer = newBuffer 61 | }) 62 | resolve(buffer) 63 | } 64 | resolve('empty response') 65 | } 66 | unbind() 67 | }) 68 | }) 69 | } 70 | 71 | async function write (path, buffer) { 72 | let commandId, lastRes 73 | const file = new Uint8Array(buffer) 74 | for (let i = 0; i <= file.byteLength; i += 512) { 75 | const chunk = file.slice(i, i + 512) 76 | const write = new Promise((resolve, reject) => { 77 | enqueue({ 78 | requestType: 'storageWriteRequest', 79 | args: { path: path, file: { data: chunk } }, 80 | hasNext: chunk.byteLength === 512, 81 | commandId: commandId 82 | }) 83 | const unbind = emitter.on('response', res => { 84 | unbind() 85 | if (res && res.error) { 86 | reject(res.error, res) 87 | } else { 88 | resolve(res) 89 | } 90 | }) 91 | }) 92 | await write 93 | .then(res => { 94 | emitter.emit('storageWriteRequest/progress', { 95 | progress: Math.min(file.byteLength, (i + 512 - 1)), 96 | total: file.byteLength 97 | }) 98 | lastRes = res 99 | commandId = res.commandId 100 | }) 101 | } 102 | return lastRes 103 | } 104 | 105 | function mkdir (path) { 106 | return new Promise((resolve, reject) => { 107 | enqueue({ 108 | requestType: 'storageMkdirRequest', 109 | args: { path: path } 110 | }) 111 | const unbind = emitter.on('response', res => { 112 | if (res && res.error) { 113 | reject(res.error, res) 114 | } else { 115 | resolve(res) 116 | } 117 | unbind() 118 | }) 119 | }) 120 | } 121 | 122 | function remove (path, isRecursive) { 123 | return new Promise((resolve, reject) => { 124 | enqueue({ 125 | requestType: 'storageDeleteRequest', 126 | args: { path: path, recursive: isRecursive } 127 | }) 128 | const unbind = emitter.on('response', res => { 129 | if (res && res.error) { 130 | reject(res.error, res) 131 | } else { 132 | resolve(res) 133 | } 134 | unbind() 135 | }) 136 | }) 137 | } 138 | 139 | function rename (path, oldName, newName) { 140 | return new Promise((resolve, reject) => { 141 | enqueue({ 142 | requestType: 'storageRenameRequest', 143 | args: { oldPath: path + '/' + oldName, newPath: path + '/' + newName } 144 | }) 145 | const unbind = emitter.on('response', res => { 146 | if (res && res.error) { 147 | reject(res.error, res) 148 | } else { 149 | resolve(res) 150 | } 151 | unbind() 152 | }) 153 | }) 154 | } 155 | 156 | function stat (path) { 157 | return new Promise((resolve, reject) => { 158 | enqueue({ 159 | requestType: 'storageStatRequest', 160 | args: { path: path } 161 | }) 162 | const unbind = emitter.on('response', res => { 163 | if (res && res.error) { 164 | reject(res.error, res) 165 | } else { 166 | resolve(res.chunks) 167 | } 168 | unbind() 169 | }) 170 | }) 171 | } 172 | 173 | function tarExtract (tarPath, outPath) { 174 | return new Promise((resolve, reject) => { 175 | enqueue({ 176 | requestType: 'storageTarExtractRequest', 177 | args: { tarPath, outPath } 178 | }) 179 | const unbind = emitter.on('response', res => { 180 | if (res && res.error) { 181 | reject(res.error, res) 182 | } else { 183 | resolve(res) 184 | } 185 | unbind() 186 | }) 187 | }) 188 | } 189 | 190 | export { 191 | info, 192 | list, 193 | read, 194 | write, 195 | mkdir, 196 | remove, 197 | rename, 198 | stat, 199 | tarExtract 200 | } 201 | -------------------------------------------------------------------------------- /frontend/src/flipper/protobuf/commands/system.js: -------------------------------------------------------------------------------- 1 | import { enqueue, emitter } from './core' 2 | 3 | function ping () { 4 | return new Promise((resolve, reject) => { 5 | enqueue({ 6 | requestType: 'systemPingRequest', 7 | args: {} 8 | }) 9 | const unbind = emitter.on('response', res => { 10 | if (res && res.error) { 11 | reject(res.error, res) 12 | } else { 13 | resolve(res) 14 | } 15 | unbind() 16 | }) 17 | }) 18 | } 19 | 20 | function getDatetime () { 21 | return new Promise((resolve, reject) => { 22 | enqueue({ 23 | requestType: 'systemGetDatetimeRequest', 24 | args: {} 25 | }) 26 | const unbind = emitter.on('response', res => { 27 | if (res && res.error) { 28 | reject(res.error, res) 29 | } else { 30 | if (res.chunks && res.chunks[0] && res.chunks[0].datetime) { 31 | const dt = res.chunks[0].datetime 32 | resolve(new Date(dt.year, dt.month - 1, dt.day, dt.hour, dt.minute, dt.second)) 33 | } 34 | resolve('empty response') 35 | } 36 | unbind() 37 | }) 38 | }) 39 | } 40 | 41 | function setDatetime (date) { 42 | const datetime = { 43 | hour: date.getHours(), 44 | minute: date.getMinutes(), 45 | second: date.getSeconds(), 46 | day: date.getDate(), 47 | month: date.getMonth() + 1, 48 | year: date.getFullYear(), 49 | weekday: date.getDay() || 7 50 | } 51 | return new Promise((resolve, reject) => { 52 | enqueue({ 53 | requestType: 'systemSetDatetimeRequest', 54 | args: { 55 | datetime: datetime 56 | } 57 | }) 58 | const unbind = emitter.on('response', res => { 59 | if (res && res.error) { 60 | reject(res.error, res) 61 | } else { 62 | resolve(res) 63 | } 64 | unbind() 65 | }) 66 | }) 67 | } 68 | 69 | function reboot (mode) { 70 | return new Promise((resolve, reject) => { 71 | enqueue({ 72 | requestType: 'systemRebootRequest', 73 | args: { 74 | mode: mode 75 | } 76 | }) 77 | const unbind = emitter.on('response', res => { 78 | if (res && res.error) { 79 | reject(res.error, res) 80 | } else { 81 | resolve(res) 82 | } 83 | unbind() 84 | }) 85 | }) 86 | } 87 | 88 | function deviceInfo () { 89 | return new Promise((resolve, reject) => { 90 | enqueue({ 91 | requestType: 'systemDeviceInfoRequest', 92 | args: {} 93 | }) 94 | const unbind = emitter.on('response', res => { 95 | if (res && res.error) { 96 | reject(res.error, res) 97 | } else { 98 | resolve(res.chunks) 99 | } 100 | unbind() 101 | }) 102 | }) 103 | } 104 | 105 | function powerInfo () { 106 | return new Promise((resolve, reject) => { 107 | enqueue({ 108 | requestType: 'systemPowerInfoRequest', 109 | args: {} 110 | }) 111 | const unbind = emitter.on('response', res => { 112 | if (res && res.error) { 113 | reject(res.error, res) 114 | } else { 115 | resolve(res.chunks) 116 | } 117 | unbind() 118 | }) 119 | }) 120 | } 121 | 122 | function update (manifest) { 123 | return new Promise((resolve, reject) => { 124 | enqueue({ 125 | requestType: 'systemUpdateRequest', 126 | args: { 127 | updateManifest: manifest 128 | } 129 | }) 130 | const unbind = emitter.on('response', res => { 131 | if (res && res.error) { 132 | reject(res.error, res) 133 | } else { 134 | resolve(res) 135 | } 136 | unbind() 137 | }) 138 | }) 139 | } 140 | 141 | export { 142 | ping, 143 | getDatetime, 144 | setDatetime, 145 | reboot, 146 | deviceInfo, 147 | powerInfo, 148 | update 149 | } 150 | -------------------------------------------------------------------------------- /frontend/src/flipper/protobuf/rpc.js: -------------------------------------------------------------------------------- 1 | import { PB } from './proto-compiled' 2 | import * as protobuf from 'protobufjs/minimal' 3 | import { emitter } from '../core' 4 | 5 | const commandQueue = [ 6 | { 7 | commandId: 0, 8 | requestType: 'unsolicited', 9 | chunks: [], 10 | error: undefined 11 | } 12 | ] 13 | 14 | function createRequest (requestType, args, hasNext, commandId) { 15 | const options = { 16 | commandId: commandId || commandQueue.length 17 | } 18 | options[requestType] = args 19 | if (hasNext) { 20 | options.hasNext = hasNext 21 | } 22 | 23 | const command = commandQueue.find(c => c.commandId === options.commandId) 24 | if (!command) { 25 | commandQueue.push({ 26 | commandId: options.commandId, 27 | requestType: requestType, 28 | args: hasNext ? [args] : args, 29 | chunks: [], 30 | resolved: false, 31 | error: undefined 32 | }) 33 | } 34 | 35 | const message = PB.Main.create(options) 36 | return { data: new Uint8Array(PB.Main.encodeDelimited(message).finish()), commandId: options.commandId } 37 | } 38 | 39 | function parseResponse (data) { 40 | const reader = protobuf.Reader.create(data) 41 | let command 42 | const chunks = [] 43 | while (reader.pos < reader.len) { 44 | const res = PB.Main.decodeDelimited(reader) 45 | if (res) { 46 | command = commandQueue.find(c => c.commandId === res.commandId) 47 | 48 | if (res.commandStatus && res.commandStatus !== 0 && res.commandStatus !== 6) { 49 | command.resolved = true 50 | command.error = Object.keys(PB.CommandStatus).find(key => PB.CommandStatus[key] === res.commandStatus) 51 | return command 52 | } else if (res.empty) { 53 | command.resolved = true 54 | return command 55 | } else if (res.commandId === 0) { 56 | command.data = res.guiScreenFrame.data 57 | return command 58 | } 59 | 60 | const payload = res[Object.keys(res).find(k => k === command.requestType.replace('Request', 'Response'))] 61 | chunks.push(payload) 62 | if (command.requestType === 'storageReadRequest') { 63 | emitter.emit('storageReadRequest/progress', chunks.length) 64 | } 65 | 66 | if (!res.hasNext) { 67 | command.chunks = chunks 68 | command.resolved = true 69 | return command 70 | } 71 | } 72 | } 73 | if (command.resolved) { 74 | return command 75 | } 76 | } 77 | 78 | function flushCommandQueue () { 79 | while (commandQueue.length > 1) { 80 | commandQueue.pop() 81 | } 82 | } 83 | 84 | export { 85 | createRequest, 86 | parseResponse, 87 | flushCommandQueue 88 | } 89 | -------------------------------------------------------------------------------- /frontend/src/flipper/protobuf/xbms.js: -------------------------------------------------------------------------------- 1 | export default { updating: [0, 0, 0, 0, 0, 0, 0, 0, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 240, 255, 255, 255, 255, 255, 255, 152, 1, 24, 192, 12, 0, 0, 0, 72, 16, 0, 0, 0, 0, 0, 128, 152, 1, 24, 192, 0, 0, 0, 0, 40, 16, 0, 0, 0, 0, 0, 128, 152, 53, 158, 227, 173, 177, 0, 0, 40, 16, 0, 0, 0, 0, 0, 128, 152, 109, 91, 198, 108, 219, 0, 0, 40, 16, 255, 255, 255, 255, 255, 143, 152, 109, 155, 199, 108, 219, 0, 0, 72, 16, 255, 255, 255, 255, 255, 143, 152, 109, 219, 198, 108, 219, 0, 0, 80, 16, 255, 255, 255, 255, 2, 143, 152, 109, 219, 198, 108, 219, 0, 0, 80, 16, 11, 149, 255, 255, 15, 142, 240, 60, 158, 141, 109, 243, 36, 1, 80, 16, 255, 255, 255, 255, 31, 140, 0, 12, 0, 0, 0, 192, 0, 0, 72, 16, 75, 242, 255, 255, 63, 140, 0, 12, 0, 0, 0, 112, 0, 0, 40, 16, 255, 255, 255, 255, 127, 140, 0, 0, 0, 0, 0, 0, 0, 0, 36, 16, 235, 255, 255, 255, 255, 140, 0, 0, 0, 0, 0, 0, 0, 0, 19, 16, 255, 255, 255, 255, 255, 141, 0, 0, 0, 0, 0, 0, 0, 128, 11, 16, 19, 255, 255, 255, 255, 143, 0, 0, 0, 0, 0, 0, 0, 192, 7, 16, 255, 255, 255, 255, 255, 141, 0, 0, 0, 0, 0, 0, 0, 248, 7, 16, 19, 255, 255, 255, 255, 143, 0, 0, 0, 128, 255, 7, 0, 254, 3, 16, 255, 255, 255, 255, 255, 143, 0, 0, 0, 96, 0, 56, 0, 251, 1, 16, 235, 255, 255, 255, 255, 143, 0, 0, 0, 24, 0, 192, 128, 241, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 0, 6, 0, 0, 243, 224, 0, 16, 139, 244, 255, 255, 255, 143, 0, 0, 0, 1, 0, 0, 12, 65, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 128, 0, 0, 0, 24, 98, 0, 16, 139, 255, 255, 255, 255, 143, 0, 0, 64, 0, 0, 0, 48, 52, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 32, 0, 0, 0, 96, 24, 0, 16, 235, 255, 255, 255, 255, 143, 0, 0, 16, 0, 0, 0, 192, 8, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 8, 0, 0, 0, 128, 9, 0, 16, 75, 138, 255, 255, 255, 143, 0, 0, 8, 0, 2, 0, 0, 9, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 4, 0, 1, 0, 0, 7, 0, 16, 83, 255, 255, 255, 255, 143, 0, 0, 4, 192, 192, 7, 0, 2, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 2, 0, 240, 31, 0, 0, 0, 16, 211, 255, 255, 255, 255, 143, 0, 0, 2, 0, 248, 63, 0, 4, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 1, 0, 252, 89, 0, 4, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 1, 0, 244, 73, 0, 8, 0, 16, 255, 255, 255, 255, 255, 143, 0, 0, 1, 0, 194, 131, 0, 8, 0, 16, 0, 0, 0, 0, 0, 128, 0, 128, 0, 0, 2, 128, 0, 8, 0, 16, 7, 0, 0, 0, 64, 142, 128, 143, 0, 0, 2, 128, 0, 16, 0, 16, 0, 0, 0, 0, 0, 128, 96, 240, 0, 0, 2, 128, 0, 16, 0, 16, 0, 0, 0, 0, 0, 128, 16, 128, 1, 0, 2, 128, 0, 16, 0, 240, 255, 255, 255, 255, 255, 255, 8, 0, 6, 0, 4, 64, 0, 16, 0, 16, 0, 0, 0, 0, 0, 128, 8, 0, 24, 0, 228, 67, 0, 32, 0, 16, 0, 0, 192, 255, 255, 159, 4, 0, 96, 0, 24, 44, 0, 32, 0, 16, 240, 3, 64, 0, 0, 144, 244, 0, 0, 0, 4, 16, 0, 32, 0, 144, 255, 127, 64, 255, 255, 151, 12, 3, 0, 14, 2, 32, 0, 32, 0, 16, 240, 3, 64, 0, 0, 144, 12, 12, 128, 17, 0, 0, 0, 32, 0, 16, 0, 112, 192, 255, 255, 159, 12, 48, 64, 32, 0, 0, 0, 32, 0, 16, 0, 0, 0, 0, 0, 128, 8, 192, 32, 32, 0, 0, 0, 32, 0, 240, 255, 255, 255, 255, 255, 255, 8, 0, 19, 32, 0, 0, 0, 32, 0, 16, 0, 0, 0, 0, 0, 128, 16, 0, 28, 34, 0, 0, 0, 32, 0, 208, 124, 251, 223, 239, 234, 182, 32, 0, 48, 17, 0, 0, 0, 32, 0, 80, 0, 0, 0, 0, 0, 160, 64, 0, 192, 9, 15, 0, 0, 32, 0, 16, 0, 0, 0, 0, 0, 128, 128, 0, 0, 254, 16, 0, 0, 32, 0, 80, 124, 243, 63, 252, 249, 163, 0, 3, 0, 0, 0, 0, 0, 32, 0, 80, 124, 16, 32, 28, 57, 162, 0, 4, 0, 0, 0, 0, 0, 32, 0, 16, 108, 211, 44, 252, 249, 163, 0, 24, 0, 0, 0, 0, 0, 32, 0, 80, 108, 224, 31, 0, 0, 128, 0, 96, 0, 0, 0, 0, 0, 16, 0, 16, 108, 192, 12, 0, 0, 160, 0, 192, 1, 0, 0, 0, 0, 16, 0, 80, 40, 192, 12, 204, 152, 129, 0, 192, 15, 0, 0, 0, 0, 16, 0, 80, 40, 192, 12, 84, 169, 162, 0, 192, 255, 0, 0, 0, 0, 16, 0, 80, 40, 240, 63, 220, 185, 163, 0, 192, 255, 15, 0, 0, 0, 16, 0, 80, 40, 208, 44, 0, 0, 160, 0, 160, 255, 3, 0, 0, 0, 16, 0, 80, 40, 208, 44, 0, 0, 160, 0, 32, 252, 0, 0, 0, 0, 16, 0, 16, 40, 224, 31, 252, 255, 163, 0, 32, 0, 0, 0, 0, 0, 16, 0, 80, 40, 192, 12, 252, 255, 131] } 2 | -------------------------------------------------------------------------------- /frontend/src/flipper/serial.js: -------------------------------------------------------------------------------- 1 | import { Operation } from './util' 2 | import * as flipper from './core' 3 | 4 | const operation = new Operation() 5 | const filters = [ 6 | { usbVendorId: 0x0483, usbProductId: 0x5740 } 7 | ] 8 | 9 | const serial = new Worker(new URL('./workers/webSerial.js', import.meta.url)) 10 | serial.onmessage = (e) => { 11 | if (e.data.operation === 'cli output') { 12 | flipper.emitter.emit('cli output', e.data.data) 13 | } else if (e.data.operation === 'raw output') { 14 | flipper.emitter.emit('raw output', e.data.data) 15 | } else if (e.data.operation === 'write/end') { 16 | flipper.emitter.emit('write/end') 17 | } else { 18 | operation.terminate(e.data) 19 | } 20 | } 21 | 22 | async function connect () { 23 | const ports = await navigator.serial.getPorts({ filters }) 24 | if (ports.length === 0) { 25 | throw new Error('No known ports') 26 | } 27 | const connect = operation.create(serial, 'connect') 28 | await connect 29 | } 30 | 31 | async function disconnect () { 32 | const disconnect = operation.create(serial, 'disconnect') 33 | await disconnect 34 | } 35 | 36 | async function write (mode, data) { 37 | if (mode !== 'raw') { 38 | const write = operation.create(serial, 'write', { mode: mode, data: [data] }) 39 | await write 40 | } else { 41 | serial.postMessage({ operation: 'write', data: { mode: mode, data: [data] } }) 42 | } 43 | } 44 | 45 | function read (mode) { 46 | serial.postMessage({ operation: 'read', data: mode }) 47 | } 48 | 49 | function closeReader () { 50 | serial.postMessage({ operation: 'stop reading' }) 51 | } 52 | 53 | export { 54 | connect, 55 | disconnect, 56 | write, 57 | read, 58 | closeReader 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/flipper/util.js: -------------------------------------------------------------------------------- 1 | import { untar } from '../untar/untar.js' 2 | import pako from 'pako' 3 | 4 | class Operation { 5 | constructor () { 6 | this.resolve = undefined 7 | this.reject = undefined 8 | } 9 | 10 | create (worker, operation, data) { 11 | return new Promise((resolve, reject) => { 12 | worker.postMessage({ operation: operation, data: data }) 13 | this.resolve = resolve 14 | this.reject = reject 15 | }) 16 | } 17 | 18 | terminate (event) { 19 | if (event.status === 1) { 20 | this.resolve(event.data) 21 | } else { 22 | this.reject(event.error) 23 | } 24 | } 25 | } 26 | 27 | function unpack (buffer) { 28 | const ungzipped = pako.ungzip(new Uint8Array(buffer)) 29 | return untar(ungzipped.buffer) 30 | } 31 | 32 | export { 33 | Operation, 34 | unpack 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/flipper/workers/webSerial.js: -------------------------------------------------------------------------------- 1 | onmessage = function (event) { 2 | switch (event.data.operation) { 3 | case 'connect': 4 | connect() 5 | break 6 | case 'disconnect': 7 | disconnect() 8 | break 9 | case 'read': 10 | read(event.data.data) 11 | break 12 | case 'stop reading': 13 | reader.cancel() 14 | break 15 | case 'write': 16 | enqueue(event.data.data) 17 | break 18 | } 19 | } 20 | 21 | let port, reader, readComplete = false, writerIdle = true 22 | const writeQueue = [] 23 | 24 | async function connect () { 25 | const filters = [ 26 | { usbVendorId: 0x0483, usbProductId: 0x5740 } 27 | ] 28 | const ports = await navigator.serial.getPorts({ filters }) 29 | port = ports[0] 30 | port.open({ baudRate: 1 }) 31 | .then(() => { 32 | self.postMessage({ 33 | operation: 'connect', 34 | status: 1 35 | }) 36 | }) 37 | .catch(async error => { 38 | if (error.toString().includes('The port is already open')) { 39 | await port.close() 40 | return connect() 41 | } else { 42 | self.postMessage({ 43 | operation: 'connect', 44 | status: 0, 45 | error: error 46 | }) 47 | } 48 | }) 49 | } 50 | 51 | function disconnect () { 52 | if (port && !port.closed) { 53 | port.close() 54 | .then(() => { 55 | self.postMessage({ 56 | operation: 'disconnect', 57 | status: 1 58 | }) 59 | }) 60 | .catch(error => { 61 | if (!(error.toString().includes('The port is already closed.'))) { 62 | self.postMessage({ 63 | operation: 'disconnect', 64 | status: 0, 65 | error: error 66 | }) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | function enqueue (entry) { 73 | writeQueue.push(entry) 74 | if (writerIdle) { 75 | write() 76 | } 77 | } 78 | 79 | async function write () { 80 | writerIdle = false 81 | while (writeQueue.length) { 82 | const entry = writeQueue[0] 83 | if (!port.writable) { 84 | self.postMessage({ 85 | operation: 'write', 86 | status: 0, 87 | error: 'Writable stream closed' 88 | }) 89 | return 90 | } 91 | const writer = port.writable.getWriter() 92 | 93 | if (entry.mode.startsWith('cli')) { 94 | if (entry.mode === 'cli/delimited') { 95 | entry.data.push('\r\n') 96 | } 97 | const encoder = new TextEncoder() 98 | entry.data.forEach(async (line, i) => { 99 | let message = line 100 | if (entry.data[i + 1]) { 101 | message = line + '\r\n' 102 | } 103 | await writer.write(encoder.encode(message).buffer) 104 | }) 105 | } else if (entry.mode === 'raw') { 106 | await writer.write(entry.data[0].buffer) 107 | } else { 108 | throw new Error('Unknown write mode:', entry.mode) 109 | } 110 | 111 | await writer.close() 112 | .then(() => { 113 | writeQueue.shift() 114 | self.postMessage({ 115 | operation: 'write/end' 116 | }) 117 | self.postMessage({ 118 | operation: 'write', 119 | status: 1 120 | }) 121 | }) 122 | .catch(error => { 123 | self.postMessage({ 124 | operation: 'write', 125 | status: 0, 126 | error: error 127 | }) 128 | }) 129 | } 130 | writerIdle = true 131 | } 132 | 133 | async function read (mode) { 134 | try { 135 | reader = port.readable.getReader() 136 | } catch (error) { 137 | self.postMessage({ 138 | operation: 'read', 139 | status: 0, 140 | error: error 141 | }) 142 | if (!error.toString().includes('locked to a reader')) { 143 | throw error 144 | } 145 | } 146 | const decoder = new TextDecoder() 147 | let buffer = new Uint8Array(0) 148 | readComplete = false 149 | 150 | while (!readComplete) { 151 | await reader.read() 152 | .then(({ done, value }) => { 153 | if (done) { 154 | readComplete = true 155 | } else { 156 | if (mode) { 157 | self.postMessage({ 158 | operation: mode + ' output', 159 | data: value 160 | }) 161 | } else { 162 | const newBuffer = new Uint8Array(buffer.length + value.length) 163 | newBuffer.set(buffer) 164 | newBuffer.set(value, buffer.length) 165 | buffer = newBuffer 166 | 167 | if (decoder.decode(buffer.slice(-12)).replace(/\s/g, '').endsWith('>:\x07')) { 168 | readComplete = true 169 | self.postMessage({ 170 | operation: 'read', 171 | data: 'read', 172 | status: 1 173 | }) 174 | } 175 | } 176 | } 177 | }) 178 | .catch(error => { 179 | if (error.toString().includes('The device has been lost.')) { 180 | readComplete = true 181 | } else { 182 | throw error 183 | } 184 | }) 185 | } 186 | await reader.cancel() 187 | .then(() => { 188 | self.postMessage({ 189 | operation: 'read', 190 | status: 1, 191 | data: buffer 192 | }) 193 | }) 194 | .catch(error => { 195 | self.postMessage({ 196 | operation: 'read', 197 | status: 0, 198 | error: error 199 | }) 200 | }) 201 | } 202 | -------------------------------------------------------------------------------- /frontend/src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Momentum FW for Flipper Zero 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/layouts/PacksLayout.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 353 | -------------------------------------------------------------------------------- /frontend/src/layouts/UpdateLayout.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 381 | -------------------------------------------------------------------------------- /frontend/src/pages/Update.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 352 | -------------------------------------------------------------------------------- /frontend/src/quasar.d.ts: -------------------------------------------------------------------------------- 1 | // Forces TS to apply `@quasar/app` augmentations of `quasar` package 2 | // Removing this would break `quasar/wrappers` imports as those typings are declared 3 | // into `@quasar/app` 4 | // As a side effect, since `@quasar/app` reference `quasar` to augment it, 5 | // this declaration also apply `quasar` own 6 | // augmentations (eg. adds `$q` into Vue component context) 7 | /// 8 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers' 2 | import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' 3 | import routes from './routes' 4 | 5 | /* 6 | * If not building with SSR mode, you can 7 | * directly export the Router instantiation; 8 | * 9 | * The function below can be async too; either use 10 | * async/await or return a Promise which resolves 11 | * with the Router instance. 12 | */ 13 | 14 | export default route(function (/* { store, ssrContext } */) { 15 | const createHistory = process.env.SERVER 16 | ? createMemoryHistory 17 | : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) 18 | 19 | const Router = createRouter({ 20 | scrollBehavior: () => ({ left: 0, top: 0 }), 21 | routes, 22 | 23 | // Leave this as is and make changes in quasar.conf.js instead! 24 | // quasar.conf.js -> build -> vueRouterMode 25 | // quasar.conf.js -> build -> publicPath 26 | history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE) 27 | }) 28 | 29 | return Router 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/src/router/routes.js: -------------------------------------------------------------------------------- 1 | 2 | const routes = [ 3 | { 4 | path: '/update-frame', 5 | component: () => import('layouts/UpdateLayout.vue'), 6 | children: [ 7 | { path: '/update-frame', component: () => import('pages/Update.vue') } 8 | ] 9 | }, 10 | { 11 | path: '/asset-packs-frame', 12 | component: () => import('layouts/PacksLayout.vue'), 13 | children: [ 14 | { path: '/asset-packs-frame', component: () => import('pages/Packs.vue') } 15 | ] 16 | } 17 | ] 18 | 19 | export default routes 20 | -------------------------------------------------------------------------------- /frontend/src/untar/ProgressivePromise.js: -------------------------------------------------------------------------------- 1 | /** 2 | Returns a Promise decorated with a progress() event. 3 | */ 4 | export function ProgressivePromise (fn) { 5 | if (typeof Promise !== 'function') { 6 | throw new Error('Promise implementation not available in this environment.') 7 | } 8 | 9 | const progressCallbacks = [] 10 | const progressHistory = [] 11 | 12 | function doProgress (value) { 13 | for (let i = 0, l = progressCallbacks.length; i < l; ++i) { 14 | progressCallbacks[i](value) 15 | } 16 | 17 | progressHistory.push(value) 18 | } 19 | 20 | const promise = new Promise(function (resolve, reject) { 21 | fn(resolve, reject, doProgress) 22 | }) 23 | 24 | promise.progress = function (cb) { 25 | if (typeof cb !== 'function') { 26 | throw new Error('cb is not a function.') 27 | } 28 | 29 | // Report the previous progress history 30 | for (let i = 0, l = progressHistory.length; i < l; ++i) { 31 | cb(progressHistory[i]) 32 | } 33 | 34 | progressCallbacks.push(cb) 35 | return promise 36 | } 37 | 38 | const origThen = promise.then 39 | 40 | promise.then = function (onSuccess, onFail, onProgress) { 41 | origThen.call(promise, onSuccess, onFail) 42 | 43 | if (onProgress !== undefined) { 44 | promise.progress(onProgress) 45 | } 46 | 47 | return promise 48 | } 49 | 50 | return promise 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/untar/untar-worker.js: -------------------------------------------------------------------------------- 1 | function UntarWorker () { 2 | 3 | } 4 | 5 | UntarWorker.prototype = { 6 | onmessage: function (msg) { 7 | try { 8 | if (msg.data.type === 'extract') { 9 | this.untarBuffer(msg.data.buffer) 10 | } else { 11 | throw new Error('Unknown message type: ' + msg.data.type) 12 | } 13 | } catch (err) { 14 | this.postError(err) 15 | } 16 | }, 17 | 18 | postError: function (err) { 19 | // console.info("postError(" + err.message + ")" + " " + JSON.stringify(err)); 20 | this.postMessage({ type: 'error', data: { message: err.message } }) 21 | }, 22 | 23 | postLog: function (level, msg) { 24 | // console.info("postLog"); 25 | this.postMessage({ type: 'log', data: { level: level, msg: msg } }) 26 | }, 27 | 28 | untarBuffer: function (arrayBuffer) { 29 | try { 30 | const tarFileStream = new UntarFileStream(arrayBuffer) 31 | while (tarFileStream.hasNext()) { 32 | const file = tarFileStream.next() 33 | 34 | this.postMessage({ type: 'extract', data: file }, [file.buffer]) 35 | } 36 | 37 | this.postMessage({ type: 'complete' }) 38 | } catch (err) { 39 | this.postError(err) 40 | } 41 | }, 42 | 43 | postMessage: function (msg, transfers) { 44 | // console.info("postMessage(" + msg + ", " + JSON.stringify(transfers) + ")"); 45 | self.postMessage(msg, transfers) 46 | } 47 | } 48 | 49 | if (typeof self !== 'undefined') { 50 | // We're running in a worker thread 51 | const worker = new UntarWorker() 52 | self.onmessage = function (msg) { worker.onmessage(msg) } 53 | } 54 | 55 | // Source: https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330 56 | // Unmarshals an Uint8Array to string. 57 | function decodeUTF8 (bytes) { 58 | let s = '' 59 | let i = 0 60 | while (i < bytes.length) { 61 | let c = bytes[i++] 62 | if (c > 127) { 63 | if (c > 191 && c < 224) { 64 | if (i >= bytes.length) throw new Error('UTF-8 decode: incomplete 2-byte sequence') 65 | c = (c & 31) << 6 | bytes[i] & 63 66 | } else if (c > 223 && c < 240) { 67 | if (i + 1 >= bytes.length) throw new Error('UTF-8 decode: incomplete 3-byte sequence') 68 | c = (c & 15) << 12 | (bytes[i] & 63) << 6 | bytes[++i] & 63 69 | } else if (c > 239 && c < 248) { 70 | if (i + 2 >= bytes.length) throw new Error('UTF-8 decode: incomplete 4-byte sequence') 71 | c = (c & 7) << 18 | (bytes[i] & 63) << 12 | (bytes[++i] & 63) << 6 | bytes[++i] & 63 72 | } else throw new Error('UTF-8 decode: unknown multibyte start 0x' + c.toString(16) + ' at index ' + (i - 1)) 73 | ++i 74 | } 75 | 76 | if (c <= 0xffff) s += String.fromCharCode(c) 77 | else if (c <= 0x10ffff) { 78 | c -= 0x10000 79 | s += String.fromCharCode(c >> 10 | 0xd800) 80 | s += String.fromCharCode(c & 0x3FF | 0xdc00) 81 | } else throw new Error('UTF-8 decode: code point 0x' + c.toString(16) + ' exceeds UTF-16 reach') 82 | } 83 | return s 84 | } 85 | 86 | function PaxHeader (fields) { 87 | this._fields = fields 88 | } 89 | 90 | PaxHeader.parse = function (buffer) { 91 | // https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.bpxa500/paxex.htm 92 | // An extended header shall consist of one or more records, each constructed as follows: 93 | // "%d %s=%s\n", , , 94 | 95 | // The extended header records shall be encoded according to the ISO/IEC10646-1:2000 standard (UTF-8). 96 | // The field, , equals sign, and shown shall be limited to the portable character set, as 97 | // encoded in UTF-8. The and fields can be any UTF-8 characters. The field shall be the 98 | // decimal length of the extended header record in octets, including the trailing . 99 | 100 | let bytes = new Uint8Array(buffer) 101 | const fields = [] 102 | 103 | while (bytes.length > 0) { 104 | // Decode bytes up to the first space character; that is the total field length 105 | const fieldLength = parseInt(decodeUTF8(bytes.subarray(0, bytes.indexOf(0x20)))) 106 | const fieldText = decodeUTF8(bytes.subarray(0, fieldLength)) 107 | const fieldMatch = fieldText.match(/^\d+ ([^=]+)=(.*)\n$/) 108 | 109 | if (fieldMatch === null) { 110 | throw new Error('Invalid PAX header data format.') 111 | } 112 | 113 | const fieldName = fieldMatch[1] 114 | let fieldValue = fieldMatch[2] 115 | 116 | if (fieldValue.length === 0) { 117 | fieldValue = null 118 | } else if (fieldValue.match(/^\d+$/) !== null) { 119 | // If it's a integer field, parse it as int 120 | fieldValue = parseInt(fieldValue) 121 | } 122 | // Don't parse float values since precision is lost 123 | 124 | const field = { 125 | name: fieldName, 126 | value: fieldValue 127 | } 128 | 129 | fields.push(field) 130 | 131 | bytes = bytes.subarray(fieldLength) // Cut off the parsed field data 132 | } 133 | 134 | return new PaxHeader(fields) 135 | } 136 | 137 | PaxHeader.prototype = { 138 | applyHeader: function (file) { 139 | // Apply fields to the file 140 | // If a field is of value null, it should be deleted from the file 141 | // https://www.mkssoftware.com/docs/man4/pax.4.asp 142 | 143 | this._fields.forEach(function (field) { 144 | let fieldName = field.name 145 | const fieldValue = field.value 146 | 147 | if (fieldName === 'path') { 148 | // This overrides the name and prefix fields in the following header block. 149 | fieldName = 'name' 150 | 151 | if (file.prefix !== undefined) { 152 | delete file.prefix 153 | } 154 | } else if (fieldName === 'linkpath') { 155 | // This overrides the linkname field in the following header block. 156 | fieldName = 'linkname' 157 | } 158 | 159 | if (fieldValue === null) { 160 | delete file[fieldName] 161 | } else { 162 | file[fieldName] = fieldValue 163 | } 164 | }) 165 | } 166 | } 167 | 168 | function LongFieldHeader (fieldName, fieldValue) { 169 | this._fieldName = fieldName 170 | this._fieldValue = fieldValue 171 | } 172 | 173 | LongFieldHeader.parse = function (fieldName, buffer) { 174 | const bytes = new Uint8Array(buffer) 175 | return new LongFieldHeader(fieldName, decodeUTF8(bytes)) 176 | } 177 | 178 | LongFieldHeader.prototype = { 179 | applyHeader: function (file) { 180 | file[this._fieldName] = this._fieldValue 181 | } 182 | } 183 | 184 | function TarFile () { 185 | 186 | } 187 | 188 | function UntarStream (arrayBuffer) { 189 | this._bufferView = new DataView(arrayBuffer) 190 | this._position = 0 191 | } 192 | 193 | UntarStream.prototype = { 194 | readString: function (charCount) { 195 | // console.log("readString: position " + this.position() + ", " + charCount + " chars"); 196 | const charSize = 1 197 | const byteCount = charCount * charSize 198 | 199 | const charCodes = [] 200 | 201 | for (let i = 0; i < charCount; ++i) { 202 | const charCode = this._bufferView.getUint8(this.position() + (i * charSize), true) 203 | if (charCode !== 0) { 204 | charCodes.push(charCode) 205 | } else { 206 | break 207 | } 208 | } 209 | 210 | this.seek(byteCount) 211 | 212 | return String.fromCharCode.apply(null, charCodes) 213 | }, 214 | 215 | readBuffer: function (byteCount) { 216 | let buf 217 | 218 | if (typeof ArrayBuffer.prototype.slice === 'function') { 219 | buf = this._bufferView.buffer.slice(this.position(), this.position() + byteCount) 220 | } else { 221 | buf = new ArrayBuffer(byteCount) 222 | const target = new Uint8Array(buf) 223 | const src = new Uint8Array(this._bufferView.buffer, this.position(), byteCount) 224 | target.set(src) 225 | } 226 | 227 | this.seek(byteCount) 228 | return buf 229 | }, 230 | 231 | seek: function (byteCount) { 232 | this._position += byteCount 233 | }, 234 | 235 | peekUint32: function () { 236 | return this._bufferView.getUint32(this.position(), true) 237 | }, 238 | 239 | position: function (newpos) { 240 | if (newpos === undefined) { 241 | return this._position 242 | } else { 243 | this._position = newpos 244 | } 245 | }, 246 | 247 | size: function () { 248 | return this._bufferView.byteLength 249 | } 250 | } 251 | 252 | function UntarFileStream (arrayBuffer) { 253 | this._stream = new UntarStream(arrayBuffer) 254 | this._globalPaxHeader = null 255 | } 256 | 257 | UntarFileStream.prototype = { 258 | hasNext: function () { 259 | // A tar file ends with 4 zero bytes 260 | return this._stream.position() + 4 < this._stream.size() && this._stream.peekUint32() !== 0 261 | }, 262 | 263 | next: function () { 264 | return this._readNextFile() 265 | }, 266 | 267 | _readNextFile: function () { 268 | const stream = this._stream 269 | let file = new TarFile() 270 | let isHeaderFile = false 271 | let header = null 272 | 273 | const headerBeginPos = stream.position() 274 | const dataBeginPos = headerBeginPos + 512 275 | 276 | // Read header 277 | file.name = stream.readString(100) 278 | file.mode = stream.readString(8) 279 | file.uid = parseInt(stream.readString(8)) 280 | file.gid = parseInt(stream.readString(8)) 281 | file.size = parseInt(stream.readString(12), 8) 282 | file.mtime = parseInt(stream.readString(12), 8) 283 | file.checksum = parseInt(stream.readString(8)) 284 | file.type = stream.readString(1) 285 | file.linkname = stream.readString(100) 286 | file.ustarFormat = stream.readString(6) 287 | 288 | if (file.ustarFormat.indexOf('ustar') > -1) { 289 | file.version = stream.readString(2) 290 | file.uname = stream.readString(32) 291 | file.gname = stream.readString(32) 292 | file.devmajor = parseInt(stream.readString(8)) 293 | file.devminor = parseInt(stream.readString(8)) 294 | file.namePrefix = stream.readString(155) 295 | 296 | if (file.namePrefix.length > 0) { 297 | file.name = file.namePrefix + '/' + file.name 298 | } 299 | } 300 | 301 | stream.position(dataBeginPos) 302 | 303 | // Derived from https://www.mkssoftware.com/docs/man4/pax.4.asp 304 | // and https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.bpxa500/pxarchfm.htm 305 | switch (file.type) { 306 | case '0': // Normal file is either "0" or "\0". 307 | case '': // In case of "\0", readString returns an empty string, that is "". 308 | file.buffer = stream.readBuffer(file.size) 309 | break 310 | case '1': // Link to another file already archived 311 | // TODO Should we do anything with these? 312 | break 313 | case '2': // Symbolic link 314 | // TODO Should we do anything with these? 315 | break 316 | case '3': // Character special device (what does this mean??) 317 | break 318 | case '4': // Block special device 319 | break 320 | case '5': // Directory 321 | break 322 | case '6': // FIFO special file 323 | break 324 | case '7': // Reserved 325 | break 326 | case 'g': // Global PAX header 327 | isHeaderFile = true 328 | this._globalHeader = PaxHeader.parse(stream.readBuffer(file.size)) 329 | break 330 | case 'K': 331 | isHeaderFile = true 332 | header = LongFieldHeader.parse('linkname', stream.readBuffer(file.size)) 333 | break 334 | case 'L': // Indicates that the next file has a long name (over 100 chars), and therefore keeps the name of the file in this block's buffer. http://www.gnu.org/software/tar/manual/html_node/Standard.html 335 | isHeaderFile = true 336 | header = LongFieldHeader.parse('name', stream.readBuffer(file.size)) 337 | break 338 | case 'x': // PAX header 339 | isHeaderFile = true 340 | header = PaxHeader.parse(stream.readBuffer(file.size)) 341 | break 342 | default: // Unknown file type 343 | break 344 | } 345 | 346 | if (file.buffer === undefined) { 347 | file.buffer = new ArrayBuffer(0) 348 | } 349 | 350 | let dataEndPos = dataBeginPos + file.size 351 | 352 | // File data is padded to reach a 512 byte boundary; skip the padded bytes too. 353 | if (file.size % 512 !== 0) { 354 | dataEndPos += 512 - (file.size % 512) 355 | } 356 | 357 | stream.position(dataEndPos) 358 | 359 | if (isHeaderFile) { 360 | file = this._readNextFile() 361 | } 362 | 363 | if (this._globalPaxHeader !== null) { 364 | this._globalPaxHeader.applyHeader(file) 365 | } 366 | 367 | if (header !== null) { 368 | header.applyHeader(file) 369 | } 370 | 371 | return file 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /frontend/src/untar/untar.js: -------------------------------------------------------------------------------- 1 | import { ProgressivePromise } from './ProgressivePromise' 2 | 3 | const global = window || this 4 | 5 | /** 6 | Returns a ProgressivePromise. 7 | */ 8 | export function untar (arrayBuffer) { 9 | if (!(arrayBuffer instanceof ArrayBuffer)) { 10 | throw new TypeError('arrayBuffer is not an instance of ArrayBuffer.') 11 | } 12 | 13 | if (!global.Worker) { 14 | throw new Error('Worker implementation is not available in this environment.') 15 | } 16 | 17 | return new ProgressivePromise(function (resolve, reject, progress) { 18 | const worker = new Worker(new URL('./untar-worker.js', import.meta.url)) 19 | 20 | const files = [] 21 | 22 | worker.onerror = function (err) { 23 | reject(err) 24 | } 25 | 26 | worker.onmessage = function (message) { 27 | message = message.data 28 | let file 29 | switch (message.type) { 30 | case 'log': 31 | console[message.data.level]('Worker: ' + message.data.msg) 32 | break 33 | case 'extract': 34 | file = decorateExtractedFile(message.data) 35 | if (file.name.endsWith('\x00')) { 36 | file.name = file.name.slice(0, -1) 37 | } 38 | files.push(file) 39 | progress(file) 40 | break 41 | case 'complete': 42 | worker.terminate() 43 | resolve(files) 44 | break 45 | case 'error': 46 | worker.terminate() 47 | reject(new Error(message.data.message)) 48 | break 49 | default: 50 | worker.terminate() 51 | reject(new Error('Unknown message from worker: ' + message.type)) 52 | break 53 | } 54 | } 55 | 56 | // console.info("Sending arraybuffer to worker for extraction."); 57 | worker.postMessage({ type: 'extract', buffer: arrayBuffer }, [arrayBuffer]) 58 | }) 59 | } 60 | 61 | const decoratedFileProps = { 62 | blob: { 63 | get: function () { 64 | return this._blob || (this._blob = new Blob([this.buffer])) 65 | } 66 | }, 67 | getBlobUrl: { 68 | value: function () { 69 | return this._blobUrl || (this._blobUrl = URL.createObjectURL(this.blob)) 70 | } 71 | }, 72 | readAsString: { 73 | value: function () { 74 | const buffer = this.buffer 75 | const charCount = buffer.byteLength 76 | const charSize = 1 77 | const bufferView = new DataView(buffer) 78 | 79 | const charCodes = [] 80 | 81 | for (let i = 0; i < charCount; ++i) { 82 | const charCode = bufferView.getUint8(i * charSize, true) 83 | charCodes.push(charCode) 84 | } 85 | 86 | return (this._string = String.fromCharCode.apply(null, charCodes)) 87 | } 88 | }, 89 | readAsJSON: { 90 | value: function () { 91 | return JSON.parse(this.readAsString()) 92 | } 93 | } 94 | } 95 | 96 | function decorateExtractedFile (file) { 97 | Object.defineProperties(file, decoratedFileProps) 98 | return file 99 | } 100 | -------------------------------------------------------------------------------- /frontend/src/util/util.js: -------------------------------------------------------------------------------- 1 | import { untar } from '../untar/untar.js' 2 | import pako from 'pako' 3 | 4 | class Operation { 5 | constructor () { 6 | this.resolve = undefined 7 | this.reject = undefined 8 | } 9 | 10 | create (worker, operation, data) { 11 | return new Promise((resolve, reject) => { 12 | worker.postMessage({ operation: operation, data: data }) 13 | this.resolve = resolve 14 | this.reject = reject 15 | }) 16 | } 17 | 18 | terminate (event) { 19 | if (event.status === 1) { 20 | this.resolve(event.data) 21 | } else { 22 | this.reject(event.error) 23 | } 24 | } 25 | } 26 | 27 | async function fetchPacks () { 28 | const response = await fetch('https://up.momentum-fw.dev/asset-packs/directory.json') 29 | if (response.status >= 400) { 30 | throw new Error('Failed to fetch asset packs (' + response.status + ')') 31 | } 32 | const data = await response.json() 33 | 34 | const packs = data.packs.map((pack) => { 35 | pack.stats.updated = new Date(pack.stats.updated * 1000) 36 | pack.stats.added = new Date(pack.stats.added * 1000) 37 | for (const file of pack.files) { 38 | if (file.type === 'pack_targz') { 39 | pack.tarFile = file 40 | } else if (file.type === 'pack_zip') { 41 | pack.zipFile = file 42 | } 43 | } 44 | if (pack.tarFile && pack.zipFile) { 45 | return pack 46 | } else { 47 | return null 48 | } 49 | }).filter(pack => pack) 50 | 51 | return packs 52 | } 53 | 54 | function fetchChannels (target) { 55 | return fetch('https://up.momentum-fw.dev/firmware/directory.json') 56 | .then((response) => { 57 | if (response.status >= 400) { 58 | throw new Error('Failed to fetch firmware channels (' + response.status + ')') 59 | } 60 | return response.json() 61 | }) 62 | .then((data) => { 63 | const release = data.channels.find(e => e.id === 'release') 64 | const dev = data.channels.find(e => e.id === 'development') 65 | 66 | function formatChannel (channel) { 67 | channel.versions.sort((a, b) => { 68 | if ((a.version.startsWith('mntm-') && b.version.startsWith('mntm-')) && 69 | (parseInt(a.version.slice('mntm-'.length)) < parseInt(b.version.slice('mntm-'.length)))) return 1 70 | else return -1 71 | }) 72 | const output = { 73 | version: '', 74 | date: '', 75 | url: '', 76 | files: [], 77 | changelog: '' 78 | } 79 | const updater = channel.versions[0].files.find(file => file.target === 'f' + target && file.type === 'update_tgz') 80 | if (updater) { 81 | output.url = updater.url 82 | } 83 | output.version = channel.versions[0].version 84 | output.date = new Date(channel.versions[0].timestamp * 1000).toISOString().slice(0, 10) 85 | output.files = channel.versions[0].files.sort((a, b) => { 86 | if (a.url.match(/[\w.]+$/g)[0] > b.url.match(/[\w.]+$/g)[0]) return 1 87 | else return -1 88 | }) 89 | let changelog = channel.versions[0].changelog 90 | changelog = changelog.replaceAll(/^( *)[-*] (.*?)\r?\n/gm, (match, g1, g2, offset, string, groups) => `
  • ${g2}
  • `) 91 | for (let i = 5; i > 0; i--) { 92 | changelog = changelog.replaceAll(RegExp(`^${'#'.repeat(i)} (.*?)(\r?\n)+`, 'gm'), `$1`) 93 | } 94 | // TODO: use q-markdown 95 | // eslint-disable-next-line 96 | changelog = changelog.replaceAll(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1') 97 | changelog = changelog.replaceAll(/\*\*(.*?)\*\*/g, '$1') 98 | changelog = changelog.replaceAll(/__(.*?)__/g, '$1') 99 | changelog = changelog.replaceAll(/`(.*?)`/g, '$1') 100 | changelog = changelog.replaceAll(/^\s*\[\/\/\]:.*?(\r?\n)+/gm, '') 101 | changelog = changelog.trimEnd().replaceAll(/\r?\n/g, '\n
    \n') 102 | output.changelog = changelog 103 | return output 104 | } 105 | 106 | const releaseChannel = formatChannel(release) 107 | const devChannel = formatChannel(dev) 108 | 109 | return { release: releaseChannel, dev: devChannel } 110 | }) 111 | } 112 | 113 | async function fetchFirmware (url) { 114 | const buffer = await fetch(url) 115 | .then(async response => { 116 | if (response.status >= 400) { 117 | throw new Error('Failed to fetch resources (' + response.status + ')') 118 | } 119 | const buffer = await response.arrayBuffer() 120 | return unpack(buffer) 121 | }) 122 | 123 | return buffer 124 | } 125 | 126 | async function fetchRegions () { 127 | return fetch('https://update.flipperzero.one/regions/api/v0/bundle') 128 | .then((response) => { 129 | if (response.status >= 400) { 130 | throw new Error('Failed to fetch region (' + response.status + ')') 131 | } 132 | return response.json() 133 | }) 134 | .then(result => { 135 | if (result.error) { 136 | throw new Error(result.error.text) 137 | } else if (result.success) { 138 | return result.success 139 | } 140 | }) 141 | } 142 | 143 | function unpack (buffer) { 144 | const ungzipped = pako.ungzip(new Uint8Array(buffer)) 145 | return untar(ungzipped.buffer) 146 | } 147 | 148 | function bytesToSize (bytes) { 149 | const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'] 150 | if (bytes === 0) return 'n/a' 151 | const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10) 152 | if (i === 0) return `${bytes} ${sizes[i]})` 153 | return `${(bytes / (1024 ** i)).toFixed(1)}${sizes[i]}` 154 | } 155 | 156 | export { 157 | Operation, 158 | fetchPacks, 159 | fetchChannels, 160 | fetchFirmware, 161 | fetchRegions, 162 | unpack, 163 | bytesToSize 164 | } 165 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/asset-packs-frame/index.html: -------------------------------------------------------------------------------- 1 | Momentum FW for Flipper Zero
    -------------------------------------------------------------------------------- /public/asset-packs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Momentum FW Asset Packs 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    29 |
    30 | 55 |
    56 |
    57 |
    58 |
    59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #8f8fe9 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/css/app.d4a8c1ed.css: -------------------------------------------------------------------------------- 1 | body{background-color:#000!important;color:#fff!important}::-webkit-scrollbar{background:#0000;width:16px}::-webkit-scrollbar-track{background:#0000;border:3px solid #0000;margin:14px 0;overflow:auto}::-webkit-scrollbar-thumb{background:#666;background-clip:padding-box;border:6px solid #0000;border-radius:16px;min-height:24px;overflow:auto;padding:50px;-webkit-transition:height .2s ease-in-out;transition:height .2s ease-in-out}::-webkit-scrollbar-thumb:hover{background-color:#bbb}.device-screen .info{margin:16px 3rem 0 0}.device-screen .info p{display:flex;flex-wrap:nowrap;font-family:monospace;font-size:16px;justify-content:space-between;margin:0 0 12px}.device-screen .info p span:first-of-type{font-weight:600;margin-right:1.5rem}.device-screen .flipper{background-image:url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl81IiB4PSIwIiB5PSIwIiB2aWV3Qm94PSIwIDAgMzYwIDE1NiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzYwIDE1NiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48c3R5bGUgdHlwZT0idGV4dC9jc3MiIGlkPSJzdHlsZTIiPi5zdDF7ZmlsbDojZmU4YTJjfTwvc3R5bGU+PGcgaWQ9Imc5OSI+PGxpbmVhckdyYWRpZW50IGlkPSJTVkdJRF8xXyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxNzkuODg3IiB5MT0iMTU2LjQ3MyIgeDI9IjE3OS44ODciIHkyPSItMS4yODUiPjxzdG9wIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29sb3I6IzNjMWEwMCIgaWQ9InN0b3A0Ii8+PHN0b3Agb2Zmc2V0PSIuNTUxIiBzdHlsZT0ic3RvcC1jb2xvcjojMjIwZjAwIiBpZD0ic3RvcDYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiMwOTA0MDAiIGlkPSJzdG9wOCIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0iTTMwMS4zIDEuMkg3MS42Yy05LjggMC0xNyAxMC4zLTE3IDEwLjNTNS4zIDc1LjQgMy4yIDc4LjNjLTMgNC0yLjEgMTQuMSAwIDE3IDUuOSA4IDM0LjUgNDcuMSAzNS44IDQ4LjggMS40IDEuNyA3LjcgMTEuMSAxNi41IDExLjFIMzM5YzguNyAwIDE5LjYtMjAuMiAxOS42LTI1LjJzLS4zLTU4LjktLjMtNjQuNWMwLTMuNy0yLjYtOC40LTUuMS0xMS41LTIuMy0yLjgtMzUuMy00NC0zOC4yLTQ3LjUtMi41LTIuOS04LjItNS4zLTEzLjctNS4zeiIgaWQ9InBhdGgxMSIgc3R5bGU9ImZpbGw6dXJsKCNTVkdJRF8xXykiLz48ZyBpZD0iZzE1Ij48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMzAyIDNjNC41IDAgMTAgMy4zIDEyIDUuNyAyLjkgMy40IDM1LjMgNDQgMzggNDcuNCAzLjEgMy45IDQuOSA3LjUgNC45IDkuN3Y2NC41YzAgNC0xMC44IDIyLjctMTcuMiAyMi43SDU1LjFjLTQuNSAwLTkuMy0zLjQtMTQuNS0xMC0uMi0uMi0uMy0uNC0uNC0uNS0uNy0uOS0xMC4xLTEzLjctMTguNC0yNUMxNC41IDEwNy42IDcgOTcuNCA0LjMgOTMuNyAzIDkyIDIuMSA4My4zIDQuMyA4MC40YzIuMS0yLjggNTEuMS02Ni4xIDUxLjYtNjYuN2wuMS0uMS4xLS4xYzAtLjEgNy4xLTEwLjUgMTUuMi0xMC41SDMwMm0wLTNINzEuM2MtOS45IDAtMTcuOCAxMS44LTE3LjggMTEuOFM0LjEgNzUuNyAxLjkgNzguNmMtMi45IDQtMi4xIDE0LjEgMCAxNyA2IDggMzQuNiA0Ny4xIDM2IDQ4LjggMS40IDEuNyA4LjUgMTEuNyAxNy4zIDExLjdoMjg0LjdjOC44IDAgMjAuMi0yMC44IDIwLjItMjUuN1Y2NS45YzAtMy43LTMuMS04LjQtNS42LTExLjUtMi4zLTIuOC0zNS4yLTQ0LTM4LjEtNDcuNS0yLjUtMy04LjgtNi45LTE0LjQtNi45eiIgaWQ9InBhdGgxMyIvPjwvZz48ZyBpZD0iZzQ5Ij48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMjIxLjMgOThIOTIuN0M4OSA5OCA4NiA5NSA4NiA5MS4zVjI1LjdjMC0zLjcgMy02LjcgNi43LTYuN2gxMjguNmMzLjcgMCA2LjcgMyA2LjcgNi43djY1LjZjMCAzLjctMyA2LjctNi43IDYuN3pNOTIuNyAyMGMtMy4xIDAtNS43IDIuNi01LjcgNS43djY1LjZjMCAzLjEgMi42IDUuNyA1LjcgNS43aDEyOC42YzMuMSAwIDUuNy0yLjYgNS43LTUuN1YyNS43YzAtMy4xLTIuNi01LjctNS43LTUuN0g5Mi43eiIgaWQ9InBhdGg0NyIvPjwvZz48ZyBpZD0iZzUzIj48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMjc0LjUgOTRDMjU0LjkgOTQgMjM5IDc4LjEgMjM5IDU4LjVTMjU0LjkgMjMgMjc0LjUgMjMgMzEwIDM4LjkgMzEwIDU4LjUgMjk0LjEgOTQgMjc0LjUgOTR6bTAtNzBjLTE5IDAtMzQuNSAxNS41LTM0LjUgMzQuNVMyNTUuNSA5MyAyNzQuNSA5MyAzMDkgNzcuNSAzMDkgNTguNSAyOTMuNSAyNCAyNzQuNSAyNHoiIGlkPSJwYXRoNTEiLz48L2c+PGcgaWQ9Imc1NyI+PHBhdGggY2xhc3M9InN0MSIgZD0iTTMyNi42IDk0Yy02LjMgMC0xMS41LTUuMi0xMS41LTExLjVTMzIwLjIgNzEgMzI2LjYgNzFzMTEuNSA1LjIgMTEuNSAxMS41UzMzMi45IDk0IDMyNi42IDk0em0tLjEtMjJjLTUuOCAwLTEwLjUgNC43LTEwLjUgMTAuNVMzMjAuNyA5MyAzMjYuNSA5MyAzMzcgODguMyAzMzcgODIuNSAzMzIuMyA3MiAzMjYuNSA3MnoiIGlkPSJwYXRoNTUiLz48L2c+PGcgaWQ9Imc2MSI+PHBhdGggY2xhc3M9InN0MSIgZD0iTTMyOC44IDg4aC02LjRjLS4zIDAtLjUtLjItLjUtLjV2LTJjMC0uMy4yLS41LjUtLjVoNS45Yy42IDAgMS0uMiAxLjMtLjYuMy0uNC41LS45LjUtMS40IDAtMS0uNS0xLjktMS43LTEuOUgzMjR2MS41YzAgLjItLjEuNC0uMy41LS4yLjEtLjQgMC0uNS0uMWwtMi45LTNjLS4yLS4yLS4yLS41IDAtLjdsMi44LTNjLjEtLjEuNC0uMi41LS4xLjIuMS4zLjMuMy41Vjc4aDQuOWMyLjggMCA0LjIgMi41IDQuMiA1IDAgMS4zLS40IDIuNS0xIDMuNC0uOCAxLTEuOSAxLjYtMy4yIDEuNnptLTUuOS0xaDUuOWMxIDAgMS44LS40IDIuNC0xLjIuNS0uNy44LTEuNy44LTIuOCAwLTEuOS0xLTQtMy4yLTRoLTUuNGMtLjMgMC0uNS0uMi0uNS0uNXYtLjhsLTEuNyAxLjggMS43IDEuOHYtLjhjMC0uMy4yLS41LjUtLjVoNC45YzEuOCAwIDIuNyAxLjUgMi43IDIuOSAwIC44LS4yIDEuNS0uNyAyLS41LjYtMS4zIDEtMi4xIDFoLTUuNFY4N3oiIGlkPSJwYXRoNTkiLz48L2c+PGcgaWQ9Imc2NSI+PHBhdGggY2xhc3M9InN0MSIgZD0iTTI3NC41IDcwYy02LjMgMC0xMS41LTUuMi0xMS41LTExLjVTMjY4LjIgNDcgMjc0LjUgNDcgMjg2IDUyLjIgMjg2IDU4LjUgMjgwLjggNzAgMjc0LjUgNzB6bTAtMjJjLTUuOCAwLTEwLjUgNC43LTEwLjUgMTAuNVMyNjguNyA2OSAyNzQuNSA2OSAyODUgNjQuMyAyODUgNTguNSAyODAuMyA0OCAyNzQuNSA0OHoiIGlkPSJwYXRoNjMiLz48L2c+PGcgaWQ9Imc2OSI+PHBhdGggY2xhc3M9InN0MSIgZD0iTTI3OS4yIDQwaC05LjNjLS40IDAtLjgtLjItLjktLjUtLjItLjMtLjItLjcgMC0xLjFsNC43LTcuOWMuNC0uNyAxLjUtLjcgMS45IDBsNC43IDcuOWMuMi4zLjIuNyAwIDEuMS0uNC4zLS44LjUtMS4xLjV6bS00LjctOS4xcy0uMS4xIDAgMGwtNC43IDcuOXYuMWg5LjV2LS4xbC00LjgtNy45Yy4xLjEgMCAwIDAgMHoiIGlkPSJwYXRoNjciLz48L2c+PGcgaWQ9Imc3MyI+PHBhdGggY2xhc3M9InN0MSIgZD0iTTI3NC41IDg3Yy0uNCAwLS44LS4yLTEtLjVsLTQuNy03LjljLS4yLS4zLS4yLS43IDAtMS4xLjItLjMuNi0uNS45LS41aDkuM2MuNCAwIC44LjIuOS41LjIuMy4yLjcgMCAxLjFsLTQuNyA3LjljLjEuMy0uMy41LS43LjV6bS00LjYtOWMtLjEgMC0uMSAwIDAgMC0uMS4xLS4xLjEtLjEuMmw0LjcgOC4xYzAgLjEuMS4xLjIgMGw0LjctOC4xdi0uMXMwLS4xLS4xLS4xaC05LjR6IiBpZD0icGF0aDcxIi8+PC9nPjxnIGlkPSJnNzciPjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0yOTQuMSA2NC4yYy0uMiAwLS40IDAtLjUtLjEtLjMtLjItLjUtLjYtLjUtLjl2LTkuM2MwLS40LjItLjguNS0uOS4zLS4yLjctLjIgMS4xIDBsNy45IDQuN2MuMy4yLjUuNi41IDFzLS4yLjgtLjUgMWwtNy45IDQuN2MtLjMtLjItLjQtLjItLjYtLjJ6bTAtMTAuNWMtLjEgMC0uMS4xIDAgMC0uMSAwLS4xLjEtLjEuMXY5LjRoLjFsNy45LTQuN3YtLjJsLTcuOS00LjZjMCAuMSAwIDAgMCAweiIgaWQ9InBhdGg3NSIvPjwvZz48ZyBpZD0iZzgxIj48cGF0aCBjbGFzcz0ic3QxIiBkPSJNMjU0LjkgNjQuMmMtLjIgMC0uNCAwLS41LS4xbC03LjktNC43Yy0uMy0uMi0uNS0uNi0uNS0xcy4yLS44LjUtMWw3LjktNC43Yy4zLS4yLjctLjIgMS4xIDAgLjMuMi41LjYuNS45djkuM2MwIC40LS4yLjgtLjUuOS0uMi40LS40LjQtLjYuNHptMC0xMC41czAgLjEgMCAwbC03LjkgNC43di4ybDcuOSA0LjdoLjF2LTkuNGMwLS4xIDAtLjEtLjEtLjIuMS4xLjEgMCAwIDB6IiBpZD0icGF0aDc5Ii8+PC9nPjxnIGlkPSJnODUiPjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0zMjcuNSA5OEgyNzVjLTIyLjQgMC00MC0xNy4xLTQwLTM5IDAtMjIgMTcuOS00MCA0MC00MCAxMS4xIDAgMjEuNyA0LjYgMjkuMyAxMi44bDMzLjkgNDAuOGM0IDQuMyAzLjkgOC43IDMuOSAxMS4zdi42Yy0uMSA2LjItOCAxMy41LTE0LjYgMTMuNXpNMjc1IDIwYy0yMS41IDAtMzkgMTcuNS0zOSAzOSAwIDIxLjMgMTcuMSAzOCAzOSAzOGg1Mi41YzYgMCAxMy41LTYuOSAxMy41LTEyLjZ2LS42YzAtMi41LjEtNi42LTMuNi0xMC42bC0zMy45LTQwLjhDMjk2LjEgMjQuNSAyODUuNyAyMCAyNzUgMjB6IiBpZD0icGF0aDgzIi8+PC9nPjxnIGlkPSJnODkiPjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0yMzguNSA5OGMtMS45IDAtMy41LTEuNi0zLjUtMy41czEuNi0zLjUgMy41LTMuNSAzLjUgMS42IDMuNSAzLjUtMS42IDMuNS0zLjUgMy41em0wLTZjLTEuNCAwLTIuNSAxLjEtMi41IDIuNXMxLjEgMi41IDIuNSAyLjUgMi41LTEuMSAyLjUtMi41LTEuMS0yLjUtMi41LTIuNXoiIGlkPSJwYXRoODciLz48L2c+PGcgaWQ9Imc5MyI+PHBhdGggY2xhc3M9InN0MSIgZD0iTTMzOS41IDEyMEg3MC4zYy05LjEgMC0xMi41LTMuOC0xNy43LTkuNWwtLjYtLjdjLTQuOS01LjUtMTEuNy0xMy41LTE1LjgtMTguOC0zLjgtNC45LTIuMy03LjMuNi0xMmwuMi0uM2MxLjYtMi41IDExLjEtMTYgMjAuMy0yOS4xIDkuMi0xMyAxOC43LTI2LjUgMjAuNS0yOS4zQzgyLjUgMTMuMSA4OC41IDEwIDk4IDEwaDE5OS42YzMuNiAwIDguOCAxLjcgMTIgNS41bC44LjlDMzMxIDQxIDM0Mi43IDU1LjEgMzQ1LjMgNTguM2M0LjIgNS4zIDQuOCA3LjcgNC44IDExLjR2MzkuN2MtLjEgNS00LjQgMTAuNi0xMC42IDEwLjZ6TTk4IDExYy05LjEgMC0xNC45IDIuOS0xOS4yIDkuOC0xLjggMi44LTExLjQgMTYuMy0yMC42IDI5LjRDNDkgNjMuMyAzOS41IDc2LjggMzcuOSA3OS4zbC0uMi4zYy0zIDQuNy00LjEgNi41LS43IDEwLjggNCA1LjIgMTAuOCAxMy4yIDE1LjcgMTguN2wuNi43YzUuMSA1LjcgOC4zIDkuMiAxNi45IDkuMmgyNjkuMmM1LjYgMCA5LjUtNS4xIDkuNS05LjZWNjkuN2MwLTMuNi0uNy01LjctNC42LTEwLjgtMi41LTMuMi0xNC4yLTE3LjMtMzQuOC00MS45bC0uOC0uOWMtMi45LTMuNS03LjktNS4xLTExLjItNS4xSDk4eiIgaWQ9InBhdGg5MSIvPjwvZz48ZyBpZD0iZzk3Ij48cmVjdCB4PSI4OSIgeT0iMjIiIGNsYXNzPSJzdDEiIHdpZHRoPSIxMzYiIGhlaWdodD0iNzMiIGlkPSJyZWN0OTUiIHJ5PSI0Ii8+PC9nPjwvZz48L3N2Zz4=);background-position:50%;background-repeat:no-repeat;background-size:360px;height:165px;padding:31px 0 0 97px;width:368px}.device-screen .flipper h5{color:#f58235;font-weight:600;margin:25px 0 0;text-align:center;transform:skew(-9deg,0deg) scaleY(.9);width:71%}.device-screen .flipper canvas{transform:scale(1)}.device-screen .updater{margin:1.5rem 0}.device-screen .updater p{margin:0 0 8px}.device-screen .updater div.flex{flex-direction:row}@media (max-width:750px){.device-screen{margin-top:2rem}.device-screen>.flex{flex-direction:column-reverse}.device-screen .info{margin:2rem auto 0;width:275px}}@media (max-width:599px){.device-screen .updater div.flex{flex-direction:column}}@media (max-width:380px){.device-screen .flipper{background-size:300px;height:133px;padding:18px 0 0 67px;width:300px}.device-screen .flipper canvas{transform:scale(.85)}}.fw-option-label{background-color:#a883e9!important;border-radius:5px;padding:.2rem .4rem;width:-moz-fit-content;width:fit-content}.q-btn.main-btn{border:2px solid #a883e9;border-radius:18px;color:#fff;overflow:hidden;padding:10px 20px;transition-duration:.5s}.q-btn.main-btn:not(.disabled):hover{background-color:#a883e9}.q-btn.main-btn.attention{border-color:#fd923f;color:#fd923f}.q-btn.main-btn.attention:not(.disabled):hover{background-color:#fd923f;color:#fff}.q-btn.main-btn.negative{border-color:#f05145;color:#f05145}.q-btn.main-btn.negative:not(.disabled):hover{background-color:#f05145;color:#fff}.q-btn.flat-btn{color:#fff;min-height:unset;transition-duration:.5s}.q-btn.flat-btn:hover{color:#a883e9}.q-btn.flat-btn .q-focus-helper{display:none}.q-select{border:2px solid #fff;border-radius:24px;color:#fff;transition-duration:.5s}.q-select:active,.q-select:focus-within,.q-select:hover{border-color:#a883e9}.q-select>*>*{padding:0 7px 0 14px}.q-select>*>*>:last-of-type{padding-left:0}.q-select .q-field__native>*{padding:.2rem .4rem}.q-select .q-field__native#fw-select>*{background-color:#a883e9!important;border-radius:5px;width:-moz-fit-content;width:fit-content}.q-input{border:2px solid #fff;border-radius:14px;color:#fff;padding:0 8px;transition-duration:.5s}.q-input:active,.q-input:focus-within,.q-input:hover{border-color:#a883e9}#changelog{background-color:#111;border:2px solid #bbb;border-radius:24px;font-size:16px;height:calc(100vh - 400px);min-height:320px;overflow-x:hidden;overflow-y:scroll;padding:0 30px;width:690px}#changelog h1,#changelog h2,#changelog h3,#changelog h4,#changelog h5{font-size:24px;margin:0;text-decoration:underline;text-decoration-color:#a883e9!important}#changelog a{color:#a883e9;display:contents}#changelog code{background:#333;border-radius:5px;padding:.1em .3em}#changelog ::marker{color:#a883e9}#changelog>:first-child{padding-top:20px}#changelog>:last-child{padding-bottom:20px}.packs-grid{display:flex;flex-wrap:wrap;justify-content:center;max-width:1000px}.packs-grid .q-card{border:2px solid #bbb;border-radius:24px;cursor:pointer;margin:12px;width:300px}.packs-grid .q-card .q-carousel{aspect-ratio:128/64;height:auto;width:100%}.packs-grid .q-card .q-carousel .q-carousel__prev-arrow{left:6px}.packs-grid .q-card .q-carousel .q-carousel__next-arrow{right:6px}.packs-grid .q-card .q-carousel .q-carousel__navigation{bottom:0}.packs-grid .q-card .q-carousel .q-carousel__control{opacity:0;transition:opacity .25s ease-out}.packs-grid .q-card .q-carousel:hover .q-carousel__control{opacity:.69} -------------------------------------------------------------------------------- /public/css/responsive.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 992px) and (max-width: 1199px) { 2 | .nav-menu .navbar-logo { 3 | font-size: 42px; 4 | } 5 | .nav-menu #nav .nav-link { 6 | padding: 35px 7.5px; 7 | } 8 | .nav-menu #nav .nav-link.install-button { 9 | padding: 27px 7.5px; 10 | } 11 | header.sticky .nav-menu #nav .nav-link.install-button { 12 | padding: 15px 15px; 13 | } 14 | .install-button > a { 15 | color: #fff; 16 | height: 40px; 17 | min-width: 120px; 18 | line-height: 36px; 19 | font-size: 14px; 20 | } 21 | .home-thumb { 22 | height: 35%; 23 | top: 50%; 24 | right: -20px; 25 | left: unset; 26 | } 27 | header.sticky .nav-menu #nav .nav-link { 28 | padding: 23px 7.5px; 29 | } 30 | } 31 | 32 | @media (min-width: 768px) and (max-width: 991px) { 33 | header { 34 | padding: 0; 35 | } 36 | .nav-menu .navbar-logo { 37 | font-size: 48px; 38 | height: 80px; 39 | } 40 | header.sticky .nav-menu .navbar-logo { 41 | height: 35px; 42 | } 43 | header.sticky, .navbar { 44 | height: 50px; 45 | } 46 | .navbar-collapse.show, .navbar-collapse.collapsing { 47 | position: absolute; 48 | right: 0px; 49 | top: 80px; 50 | text-align: right !important; 51 | } 52 | header.sticky .navbar-collapse.show, header.sticky .navbar-collapse.collapsing { 53 | top: 50px; 54 | } 55 | header .nav-menu #nav .nav-link, 56 | header .nav-menu #nav .nav-link.install-button, 57 | header.sticky .nav-menu #nav .nav-link, 58 | header.sticky .nav-menu #nav .nav-link.install-button { 59 | padding: 7.5px 15px; 60 | } 61 | #navbar { 62 | padding: 15px; 63 | border-radius: 3px; 64 | background-color: #0b0b0b; 65 | text-align: left; 66 | } 67 | .install-button { 68 | text-align: left; 69 | } 70 | .home-heading > h2 { 71 | font-size: 88px; 72 | } 73 | .home-thumb { 74 | height: 35%; 75 | top: 60%; 76 | } 77 | .home-heading { 78 | margin-bottom: 70px; 79 | } 80 | .single-special { 81 | padding: 30px 10px; 82 | } 83 | .special-description-area.mt-150 { 84 | margin-top: 50px; 85 | } 86 | .special-description-content > h2 { 87 | font-size: 30px; 88 | } 89 | .section-heading > h2 { 90 | font-size: 38px; 91 | } 92 | .address-text > p, 93 | .phone-text > p, 94 | .email-text > p { 95 | font-size: 16px; 96 | } 97 | .section-heading { 98 | margin-bottom: 50px; 99 | } 100 | .install-button > a { 101 | margin-top: 4px; 102 | display: inline-block; 103 | border: 2px solid #a883e9; 104 | height: 40px; 105 | min-width: 130px; 106 | line-height: 36px; 107 | font-size: 14px; 108 | } 109 | } 110 | 111 | @media (min-width: 320px) and (max-width: 767px) { 112 | header { 113 | padding: 0; 114 | } 115 | .nav-menu .navbar-logo { 116 | font-size: 48px; 117 | } 118 | header.sticky, .navbar { 119 | height: 50px; 120 | } 121 | .navbar-collapse.show, .navbar-collapse.collapsing { 122 | position: absolute; 123 | right: 0px; 124 | top: 90px; 125 | text-align: right !important; 126 | } 127 | header.sticky .navbar-collapse.show, header.sticky .navbar-collapse.collapsing { 128 | top: 50px; 129 | } 130 | header .nav-menu #nav .nav-link, 131 | header .nav-menu #nav .nav-link.install-button, 132 | header.sticky .nav-menu #nav .nav-link, 133 | header.sticky .nav-menu #nav .nav-link.install-button { 134 | padding: 5px 15px; 135 | } 136 | #navbar { 137 | padding: 20px; 138 | border-radius: 3px; 139 | background-color: #0b0b0b; 140 | text-align: left; 141 | } 142 | .install-button { 143 | text-align: left; 144 | } 145 | header { 146 | top: 0; 147 | } 148 | .home-text .cd-intro > p { 149 | font-size: 14px; 150 | } 151 | .home-heading > h2 { 152 | font-size: 48px; 153 | } 154 | section#home > .container { 155 | padding-top: 50px; 156 | } 157 | .home-thumb { 158 | height: 30%; 159 | right: -10px; 160 | top: 57%; 161 | left: unset; 162 | } 163 | .logo-area > a > h2 { 164 | font-size: 40px; 165 | margin-top: 10px; 166 | } 167 | .section-heading > h2 { 168 | font-size: 32px; 169 | } 170 | .single-special { 171 | margin-bottom: 30px; 172 | } 173 | .special-description-area.mt-150 { 174 | margin-top: 50px; 175 | } 176 | .special-description-content > h2 { 177 | font-size: 30px; 178 | } 179 | .app-download-btn:first-child { 180 | margin-right: 0; 181 | } 182 | .app-download-area { 183 | display: block; 184 | } 185 | .app-download-btn { 186 | margin-bottom: 20px; 187 | } 188 | .discord-description > h2 { 189 | font-size: 32px; 190 | } 191 | .discord-button { 192 | text-align: left; 193 | margin-top: 20px; 194 | } 195 | .contact-from { 196 | margin-top: 30px; 197 | } 198 | .home-heading > h3 { 199 | font-size: 252px; 200 | top: -105px; 201 | left: -1px; 202 | } 203 | .home-heading, 204 | .single-cool-fact { 205 | margin-bottom: 50px; 206 | } 207 | } 208 | 209 | @media (min-width: 480px) and (max-width: 767px) { 210 | .home-heading > h3 { 211 | font-size: 250px; 212 | left: -100px; 213 | } 214 | .home-heading > h2 { 215 | font-size: 70px; 216 | } 217 | .home-thumb { 218 | left: unset; 219 | } 220 | .home-heading { 221 | margin-bottom: 50px; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff -------------------------------------------------------------------------------- /public/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff -------------------------------------------------------------------------------- /public/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff -------------------------------------------------------------------------------- /public/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff -------------------------------------------------------------------------------- /public/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff -------------------------------------------------------------------------------- /public/fonts/KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff -------------------------------------------------------------------------------- /public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff -------------------------------------------------------------------------------- /public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.0383092b.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.0383092b.woff2 -------------------------------------------------------------------------------- /public/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/ionicons.eot -------------------------------------------------------------------------------- /public/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/ionicons.ttf -------------------------------------------------------------------------------- /public/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/ionicons.woff -------------------------------------------------------------------------------- /public/fonts/materialdesignicons-webfont.d8e8e0f7.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/materialdesignicons-webfont.d8e8e0f7.woff -------------------------------------------------------------------------------- /public/fonts/materialdesignicons-webfont.e9db4005.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/fonts/materialdesignicons-webfont.e9db4005.woff2 -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/icon.png -------------------------------------------------------------------------------- /public/img/hero-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/img/hero-background.png -------------------------------------------------------------------------------- /public/img/hero-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/img/hero-image.png -------------------------------------------------------------------------------- /public/js/254.3e4a3252.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={7254:(e,t,a)=>{a(4748),a(5584),onmessage=function(e){switch(e.data.operation){case"connect":c();break;case"disconnect":l();break;case"read":f(e.data.data);break;case"stop reading":r.cancel();break;case"write":d(e.data.data);break}};let s,r,o=!1,n=!0;const i=[];async function c(){const e=[{usbVendorId:1155,usbProductId:22336}],t=await navigator.serial.getPorts({filters:e});s=t[0],s.open({baudRate:1}).then((()=>{self.postMessage({operation:"connect",status:1})})).catch((async e=>{if(e.toString().includes("The port is already open"))return await s.close(),c();self.postMessage({operation:"connect",status:0,error:e})}))}function l(){s&&!s.closed&&s.close().then((()=>{self.postMessage({operation:"disconnect",status:1})})).catch((e=>{e.toString().includes("The port is already closed.")||self.postMessage({operation:"disconnect",status:0,error:e})}))}function d(e){i.push(e),n&&p()}async function p(){n=!1;while(i.length){const e=i[0];if(!s.writable)return void self.postMessage({operation:"write",status:0,error:"Writable stream closed"});const t=s.writable.getWriter();if(e.mode.startsWith("cli")){"cli/delimited"===e.mode&&e.data.push("\r\n");const a=new TextEncoder;e.data.forEach((async(s,r)=>{let o=s;e.data[r+1]&&(o=s+"\r\n"),await t.write(a.encode(o).buffer)}))}else{if("raw"!==e.mode)throw new Error("Unknown write mode:",e.mode);await t.write(e.data[0].buffer)}await t.close().then((()=>{i.shift(),self.postMessage({operation:"write/end"}),self.postMessage({operation:"write",status:1})})).catch((e=>{self.postMessage({operation:"write",status:0,error:e})}))}n=!0}async function f(e){try{r=s.readable.getReader()}catch(n){if(self.postMessage({operation:"read",status:0,error:n}),!n.toString().includes("locked to a reader"))throw n}const t=new TextDecoder;let a=new Uint8Array(0);o=!1;while(!o)await r.read().then((({done:s,value:r})=>{if(s)o=!0;else if(e)self.postMessage({operation:e+" output",data:r});else{const e=new Uint8Array(a.length+r.length);e.set(a),e.set(r,a.length),a=e,t.decode(a.slice(-12)).replace(/\s/g,"").endsWith(">:")&&(o=!0,self.postMessage({operation:"read",data:"read",status:1}))}})).catch((e=>{if(!e.toString().includes("The device has been lost."))throw e;o=!0}));await r.cancel().then((()=>{self.postMessage({operation:"read",status:1,data:a})})).catch((e=>{self.postMessage({operation:"read",status:0,error:e})}))}}},t={};function a(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s].call(o.exports,o,o.exports,a),o.exports}a.m=e,a.x=()=>{var e=a.O(void 0,[121],(()=>a(7254)));return e=a.O(e),e},(()=>{var e=[];a.O=(t,s,r,o)=>{if(!s){var n=1/0;for(d=0;d=o)&&Object.keys(a.O).every((e=>a.O[e](s[c])))?s.splice(c--,1):(i=!1,o0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[s,r,o]}})(),(()=>{a.d=(e,t)=>{for(var s in t)a.o(t,s)&&!a.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})}})(),(()=>{a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,s)=>(a.f[s](e,t),t)),[]))})(),(()=>{a.u=e=>"js/vendor.52055a46.js"})(),(()=>{a.miniCssF=e=>"css/vendor.9b29db89.css"})(),(()=>{a.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()})(),(()=>{a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{a.j=254})(),(()=>{a.p="/"})(),(()=>{var e={254:1},t=t=>{var[s,o,n]=t;for(var i in o)a.o(o,i)&&(a.m[i]=o[i]);n&&n(a);while(s.length)e[s.pop()]=1;r(t)};a.f.i=(t,s)=>{e[t]||importScripts(a.p+a.u(t))};var s=globalThis["webpackChunkmomentum_fw_dev"]=globalThis["webpackChunkmomentum_fw_dev"]||[],r=s.push.bind(s);s.push=t})(),(()=>{var e=a.x;a.x=()=>a.e(121).then(e)})();a.x()})(); -------------------------------------------------------------------------------- /public/js/665.e7cdfcdd.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={665:(e,t,r)=>{function n(){}if(r(4748),r(5584),n.prototype={onmessage:function(e){try{if("extract"!==e.data.type)throw new Error("Unknown message type: "+e.data.type);this.untarBuffer(e.data.buffer)}catch(t){this.postError(t)}},postError:function(e){this.postMessage({type:"error",data:{message:e.message}})},postLog:function(e,t){this.postMessage({type:"log",data:{level:e,msg:t}})},untarBuffer:function(e){try{const t=new u(e);while(t.hasNext()){const e=t.next();this.postMessage({type:"extract",data:e},[e.buffer])}this.postMessage({type:"complete"})}catch(t){this.postError(t)}},postMessage:function(e,t){self.postMessage(e,t)}},"undefined"!==typeof self){const e=new n;self.onmessage=function(t){e.onmessage(t)}}function i(e){let t="",r=0;while(r127){if(n>191&&n<224){if(r>=e.length)throw new Error("UTF-8 decode: incomplete 2-byte sequence");n=(31&n)<<6|63&e[r]}else if(n>223&&n<240){if(r+1>=e.length)throw new Error("UTF-8 decode: incomplete 3-byte sequence");n=(15&n)<<12|(63&e[r])<<6|63&e[++r]}else{if(!(n>239&&n<248))throw new Error("UTF-8 decode: unknown multibyte start 0x"+n.toString(16)+" at index "+(r-1));if(r+2>=e.length)throw new Error("UTF-8 decode: incomplete 4-byte sequence");n=(7&n)<<18|(63&e[r])<<12|(63&e[++r])<<6|63&e[++r]}++r}if(n<=65535)t+=String.fromCharCode(n);else{if(!(n<=1114111))throw new Error("UTF-8 decode: code point 0x"+n.toString(16)+" exceeds UTF-16 reach");n-=65536,t+=String.fromCharCode(n>>10|55296),t+=String.fromCharCode(1023&n|56320)}}return t}function a(e){this._fields=e}function s(e,t){this._fieldName=e,this._fieldValue=t}function o(){}function f(e){this._bufferView=new DataView(e),this._position=0}function u(e){this._stream=new f(e),this._globalPaxHeader=null}a.parse=function(e){let t=new Uint8Array(e);const r=[];while(t.length>0){const e=parseInt(i(t.subarray(0,t.indexOf(32)))),n=i(t.subarray(0,e)),a=n.match(/^\d+ ([^=]+)=(.*)\n$/);if(null===a)throw new Error("Invalid PAX header data format.");const s=a[1];let o=a[2];0===o.length?o=null:null!==o.match(/^\d+$/)&&(o=parseInt(o));const f={name:s,value:o};r.push(f),t=t.subarray(e)}return new a(r)},a.prototype={applyHeader:function(e){this._fields.forEach((function(t){let r=t.name;const n=t.value;"path"===r?(r="name",void 0!==e.prefix&&delete e.prefix):"linkpath"===r&&(r="linkname"),null===n?delete e[r]:e[r]=n}))}},s.parse=function(e,t){const r=new Uint8Array(t);return new s(e,i(r))},s.prototype={applyHeader:function(e){e[this._fieldName]=this._fieldValue}},f.prototype={readString:function(e){const t=1,r=e*t,n=[];for(let i=0;i-1&&(t.version=e.readString(2),t.uname=e.readString(32),t.gname=e.readString(32),t.devmajor=parseInt(e.readString(8)),t.devminor=parseInt(e.readString(8)),t.namePrefix=e.readString(155),t.namePrefix.length>0&&(t.name=t.namePrefix+"/"+t.name)),e.position(f),t.type){case"0":case"":t.buffer=e.readBuffer(t.size);break;case"1":break;case"2":break;case"3":break;case"4":break;case"5":break;case"6":break;case"7":break;case"g":r=!0,this._globalHeader=a.parse(e.readBuffer(t.size));break;case"K":r=!0,n=s.parse("linkname",e.readBuffer(t.size));break;case"L":r=!0,n=s.parse("name",e.readBuffer(t.size));break;case"x":r=!0,n=a.parse(e.readBuffer(t.size));break;default:break}void 0===t.buffer&&(t.buffer=new ArrayBuffer(0));let u=f+t.size;return t.size%512!==0&&(u+=512-t.size%512),e.position(u),r&&(t=this._readNextFile()),null!==this._globalPaxHeader&&this._globalPaxHeader.applyHeader(t),null!==n&&n.applyHeader(t),t}}}},t={};function r(n){var i=t[n];if(void 0!==i)return i.exports;var a=t[n]={exports:{}};return e[n].call(a.exports,a,a.exports,r),a.exports}r.m=e,r.x=()=>{var e=r.O(void 0,[121],(()=>r(665)));return e=r.O(e),e},(()=>{var e=[];r.O=(t,n,i,a)=>{if(!n){var s=1/0;for(l=0;l=a)&&Object.keys(r.O).every((e=>r.O[e](n[f])))?n.splice(f--,1):(o=!1,a0&&e[l-1][2]>a;l--)e[l]=e[l-1];e[l]=[n,i,a]}})(),(()=>{r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})}})(),(()=>{r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((t,n)=>(r.f[n](e,t),t)),[]))})(),(()=>{r.u=e=>"js/vendor.52055a46.js"})(),(()=>{r.miniCssF=e=>"css/vendor.9b29db89.css"})(),(()=>{r.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()})(),(()=>{r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{r.j=665})(),(()=>{r.p="/"})(),(()=>{var e={665:1},t=t=>{var[n,a,s]=t;for(var o in a)r.o(a,o)&&(r.m[o]=a[o]);s&&s(r);while(n.length)e[n.pop()]=1;i(t)};r.f.i=(t,n)=>{e[t]||importScripts(r.p+r.u(t))};var n=globalThis["webpackChunkmomentum_fw_dev"]=globalThis["webpackChunkmomentum_fw_dev"]||[],i=n.push.bind(n);n.push=t})(),(()=>{var e=r.x;r.x=()=>r.e(121).then(e)})();r.x()})(); -------------------------------------------------------------------------------- /public/js/85.1db80424.js: -------------------------------------------------------------------------------- 1 | "use strict";(globalThis["webpackChunkmomentum_fw_dev"]=globalThis["webpackChunkmomentum_fw_dev"]||[]).push([[85],{6085:(e,t,s)=>{s.r(t),s.d(t,{default:()=>S});var o=s(1758);function i(e,t,s,i,n,a){const r=(0,o.g2)("router-view"),c=(0,o.g2)("q-page-container"),l=(0,o.g2)("q-layout");return(0,o.uX)(),(0,o.Wv)(l,{view:"hhh LpR fff"},{default:(0,o.k6)((()=>[(0,o.bF)(c,{class:"flex justify-center"},{default:(0,o.k6)((()=>[(0,o.bF)(r,{flipper:e.flipper,serialSupported:e.flags.serialSupported,rpcActive:e.flags.rpcActive,rpcToggling:e.flags.rpcToggling,connected:e.flags.connected,info:e.info,onSelectPort:e.selectPort,onSetRpcStatus:e.setRpcStatus,onSetInfo:e.setInfo,onUpdate:e.onUpdateStage,onShowNotif:e.showNotif,onLog:e.log},null,8,["flipper","serialSupported","rpcActive","rpcToggling","connected","info","onSelectPort","onSetRpcStatus","onSetInfo","onUpdate","onShowNotif","onLog"])])),_:1})])),_:1})}s(4748);var n=s(8734),a=s(4907),r=s(9563),c=s(2314),l=s(2458),g=s.n(l);let h;const p=(0,o.pM)({name:"PacksLayout",setup(){const e=(0,a.A)();return{flipper:(0,n.KR)(r),info:(0,n.KR)(null),flags:(0,n.KR)({serialSupported:!1,portSelectRequired:!1,connected:!1,rpcActive:!1,updateInProgress:!1,settingsView:!1}),reconnectLoop:(0,n.KR)(null),connectionStatus:(0,n.KR)("Ready to connect"),logger:g(),notify:e.notify}},methods:{async connect(){await this.flipper.connect().then((()=>{this.flags.portSelectRequired=!1,this.connectionStatus="Flipper connected",this.flags.connected=!0,this.log({level:"info",message:"Main: Flipper connected"}),h&&h()})).catch((e=>{"Error: No known ports"===e.toString()?this.flags.portSelectRequired=!0:this.connectionStatus=e.toString()}))},async selectPort(){const e=[{usbVendorId:1155,usbProductId:22336}];return await navigator.serial.requestPort({filters:e}),this.start(!0)},async disconnect(){await this.flipper.disconnect().then((()=>{this.connectionStatus="Disconnected",this.flags.connected=!1,this.info=null,this.textInfo=""})).catch((async e=>{if(e.toString().includes("Cannot cancel a locked stream"))return this.flags.rpcActive?await this.stopRpc():(this.flipper.closeReader(),await(0,c.A)(300)),this.disconnect();this.connectionStatus=e.toString()})),this.log({level:"info",message:"Main: Flipper disconnected"})},async startRpc(){this.flags.rpcToggling=!0;const e=await this.flipper.commands.startRpcSession(this.flipper);if(!e.resolved||e.error)throw new Error("Couldn't start rpc session");this.flags.rpcActive=!0,this.flags.rpcToggling=!1,this.log({level:"info",message:"Main: RPC started"})},async stopRpc(){this.flags.rpcToggling=!0,await this.flipper.commands.stopRpcSession(),this.flags.rpcActive=!1,this.flags.rpcToggling=!1,this.log({level:"info",message:"Main: RPC stopped"})},async readInfo(){this.info={};let e=await this.flipper.commands.system.deviceInfo().catch((e=>this.rpcErrorHandler(e,"system.deviceInfo"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: system.deviceInfo: OK"})}));for(const t of e)this.info[t.key]=t.value;e=await this.flipper.commands.system.powerInfo().catch((e=>this.rpcErrorHandler(e,"system.powerInfo"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: system.powerInfo: OK"})}));for(const t of e)this.info[t.key]=t.value;if(await(0,c.A)(300),e=await this.flipper.commands.storage.list("/ext").catch((e=>this.rpcErrorHandler(e,"storage.list"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: storage.list: /ext"})})),e&&"object"===typeof e&&e.length){const t=e.find((e=>"Manifest"===e.name));this.info.storage_databases_present=t?"installed":"missing",e=await this.flipper.commands.storage.info("/ext").catch((e=>this.rpcErrorHandler(e,"storage.info"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: storage.info: /ext"})})),this.info.storage_sdcard_present="installed",this.info.storage_sdcard_totalSpace=e.totalSpace,this.info.storage_sdcard_freeSpace=e.freeSpace}else this.info.storage_sdcard_present="missing",this.info.storage_databases_present="missing";await(0,c.A)(200),e=await this.flipper.commands.storage.info("/int").catch((e=>this.rpcErrorHandler(e,"storage.info"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: storage.info: /int"})})),this.info.storage_internal_totalSpace=e.totalSpace,this.info.storage_internal_freeSpace=e.freeSpace,this.log({level:"info",message:"Main: Fetched device info"}),this.info={...this.info}},findKnownDevices(){const e=[{usbVendorId:1155,usbProductId:22336}];return navigator.serial.getPorts({filters:e})},autoReconnect(){this.reconnectLoop&&(clearInterval(this.reconnectLoop),this.reconnectLoop=null),this.reconnectLoop=setInterval((async()=>{const e=await this.findKnownDevices();if(e&&e.length>0)return clearInterval(this.reconnectLoop),this.reconnectLoop=null,await this.start()}),3e3)},setRpcStatus(e){this.flags.rpcActive=e},setInfo(e){this.info=e},onUpdateStage(e){"start"===e?this.flags.updateInProgress=!0:"end"===e&&(this.flags.updateInProgress=!1)},showNotif({message:e,color:t,reloadBtn:s}){const o=[];s&&o.push({label:"Reload",color:"white",handler:()=>{location.reload()}}),0===o.length?o.push({icon:"close",color:"white",class:"q-px-sm"}):o.push({label:"Dismiss",color:"white"}),h=this.notify({message:e,color:t,textColor:"white",position:"bottom-right",timeout:0,group:!0,actions:o})},log({level:e,message:t}){switch(e){case"error":this.logger.error(t);break;case"warn":this.logger.warn(t);break;case"info":this.logger.info(t);break;case"debug":this.logger.debug(t);break}},rpcErrorHandler(e,t){e=e.toString(),this.showNotif({message:`RPC error in command '${t}': ${e}`,color:"negative"}),this.log({level:"error",message:`Main: RPC error in command '${t}': ${e}`})},async start(e){const t=await this.findKnownDevices();if(t&&t.length>0)await this.connect(),await this.startRpc(),await this.readInfo();else if(this.flags.portSelectRequired=!0,e)return this.selectPort()}},async mounted(){"serial"in navigator&&(this.flags.serialSupported=!0,await this.start(),navigator.serial.addEventListener("disconnect",(e=>{this.autoReconnect()})),navigator.serial.addEventListener("disconnect",(e=>{this.flags.updateInProgress||(this.showNotif({message:"Flipper has been disconnected"}),this.flags.connected=!1,this.flags.portSelectRequired=!0),this.log({level:"info",message:"Main: Flipper has been disconnected"})}))),this.logger.setLevel("debug",!0);const e=this.logger.methodFactory;this.logger.methodFactory=function(t,s,o){const i=e(t,s,o);return function(e){"debug"!==t&&i(e)}}}});var f=s(2807),d=s(557),u=s(5205),m=s(8582),v=s.n(m);const w=(0,f.A)(p,[["render",i]]),S=w;v()(p,"components",{QLayout:d.A,QPageContainer:u.A})}}]); -------------------------------------------------------------------------------- /public/js/890.065c7545.js: -------------------------------------------------------------------------------- 1 | "use strict";(globalThis["webpackChunkmomentum_fw_dev"]=globalThis["webpackChunkmomentum_fw_dev"]||[]).push([[890],{3890:(e,t,s)=>{s.r(t),s.d(t,{default:()=>L});var o=s(1758);const n={key:0,class:"flex-center column q-my-xl"},i=(0,o.Lk)("p",null,"Waiting for Flipper...",-1),a={key:1,class:"column text-center q-px-lg q-py-lg"},r=(0,o.Lk)("h5",null,"Unsupported browser",-1),l=(0,o.Lk)("p",null,[(0,o.eW)(" Your browser doesn't support WebSerial API. For better experience we recommend using Chrome for desktop."),(0,o.Lk)("br"),(0,o.Lk)("a",{style:{color:"#a883e9"},href:"https://caniuse.com/web-serial"},"Full list of supported browsers")],-1),c=[r,l];function g(e,t,s,r,l,g){const p=(0,o.g2)("router-view"),f=(0,o.g2)("q-btn"),h=(0,o.g2)("q-spinner"),d=(0,o.g2)("q-page"),u=(0,o.g2)("q-page-container"),m=(0,o.g2)("q-layout");return(0,o.uX)(),(0,o.Wv)(m,{view:"hhh LpR fff"},{default:(0,o.k6)((()=>[(0,o.bF)(u,{class:"flex justify-center"},{default:(0,o.k6)((()=>[e.flags.updateInProgress||e.flags.serialSupported&&null!==e.info&&this.info.storage_databases_present?((0,o.uX)(),(0,o.Wv)(p,{key:0,flipper:e.flipper,rpcActive:e.flags.rpcActive,connected:e.flags.connected,info:e.info,onSetRpcStatus:e.setRpcStatus,onSetInfo:e.setInfo,onUpdate:e.onUpdateStage,onShowNotif:e.showNotif,onLog:e.log},null,8,["flipper","rpcActive","connected","info","onSetRpcStatus","onSetInfo","onUpdate","onShowNotif","onLog"])):((0,o.uX)(),(0,o.Wv)(d,{key:1,class:"flex-center column"},{default:(0,o.k6)((()=>[!e.flags.serialSupported||e.flags.connected&&null!=e.info&&e.flags.rpcActive&&!e.flags.rpcToggling?(0,o.Q3)("",!0):((0,o.uX)(),(0,o.CE)("div",n,[e.flags.portSelectRequired||!e.flags.connected&&!e.flags.portSelectRequired?((0,o.uX)(),(0,o.Wv)(f,{key:0,onClick:t[0]||(t[0]=t=>e.flags.portSelectRequired?e.selectPort():e.start(!0)),flat:"",class:"q-mt-md main-btn"},{default:(0,o.k6)((()=>[(0,o.eW)(" Connect ")])),_:1})):((0,o.uX)(),(0,o.CE)(o.FK,{key:1},[(0,o.bF)(h,{color:"primary",size:"3em",class:"q-mb-md"}),i],64))])),e.flags.serialSupported?(0,o.Q3)("",!0):((0,o.uX)(),(0,o.CE)("div",a,c))])),_:1}))])),_:1})])),_:1})}s(4748);var p=s(8734),f=s(4907),h=s(9563),d=s(2314),u=s(2458),m=s.n(u);let v;const w=(0,o.pM)({name:"UpdateLayout",setup(){const e=(0,f.A)();return{flipper:(0,p.KR)(h),info:(0,p.KR)(null),flags:(0,p.KR)({serialSupported:!1,portSelectRequired:!1,connected:!1,rpcActive:!1,updateInProgress:!1,settingsView:!1}),reconnectLoop:(0,p.KR)(null),connectionStatus:(0,p.KR)("Ready to connect"),logger:m(),notify:e.notify}},methods:{async connect(){await this.flipper.connect().then((()=>{this.flags.portSelectRequired=!1,this.connectionStatus="Flipper connected",this.flags.connected=!0,this.log({level:"info",message:"Main: Flipper connected"}),v&&v()})).catch((e=>{"Error: No known ports"===e.toString()?this.flags.portSelectRequired=!0:this.connectionStatus=e.toString()}))},async selectPort(){const e=[{usbVendorId:1155,usbProductId:22336}];return await navigator.serial.requestPort({filters:e}),this.start(!0)},async disconnect(){await this.flipper.disconnect().then((()=>{this.connectionStatus="Disconnected",this.flags.connected=!1,this.info=null,this.textInfo=""})).catch((async e=>{if(e.toString().includes("Cannot cancel a locked stream"))return this.flags.rpcActive?await this.stopRpc():(this.flipper.closeReader(),await(0,d.A)(300)),this.disconnect();this.connectionStatus=e.toString()})),this.log({level:"info",message:"Main: Flipper disconnected"})},async startRpc(){this.flags.rpcToggling=!0;const e=await this.flipper.commands.startRpcSession(this.flipper);if(!e.resolved||e.error)throw new Error("Couldn't start rpc session");this.flags.rpcActive=!0,this.flags.rpcToggling=!1,this.log({level:"info",message:"Main: RPC started"})},async stopRpc(){this.flags.rpcToggling=!0,await this.flipper.commands.stopRpcSession(),this.flags.rpcActive=!1,this.flags.rpcToggling=!1,this.log({level:"info",message:"Main: RPC stopped"})},async readInfo(){this.info={};let e=await this.flipper.commands.system.deviceInfo().catch((e=>this.rpcErrorHandler(e,"system.deviceInfo"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: system.deviceInfo: OK"})}));for(const t of e)this.info[t.key]=t.value;e=await this.flipper.commands.system.powerInfo().catch((e=>this.rpcErrorHandler(e,"system.powerInfo"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: system.powerInfo: OK"})}));for(const t of e)this.info[t.key]=t.value;if(await(0,d.A)(300),e=await this.flipper.commands.storage.list("/ext").catch((e=>this.rpcErrorHandler(e,"storage.list"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: storage.list: /ext"})})),e&&"object"===typeof e&&e.length){const t=e.find((e=>"Manifest"===e.name));this.info.storage_databases_present=t?"installed":"missing",e=await this.flipper.commands.storage.info("/ext").catch((e=>this.rpcErrorHandler(e,"storage.info"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: storage.info: /ext"})})),this.info.storage_sdcard_present="installed",this.info.storage_sdcard_totalSpace=e.totalSpace,this.info.storage_sdcard_freeSpace=e.freeSpace}else this.info.storage_sdcard_present="missing",this.info.storage_databases_present="missing";await(0,d.A)(200),e=await this.flipper.commands.storage.info("/int").catch((e=>this.rpcErrorHandler(e,"storage.info"))).finally((()=>{this.$emit("log",{level:"debug",message:"Main: storage.info: /int"})})),this.info.storage_internal_totalSpace=e.totalSpace,this.info.storage_internal_freeSpace=e.freeSpace,this.log({level:"info",message:"Main: Fetched device info"})},findKnownDevices(){const e=[{usbVendorId:1155,usbProductId:22336}];return navigator.serial.getPorts({filters:e})},autoReconnect(){this.reconnectLoop&&(clearInterval(this.reconnectLoop),this.reconnectLoop=null),this.reconnectLoop=setInterval((async()=>{const e=await this.findKnownDevices();if(e&&e.length>0)return clearInterval(this.reconnectLoop),this.reconnectLoop=null,await this.start()}),3e3)},setRpcStatus(e){this.flags.rpcActive=e},setInfo(e){this.info=e},onUpdateStage(e){"start"===e?this.flags.updateInProgress=!0:"end"===e&&(this.flags.updateInProgress=!1)},showNotif({message:e,color:t,reloadBtn:s}){const o=[];s&&o.push({label:"Reload",color:"white",handler:()=>{location.reload()}}),0===o.length?o.push({icon:"close",color:"white",class:"q-px-sm"}):o.push({label:"Dismiss",color:"white"}),v=this.notify({message:e,color:t,textColor:"white",position:"bottom-right",timeout:0,group:!0,actions:o})},log({level:e,message:t}){switch(e){case"error":this.logger.error(t);break;case"warn":this.logger.warn(t);break;case"info":this.logger.info(t);break;case"debug":this.logger.debug(t);break}},rpcErrorHandler(e,t){e=e.toString(),this.showNotif({message:`RPC error in command '${t}': ${e}`,color:"negative"}),this.log({level:"error",message:`Main: RPC error in command '${t}': ${e}`})},async start(e){const t=await this.findKnownDevices();if(t&&t.length>0)await this.connect(),await this.startRpc(),await this.readInfo();else if(this.flags.portSelectRequired=!0,e)return this.selectPort()}},async mounted(){"serial"in navigator&&(this.flags.serialSupported=!0,await this.start(),navigator.serial.addEventListener("disconnect",(e=>{this.autoReconnect()})),navigator.serial.addEventListener("disconnect",(e=>{this.flags.updateInProgress||(this.showNotif({message:"Flipper has been disconnected"}),this.flags.connected=!1,this.flags.portSelectRequired=!0),this.log({level:"info",message:"Main: Flipper has been disconnected"})}))),this.logger.setLevel("debug",!0);const e=this.logger.methodFactory;this.logger.methodFactory=function(t,s,o){const n=e(t,s,o);return function(e){"debug"!==t&&n(e)}}}});var S=s(2807),y=s(557),b=s(5205),R=s(7716),k=s(1693),I=s(564),_=s(8582),q=s.n(_);const A=(0,S.A)(w,[["render",g]]),L=A;q()(w,"components",{QLayout:y.A,QPageContainer:b.A,QPage:R.A,QBtn:k.A,QSpinner:I.A})}}]); -------------------------------------------------------------------------------- /public/js/active.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | 4 | // :: ScrollUp 5 | if ($.fn.scrollUp) { 6 | $.scrollUp({ 7 | scrollSpeed: 500, 8 | scrollText: '' 9 | }); 10 | } 11 | 12 | // :: onePageNav 13 | if ($.fn.onePageNav) { 14 | $('#nav').onePageNav({ 15 | currentClass: 'active', 16 | scrollSpeed: 750, 17 | easing: 'easeInOutQuint' 18 | }); 19 | } 20 | 21 | $('a[href="#"]').click(function ($) { 22 | $.preventDefault() 23 | }); 24 | 25 | var $window = $(window); 26 | 27 | if ($window.width() > 767) { 28 | new WOW().init(); 29 | } 30 | 31 | // :: Sticky Header 32 | $window.on('scroll', function () { 33 | if ($window.scrollTop() > 48) { 34 | $('header').addClass('sticky slideInDown'); 35 | } else { 36 | $('header').removeClass('sticky slideInDown'); 37 | } 38 | }); 39 | 40 | })(jQuery); 41 | 42 | // :: Video Slideshow 43 | $(document).ready(function () { 44 | var pos = 0, 45 | slides = $('.slide'), 46 | numOfSlides = slides.length; 47 | 48 | function nextSlide() { 49 | // `[]` returns a vanilla DOM object from a jQuery object/collection 50 | slides[pos].video.stopVideo() 51 | slides.eq(pos).animate({ left: '-100%' }, 500); 52 | pos = (pos >= numOfSlides - 1 ? 0 : ++pos); 53 | slides.eq(pos).css({ left: '100%' }).animate({ left: 0 }, 500); 54 | } 55 | 56 | function previousSlide() { 57 | slides[pos].video.stopVideo() 58 | slides.eq(pos).animate({ left: '100%' }, 500); 59 | pos = (pos == 0 ? numOfSlides - 1 : --pos); 60 | slides.eq(pos).css({ left: '-100%' }).animate({ left: 0 }, 500); 61 | } 62 | 63 | $('.left').click(previousSlide); 64 | $('.right').click(nextSlide); 65 | }) 66 | 67 | function onYouTubeIframeAPIReady() { 68 | $('.slide').each(function (index, slide) { 69 | // Get the `.video` element inside each `.slide` 70 | var iframe = $(slide).find('.video')[0] 71 | // Create a new YT.Player from the iFrame, and store it on the `.slide` DOM object 72 | slide.video = new YT.Player(iframe) 73 | }) 74 | } 75 | 76 | -------------------------------------------------------------------------------- /public/js/app.76009145.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={9252:(e,t,r)=>{var n=r(9104),o=r(6501),a=r(8734),i=r(1758);function u(e,t,r,n,o,a){const u=(0,i.g2)("router-view");return(0,i.uX)(),(0,i.Wv)(u)}const s=(0,i.pM)({name:"App"});var c=r(2807);const l=(0,c.A)(s,[["render",u]]),d=l;var p=r(1573),f=r(455);const m=[{path:"/update-frame",component:()=>Promise.all([r.e(121),r.e(996),r.e(890)]).then(r.bind(r,3890)),children:[{path:"/update-frame",component:()=>Promise.all([r.e(121),r.e(996),r.e(144)]).then(r.bind(r,4144))}]},{path:"/asset-packs-frame",component:()=>Promise.all([r.e(121),r.e(996),r.e(85)]).then(r.bind(r,6085)),children:[{path:"/asset-packs-frame",component:()=>Promise.all([r.e(121),r.e(996),r.e(842)]).then(r.bind(r,6842))}]}],v=m,h=(0,p.wE)((function(){const e=f.LA,t=(0,f.aE)({scrollBehavior:()=>({left:0,top:0}),routes:v,history:e("/")});return t}));async function b(e,t){const r=e(d);r.use(o.A,t);const n=(0,a.IG)("function"===typeof h?await h({}):h);return{app:r,router:n}}var g=r(1627);const y={config:{},plugins:{Notify:g.A}};async function w({app:e,router:t}){e.use(t),e.mount("#q-app")}b(n.Ef,y).then(w)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n].call(a.exports,a,a.exports,r),a.exports}r.m=e,(()=>{var e=[];r.O=(t,n,o,a)=>{if(!n){var i=1/0;for(l=0;l=a)&&Object.keys(r.O).every((e=>r.O[e](n[s])))?n.splice(s--,1):(u=!1,a0&&e[l-1][2]>a;l--)e[l]=e[l-1];e[l]=[n,o,a]}})(),(()=>{r.n=e=>{var t=e&&e.__esModule?()=>e["default"]:()=>e;return r.d(t,{a:t}),t}})(),(()=>{r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})}})(),(()=>{r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((t,n)=>(r.f[n](e,t),t)),[]))})(),(()=>{r.u=e=>"js/"+(996===e?"chunk-common":e)+"."+{85:"1db80424",144:"36e4dd28",254:"3e4a3252",665:"e7cdfcdd",842:"c344dabf",890:"065c7545",996:"4e59ed1c"}[e]+".js"})(),(()=>{r.miniCssF=e=>{}})(),(()=>{r.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()})(),(()=>{r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{var e={},t="momentum-fw.dev:";r.l=(n,o,a,i)=>{if(e[n])e[n].push(o);else{var u,s;if(void 0!==a)for(var c=document.getElementsByTagName("script"),l=0;l{u.onerror=u.onload=null,clearTimeout(f);var o=e[n];if(delete e[n],u.parentNode&&u.parentNode.removeChild(u),o&&o.forEach((e=>e(r))),t)return t(r)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=p.bind(null,u.onerror),u.onload=p.bind(null,u.onload),s&&document.head.appendChild(u)}}})(),(()=>{r.r=e=>{"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}})(),(()=>{r.j=524})(),(()=>{r.p="/"})(),(()=>{r.b=document.baseURI||self.location.href;var e={524:0};r.f.j=(t,n)=>{var o=r.o(e,t)?e[t]:void 0;if(0!==o)if(o)n.push(o[2]);else{var a=new Promise(((r,n)=>o=e[t]=[r,n]));n.push(o[2]=a);var i=r.p+r.u(t),u=new Error,s=n=>{if(r.o(e,t)&&(o=e[t],0!==o&&(e[t]=void 0),o)){var a=n&&("load"===n.type?"missing":n.type),i=n&&n.target&&n.target.src;u.message="Loading chunk "+t+" failed.\n("+a+": "+i+")",u.name="ChunkLoadError",u.type=a,u.request=i,o[1](u)}};r.l(i,s,"chunk-"+t,t)}},r.O.j=t=>0===e[t];var t=(t,n)=>{var o,a,[i,u,s]=n,c=0;if(i.some((t=>0!==e[t]))){for(o in u)r.o(u,o)&&(r.m[o]=u[o]);if(s)var l=s(r)}for(t&&t(n);cr(9252)));n=r.O(n)})(); -------------------------------------------------------------------------------- /public/logo-collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/logo-collapsed.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/logo.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Next-Flip/Momentum-Website/b9a915b78c8e2f39bdcd4ab251a43f681ea41310/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Momentum", 3 | "short_name": "Momentum", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Cabin:400,500,700|Montserrat:400,500,700'); 2 | @import 'css/bootstrap.min.css'; 3 | @import 'css/animate.css'; 4 | @import 'css/ionicons.min.css'; 5 | 6 | /* -------------------------- 7 | :: Base CSS 8 | -------------------------- */ 9 | 10 | * { 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | body { 16 | font-family: 'Cabin', sans-serif; 17 | background-color: #000; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | font-family: 'Cabin', sans-serif; 27 | font-weight: 400; 28 | color: #000; 29 | line-height: 1.2; 30 | } 31 | 32 | .section-padding-100 { 33 | padding-top: 100px; 34 | padding-bottom: 100px; 35 | } 36 | 37 | .section-padding-100-50 { 38 | padding-top: 100px; 39 | padding-bottom: 50px; 40 | } 41 | 42 | .section-padding-100-70 { 43 | padding-top: 100px; 44 | padding-bottom: 70px; 45 | } 46 | 47 | .section-padding-50 { 48 | padding-top: 50px; 49 | padding-bottom: 50px; 50 | } 51 | 52 | .section-padding-50-20 { 53 | padding-top: 50px; 54 | padding-bottom: 20px; 55 | } 56 | 57 | .section-padding-150 { 58 | padding-top: 150px; 59 | padding-bottom: 150px; 60 | } 61 | 62 | .section-padding-200 { 63 | padding-top: 200px; 64 | padding-bottom: 200px; 65 | } 66 | 67 | .section-padding-0-100 { 68 | padding-top: 0; 69 | padding-bottom: 100px; 70 | } 71 | 72 | .section-padding-70 { 73 | padding-top: 70px; 74 | padding-bottom: 70px; 75 | } 76 | 77 | .section-padding-0-50 { 78 | padding-top: 0; 79 | padding-bottom: 50px; 80 | } 81 | 82 | img { 83 | max-width: 95%; 84 | max-height: 100%; 85 | } 86 | 87 | .mt-15 { 88 | margin-top: 15px; 89 | } 90 | 91 | .mt-30 { 92 | margin-top: 30px; 93 | } 94 | 95 | .mt-40 { 96 | margin-top: 40px; 97 | } 98 | 99 | .mt-50 { 100 | margin-top: 50px; 101 | } 102 | 103 | .mt-100 { 104 | margin-top: 100px; 105 | } 106 | 107 | .mt-150 { 108 | margin-top: 150px; 109 | } 110 | 111 | .mr-15 { 112 | margin-right: 15px; 113 | } 114 | 115 | .mr-30 { 116 | margin-right: 30px; 117 | } 118 | 119 | .mr-50 { 120 | margin-right: 50px; 121 | } 122 | 123 | .mr-100 { 124 | margin-right: 100px; 125 | } 126 | 127 | .mb-15 { 128 | margin-bottom: 15px; 129 | } 130 | 131 | .mb-30 { 132 | margin-bottom: 30px; 133 | } 134 | 135 | .mb-50 { 136 | margin-bottom: 50px; 137 | } 138 | 139 | .mb-100 { 140 | margin-bottom: 100px; 141 | } 142 | 143 | .ml-15 { 144 | margin-left: 15px; 145 | } 146 | 147 | .ml-30 { 148 | margin-left: 30px; 149 | } 150 | 151 | .ml-50 { 152 | margin-left: 50px; 153 | } 154 | 155 | .ml-100 { 156 | margin-left: 100px; 157 | } 158 | 159 | ul, 160 | ol { 161 | margin: 0; 162 | padding: 0; 163 | } 164 | 165 | #scrollUp { 166 | bottom: 0; 167 | font-size: 24px; 168 | right: 30px; 169 | width: 50px; 170 | background-color: #a883e9; 171 | color: #fff; 172 | text-align: center; 173 | height: 50px; 174 | line-height: 50px; 175 | } 176 | 177 | a, 178 | a:hover, 179 | a:focus, 180 | a:active { 181 | text-decoration: none; 182 | -webkit-transition-duration: 500ms; 183 | -o-transition-duration: 500ms; 184 | transition-duration: 500ms; 185 | } 186 | 187 | li { 188 | list-style: none; 189 | } 190 | 191 | p { 192 | color: #726a84; 193 | font-size: 16px; 194 | font-weight: 300; 195 | margin-top: 0; 196 | } 197 | 198 | .heading-text>p { 199 | font-size: 16px; 200 | } 201 | 202 | .section-heading>h2 { 203 | font-weight: 300; 204 | color: #ddd; 205 | font-size: 48px; 206 | margin: 0; 207 | } 208 | 209 | 210 | .section-heading { 211 | margin-bottom: 60px; 212 | } 213 | 214 | .line-shape-white, 215 | .line-shape { 216 | width: 80px; 217 | height: 2px; 218 | background-color: #a883e9; 219 | margin-top: 15px; 220 | } 221 | 222 | .line-shape { 223 | margin-left: calc(50% - 40px); 224 | } 225 | 226 | .table { 227 | display: table; 228 | height: 100%; 229 | left: 0; 230 | position: relative; 231 | top: 0; 232 | width: 100%; 233 | z-index: 2; 234 | } 235 | 236 | .table-cell { 237 | display: table-cell; 238 | vertical-align: middle; 239 | } 240 | 241 | ::-webkit-scrollbar { 242 | background: #0000; 243 | width: 16px 244 | } 245 | 246 | ::-webkit-scrollbar-track { 247 | background: #0000; 248 | border: 3px solid #0000; 249 | margin: 14px 0; 250 | overflow: auto 251 | } 252 | 253 | ::-webkit-scrollbar-thumb { 254 | background: #666; 255 | background-clip: padding-box; 256 | border: 6px solid #0000; 257 | border-radius: 16px; 258 | min-height: 24px; 259 | overflow: auto; 260 | padding: 50px; 261 | -webkit-transition: height .2s ease-in-out; 262 | transition: height .2s ease-in-out 263 | } 264 | 265 | ::-webkit-scrollbar-thumb:hover { 266 | background-color: #bbb 267 | } 268 | 269 | /* -------------------------- 270 | :: Header Bar 271 | -------------------------- */ 272 | 273 | header { 274 | left: 0; 275 | position: absolute; 276 | width: 100%; 277 | z-index: 99; 278 | top: 0; 279 | padding: 0 4%; 280 | } 281 | 282 | .nav-menu .navbar-logo { 283 | font-size: 72px; 284 | font-weight: 700; 285 | color: #fff; 286 | margin: 0; 287 | line-height: 1; 288 | padding: 0; 289 | content:url("/logo.png"); 290 | height: 100px; 291 | } 292 | 293 | 294 | .nav-menu .navbar-logo:hover, 295 | .nav-menu .navbar-logo:focus { 296 | color: #fff; 297 | } 298 | 299 | 300 | .nav-menu { 301 | position: relative; 302 | z-index: 2; 303 | } 304 | 305 | .nav-menu #nav .nav-link { 306 | color: #fff; 307 | display: block; 308 | font-size: 16px; 309 | font-weight: 500; 310 | border-radius: 30px; 311 | -webkit-transition-duration: 500ms; 312 | -o-transition-duration: 500ms; 313 | transition-duration: 500ms; 314 | padding: 35px 15px; 315 | } 316 | 317 | .nav-menu #nav .nav-link.install-button { 318 | padding: 22px 15px; 319 | } 320 | 321 | 322 | .nav-menu nav ul li>a:hover { 323 | color: #a883e9; 324 | } 325 | 326 | .install-button { 327 | text-align: right; 328 | } 329 | 330 | .install-button>a { 331 | color: #fff; 332 | font-weight: 500; 333 | display: inline-block; 334 | border: 2px solid #a883e9; 335 | height: 50px; 336 | min-width: 140px; 337 | line-height: 46px; 338 | text-align: center; 339 | border-radius: 24px; 340 | } 341 | 342 | .install-button>a:hover { 343 | background: #a883e9; 344 | color: #fff; 345 | border-color: transparent; 346 | -webkit-transition-duration: 500ms; 347 | -o-transition-duration: 500ms; 348 | transition-duration: 500ms; 349 | } 350 | 351 | /* sticky css */ 352 | 353 | header.sticky { 354 | background-color: #0b0b0b; 355 | -webkit-box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 356 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); 357 | height: 70px; 358 | position: fixed; 359 | top: 0; 360 | z-index: 99; 361 | } 362 | 363 | .navbar-collapse { 364 | align-self: flex-start; 365 | } 366 | 367 | header.sticky .nav-menu .navbar-logo { 368 | font-size: 50px; 369 | content:url("/logo-collapsed.png"); 370 | height: 40px; 371 | } 372 | 373 | header.sticky .nav-menu #nav .nav-link { 374 | padding: 23px 15px; 375 | } 376 | 377 | header.sticky .nav-menu #nav .nav-link.install-button { 378 | padding: 10px 15px; 379 | } 380 | 381 | header.sticky .navbar { 382 | padding: 0; 383 | } 384 | 385 | 386 | /* -------------------------- 387 | :: Home Section 388 | -------------------------- */ 389 | 390 | section#home { 391 | background-image: url(img/hero-background.png); 392 | height: 700px; 393 | position: relative; 394 | z-index: 1; 395 | background-position: bottom center; 396 | background-size: cover; 397 | } 398 | 399 | section#home>.container { 400 | padding-top: 200px; 401 | position: relative 402 | } 403 | 404 | .home-thumb { 405 | height: 50%; 406 | position: absolute; 407 | top: 35%; 408 | left: 50%; 409 | z-index: 9; 410 | } 411 | 412 | .home-thumb img { 413 | height: auto; 414 | width: 100%; 415 | rotate: 8deg; 416 | } 417 | 418 | .home-heading>h2 { 419 | font-size: 100px; 420 | color: #ffffff; 421 | font-weight: 700; 422 | position: relative; 423 | z-index: 3; 424 | } 425 | 426 | .home-heading>h4 { 427 | color: #ffffff; 428 | font-size: 30px; 429 | font-weight: 700; 430 | position: relative; 431 | z-index: 3; 432 | } 433 | 434 | .home-heading>p { 435 | color: #fff; 436 | } 437 | 438 | .home-heading>h3 { 439 | font-size: 269px; 440 | position: absolute; 441 | top: -134px; 442 | font-weight: 900; 443 | left: -56px; 444 | z-index: -1; 445 | color: #fff; 446 | opacity: .1; 447 | filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=10)"; 448 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=10)"; 449 | } 450 | 451 | .home-heading { 452 | margin-bottom: 100px; 453 | } 454 | 455 | /* -------------------------- 456 | :: Special Section 457 | -------------------------- */ 458 | 459 | .single-icon>i { 460 | font-size: 36px; 461 | color: #fff; 462 | } 463 | 464 | .single-special>h4 { 465 | font-size: 22px; 466 | color: #fff; 467 | } 468 | 469 | .single-special>h4 { 470 | font-size: 22px; 471 | color: #a883e9; 472 | margin-bottom: 15px; 473 | } 474 | 475 | .single-special { 476 | border: 1px solid #eff2f6; 477 | padding: 40px; 478 | border-radius: 30px; 479 | -webkit-transition-duration: 800ms; 480 | -o-transition-duration: 800ms; 481 | transition-duration: 800ms; 482 | margin-bottom: 30px; 483 | } 484 | 485 | .single-special:hover { 486 | -webkit-box-shadow: 0 10px 90px rgba(0, 0, 0, 0.08); 487 | box-shadow: 0 10px 90px rgba(0, 0, 0, 0.08); 488 | } 489 | 490 | .single-special p { 491 | margin-bottom: 0; 492 | } 493 | 494 | /* -------------------------- 495 | :: Features Section 496 | -------------------------- */ 497 | 498 | .single-feature { 499 | margin-bottom: 50px; 500 | } 501 | 502 | .single-feature>p { 503 | margin-bottom: 0; 504 | } 505 | 506 | .single-feature>i { 507 | color: #a883e9; 508 | font-size: 30px; 509 | display: inline-block; 510 | float: left; 511 | margin-right: 10px; 512 | margin-top: -9px; 513 | } 514 | 515 | .single-feature>h5 { 516 | font-size: 22px; 517 | color: #fff; 518 | } 519 | 520 | .single-feature>p { 521 | margin-top: 15px; 522 | } 523 | 524 | /* -------------------------- 525 | :: Discord Section 526 | -------------------------- */ 527 | 528 | section#discord { 529 | background: linear-gradient(to left, #a95cf1, #6d61dd); 530 | } 531 | 532 | .discord-description>h2 { 533 | color: #ffffff; 534 | font-size: 36px; 535 | } 536 | 537 | .discord-description>p { 538 | color: #ffffff; 539 | margin-bottom: 0; 540 | } 541 | 542 | .discord-button>a { 543 | background: #ffffff; 544 | height: 45px; 545 | min-width: 155px; 546 | display: inline-block; 547 | text-align: center; 548 | line-height: 45px; 549 | color: #7850bd; 550 | font-weight: 500; 551 | border-radius: 23px; 552 | } 553 | 554 | .discord-button>a:hover { 555 | background: #5b32b4; 556 | color: #fff; 557 | -webkit-transition-duration: 500ms; 558 | -o-transition-duration: 500ms; 559 | transition-duration: 500ms; 560 | } 561 | 562 | .discord-button { 563 | text-align: right; 564 | } 565 | 566 | /* -------------------------- 567 | :: Table Section 568 | -------------------------- */ 569 | 570 | .table-responsive { 571 | border-radius: 22px; 572 | color: #fff; 573 | border: 2px solid #bbb; 574 | } 575 | 576 | .table-responsive tbody, 577 | .table-responsive td, 578 | .table-responsive tfoot, 579 | .table-responsive th, 580 | .table-responsive thead, 581 | .table-responsive tr { 582 | border: 0; 583 | font-size: 20px; 584 | } 585 | 586 | .table-responsive thead tr { 587 | background-color: #333; 588 | color: var(--white-color); 589 | } 590 | 591 | .table-responsive thead th { 592 | padding: 22px 16px !important; 593 | } 594 | 595 | .table-responsive tbody tr:nth-child(even) { 596 | background-color: #111; 597 | } 598 | 599 | .table { 600 | margin: 0; 601 | } 602 | 603 | .table>:not(caption)>*>* { 604 | padding: 18px 16px; 605 | } 606 | 607 | .table i { 608 | font-size: 28px; 609 | } 610 | 611 | .table .ion-help-circled { 612 | color: #b3b3b3; 613 | } 614 | 615 | .table .ion-close-circled { 616 | color: #ca8300; 617 | } 618 | 619 | .table .ion-checkmark-circled { 620 | color: #9d4aeb; 621 | background: radial-gradient(white 40%, transparent 40%); 622 | } 623 | 624 | 625 | /* -------------------------- 626 | :: Video Section 627 | -------------------------- */ 628 | 629 | .video-slider-container { 630 | aspect-ratio: 16/9; 631 | margin: auto; 632 | padding: 0; 633 | max-height: 20em; 634 | position: relative; 635 | overflow: visible; 636 | max-width: 80%; 637 | } 638 | 639 | .video-slider { 640 | aspect-ratio: 16/9; 641 | position: relative; 642 | overflow: hidden; 643 | background: #444; 644 | border-radius: 24px; 645 | border: 2px solid white; 646 | } 647 | 648 | .slide { 649 | position: absolute; 650 | top: 0; 651 | left: 100%; 652 | height: 100%; 653 | width: 100%; 654 | text-align: center; 655 | overflow: hidden; 656 | } 657 | 658 | .slide:first-child { 659 | left: 0; 660 | } 661 | 662 | .video { 663 | height: 100%; 664 | width: 100%; 665 | border: 0; 666 | } 667 | 668 | .slide-arrow { 669 | position: absolute; 670 | font-size: 36px; 671 | top: calc(50% - 0.5em); 672 | width: fit-content; 673 | height: fit-content; 674 | z-index: 1; 675 | cursor: pointer; 676 | } 677 | 678 | .slide-arrow::before { 679 | color: white; 680 | opacity: .5; 681 | transition: all .3s; 682 | } 683 | 684 | .slide-arrow:hover::before { 685 | opacity: 1; 686 | color: red; 687 | } 688 | 689 | .slide-arrow.left { 690 | left: -10%; 691 | } 692 | 693 | .slide-arrow.right { 694 | right: -10%; 695 | } 696 | -------------------------------------------------------------------------------- /public/update-frame/index.html: -------------------------------------------------------------------------------- 1 | Momentum FW for Flipper Zero
    -------------------------------------------------------------------------------- /public/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Momentum FW Web-Updater 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    29 |
    30 | 55 |
    56 |
    57 |
    58 |
    59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | --------------------------------------------------------------------------------