├── app ├── pnpm-workspace.yaml ├── icon.png ├── icon@2x.png ├── images │ ├── full.png │ ├── icon.png │ ├── icon-dark.png │ ├── icon@2x.png │ ├── IconTemplate.png │ └── icon-dark@2x.png ├── IconTemplate.png ├── webfonts │ ├── fa-solid-900.eot │ ├── fa-solid-900.ttf │ ├── fa-brands-400.eot │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-regular-400.eot │ ├── fa-regular-400.ttf │ ├── fa-solid-900.woff │ ├── fa-solid-900.woff2 │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.woff │ └── fa-regular-400.woff2 ├── package.json ├── js │ ├── loader.js │ └── tippy-headless.umd.min.js ├── rtest.js ├── remote.js ├── input.html ├── server_runner.js ├── ws_remote.js ├── hotkey.html ├── index.html ├── remote_reference.js ├── css │ ├── main.css │ ├── loaders.css │ ├── select2.min.css │ ├── pure-min.css │ └── select2-inverted.css ├── main.js ├── pyscripts.js └── pnpm-lock.yaml ├── screenshot.png ├── buttonpress.gif ├── screenshot_new.png ├── .gitignore ├── fixversion.py ├── electron_docker.sh ├── LICENSE ├── travis.yml ├── package.json └── README.md /app/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - electron 3 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/icon.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/screenshot.png -------------------------------------------------------------------------------- /app/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/icon@2x.png -------------------------------------------------------------------------------- /buttonpress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/buttonpress.gif -------------------------------------------------------------------------------- /app/images/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/images/full.png -------------------------------------------------------------------------------- /app/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/images/icon.png -------------------------------------------------------------------------------- /screenshot_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/screenshot_new.png -------------------------------------------------------------------------------- /app/IconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/IconTemplate.png -------------------------------------------------------------------------------- /app/images/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/images/icon-dark.png -------------------------------------------------------------------------------- /app/images/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/images/icon@2x.png -------------------------------------------------------------------------------- /app/images/IconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/images/IconTemplate.png -------------------------------------------------------------------------------- /app/images/icon-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/images/icon-dark@2x.png -------------------------------------------------------------------------------- /app/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /app/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /app/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /app/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /app/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /app/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /app/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /app/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /app/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /app/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /app/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /app/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DandelionSprout/atv-desktop-remote/main/app/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | app/node_modules 3 | .DS_Store 4 | dist 5 | atv_ws_env 6 | old 7 | *.gz 8 | pytest 9 | build/pyscripts.js 10 | stopserver 11 | .env* 12 | pyatv 13 | old_dist 14 | -------------------------------------------------------------------------------- /fixversion.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | args = sys.argv[1:] 5 | 6 | build_pkg = json.load(open('package.json')) 7 | ver = build_pkg['version'] 8 | app_pkg = json.load(open('app/package.json')) 9 | appver = app_pkg['version'] 10 | 11 | if len(args) > 0: 12 | newver = args[0] 13 | else: 14 | newver = ver 15 | 16 | build_pkg['version'] = newver 17 | app_pkg['version'] = newver 18 | 19 | 20 | json.dump(app_pkg, open('app/package.json', 'w'), indent=4) 21 | json.dump(build_pkg, open('package.json', 'w'), indent=4) 22 | 23 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atv-mac-remote", 3 | "version": "1.4.3", 4 | "description": "ATV Remote", 5 | "productName": "ATV Remote", 6 | "scripts": { 7 | "start": "cd .. && npx electron app/main.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "main": "main.js", 13 | "dependencies": { 14 | "@electron/remote": "^2.1.2", 15 | "bluebird": "^3.7.2", 16 | "electron-positioner": "^4.1.0", 17 | "jquery": "^3.5.1", 18 | "menubar": "^9.0.2", 19 | "ws": "^8.3.0" 20 | } 21 | } -------------------------------------------------------------------------------- /electron_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # -v ${PWD##*/}-node-modules:/project/node_modules \ 4 | 5 | 6 | docker run --rm -ti \ 7 | --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \ 8 | --env ELECTRON_CACHE="/root/.cache/electron" \ 9 | --env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \ 10 | -v ${PWD}:/project \ 11 | -v $(PWD)/node_modules:/projects/node_modules \ 12 | -v ~/.cache/electron:/root/.cache/electron \ 13 | -v ~/.cache/electron-builder:/root/.cache/electron-builder \ 14 | electronuserland/builder:wine $1 15 | -------------------------------------------------------------------------------- /app/js/loader.js: -------------------------------------------------------------------------------- 1 | function getLoader() { 2 | return `
3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ` 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 bsharper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | osx_image: xcode10.2 5 | language: node_js 6 | node_js: "10" 7 | env: 8 | - ELECTRON_CACHE=$HOME/.cache/electron 9 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 10 | 11 | - os: linux 12 | services: docker 13 | language: generic 14 | 15 | cache: 16 | directories: 17 | - node_modules 18 | - $HOME/.cache/electron 19 | - $HOME/.cache/electron-builder 20 | 21 | script: 22 | - | 23 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 24 | docker run --rm \ 25 | --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') \ 26 | -v ${PWD}:/project \ 27 | -v ~/.cache/electron:/root/.cache/electron \ 28 | -v ~/.cache/electron-builder:/root/.cache/electron-builder \ 29 | electronuserland/builder:wine \ 30 | /bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn release --linux --win" 31 | else 32 | yarn release 33 | fi 34 | before_cache: 35 | - rm -rf $HOME/.cache/electron-builder/wine 36 | 37 | branches: 38 | except: 39 | - "/^v\\d+\\.\\d+\\.\\d+$/" 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atv-mac-remote", 3 | "version": "1.4.3", 4 | "main": "app/main.js", 5 | "description": "ATV Remote", 6 | "productName": "ATV Remote", 7 | "build": { 8 | "productName": "ATV Remote", 9 | "appId": "com.electron.atvMacRemote", 10 | "mac": { 11 | "category": "public.app-category.utilities", 12 | "hardenedRuntime": true, 13 | "gatekeeperAssess": false, 14 | "entitlements": "build/entitlements.mac.plist", 15 | "entitlementsInherit": "build/entitlements.mac.plist" 16 | } 17 | }, 18 | "scripts": { 19 | "embed": "node build/create_python_embed.js --overwrite", 20 | "start": "electron app", 21 | "postinstall": "electron-builder install-app-deps", 22 | "build": "npx electron-builder -m", 23 | "docker-build": "./electron_docker.sh 'npm run build:win'", 24 | "build:win": "npx electron-builder -w", 25 | "build-arm": "npx electron-builder -m --arm64" 26 | }, 27 | "devDependencies": { 28 | "electron": "^38.2.1", 29 | "electron-builder": "^26.0.20" 30 | }, 31 | "dependencies": { 32 | "@electron/remote": "^2.1.2", 33 | "menubar": "^9.5.1" 34 | }, 35 | "pnpm": { 36 | "onlyBuiltDependencies": [ 37 | "electron" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/rtest.js: -------------------------------------------------------------------------------- 1 | var Promise = require("bluebird"); 2 | 3 | Promise.config({ 4 | cancellation: true 5 | }); 6 | 7 | var rmt = require('./remote') 8 | const readline = require('readline'); 9 | 10 | function askQuestion(query) { 11 | const rl = readline.createInterface({ 12 | input: process.stdin, 13 | output: process.stdout, 14 | }); 15 | 16 | return new Promise(resolve => rl.question(query, ans => { 17 | rl.close(); 18 | resolve(ans); 19 | })) 20 | } 21 | 22 | async function main () { 23 | console.log(`Starting device scan...`) 24 | var ds = await rmt.scanDevices(); 25 | console.log(ds); 26 | if (ds.length === 0) { 27 | console.log('No devices found'); 28 | return; 29 | } 30 | var d = ds[0]; 31 | console.log(`Starting pairing to ${d}`) 32 | try { 33 | await rmt.startPairWithTimeout(d, 30000) // 30 seconds 34 | } catch (err) { 35 | if (err instanceof Promise.TimeoutError) { 36 | console.log(`Start pairing timed out (${err})`) 37 | } else { 38 | console.log(`Could not start pairing: ${err}`) 39 | } 40 | return false; 41 | } 42 | var pin = await askQuestion('Enter pin number: ') 43 | pin = pin.replace(/[^0-9]/g, '') // replace any non-numbers with numbers 44 | console.log(`Finishing pairing with pin number: ${pin}`) 45 | try { 46 | var creds = await rmt.finishPairWithTimeout(pin, 300000) // 5 minute timeout 47 | } catch (err) { 48 | console.log(`Pairing timed out`) 49 | return false; 50 | } 51 | console.log(`All done\nCreds: ${creds}`) 52 | return true; 53 | } 54 | 55 | (async () => { 56 | await main(); 57 | })() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ATV Desktop Remote 2 | A simple menubar app that allows you to control an Apple TV from your desktop 3 | 4 | ![What this application looks like when running in either light or dark mode](screenshot_new.png) 5 | 6 | 7 | 8 | ## Installation 9 | 10 | If you have homebrew installed `brew install atv-remote` 11 | 12 | macOS and Windows versions can be downloaded from here: https://github.com/bsharper/atv-desktop-remote/releases 13 | 14 | 15 | ## Usage 16 | 17 | 1. Keys are mapped to the keyboard when the application is open (pressing return or enter on the keyboard for select, delete for Menu, etc). 18 | 2. Press `Option`, or `Alt` on Windows, to see what the characters are mapped to when the application is open. 19 | 3. Long press buttons now works to simulate long presses on the remote 20 | 21 |

22 | long press button animation 23 |

24 | 25 | 26 | 27 | ## Running 28 | 29 | 1. Run `npm install` (`yarn` and `pnpm` should also work) 30 | 2. Run `npm start` 31 | 3. The application runs in the menubar. Look for a tiny remote icon and click on it. Right-click for more options. 32 | 4. The first time the app runs it will need to pair with an Apple TV. You can pair with more than one. 33 | 5. Press `Cmd+Shift+R` to open the application from anywhere. On Windows its `Win+Shift+R` 34 | 35 | ## Building 36 | 37 | 1. `electron-builder` is used to create a standalone application. 38 | 39 | ## Notes 40 | 41 | This is cobbled together from a few projects I've worked on. It works well enough for me for daily use, so I figured others might like it. 42 | 43 | This project is built using [pyatv.](https://pyatv.dev/) 44 | 45 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/brianharper) 46 | -------------------------------------------------------------------------------- /app/remote.js: -------------------------------------------------------------------------------- 1 | var Promise = require("bluebird"); 2 | 3 | Promise.config({ 4 | cancellation: true 5 | }); 6 | 7 | var atv = require('node-appletv-x') 8 | 9 | var devices = {} 10 | var device = false; 11 | var lastScan = {} 12 | var callback = () => {} 13 | var defaultTimeout = 30000 14 | 15 | function timeoutPromise(asFunc, timeout) { 16 | return new Promise((resolve, reject) => { 17 | var p = asFunc(); 18 | var st = setTimeout(() => { 19 | reject(new Promise.TimeoutError()) 20 | }, timeout) 21 | p.then(r => { 22 | clearTimeout(st); 23 | resolve(p); 24 | }) 25 | }) 26 | } 27 | 28 | async function scanDevices() { 29 | devices = await atv.scan(); 30 | lastScan = {} 31 | var ks = []; 32 | devices.forEach(d => { 33 | var k = `${d.name} (${d.address})` 34 | lastScan[k] = d; 35 | ks.push(k); 36 | }) 37 | return ks 38 | } 39 | 40 | async function startPair(id) { 41 | console.log(`remote: startPair: ${id}`) 42 | console.log(`lastScan: ${Object.keys(lastScan)}`) 43 | device = lastScan[id]; 44 | //console.log(device); 45 | console.log(`Opening device connection`) 46 | await device.openConnection(); 47 | console.log(`Requesting pairing`) 48 | callback = await device.pair(); 49 | return true; 50 | } 51 | 52 | function startPairWithTimeout(id, timeout) { 53 | if (typeof timeout === 'undefined') timeout = defaultTimeout 54 | return timeoutPromise(() => { return startPair(id) }, timeout) 55 | } 56 | 57 | async function finishPair(pin) { 58 | await callback(pin); 59 | let credentials = device.credentials.toString(); 60 | var data = JSON.stringify({ credentialsString: credentials }, null, 4); 61 | device.closeConnection() 62 | return data; 63 | } 64 | 65 | 66 | 67 | function finishPairWithTimeout(pin, timeout) { 68 | if (typeof timeout === 'undefined') timeout = defaultTimeout 69 | return timeoutPromise(() => { return finishPair(pin) }, timeout) 70 | } 71 | 72 | exports.atv = atv; 73 | exports.scanDevices = scanDevices; 74 | exports.startPair = startPair; 75 | exports.finishPair = finishPair; 76 | exports.defaultTimeout = defaultTimeout; 77 | exports.startPairWithTimeout = startPairWithTimeout; 78 | exports.finishPairWithTimeout = finishPairWithTimeout; -------------------------------------------------------------------------------- /app/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Search field input 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |

This will only work if a search field is active on the connected Apple TV.

21 |

🟢 = connected device is accepting text input

22 |

⚪️ = connected device does not appear to be accepting text input

23 | 24 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /app/server_runner.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn 2 | const exec = require('child_process').exec 3 | const WebSocket = require('ws').WebSocket 4 | const readline = require('readline') 5 | const fs = require('fs') 6 | const fsp = fs.promises 7 | const EventEmitter = require('events'); 8 | const path = require('path') 9 | const sfiles = require('./pyscripts').files; 10 | 11 | var server_events = new EventEmitter(); 12 | var proc = false; 13 | 14 | var showOutputs = false; 15 | var serverRunning = false; 16 | 17 | 18 | function getWorkingPath() { 19 | return path.join(process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.local/share"), "ATV Remote"); 20 | } 21 | 22 | function fileExists(fn) { 23 | return new Promise((resolve, reject) => { 24 | fs.access(fn, err => { 25 | if (err) return resolve(false); 26 | resolve(true); 27 | }) 28 | }) 29 | } 30 | 31 | function fileExistsSync(fn) { 32 | try { 33 | fs.accessSync(fn); 34 | return true; 35 | } catch (err) { 36 | return false; 37 | } 38 | } 39 | 40 | 41 | 42 | function debounce(func, timeout = 300) { 43 | let timer; 44 | return (...args) => { 45 | clearTimeout(timer); 46 | timer = setTimeout(() => { func.apply(this, args); }, timeout); 47 | }; 48 | } 49 | 50 | function killServer() { 51 | console.log('killServer'); 52 | return new Promise((resolve, reject) => { 53 | var ws_url = 'ws://localhost:8765' 54 | var lws = new WebSocket(ws_url, { 55 | perMessageDeflate: false 56 | }); 57 | var ksto = setTimeout(() => { 58 | console.log('killServer timed out') 59 | reject(); 60 | }, 500); 61 | lws.once('open', function open() { 62 | console.log('killServer open'); 63 | clearTimeout(ksto); 64 | lws.send(JSON.stringify({ cmd: 'quit' })) 65 | resolve(); 66 | }); 67 | }) 68 | } 69 | 70 | async function stopServerFile() { 71 | var stopFilePath = path.join(getWorkingPath(), "stopserver"); 72 | await fsp.writeFile(stopFilePath, "stop"); 73 | console.log('stopserver written'); 74 | } 75 | 76 | function stopServerFileSync() { 77 | var stopFilePath = path.join(getWorkingPath(), "stopserver"); 78 | fs.writeFileSync(stopFilePath, "stop"); 79 | } 80 | 81 | function _announceServerStart() { 82 | // debounce to allow multiple interfaces to bind 83 | serverRunning = true; 84 | server_events.emit("started"); 85 | console.log(`Server started ${proc.pid}`) 86 | } 87 | 88 | var announceServerStart = debounce(_announceServerStart, 200); 89 | 90 | function testPythonExists() { 91 | return new Promise((resolve, reject) => { 92 | exec("python3 -V", (err, stdout, stderr) => { 93 | if (err) { 94 | exec("python -V", (err, stdout, stderr) => { 95 | if (err) { 96 | return reject(err); 97 | } else { 98 | var txt = stdout.replace(/\n/g, '').trim(); 99 | resolve(txt); 100 | } 101 | }); 102 | } else { 103 | var txt = stdout.replace(/\n/g, '').trim(); 104 | resolve(txt); 105 | } 106 | }) 107 | }) 108 | } 109 | 110 | async function pythonExists() { 111 | try { 112 | var r = await testPythonExists(); 113 | return r; 114 | } catch (err) { 115 | return false; 116 | } 117 | } 118 | 119 | 120 | function parseLine(streamName, line) { 121 | if (!serverRunning && line.indexOf("server listening on") > -1) { 122 | announceServerStart(); 123 | } 124 | if (showOutputs) console.log(`SERVER.${streamName}: ${line}`) 125 | } 126 | 127 | function stopServer() { 128 | serverRunning = false; 129 | try { 130 | if (proc) proc.removeAllListeners(); 131 | } catch (e) {} 132 | stopServerFileSync(); 133 | if (proc && !proc.killed) { 134 | try { 135 | proc.kill() 136 | } catch (e) {} 137 | setImmediate(() => { 138 | try { 139 | if (!proc.killed) proc.kill('SIGINT'); 140 | } catch (e) {} 141 | proc = false; 142 | }) 143 | } 144 | } 145 | 146 | function writeSupportFiles() { 147 | var wpath = getWorkingPath(); 148 | if (!fileExistsSync(wpath)) fs.mkdirSync(wpath); 149 | Object.keys(sfiles).forEach(fn => { 150 | var txt = sfiles[fn]; 151 | var out_path = path.join(wpath, fn); 152 | fs.writeFileSync(out_path, txt, { encoding: 'utf-8' }); 153 | console.log(`Writing ${fn} to ${out_path}...`) 154 | if (fn == "start_server.sh" && process.platform != "win32") { 155 | fs.chmodSync(out_path, 0o755); 156 | } 157 | }) 158 | } 159 | 160 | 161 | function startServer() { 162 | // only for testing - this bypasses the server start (assumes you are running the server manually) 163 | // announceServerStart(); 164 | // return; 165 | 166 | var wpath = getWorkingPath(); 167 | var noWriteFiles = path.join(wpath, "skip_file_write"); 168 | if (!fileExistsSync(noWriteFiles)) writeSupportFiles(); 169 | stopServer(); 170 | 171 | 172 | if (process.platform == "win32") { 173 | var bat_path = path.join(wpath, 'start_server.bat') 174 | proc = spawn('cmd.exe', ['/c', bat_path], { detached: false }) 175 | } else { 176 | var sh_path = path.join(wpath, 'start_server.sh'); 177 | console.log(sh_path) 178 | proc = spawn(sh_path, { detached: false }) 179 | } 180 | 181 | var stdout = readline.createInterface({ input: proc.stdout }); 182 | var stderr = readline.createInterface({ input: proc.stderr }); 183 | 184 | stdout.on("line", line => { 185 | parseLine("stdout", line); 186 | }) 187 | 188 | stderr.on("line", line => { 189 | parseLine("stderr", line) 190 | }) 191 | 192 | proc.on('exit', (code, signal) => { 193 | serverRunning = false; 194 | server_events.emit("stopped", code); 195 | console.log(`Server exited with code ${code}`) 196 | }); 197 | 198 | } 199 | 200 | function setShowOutputs(tf) { 201 | showOutputs = !!(tf) 202 | } 203 | 204 | function isServerRunning() { 205 | return serverRunning; 206 | } 207 | 208 | function getProc() { 209 | return proc; 210 | } 211 | 212 | process.on("beforeExit", () => { 213 | stopServer(); 214 | }) 215 | 216 | 217 | async function main() { 218 | var tf = await testPythonExists() 219 | console.log(`python exists: ${tf}`) 220 | server_events.on("started", () => { 221 | console.log('Woohoo, we are up and running'); 222 | }); 223 | startServer(); 224 | } 225 | 226 | 227 | if (require.main === module) { 228 | (async() => { 229 | main(); 230 | })(); 231 | } 232 | 233 | exports.getProc = getProc; 234 | exports.setShowOutputs = setShowOutputs; 235 | exports.showOutputs = showOutputs; 236 | exports.startServer = startServer; 237 | exports.stopServer = stopServer; 238 | exports.server_events = server_events; 239 | exports.pythonExists = pythonExists; 240 | exports.testPythonExists = testPythonExists; 241 | exports.isServerRunning = isServerRunning; 242 | -------------------------------------------------------------------------------- /app/ws_remote.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws').WebSocket 2 | const EventEmitter = require('events'); 3 | 4 | // WebSocketClient.prototype.reconnect = function(e) { 5 | // console.log(`WebSocketClient: retry in ${this.autoReconnectInterval}ms`, e); 6 | // this.instance.removeAllListeners(); 7 | // var that = this; 8 | // setTimeout(function() { 9 | // console.log("WebSocketClient: reconnecting..."); 10 | // that.open(that.url); 11 | // }, this.autoReconnectInterval); 12 | // } 13 | 14 | 15 | var ws = false; 16 | 17 | var ws_timeout = false; 18 | var ws_watchdog = false; 19 | var scanWhenOpen = false; 20 | var ws_connecting = false; 21 | var ws_connected = false; 22 | var ws_start_tm = false; 23 | var connection_failure = false; 24 | var atv_connected = false; 25 | var ws_pairDevice = ""; 26 | 27 | var ws_url = 'ws://localhost:8765' 28 | 29 | var atv_events = new EventEmitter(); 30 | var pending = [] 31 | 32 | var ws_timeout_interval = 800; 33 | 34 | function sendMessage(command, data) { 35 | if (typeof data == "undefined") data = ""; 36 | if (!ws) { 37 | pending.push([command, data]); 38 | return; 39 | } 40 | while (pending.length > 0) { 41 | var cmd_ar = pending.shift(); 42 | ws.send(JSON.stringify({ cmd: cmd_ar[0], data: cmd_ar[1] })) 43 | } 44 | console.log(`sendMessage: {cmd:${command}, data:${data}}`) 45 | ws.send(JSON.stringify({ cmd: command, data: data })) 46 | } 47 | 48 | function killServer() { 49 | var lws = new WebSocket(ws_url, { 50 | perMessageDeflate: false 51 | }); 52 | 53 | lws.once('open', function open() { 54 | lws.send(JSON.stringify({ cmd: 'quit' })) 55 | }); 56 | } 57 | 58 | function reconnect() { 59 | if (ws_timeout) return; 60 | ws_timeout = setTimeout(() => { 61 | ws_timeout = false; 62 | try { 63 | if (ws) ws.removeAllListeners(); 64 | } catch (ex) {} 65 | 66 | startWebsocket(); 67 | }, ws_timeout_interval) 68 | } 69 | 70 | function startWebsocket() { 71 | ws = new WebSocket(ws_url, { 72 | perMessageDeflate: false 73 | }); 74 | 75 | ws.once('open', function open() { 76 | ws_connected = true; 77 | console.log('ws open'); 78 | if (scanWhenOpen) ws_startScan(); 79 | //sendMessage("scan"); 80 | checkEnv(); 81 | init().then(() => { 82 | console.log('init complete'); 83 | }) 84 | }); 85 | 86 | ws.on('close', function close(code, reason) { 87 | ws_connected = false; 88 | reconnect(); 89 | switch (code) { 90 | case 1000: // 1000 indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled. 91 | console.log("WebSocket: closed"); 92 | break; 93 | default: // Abnormal closure 94 | console.log("WebSocket: closed abnormally"); 95 | break; 96 | } 97 | }); 98 | 99 | ws.on('message', function message(data) { 100 | console.log('received: %s', data); 101 | var j = JSON.parse(data); 102 | if (j.command == "scanResult") { 103 | console.log(`Results: ${j.data}`) 104 | createDropdown(j.data); 105 | } 106 | if (j.command == "pairCredentials") { 107 | console.log("pairCredentials", ws_pairDevice, j.data); 108 | saveRemote(ws_pairDevice, j.data); 109 | localStorage.setItem('atvcreds', JSON.stringify(getCreds(pairDevice))); 110 | connectToATV(); 111 | } 112 | if (j.command == "connected") { 113 | atv_connected = true; 114 | connection_failure = false; 115 | atv_events.emit("connected", atv_connected); 116 | } 117 | if (j.command == "is_connected") { 118 | console.log('got_is_connected'); 119 | atv_connected = !!(j.data) 120 | atv_events.emit("connected", atv_connected); 121 | } 122 | if (j.command == "connection_failure") { 123 | console.log(`connection_failure: ${j.data}`) 124 | atv_connected = false; 125 | connection_failure = true; 126 | atv_events.emit("connection_failure", j.data) 127 | } 128 | if (j.command == "startPair2") { 129 | $("#pairStepNum").html("2"); 130 | $("#pairProtocolName").html("Companion"); 131 | } 132 | if (j.command == "current-text") { 133 | console.log(`current text: ${j.data}`) 134 | ipcRenderer.invoke('current-text', j.data); 135 | } 136 | if (j.command == "kbfocus-status") { 137 | ipcRenderer.invoke('kbfocus-status', j.data); 138 | } 139 | }); 140 | } 141 | 142 | 143 | 144 | 145 | function ws_is_connected() { 146 | return new Promise((resolve, reject) => { 147 | atv_events.once("connected", ic => { 148 | if (ic) connection_failure = false; 149 | resolve(ic); 150 | }) 151 | sendMessage("is_connected"); 152 | }) 153 | 154 | } 155 | 156 | function ws_startScan() { 157 | connection_failure = false; 158 | if (ws_connected) sendMessage("scan"); 159 | else { 160 | scanWhenOpen = true; 161 | } 162 | } 163 | 164 | function ws_sendCommand(cmd) { 165 | //console.log(`ws_sendCommand: ${cmd}`) 166 | sendMessage("key", cmd) 167 | } 168 | 169 | function ws_sendCommandAction(cmd, taction) { 170 | // taction can be 'DoubleTap', 'Hold', 'SingleTap' 171 | //console.log(`ws_sendCommandAction: ${cmd} - ${taction}`) 172 | sendMessage("key", { "key": cmd, "taction": taction }) 173 | } 174 | 175 | function ws_connect(creds) { 176 | if (ws_connecting) return; 177 | ws_start_tm = Date.now(); 178 | ws_connecting = true; 179 | return new Promise((resolve, reject) => { 180 | console.log(`ws_connect: ${creds}`) 181 | sendMessage("connect", creds) 182 | atv_events.once("connected", ic => { 183 | ws_connecting = false; 184 | ws_start_tm = false; 185 | if (ic) { 186 | resolve(); 187 | connection_failure = false; 188 | } else { 189 | connection_failure = true; 190 | startScan(); 191 | } 192 | }); 193 | }) 194 | } 195 | 196 | function ws_startPair(dev) { 197 | connection_failure = false; 198 | console.log(`ws_startPair: ${dev}`) 199 | ws_pairDevice = dev; 200 | sendMessage("startPair", dev); 201 | } 202 | 203 | function ws_finishPair(code) { 204 | connection_failure = false; 205 | console.log(`ws_finishPair: ${code}`) 206 | sendMessage("finishPair", code); 207 | } 208 | 209 | function ws_finishPair1(code) { 210 | connection_failure = false; 211 | console.log(`ws_finishPair: ${code}`) 212 | sendMessage("finishPair1", code); 213 | } 214 | 215 | function ws_finishPair2(code) { 216 | connection_failure = false; 217 | console.log(`ws_finishPair: ${code}`) 218 | sendMessage("finishPair2", code); 219 | } 220 | 221 | function checkWSConnection() { 222 | var timedOut = false; 223 | if (ws_start_tm) { 224 | var diff = Date.now() - ws_start_tm; 225 | if (diff > 3000) { 226 | console.log('ws connection timed out, retrying') 227 | ws_connecting = false; 228 | timedOut = true; 229 | } 230 | } 231 | if (!ws_connected) { 232 | console.log('restarting websocket'); 233 | startWebsocket(); 234 | } 235 | } 236 | 237 | function ws_init() { 238 | console.log('ws_init'); 239 | startWebsocket(); 240 | setTimeout(() => { 241 | // not sure if needed, but server start now tries to install required python packages which can be slow 242 | ws_watchdog = setInterval(() => { 243 | checkWSConnection() 244 | }, 5000); 245 | }, 120000) // bump this up to 2 minutes 246 | 247 | } 248 | 249 | function incReady() { 250 | readyCount++; 251 | if (readyCount == 2) ws_init(); 252 | } 253 | 254 | function ws_server_started() { 255 | console.log(`wsserver started`) 256 | incReady(); 257 | } 258 | 259 | var readyCount = 0; 260 | 261 | $(function() { 262 | incReady(); 263 | }); -------------------------------------------------------------------------------- /app/hotkey.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Change Hotkey 7 | 11 | 12 | 13 | 52 | 53 | 54 |
55 |
56 | 57 |
58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | 70 | 71 |
72 |
73 |
74 | 75 | Hotkeys will be registered when this window is closed. For more infromation about hotkeys/accelerators, go to 76 | https://www.electronjs.org/docs/latest/api/accelerator
77 | The default value is Super+Shift+R 78 |
79 |
80 |
81 | 239 | 240 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ATV Remote 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | H - hide window 27 |
28 |
29 |
30 | 33 |
34 |
35 |
36 |
37 | ATV Remote 38 |
39 | 45 | 46 |
47 |

If this is the first time this program has started, please be patient. It may take up to five minutes to get set up.

48 |

The Python library pyatv is being installed in the directory:

49 |

50 |

This only happens the first time the program is run, it will normally start much more quickly.

51 |

If you see this message after waiting 5 minutes, review the atv_pip_install.log file in the directory above.

52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 | 67 | 68 |
69 |
70 | 73 | 74 |
75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
Python not found
You must install Python to use this program. Visit python.org to download Python, and make sure to select the option to add Python to the path.
96 | 97 | 98 | 101 | 104 | 107 | 108 | 109 | 112 | 115 | 118 | 119 | 120 | 123 | 126 | 129 | 130 | 131 | 134 | 137 | 140 | 141 | 142 | 145 | 148 | 151 | 152 |
99 |
-, _
100 |
102 |
Up
103 |
105 |
+, =
106 |
110 |
Left
111 |
113 |
SelectEnter
114 |
116 |
Right
117 |
121 |
PrevP, [
122 |
124 |
Down
125 |
127 |
NextN, ]
128 |
132 | 133 | 135 | 136 | 138 | 139 |
143 |
PlaySpace
144 |
146 | 147 | 149 |
TVT, L
150 |
153 | 154 |
155 |
156 | 157 |
158 |
159 |
160 |
161 | 162 |
163 | 164 | 165 |
166 |
167 |
168 | 169 |
170 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /app/remote_reference.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | 3 | var term = require( 'terminal-kit' ).terminal 4 | const readline = require('readline'); 5 | const ora = require('ora'); 6 | const fs = require('fs') 7 | const fsp = fs.promises; 8 | 9 | var atv = require('node-appletv-x') 10 | 11 | const configFile = 'myappletv.json' 12 | 13 | const keymap = { 14 | 'left': 'Left', 15 | 'right': 'Right', 16 | 'up': 'Up', 17 | 'down': 'Down', 18 | 'return': 'Select', 19 | 'space': (latv) => { if (latv.playing) return 'Pause'; return 'Play' }, 20 | 'backspace': 'Menu', 21 | 'escape': 'Menu', 22 | 't': 'Tv', 23 | 'l': 'LongTv' 24 | } 25 | 26 | const keyDesc = { 27 | 'space': 'Pause/Play', 28 | 'left': 'left arrow', 29 | 'right': 'right arrow', 30 | 'up': 'up arrow', 31 | 'down': 'down arrow', 32 | 't': 'TV Button', 33 | 'l': 'Long-press TV Button' 34 | } 35 | 36 | function box (str) { 37 | var ln = "=".repeat(str.length); 38 | return `${ln}\n${str}\n${ln}\n` 39 | } 40 | 41 | function exists (filepath) { 42 | return new Promise ((resolve, reject) => { 43 | fs.access(filepath, err => { 44 | if (err) return resolve (false); 45 | resolve (true); 46 | }); 47 | }) 48 | } 49 | 50 | 51 | async function checkSavedDevice () { 52 | try { 53 | var atvconfig = JSON.parse(await fsp.readFile(configFile, {encoding: 'utf-8'})); 54 | return atvconfig; 55 | } catch (err) { 56 | return false; 57 | } 58 | } 59 | 60 | function picker (items) { 61 | return new Promise ((resolve, reject) => { 62 | term.singleColumnMenu(items, (err, response) => { 63 | if (err) return reject (err); 64 | resolve(response); 65 | }); 66 | }) 67 | } 68 | 69 | function gstringify (obj) { 70 | var cache = []; 71 | var rs = JSON.stringify(obj, function(key, value) { 72 | if (typeof value === 'object' && value !== null) { 73 | if (cache.indexOf(value) !== -1) { 74 | return; 75 | } 76 | cache.push(value); 77 | } 78 | return value; 79 | }, 4); 80 | cache = null; 81 | return rs; 82 | } 83 | 84 | function inputPIN () { 85 | return new Promise ((resolve, reject) => { 86 | term( 'Enter the PIN shown on the Apple TV: ' ) 87 | term.inputField({ minLength: 4, maxLength: 4 } , ( error , input ) => { 88 | if (error) return reject (error) 89 | resolve(input) 90 | }) 91 | }) 92 | } 93 | 94 | async function pairDevice (device) { 95 | await device.openConnection(); 96 | let callback = await device.pair(); 97 | var pin = await inputPIN(); 98 | await callback(pin); 99 | let credentials = device.credentials.toString(); 100 | var data = JSON.stringify({credentialsString: credentials}, null, 4); 101 | await fsp.writeFile(configFile, data, {encoding: 'utf-8'}); 102 | device.closeConnection() 103 | return credentials 104 | } 105 | 106 | async function pickDevice () { 107 | const spinner = ora({ 108 | text: "Scanning for Apple TVs", 109 | spinner: "dots", 110 | interval: 40 111 | }) 112 | spinner.start(); 113 | var devices = await atv.scan(); 114 | spinner.succeed(); 115 | var ar = {} 116 | var ks = []; 117 | devices.forEach(d => { 118 | var k = `${d.name} ${d.address}` 119 | ar[k] = d; 120 | ks.push(k); 121 | }) 122 | term.cyan( `Select Apple TV to use:\n` ) ; 123 | try { 124 | var response = await picker(ks); 125 | } catch (err) { 126 | console.error(`Error: ${err}`) 127 | return false; 128 | } 129 | var device = ar[response.selectedText]; 130 | var credentials = await pairDevice(device); 131 | return credentials 132 | } 133 | 134 | function tryGetArtwork(myatv) { 135 | var resolved = false; 136 | return new Promise((resolve, reject) => { 137 | setTimeout(() => { 138 | if (resolved) return; 139 | resolved = true; 140 | console.log('timeout'); 141 | resolve(false); 142 | }, 1000); 143 | myatv.requestArtwork().then(r => { 144 | if (resolved) return; 145 | resolved = true; 146 | resolve(true); 147 | }).catch(err => { 148 | if (resolved) return; 149 | resolved = true; 150 | console.log(err); 151 | resolve(false); 152 | }) 153 | }) 154 | } 155 | 156 | 157 | 158 | async function getMyATV() { 159 | var credentialsString = ""; 160 | let atvconfig = await checkSavedDevice(); 161 | if (! atvconfig) { 162 | credentialsString = await pickDevice(); 163 | } else { 164 | credentialsString = atvconfig.credentialsString; 165 | } 166 | if (! credentialsString) return false; 167 | let credentials = atv.parseCredentials(credentialsString); 168 | let devices = await atv.scan(credentials.uniqueIdentifier) 169 | let device = devices[0]; 170 | await device.openConnection(credentials); 171 | device.playing = false //await tryGetArtwork(device); 172 | // device.on('message', m => { 173 | // if (m.message.type != 4) return; 174 | // if ( ! m.payload || ! m.payload.playbackState) return; 175 | // device.playing = m.payload.playbackState == 2 176 | // device.emit('playStateChange', device.playing) 177 | // }) 178 | device.on('nowPlaying', info => { 179 | if ((! info) || (! info.playbackState)) { 180 | return; 181 | } 182 | console.log(info); 183 | device.playing = info.playbackState == 'playing'; 184 | //device.emit('playStateChange', device.playing) 185 | }); 186 | device.Key = atv.AppleTV.Key; 187 | return device; 188 | } 189 | 190 | function showHelp() { 191 | term.cyan(box("AppleTV Remote Keymap")) 192 | Object.keys(keymap).forEach(k => { 193 | var nm = keymap[k]; 194 | var kn = k; 195 | if (Object.keys(keyDesc).indexOf(kn) > -1) { 196 | kn = keyDesc[k] 197 | } 198 | term.white(" Key ").brightBlue(kn).white(":").green(" %s\n", nm); 199 | }) 200 | } 201 | 202 | function keyboardInput (myatv) { 203 | return new Promise ((resolve, reject) => { 204 | const spinnerOpts = { 205 | text: "AppleTV Keyboard Remote Active (? for help)", 206 | spinner: "bouncingBall", 207 | interval: 160 208 | } 209 | const maxLines = 10; 210 | var curLine = 0; 211 | var numCommands = 0; 212 | readline.emitKeypressEvents(process.stdin); 213 | process.stdin.setRawMode(true); 214 | var spinner = ora(spinnerOpts) 215 | var exiting = false; 216 | var lines = []; 217 | for (var i=0; i { 225 | //console.log(str, key); 226 | var now = Date.now(); 227 | if (now - lastKey < 50) return; 228 | lastKey = now; 229 | if (key.sequence === '?') { 230 | if (! exiting) spinner.succeed('? pressed, showing help') 231 | showHelp(); 232 | spinner = ora(spinnerOpts) 233 | spinner.start(); 234 | } 235 | if ((key.ctrl && key.name === 'c') || (key.name === 'q')) { 236 | if (! exiting) spinner.succeed('CTRL-C or q pressed, exiting') 237 | exiting = true; 238 | resolve(); 239 | setTimeout(() => { 240 | console.log('force quitting'); 241 | process.exit() 242 | }, 1000); 243 | } else { 244 | Object.keys(keymap).forEach(k => { 245 | if (str == k || key.name == k) { 246 | var rcmd = keymap[k]; 247 | if (typeof(rcmd) === 'function') rcmd = rcmd(myatv); 248 | if (! exiting) { 249 | var txt = term.str.dim(`[${++numCommands}] AppleTV command: ${rcmd} (key press: ${k})`) 250 | spinner.succeed(txt) 251 | lines.push(`✔ ${txt}`); 252 | lines.shift(); 253 | term.previousLine(maxLines+1); 254 | lines.forEach(ln => { 255 | term.eraseLine(); 256 | process.stdout.write(ln); 257 | term.nextLine(); 258 | }) 259 | myatv.sendKeyCommand(atv.AppleTV.Key[rcmd]) 260 | term.eraseLine(); 261 | spinner = ora(spinnerOpts) 262 | spinner.start(); 263 | } 264 | } 265 | }) 266 | } 267 | }); 268 | }) 269 | } 270 | 271 | function showImage(filename) { 272 | return new Promise ((resolve, reject) => { 273 | var sz = {width: term.width*.5, height: term.width*.5}; 274 | //var sz = {width: term.width, height: term.width}; 275 | var opts = {shrink: sz}; 276 | //var opts = {}; 277 | term.drawImage(filename, opts, (err) => { 278 | if (err) return reject(err); 279 | resolve(); 280 | }) 281 | }); 282 | } 283 | 284 | function slowType(txt) { 285 | return new Promise ((resolve, reject) => { 286 | term.slowTyping(txt, (err) => { 287 | if (err) return reject(err); 288 | resolve(); 289 | }) 290 | }); 291 | } 292 | 293 | 294 | 295 | async function main() { 296 | var myatv = await getMyATV(); 297 | term.fullscreen(); 298 | //await showImage('atv2.png'); 299 | term.bold.white("AppleTV information: ").brightBlue(myatv.name).green(" (%s)\n", myatv.address); 300 | term.cyan(box("AppleTV Remote Keyboard Control")) 301 | await keyboardInput(myatv) 302 | return 303 | } 304 | 305 | if (require.main === module) { 306 | (async () => { 307 | await main() 308 | process.exit() 309 | })() 310 | } 311 | 312 | exports.getMyATV = getMyATV 313 | -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .center { 6 | text-align: center; 7 | } 8 | 9 | body { 10 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 11 | font-weight: 200; 12 | -webkit-font-smoothing: antialiased; 13 | display: flex; 14 | flex-direction: column; 15 | margin: 0; 16 | /* background-image: -webkit-radial-gradient(center, 200px 300px, white, black); */ 17 | user-select: none; 18 | overflow-x: hidden; 19 | } 20 | 21 | a:link, 22 | a:visited, 23 | a:hover, 24 | a:active { 25 | color: black; 26 | } 27 | 28 | #keyboardButtonDiv { 29 | height: 50px; 30 | } 31 | 32 | #topTextKBLink { 33 | display:none; 34 | background-color: #f2f2f2; 35 | border: 2px solid #666; 36 | border-radius: 25px; 37 | cursor: pointer; 38 | color: black; 39 | text-align: center; 40 | font-size: 18px; 41 | font-weight: bolder; 42 | transition: all 500ms cubic-bezier(0.165, 0.84, 0.44, 1); 43 | margin-left: 50px; 44 | margin-right: 50px; 45 | margin-bottom: 10px; 46 | padding: 5px; 47 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); 48 | } 49 | 50 | #topTextKBLink:hover { 51 | color: white; 52 | background-color: #5f5f5f; 53 | border: 2px solid #d1d1d1; 54 | transform: translateY(2px); 55 | box-shadow: inset 2px 2px 5px rgba(0, 0, 0, 0.3); 56 | } 57 | 58 | .darkMode #topTextKBLink { 59 | background-color: #333; 60 | border: 2px solid #aaa; 61 | color: #f2f2f2; 62 | box-shadow: 2px 2px 5px rgba(255, 255, 255, 0.2); 63 | } 64 | 65 | .darkMode #topTextKBLink:hover { 66 | background-color: #5f5f5f; 67 | border: 2px solid #ccc; 68 | transform: translateY(2px); 69 | color: black; 70 | box-shadow: inset 2px 2px 5px rgba(255, 255, 255, 0.3); 71 | } 72 | 73 | #exitLinkWrapper { 74 | cursor: pointer; 75 | } 76 | 77 | #initText { 78 | text-align: left; 79 | padding-left: 5px; 80 | font-size:0.9em; 81 | } 82 | 83 | #exitLink { 84 | float: right; 85 | } 86 | 87 | #cancelPairing { 88 | position: absolute; 89 | bottom: 50px; 90 | right: 0; 91 | } 92 | 93 | .darkMode .simpleButton { 94 | color: white; 95 | border: 1px solid white; 96 | background-color: black; 97 | } 98 | 99 | .simpleButton span { 100 | cursor: pointer; 101 | } 102 | 103 | .simpleButton { 104 | transition: all 500ms cubic-bezier(0.165, 0.84, 0.44, 1); 105 | z-index: 1001; 106 | cursor: pointer; 107 | color: black; 108 | margin: 10px; 109 | border: 1px solid black; 110 | border-radius: 10px; 111 | background-color: white; 112 | } 113 | .simpleButton:hover { 114 | background-color: #333; 115 | color: white; 116 | } 117 | 118 | .darkMode .simpleButton:hover { 119 | background-color: white; 120 | color: black; 121 | } 122 | 123 | #alwaysOnTopCheck { 124 | margin-left: 5px; 125 | margin-top: 10px; 126 | } 127 | 128 | #alwaysOnTopWrapper { 129 | margin: 10px; 130 | font-size: 18px; 131 | } 132 | 133 | .topText { 134 | font-weight: 400; 135 | 136 | } 137 | 138 | #atvDropdownContainer { 139 | margin: 0 auto; 140 | } 141 | 142 | #atv_picker { 143 | min-width: 200px; 144 | width: 100%; 145 | } 146 | 147 | #remoteDropdown { 148 | min-width: 200px; 149 | width: 100%; 150 | } 151 | 152 | #pairingElements { 153 | display: none; 154 | margin: 10px; 155 | } 156 | 157 | #pairCode:input[type="text"] { 158 | font-size: 24px; 159 | min-width: 200px; 160 | text-align: center; 161 | } 162 | 163 | #pairButton { 164 | float: right; 165 | } 166 | 167 | #results { 168 | text-align: center; 169 | } 170 | 171 | #results label { 172 | font-size: 1.5em; 173 | } 174 | 175 | #pairCodeElements label { 176 | font-size: 1.5em; 177 | } 178 | 179 | #pairCodeElements { 180 | display: none; 181 | } 182 | 183 | #loader { 184 | display: none; 185 | } 186 | 187 | #scanText { 188 | position: absolute; 189 | bottom: 10px; 190 | } 191 | 192 | #cmdFade { 193 | display: flex; 194 | justify-content: center; 195 | font-size: 2em; 196 | } 197 | 198 | #cmdWrapper { 199 | opacity: 0.5; 200 | top: 0; 201 | left: 0; 202 | width: 100%; 203 | height: 100%; 204 | } 205 | 206 | .darkMode { 207 | background-color: black; 208 | color: white; 209 | } 210 | 211 | .darkMode .footer { 212 | color: lightgray; 213 | } 214 | 215 | .darkMode a:link, 216 | .darkMode a:visited, 217 | .darkMode a:hover, 218 | .darkMode a:active { 219 | color: white; 220 | } 221 | 222 | header, 223 | footer { 224 | text-align: center; 225 | height: 60px; 226 | } 227 | 228 | footer { 229 | width: 100%; 230 | position: fixed; 231 | bottom: 15px; 232 | } 233 | 234 | header, 235 | footer, 236 | article, 237 | nav, 238 | aside { 239 | padding: 1em; 240 | } 241 | 242 | #statusText { 243 | display: none; 244 | position: fixed; 245 | bottom: 0; 246 | } 247 | 248 | .ctText { 249 | font-size: 1.5em; 250 | } 251 | 252 | .ctText_banner { 253 | text-decoration: none; 254 | text-align: center; 255 | white-space: nowrap; 256 | display: inline-flex; 257 | line-height: 1; 258 | overflow: hidden; 259 | } 260 | 261 | .ctText_banner_animated { 262 | animation: backAndForth 5s linear infinite; 263 | } 264 | 265 | @keyframes backAndForth { 266 | 0% { 267 | transform: translateX(0); 268 | } 269 | 10% { 270 | transform: translateX(0); 271 | } 272 | 45% { 273 | transform: translateX(calc(-100% + 200px)); 274 | } 275 | 55% { 276 | transform: translateX(calc(-100% + 200px)); 277 | } 278 | 90% { 279 | transform: translateX(0); 280 | } 281 | 100% { 282 | transform: translateX(0); 283 | } 284 | } 285 | 286 | .pythonErrorHeader { 287 | font-size: 1.5em; 288 | color: rgb(202, 60, 60); 289 | } 290 | 291 | .pythonError { 292 | display: none; 293 | } 294 | 295 | .directionTable { 296 | display: none; 297 | text-align: center; 298 | margin: 0 auto; 299 | margin-top: 5px 300 | } 301 | 302 | .animate { 303 | transition: all 200ms ease; 304 | } 305 | 306 | .invert { 307 | filter: invert(100%) !important; 308 | } 309 | 310 | .returnToNormal { 311 | filter: invert(0%); 312 | } 313 | 314 | .spacerRow { 315 | height: 10px; 316 | } 317 | 318 | .keyTextAlt { 319 | display: none; 320 | } 321 | 322 | .keyText { 323 | color: inherit; 324 | } 325 | 326 | .darkMode .keyText { 327 | color: white; 328 | } 329 | 330 | [data-key]:hover .keyText { 331 | text-shadow: 0 0 3px rgba(255, 255, 255, 0.8), 0 0 6px rgba(255, 255, 255, 0.6); 332 | } 333 | 334 | .darkMode [data-key]:hover .keyText { 335 | text-shadow: 0 0 3px rgba(0, 0, 0, 0.8), 0 0 6px rgba(0, 0, 0, 0.6); 336 | } 337 | 338 | [data-key].pressing .keyText { 339 | text-shadow: none !important; 340 | } 341 | 342 | .darkMode [data-key].pressing .keyText { 343 | text-shadow: none !important; 344 | } 345 | 346 | .directionCircle { 347 | cursor: pointer; 348 | border-radius: 50%; 349 | width: 72px; 350 | height: 72px; 351 | padding: 8px; 352 | padding-top: 23px; 353 | background-color: white; 354 | color: black; 355 | border: 2px solid #666; 356 | text-align: center; 357 | font-size: 18px; 358 | filter: invert(0%); 359 | transition: all 500ms cubic-bezier(0.165, 0.84, 0.44, 1) 360 | } 361 | 362 | .selectArrow.directionCircle.directionArrows { 363 | border: 3px double black; 364 | } 365 | 366 | .darkMode .selectArrow.directionCircle.directionArrows { 367 | border: 3px double white; 368 | } 369 | 370 | .directionCircle.directionArrows { 371 | background-color: rgb(226, 226, 226); 372 | border: 3px solid rgb(82, 82, 82); 373 | } 374 | 375 | .darkMode .directionCircle.directionArrows { 376 | background-color: rgb(29, 29, 29); 377 | border: 3px solid rgb(128, 128, 128); 378 | } 379 | 380 | .directionCircle:hover { 381 | transform: translateY(2px) scale(1.05); 382 | /* box-shadow: 0 0 5px 1px rgb(140 140 140); */ 383 | filter: invert(100%); 384 | /* border: 4px #666 thick; */ 385 | } 386 | 387 | .darkMode .directionCircle { 388 | background: #000; 389 | color: white; 390 | } 391 | 392 | .numberCircle { 393 | cursor: pointer; 394 | border-radius: 50%; 395 | width: 72px; 396 | height: 72px; 397 | padding: 8px; 398 | padding-top: 20px; 399 | background-color: white; 400 | color: black; 401 | border: 2px solid #666; 402 | text-align: center; 403 | font-size: 20px; 404 | } 405 | 406 | .darkMode .numberCircle { 407 | background: #000; 408 | color: white; 409 | } 410 | 411 | .keymapDiv { 412 | display: flex; 413 | justify-content: space-evenly; 414 | } 415 | 416 | [data-key] { 417 | transition: transform 0.1s ease; 418 | position: relative; 419 | overflow: hidden; 420 | } 421 | 422 | [data-key].longpress-triggered { 423 | animation: longpress-flash 0.3s ease; 424 | } 425 | 426 | .darkMode [data-key].longpress-triggered { 427 | animation: longpress-flash-dark 0.3s ease; 428 | } 429 | 430 | @keyframes longpress-flash { 431 | 0% { box-shadow: 0 0 20px rgba(100,150,255,0.8); } 432 | 100% { box-shadow: 0 0 5px rgba(100,150,255,0.3); } 433 | } 434 | 435 | @keyframes longpress-flash-dark { 436 | 0% { box-shadow: 0 0 20px rgba(150,200,255,0.8); } 437 | 100% { box-shadow: 0 0 5px rgba(150,200,255,0.3); } 438 | } 439 | 440 | .volumeKeys { 441 | opacity: 0.75; 442 | } 443 | 444 | 445 | 446 | .large-text-input-wrapper { 447 | position: relative; 448 | display: inline-block; 449 | } 450 | 451 | .large-text-input { 452 | width: 100%; 453 | padding: 10px 15px; 454 | padding-right: 40px; /* To make space for the status emoji */ 455 | font-size: 24px; 456 | border: 2px solid #aaa; 457 | border-radius: 8px; 458 | outline: none; 459 | transition: border-color 0.3s ease; 460 | } 461 | 462 | .large-text-input-wrapper::after { 463 | content: ''; 464 | position: absolute; 465 | top: 50%; 466 | right: 15px; 467 | transform: translateY(-50%); 468 | font-size: 24px; 469 | } 470 | 471 | /* .large-text-input-wrapper.bad::after { 472 | content: '⚫️'; 473 | opacity: 0.5; 474 | } */ 475 | .large-text-input-wrapper.bad::after { 476 | content: '⚪️'; 477 | opacity: 0.5; 478 | } 479 | 480 | .large-text-input-wrapper.good::after { 481 | content: '🟢'; 482 | opacity: 1.0; 483 | } 484 | 485 | .large-text-input:focus { 486 | border-color: #0077cc; 487 | } 488 | 489 | .darkMode .large-text-input { 490 | color: #fff; 491 | border-color: #555; 492 | background-color: #222; 493 | } 494 | 495 | .darkMode .large-text-input:focus { 496 | border-color: #00aaff; 497 | } 498 | 499 | .info-text { 500 | margin-left: 50px; 501 | font-size: 0.8em; 502 | color: #555; 503 | } 504 | 505 | .darkMode .info-text { 506 | margin-left: 50px; 507 | font-size: 0.8em; 508 | color: #999999; 509 | } 510 | 511 | .info-text-italics { 512 | font-style: italic; 513 | margin-left: 50px; 514 | font-size: 0.8em; 515 | color: #555; 516 | } 517 | .darkMode .info-text-italics { 518 | font-style: italic; 519 | margin-left: 50px; 520 | font-size: 0.8em; 521 | color: #999999; 522 | } 523 | 524 | #topTextHeader { 525 | margin-right: 10px; 526 | } 527 | 528 | .center h3 { 529 | line-height: 1.2; 530 | } -------------------------------------------------------------------------------- /app/css/loaders.css: -------------------------------------------------------------------------------- 1 | /* https://loading.io/css/ */ 2 | 3 | .lds-ellipsis { 4 | display: inline-block; 5 | position: relative; 6 | width: 80px; 7 | height: 80px; 8 | } 9 | .lds-ellipsis div { 10 | position: absolute; 11 | top: 33px; 12 | width: 13px; 13 | height: 13px; 14 | border-radius: 50%; 15 | background: rgb(128, 128, 128); 16 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 17 | } 18 | .lds-ellipsis div:nth-child(1) { 19 | left: 8px; 20 | animation: lds-ellipsis1 0.6s infinite; 21 | } 22 | .lds-ellipsis div:nth-child(2) { 23 | left: 8px; 24 | animation: lds-ellipsis2 0.6s infinite; 25 | } 26 | .lds-ellipsis div:nth-child(3) { 27 | left: 32px; 28 | animation: lds-ellipsis2 0.6s infinite; 29 | } 30 | .lds-ellipsis div:nth-child(4) { 31 | left: 56px; 32 | animation: lds-ellipsis3 0.6s infinite; 33 | } 34 | @keyframes lds-ellipsis1 { 35 | 0% { 36 | transform: scale(0); 37 | } 38 | 100% { 39 | transform: scale(1); 40 | } 41 | } 42 | @keyframes lds-ellipsis3 { 43 | 0% { 44 | transform: scale(1); 45 | } 46 | 100% { 47 | transform: scale(0); 48 | } 49 | } 50 | @keyframes lds-ellipsis2 { 51 | 0% { 52 | transform: translate(0, 0); 53 | } 54 | 100% { 55 | transform: translate(24px, 0); 56 | } 57 | } 58 | 59 | 60 | 61 | 62 | /* https://icons8.com/cssload/en/miscellaneous */ 63 | .cssload-dots { 64 | width: 0; 65 | height: 0; 66 | position: absolute; 67 | top: 0; 68 | left: 0; 69 | bottom: 0; 70 | right: 0; 71 | margin: auto; 72 | outline: 1px solid red; 73 | filter: url(#goo); 74 | -o-filter: url(#goo); 75 | -ms-filter: url(#goo); 76 | -webkit-filter: url(#goo); 77 | -moz-filter: url(#goo); 78 | } 79 | 80 | .cssload-dot { 81 | width: 0; 82 | height: 0; 83 | position: absolute; 84 | left: 0; 85 | top: 0; 86 | } 87 | .cssload-dot:before { 88 | content: ""; 89 | width: 34px; 90 | height: 34px; 91 | border-radius: 49px; 92 | background: rgb(251,211,1); 93 | position: absolute; 94 | left: 50%; 95 | transform: translateY(0); 96 | -o-transform: translateY(0); 97 | -ms-transform: translateY(0); 98 | -webkit-transform: translateY(0); 99 | -moz-transform: translateY(0); 100 | margin-left: -17.5px; 101 | margin-top: -17.5px; 102 | } 103 | 104 | 105 | 106 | .cssload-dot:nth-child(5):before { 107 | z-index: 100; 108 | width: 44.5px; 109 | height: 44.5px; 110 | margin-left: -21.75px; 111 | margin-top: -21.75px; 112 | animation: cssload-dot-colors 4.6s ease infinite; 113 | -o-animation: cssload-dot-colors 4.6s ease infinite; 114 | -ms-animation: cssload-dot-colors 4.6s ease infinite; 115 | -webkit-animation: cssload-dot-colors 4.6s ease infinite; 116 | -moz-animation: cssload-dot-colors 4.6s ease infinite; 117 | } 118 | 119 | 120 | .cssload-dot:nth-child(1) { 121 | animation: cssload-dot-rotate-1 4.6s 0s linear infinite; 122 | -o-animation: cssload-dot-rotate-1 4.6s 0s linear infinite; 123 | -ms-animation: cssload-dot-rotate-1 4.6s 0s linear infinite; 124 | -webkit-animation: cssload-dot-rotate-1 4.6s 0s linear infinite; 125 | -moz-animation: cssload-dot-rotate-1 4.6s 0s linear infinite; 126 | } 127 | .cssload-dot:nth-child(1):before { 128 | background-color: rgb(255,50,112); 129 | animation: cssload-dot-move 4.6s 0s ease infinite; 130 | -o-animation: cssload-dot-move 4.6s 0s ease infinite; 131 | -ms-animation: cssload-dot-move 4.6s 0s ease infinite; 132 | -webkit-animation: cssload-dot-move 4.6s 0s ease infinite; 133 | -moz-animation: cssload-dot-move 4.6s 0s ease infinite; 134 | } 135 | 136 | .cssload-dot:nth-child(2) { 137 | animation: cssload-dot-rotate-2 4.6s 1.15s linear infinite; 138 | -o-animation: cssload-dot-rotate-2 4.6s 1.15s linear infinite; 139 | -ms-animation: cssload-dot-rotate-2 4.6s 1.15s linear infinite; 140 | -webkit-animation: cssload-dot-rotate-2 4.6s 1.15s linear infinite; 141 | -moz-animation: cssload-dot-rotate-2 4.6s 1.15s linear infinite; 142 | } 143 | .cssload-dot:nth-child(2):before { 144 | background-color: rgb(32,139,241); 145 | animation: cssload-dot-move 4.6s 1.15s ease infinite; 146 | -o-animation: cssload-dot-move 4.6s 1.15s ease infinite; 147 | -ms-animation: cssload-dot-move 4.6s 1.15s ease infinite; 148 | -webkit-animation: cssload-dot-move 4.6s 1.15s ease infinite; 149 | -moz-animation: cssload-dot-move 4.6s 1.15s ease infinite; 150 | } 151 | 152 | .cssload-dot:nth-child(3) { 153 | animation: cssload-dot-rotate-3 4.6s 2.3s linear infinite; 154 | -o-animation: cssload-dot-rotate-3 4.6s 2.3s linear infinite; 155 | -ms-animation: cssload-dot-rotate-3 4.6s 2.3s linear infinite; 156 | -webkit-animation: cssload-dot-rotate-3 4.6s 2.3s linear infinite; 157 | -moz-animation: cssload-dot-rotate-3 4.6s 2.3s linear infinite; 158 | } 159 | .cssload-dot:nth-child(3):before { 160 | background-color: rgb(175,225,2); 161 | animation: cssload-dot-move 4.6s 2.3s ease infinite; 162 | -o-animation: cssload-dot-move 4.6s 2.3s ease infinite; 163 | -ms-animation: cssload-dot-move 4.6s 2.3s ease infinite; 164 | -webkit-animation: cssload-dot-move 4.6s 2.3s ease infinite; 165 | -moz-animation: cssload-dot-move 4.6s 2.3s ease infinite; 166 | } 167 | 168 | .cssload-dot:nth-child(4) { 169 | animation: cssload-dot-rotate-4 4.6s 3.45s linear infinite; 170 | -o-animation: cssload-dot-rotate-4 4.6s 3.45s linear infinite; 171 | -ms-animation: cssload-dot-rotate-4 4.6s 3.45s linear infinite; 172 | -webkit-animation: cssload-dot-rotate-4 4.6s 3.45s linear infinite; 173 | -moz-animation: cssload-dot-rotate-4 4.6s 3.45s linear infinite; 174 | } 175 | .cssload-dot:nth-child(4):before { 176 | background-color: rgb(251,211,1); 177 | animation: cssload-dot-move 4.6s 3.45s ease infinite; 178 | -o-animation: cssload-dot-move 4.6s 3.45s ease infinite; 179 | -ms-animation: cssload-dot-move 4.6s 3.45s ease infinite; 180 | -webkit-animation: cssload-dot-move 4.6s 3.45s ease infinite; 181 | -moz-animation: cssload-dot-move 4.6s 3.45s ease infinite; 182 | } 183 | 184 | @keyframes cssload-dot-move { 185 | 0% { 186 | transform: translateY(0); 187 | } 188 | 18%, 22% { 189 | transform: translateY(-68px); 190 | } 191 | 40%, 100% { 192 | transform: translateY(0); 193 | } 194 | } 195 | 196 | @-o-keyframes cssload-dot-move { 197 | 0% { 198 | -o-transform: translateY(0); 199 | } 200 | 18%, 22% { 201 | -o-transform: translateY(-68px); 202 | } 203 | 40%, 100% { 204 | -o-transform: translateY(0); 205 | } 206 | } 207 | 208 | @-ms-keyframes cssload-dot-move { 209 | 0% { 210 | -ms-transform: translateY(0); 211 | } 212 | 18%, 22% { 213 | -ms-transform: translateY(-68px); 214 | } 215 | 40%, 100% { 216 | -ms-transform: translateY(0); 217 | } 218 | } 219 | 220 | @-webkit-keyframes cssload-dot-move { 221 | 0% { 222 | -webkit-transform: translateY(0); 223 | } 224 | 18%, 22% { 225 | -webkit-transform: translateY(-68px); 226 | } 227 | 40%, 100% { 228 | -webkit-transform: translateY(0); 229 | } 230 | } 231 | 232 | @-moz-keyframes cssload-dot-move { 233 | 0% { 234 | -moz-transform: translateY(0); 235 | } 236 | 18%, 22% { 237 | -moz-transform: translateY(-68px); 238 | } 239 | 40%, 100% { 240 | -moz-transform: translateY(0); 241 | } 242 | } 243 | 244 | @keyframes cssload-dot-colors { 245 | 0% { 246 | background-color: rgb(251,211,1); 247 | } 248 | 25% { 249 | background-color: rgb(255,50,112); 250 | } 251 | 50% { 252 | background-color: rgb(32,139,241); 253 | } 254 | 75% { 255 | background-color: rgb(175,225,2); 256 | } 257 | 100% { 258 | background-color: rgb(251,211,1); 259 | } 260 | } 261 | 262 | @-o-keyframes cssload-dot-colors { 263 | 0% { 264 | background-color: rgb(251,211,1); 265 | } 266 | 25% { 267 | background-color: rgb(255,50,112); 268 | } 269 | 50% { 270 | background-color: rgb(32,139,241); 271 | } 272 | 75% { 273 | background-color: rgb(175,225,2); 274 | } 275 | 100% { 276 | background-color: rgb(251,211,1); 277 | } 278 | } 279 | 280 | @-ms-keyframes cssload-dot-colors { 281 | 0% { 282 | background-color: rgb(251,211,1); 283 | } 284 | 25% { 285 | background-color: rgb(255,50,112); 286 | } 287 | 50% { 288 | background-color: rgb(32,139,241); 289 | } 290 | 75% { 291 | background-color: rgb(175,225,2); 292 | } 293 | 100% { 294 | background-color: rgb(251,211,1); 295 | } 296 | } 297 | 298 | @-webkit-keyframes cssload-dot-colors { 299 | 0% { 300 | background-color: rgb(251,211,1); 301 | } 302 | 25% { 303 | background-color: rgb(255,50,112); 304 | } 305 | 50% { 306 | background-color: rgb(32,139,241); 307 | } 308 | 75% { 309 | background-color: rgb(175,225,2); 310 | } 311 | 100% { 312 | background-color: rgb(251,211,1); 313 | } 314 | } 315 | 316 | @-moz-keyframes cssload-dot-colors { 317 | 0% { 318 | background-color: rgb(251,211,1); 319 | } 320 | 25% { 321 | background-color: rgb(255,50,112); 322 | } 323 | 50% { 324 | background-color: rgb(32,139,241); 325 | } 326 | 75% { 327 | background-color: rgb(175,225,2); 328 | } 329 | 100% { 330 | background-color: rgb(251,211,1); 331 | } 332 | } 333 | 334 | @keyframes cssload-dot-rotate-1 { 335 | 0% { 336 | transform: rotate(-105deg); 337 | } 338 | 100% { 339 | transform: rotate(270deg); 340 | } 341 | } 342 | 343 | @-o-keyframes cssload-dot-rotate-1 { 344 | 0% { 345 | -o-transform: rotate(-105deg); 346 | } 347 | 100% { 348 | -o-transform: rotate(270deg); 349 | } 350 | } 351 | 352 | @-ms-keyframes cssload-dot-rotate-1 { 353 | 0% { 354 | -ms-transform: rotate(-105deg); 355 | } 356 | 100% { 357 | -ms-transform: rotate(270deg); 358 | } 359 | } 360 | 361 | @-webkit-keyframes cssload-dot-rotate-1 { 362 | 0% { 363 | -webkit-transform: rotate(-105deg); 364 | } 365 | 100% { 366 | -webkit-transform: rotate(270deg); 367 | } 368 | } 369 | 370 | @-moz-keyframes cssload-dot-rotate-1 { 371 | 0% { 372 | -moz-transform: rotate(-105deg); 373 | } 374 | 100% { 375 | -moz-transform: rotate(270deg); 376 | } 377 | } 378 | 379 | @keyframes cssload-dot-rotate-2 { 380 | 0% { 381 | transform: rotate(165deg); 382 | } 383 | 100% { 384 | transform: rotate(540deg); 385 | } 386 | } 387 | 388 | @-o-keyframes cssload-dot-rotate-2 { 389 | 0% { 390 | -o-transform: rotate(165deg); 391 | } 392 | 100% { 393 | -o-transform: rotate(540deg); 394 | } 395 | } 396 | 397 | @-ms-keyframes cssload-dot-rotate-2 { 398 | 0% { 399 | -ms-transform: rotate(165deg); 400 | } 401 | 100% { 402 | -ms-transform: rotate(540deg); 403 | } 404 | } 405 | 406 | @-webkit-keyframes cssload-dot-rotate-2 { 407 | 0% { 408 | -webkit-transform: rotate(165deg); 409 | } 410 | 100% { 411 | -webkit-transform: rotate(540deg); 412 | } 413 | } 414 | 415 | @-moz-keyframes cssload-dot-rotate-2 { 416 | 0% { 417 | -moz-transform: rotate(165deg); 418 | } 419 | 100% { 420 | -moz-transform: rotate(540deg); 421 | } 422 | } 423 | 424 | @keyframes cssload-dot-rotate-3 { 425 | 0% { 426 | transform: rotate(435deg); 427 | } 428 | 100% { 429 | transform: rotate(810deg); 430 | } 431 | } 432 | 433 | @-o-keyframes cssload-dot-rotate-3 { 434 | 0% { 435 | -o-transform: rotate(435deg); 436 | } 437 | 100% { 438 | -o-transform: rotate(810deg); 439 | } 440 | } 441 | 442 | @-ms-keyframes cssload-dot-rotate-3 { 443 | 0% { 444 | -ms-transform: rotate(435deg); 445 | } 446 | 100% { 447 | -ms-transform: rotate(810deg); 448 | } 449 | } 450 | 451 | @-webkit-keyframes cssload-dot-rotate-3 { 452 | 0% { 453 | -webkit-transform: rotate(435deg); 454 | } 455 | 100% { 456 | -webkit-transform: rotate(810deg); 457 | } 458 | } 459 | 460 | @-moz-keyframes cssload-dot-rotate-3 { 461 | 0% { 462 | -moz-transform: rotate(435deg); 463 | } 464 | 100% { 465 | -moz-transform: rotate(810deg); 466 | } 467 | } 468 | 469 | @keyframes cssload-dot-rotate-4 { 470 | 0% { 471 | transform: rotate(705deg); 472 | } 473 | 100% { 474 | transform: rotate(1080deg); 475 | } 476 | } 477 | 478 | @-o-keyframes cssload-dot-rotate-4 { 479 | 0% { 480 | -o-transform: rotate(705deg); 481 | } 482 | 100% { 483 | -o-transform: rotate(1080deg); 484 | } 485 | } 486 | 487 | @-ms-keyframes cssload-dot-rotate-4 { 488 | 0% { 489 | -ms-transform: rotate(705deg); 490 | } 491 | 100% { 492 | -ms-transform: rotate(1080deg); 493 | } 494 | } 495 | 496 | @-webkit-keyframes cssload-dot-rotate-4 { 497 | 0% { 498 | -webkit-transform: rotate(705deg); 499 | } 500 | 100% { 501 | -webkit-transform: rotate(1080deg); 502 | } 503 | } 504 | 505 | @-moz-keyframes cssload-dot-rotate-4 { 506 | 0% { 507 | -moz-transform: rotate(705deg); 508 | } 509 | 100% { 510 | -moz-transform: rotate(1080deg); 511 | } 512 | } -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, powerMonitor, Tray, Menu, nativeImage, globalShortcut, webContents } = require('electron') 2 | var win; 3 | const { ipcMain } = require('electron') 4 | const path = require('path'); 5 | require('@electron/remote/main').initialize() 6 | const menubar = require('menubar').menubar; 7 | const util = require('util'); 8 | var secondWindow; 9 | process.env['MYPATH'] = path.join(process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.local/share"), "ATV Remote"); 10 | const lodash = _ = require('./js/lodash.min'); 11 | const server_runner = require('./server_runner') 12 | const fs = require('fs'); 13 | server_runner.startServer(); 14 | 15 | 16 | global["server_runner"] = server_runner; 17 | 18 | const preloadWindow = true; 19 | const readyEvent = preloadWindow ? "ready" : "after-create-window"; 20 | 21 | const volumeButtons = ['VolumeUp', 'VolumeDown', 'VolumeMute'] 22 | 23 | var handleVolumeButtonsGlobal = false; 24 | 25 | var mb; 26 | var kbHasFocus; 27 | 28 | console._log = console.log; 29 | console.log = function() { 30 | let txt = util.format(...[].slice.call(arguments)) + '\n' 31 | process.stdout.write(txt); 32 | if (win && win.webContents) { 33 | win.webContents.send('mainLog', txt); 34 | } 35 | } 36 | 37 | 38 | 39 | const gotTheLock = app.requestSingleInstanceLock() 40 | 41 | if (!gotTheLock) { 42 | app.quit() 43 | } else { 44 | app.on('second-instance', (event, commandLine, workingDirectory) => { 45 | console.log('second instance tried to open'); 46 | showWindow(); 47 | }) 48 | } 49 | function createHotkeyWindow() { 50 | hotkeyWindow = new BrowserWindow({ 51 | width: 500, 52 | height: 500, 53 | webPreferences: { 54 | nodeIntegration: true, 55 | enableRemoteModule: true, 56 | contextIsolation: false 57 | } 58 | }); 59 | require("@electron/remote/main").enable(hotkeyWindow.webContents); 60 | hotkeyWindow.loadFile('hotkey.html'); 61 | hotkeyWindow.setMenu(null); 62 | hotkeyWindow.on('close', (event) => { 63 | event.preventDefault(); 64 | hotkeyWindow.hide(); 65 | registerHotkeys(); 66 | }); 67 | } 68 | 69 | function createInputWindow() { 70 | secondWindow = new BrowserWindow({ 71 | webPreferences: { 72 | nodeIntegration: true, 73 | contextIsolation: false, 74 | enableRemoteModule: true 75 | }, 76 | hide: true, 77 | width: 600, 78 | height: 250, 79 | minimizable: false, 80 | maximizable: false 81 | }); 82 | require("@electron/remote/main").enable(secondWindow.webContents); 83 | secondWindow.loadFile('input.html'); 84 | secondWindow.on('close', (event) => { 85 | event.preventDefault(); 86 | secondWindow.webContents.send('closeInputWindow'); 87 | showWindowThrottle(); 88 | }); 89 | secondWindow.on("blur", () => { 90 | if (mb.window.isAlwaysOnTop()) return; 91 | showWindowThrottle(); 92 | }) 93 | secondWindow.setMenu(null); 94 | secondWindow.hide(); 95 | } 96 | 97 | function createWindow() { 98 | mb = menubar({ 99 | preloadWindow: preloadWindow, 100 | showDockIcon: false, 101 | browserWindow: { 102 | width: 300, 103 | height: 500, 104 | alwaysOnTop: false, 105 | webPreferences: { 106 | nodeIntegration: true, 107 | enableRemoteModule: true, 108 | contextIsolation: false 109 | } 110 | } 111 | }) 112 | global['MB'] = mb; 113 | mb.on(readyEvent, () => { 114 | require("@electron/remote/main").enable(mb.window.webContents); 115 | win = mb.window; 116 | 117 | var webContents = win.webContents; 118 | createInputWindow() 119 | 120 | 121 | win.on('close', () => { 122 | console.log('window closed, quitting') 123 | app.exit(); 124 | }) 125 | win.on('show', () => { 126 | win.webContents.send('shortcutWin'); 127 | if (handleVolumeButtonsGlobal) handleVolume(); 128 | }) 129 | 130 | win.on('hide', () => { 131 | if (handleVolumeButtonsGlobal) unhandleVolume(); 132 | }) 133 | 134 | win.webContents.on('will-navigate', (e, url) => { 135 | console.log(`will-navigate`, url); 136 | }) 137 | ipcMain.on('input-change', (event, data) => { 138 | console.log('Received input:', data); 139 | win.webContents.send('input-change', data); 140 | }); 141 | ipcMain.handle("loadHotkeyWindow", (event) => { 142 | createHotkeyWindow(); 143 | }) 144 | ipcMain.handle('debug', (event, arg) => { 145 | console.log(`ipcDebug: ${arg}`) 146 | }) 147 | ipcMain.handle('quit', event => { 148 | server_runner.stopServer(); 149 | app.exit() 150 | }); 151 | ipcMain.handle('alwaysOnTop', (event, arg) => { 152 | var tf = arg == "true"; 153 | console.log(`setting alwaysOnTop: ${tf}`) 154 | mb.window.setAlwaysOnTop(tf); 155 | 156 | }) 157 | ipcMain.handle('uimode', (event, arg) => { 158 | secondWindow.webContents.send('uimode', arg); 159 | }); 160 | 161 | 162 | ipcMain.handle('hideWindow', (event) => { 163 | console.log('hiding window'); 164 | mb.hideWindow(); 165 | }); 166 | ipcMain.handle('isProduction', (event) => { 167 | return (!process.defaultApp); 168 | }); 169 | ipcMain.handle('isWSRunning', (event, arg) => { 170 | console.log('isWSRunning'); 171 | if (server_runner.isServerRunning()) win.webContents.send('wsserver_started') 172 | }) 173 | 174 | ipcMain.handle('closeInputOpenRemote', (event, arg) => { 175 | console.log('closeInputOpenRemote'); 176 | showWindow(); 177 | }) 178 | ipcMain.handle('openInputWindow', (event, arg) => { 179 | console.log('openInputWindow'); 180 | secondWindow.show(); 181 | secondWindow.webContents.send('openInputWindow'); 182 | }); 183 | ipcMain.handle('current-text', (event, arg) => { 184 | console.log('current-text', arg); 185 | secondWindow.webContents.send('current-text', arg); 186 | }); 187 | ipcMain.handle('kbfocus-status', (event, arg) => { 188 | secondWindow.webContents.send('kbfocus-status', arg); 189 | kbHasFocus = arg; 190 | }) 191 | ipcMain.handle('kbfocus', () => { 192 | win.webContents.send('kbfocus'); 193 | }) 194 | 195 | powerMonitor.addListener('resume', event => { 196 | win.webContents.send('powerResume'); 197 | }) 198 | 199 | win.on('ready-to-show', () => { 200 | console.log('ready to show') 201 | if (server_runner.isServerRunning()) { 202 | win.webContents.send("wsserver_started") 203 | } 204 | }) 205 | 206 | if (server_runner.isServerRunning()) { 207 | console.log(`server already running`) 208 | win.webContents.send("wsserver_started") 209 | } else { 210 | console.log(`server waiting for event`) 211 | server_runner.server_events.on("started", () => { 212 | win.webContents.send("wsserver_started") 213 | }) 214 | } 215 | }) 216 | } 217 | 218 | function showWindow() { 219 | secondWindow.hide(); 220 | try { 221 | app.show(); 222 | } catch (err) { 223 | //console.log(err); 224 | // this happens in windows, doesn't seem to affect anything though 225 | } 226 | mb.showWindow(); 227 | setTimeout(() => { 228 | mb.window.focus(); 229 | }, 200); 230 | } 231 | 232 | var showWindowThrottle = lodash.throttle(showWindow, 100); 233 | 234 | function hideWindow() { 235 | mb.hideWindow(); 236 | try { 237 | app.hide(); 238 | } catch (err) { 239 | // console.log(err); 240 | // not sure if this affects windows like app.show does. 241 | } 242 | } 243 | 244 | function getWorkingPath() { 245 | var rp = process.resourcesPath; 246 | if (!rp && process.argv.length > 1) rp = path.resolve(process.argv[1]); 247 | if (!app.isPackaged) { 248 | rp = path.resolve(`${path.dirname(process.argv[1])}/../atv_py_env`) 249 | } 250 | return rp 251 | } 252 | 253 | function unhandleVolume() { 254 | volumeButtons.forEach(btn => { 255 | console.log(`unregister: ${btn}`) 256 | globalShortcut.unregister(btn); 257 | }) 258 | } 259 | 260 | function handleVolume() { 261 | volumeButtons.forEach(btn => { 262 | console.log(`register: ${btn}`) 263 | globalShortcut.register(btn, () => { 264 | var keys = { 265 | "VolumeUp": "volume_up", 266 | "VolumeDown": "volume_down", 267 | "VolumeMute": "volume_mute" 268 | } 269 | var key = keys[btn] 270 | console.log(`sending ${key} for ${btn}`) 271 | win.webContents.send('sendCommand', key); 272 | }) 273 | }) 274 | } 275 | function registerHotkeys() { 276 | var hotkeyPath = path.join(process.env['MYPATH'], "hotkey.txt") 277 | try { 278 | globalShortcut.unregisterAll(); 279 | } catch (err) { 280 | console.log(`Error unregistering hotkeys: ${err}`) 281 | } 282 | var registered = false; 283 | if (fs.existsSync(hotkeyPath)) { 284 | 285 | var hotkeys = fs.readFileSync(hotkeyPath, {encoding: 'utf-8'}).trim(); 286 | if (hotkeys.indexOf(",") > -1) { 287 | hotkeys = hotkeys.split(',').map(el => { return el.trim() }); 288 | } else { 289 | hotkeys = [hotkeys]; 290 | } 291 | console.log(`Registering custom hotkeys: ${hotkeys}`) 292 | var errs = hotkeys.map(hotkey => { 293 | console.log(`Registering hotkey: ${hotkey}`) 294 | return globalShortcut.register(hotkey, () => { 295 | if (mb.window.isVisible()) { 296 | hideWindow(); 297 | } else { 298 | showWindow(); 299 | } 300 | win.webContents.send('shortcutWin'); 301 | }) 302 | }) 303 | // var ret = globalShortcut.registerAll(hotkeys, () => { 304 | // if (mb.window.isVisible()) { 305 | // hideWindow(); 306 | // } else { 307 | // showWindow(); 308 | // } 309 | // win.webContents.send('shortcutWin'); 310 | // }) 311 | var ret = errs.every(el => { return el }); 312 | if (!ret) { 313 | errs.forEach((err, idx) => { 314 | if (!err) { 315 | console.log(`Error registering hotkey: ${hotkeys[idx]}`) 316 | } 317 | }) 318 | console.log(`Error registering hotkeys: ${hotkeys}`) 319 | } else { 320 | registered =true; 321 | } 322 | } 323 | if (!registered) { 324 | globalShortcut.registerAll(['Super+Shift+R', 'Command+Control+R'], () => { 325 | if (mb.window.isVisible()) { 326 | hideWindow(); 327 | } else { 328 | showWindow(); 329 | } 330 | win.webContents.send('shortcutWin'); 331 | }) 332 | } 333 | } 334 | 335 | app.whenReady().then(() => { 336 | 337 | server_runner.testPythonExists().then(r => { 338 | console.log(`python exists: ${r}`) 339 | }).catch(err => { 340 | console.log(`python does not exist: ${err}`) 341 | }) 342 | 343 | createWindow(); 344 | registerHotkeys(); 345 | 346 | var version = app.getVersion(); 347 | app.setAboutPanelOptions({ 348 | applicationName: "ATV Remote", 349 | applicationVersion: version, 350 | version: version, 351 | credits: "Brian Harper", 352 | copyright: "Copyright 2025", 353 | website: "https://github.com/bsharper", 354 | iconPath: "./images/full.png" 355 | }); 356 | }) 357 | 358 | app.on("before-quit", () => { 359 | server_runner.stopServer(); 360 | }) 361 | 362 | app.on('window-all-closed', () => { 363 | app.quit() 364 | }) 365 | 366 | app.on('activate', () => { 367 | if (BrowserWindow.getAllWindows().length === 0) { 368 | createWindow() 369 | } 370 | }) 371 | -------------------------------------------------------------------------------- /app/css/select2.min.css: -------------------------------------------------------------------------------- 1 | .select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline;list-style:none;padding:0}.select2-container .select2-selection--multiple .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;margin-left:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option--selectable{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px;padding-right:0px}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;padding-bottom:5px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:20px;margin-right:10px;margin-top:5px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--default .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-right:1px solid #aaa;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#999;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus{background-color:#f1f1f1;color:#333;outline:none}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-left:1px solid #aaa;border-right:none;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear{float:left;margin-left:10px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--group{padding:0}.select2-container--default .select2-results__option--disabled{color:#999}.select2-container--default .select2-results__option--selected{background-color:#ddd}.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;padding-bottom:5px;padding-right:5px}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--classic .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#888;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;outline:none}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option--group{padding:0}.select2-container--classic .select2-results__option--disabled{color:grey}.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} 2 | -------------------------------------------------------------------------------- /app/css/pure-min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Pure v2.0.3 3 | Copyright 2013 Yahoo! 4 | Licensed under the BSD License. 5 | https://github.com/pure-css/pure/blob/master/LICENSE.md 6 | */ 7 | /*! 8 | normalize.css v | MIT License | git.io/normalize 9 | Copyright (c) Nicolas Gallagher and Jonathan Neal 10 | */ 11 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129fea;outline:1px auto #129fea}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} -------------------------------------------------------------------------------- /app/pyscripts.js: -------------------------------------------------------------------------------- 1 | const files = { 2 | "wsserver.py": "import os\nimport sys\nimport json\n\n## Add pyatv to path for local development (fewer red lines)\n#sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pyatv'))\n\nimport pyatv\nimport random\nimport asyncio\nfrom pyatv.const import InputAction\nfrom pyatv import const as pyatv_const\nimport websockets\n\nimport logging\nlogger = logging.getLogger('websockets')\nlogger.setLevel(logging.DEBUG)\nlogger.addHandler(logging.StreamHandler())\n\n\ninterface = pyatv.interface\npair = pyatv.pair\nProtocol = pyatv_const.Protocol\n\n\nmy_name = os.path.basename(sys.argv[0])\n\ntry:\n loop = asyncio.get_event_loop()\nexcept RuntimeError:\n loop = asyncio.new_event_loop()\n asyncio.set_event_loop(loop)\n\nscan_lookup = {}\nfilter_atvs = True # Should only return ATVs and not other device types (HomePods, Macs, etc)\npairing_atv = False\nactive_pairing = False\nactive_device = False\nactive_remote = False\nactive_ws = False\ndefault_port = 8765\npairing_creds = {}\n\n# Store the AppleTVConfig for reconnection using pyatv's built-in features\ncurrent_config = None\n\nclass ATVKeyboardListener(interface.KeyboardListener):\n global active_ws\n def focusstate_update(self, old_state, new_state):\n print('Focus state changed from {0:s} to {1:s}'.format(old_state, new_state), flush=True)\n if active_ws:\n try:\n loop.run_until_complete(sendCommand(active_ws, \"keyboard_changestate\", [old_state, new_state]))\n except Exception as ex:\n print (f\"change state error: {ex}\", flush=True)\n\nclass ATVConnectionListener(interface.DeviceListener):\n \"\"\"Listener for Apple TV connection state changes.\"\"\"\n \n def connection_lost(self, exception):\n \"\"\"Called when connection is lost.\"\"\"\n global active_device, active_remote, current_config, active_ws\n print(f\"Connection lost: {exception}\", flush=True)\n \n # Only attempt reconnection if we have a valid config and were previously connected\n if current_config and active_device and active_remote:\n if active_ws:\n try:\n loop.run_until_complete(sendCommand(active_ws, \"connection_lost\"))\n except Exception as ex:\n print(f\"Failed to notify client of connection loss: {ex}\", flush=True)\n \n # Attempt automatic reconnection in background\n loop.create_task(attempt_reconnection())\n else:\n # No valid config to reconnect with, just reset state\n active_device = False\n active_remote = False\n \n def connection_closed(self):\n \"\"\"Called when connection is closed.\"\"\"\n global active_device, active_remote, active_ws\n print(\"Connection closed\", flush=True)\n \n # Don't attempt reconnection on clean close - user likely disconnected intentionally\n active_device = False\n active_remote = False\n \n if active_ws:\n try:\n loop.run_until_complete(sendCommand(active_ws, \"connection_closed\"))\n except Exception as ex:\n print(f\"Failed to notify client of connection close: {ex}\", flush=True)\n\nclass ATVPowerListener(interface.PowerListener):\n \"\"\"Listener for Apple TV power state changes.\"\"\"\n \n def powerstate_update(self, old_state, new_state):\n \"\"\"Called when power state changes.\"\"\"\n print(f'Power state changed from {old_state} to {new_state}', flush=True)\n if active_ws:\n try:\n loop.run_until_complete(sendCommand(active_ws, \"power_state_changed\", {\n \"old_state\": str(old_state),\n \"new_state\": str(new_state)\n }))\n except Exception as ex:\n print(f\"Power state error: {ex}\", flush=True)\n \n\n\nasync def sendCommand (ws, command, data=[]):\n r = {\"command\": command, \"data\": data}\n await ws.send(json.dumps(r))\n\nasync def attempt_reconnection():\n \"\"\"Attempt to reconnect using pyatv's built-in reconnection.\"\"\"\n global active_device, active_remote, current_config, active_ws\n \n if not current_config:\n print(\"No config available for reconnection\", flush=True)\n return False\n \n # Don't attempt reconnection if we're in the middle of pairing\n if active_pairing or pairing_atv:\n print(\"Skipping reconnection - pairing in progress\", flush=True)\n return False\n \n try:\n print(\"Attempting automatic reconnection...\", flush=True)\n \n # Close old connection first\n if active_device:\n try:\n await active_device.close()\n except:\n pass\n active_device = False\n active_remote = False\n \n # Reconnect using stored config\n device = await pyatv.connect(current_config, loop)\n remote = device.remote_control\n active_device = device\n active_remote = remote\n \n # Set up listeners again\n device.listener = ATVConnectionListener()\n device.power.listener = ATVPowerListener()\n kblistener = ATVKeyboardListener()\n device.keyboard.listener = kblistener\n \n print(\"Automatic reconnection successful\", flush=True)\n \n if active_ws:\n await sendCommand(active_ws, \"reconnected\")\n \n return True\n \n except Exception as ex:\n print(f\"Automatic reconnection failed: {ex}\", flush=True)\n active_device = False\n active_remote = False\n \n if active_ws:\n await sendCommand(active_ws, \"reconnection_failed\")\n \n return False\n\nasync def parseRequest(j, websocket):\n global scan_lookup, pairing_atv, active_pairing, active_device, active_remote, active_ws, pairing_creds\n active_ws = websocket\n if \"cmd\" in j.keys():\n cmd = j[\"cmd\"]\n else:\n return\n #print (f\"got command: {cmd}\", flush=True)\n \n data = False\n if \"data\" in j.keys():\n data = j[\"data\"]\n \n if cmd == \"quit\":\n print (\"quit command\")\n await close_active_device()\n await asyncio.sleep(0.5)\n sys.exit(0)\n return\n \n if cmd == \"disconnect\":\n print (\"disconnect command\")\n await close_active_device()\n await reset_globals()\n await sendCommand(websocket, \"disconnected\")\n return\n \n if cmd == \"scan\":\n atvs = await pyatv.scan(loop)\n ar = []\n scan_lookup = {}\n if filter_atvs:\n atvs = [x for x in atvs if \"TV\" in x.device_info.model_str]\n for atv in atvs:\n txt = f\"{atv.name} ({atv.address})\"\n ar.append(txt)\n scan_lookup[txt] = atv\n\n await sendCommand(websocket, \"scanResult\", ar)\n\n if cmd == \"echo\":\n await sendCommand(websocket, \"echo_reply\", data)\n\n if cmd == \"startPair\":\n print (\"startPair\")\n atv = scan_lookup[data]\n pairing_atv = atv\n print (\"pairing atv %s\" % (atv))\n pairing = await pair(atv, Protocol.AirPlay, loop)\n active_pairing = pairing\n await pairing.begin()\n\n if cmd == \"finishPair1\":\n print(\"finishPair %s\" % (data))\n pairing = active_pairing\n pairing.pin(data)\n await pairing.finish()\n if pairing.has_paired:\n print(\"Paired with device!\")\n print(\"Credentials:\", pairing.service.credentials)\n else:\n print(\"Did not pair with device!\")\n return\n creds = pairing.service.credentials\n id = pairing_atv.identifier\n nj = {\"credentials\": creds, \"identifier\": id}\n pairing_creds = nj\n await sendCommand(websocket, \"startPair2\")\n #await sendCommand(websocket, \"pairCredentials1\", nj)\n atv = pairing_atv\n print (\"pairing atv %s\" % (atv))\n pairing = await pair(atv, Protocol.Companion, loop)\n active_pairing = pairing\n await pairing.begin()\n\n if cmd == \"finishPair2\":\n print(\"finishPair %s\" % (data))\n pairing = active_pairing\n pairing.pin(data)\n await pairing.finish()\n if pairing.has_paired:\n print(\"Paired with device!\")\n print(\"Credentials:\", pairing.service.credentials)\n else:\n print(\"Did not pair with device!\")\n pairing_creds[\"Companion\"] = pairing.service.credentials\n await sendCommand(websocket, \"pairCredentials\", pairing_creds)\n \n \n if cmd == \"finishPair\":\n print(\"finishPair %s\" % (data))\n pairing = active_pairing\n pairing.pin(data)\n await pairing.finish()\n if pairing.has_paired:\n print(\"Paired with device!\")\n print(\"Credentials:\", pairing.service.credentials)\n else:\n print(\"Did not pair with device!\")\n creds = pairing.service.credentials\n id = pairing_atv.identifier\n nj = {\"credentials\": creds, \"identifier\": id}\n await sendCommand(websocket, \"pairCredentials\", nj)\n\n if cmd == \"kbfocus\":\n if not active_device:\n return\n kbfocus = active_device.keyboard.text_focus_state == pyatv_const.KeyboardFocusState.Focused\n await sendCommand(websocket, \"kbfocus-status\", kbfocus)\n \n if cmd == \"settext\":\n text = data[\"text\"]\n if active_device.keyboard.text_focus_state != pyatv_const.KeyboardFocusState.Focused:\n return\n await active_device.keyboard.text_set(text)\n \n if cmd == \"gettext\":\n print (f\"gettext focus compare {active_device.keyboard.text_focus_state} == {pyatv_const.KeyboardFocusState.Focused}\", flush=True)\n if active_device.keyboard.text_focus_state != pyatv_const.KeyboardFocusState.Focused:\n return\n ctext = await active_device.keyboard.text_get()\n print (f\"Current text: {ctext}\", flush=True)\n await sendCommand(websocket, \"current-text\", ctext)\n \n if cmd == \"connect\":\n id = data[\"identifier\"]\n creds = data[\"credentials\"]\n stored_credentials = { Protocol.AirPlay: creds }\n if \"Companion\" in data.keys():\n companion_creds = data[\"Companion\"]\n stored_credentials[Protocol.Companion] = companion_creds\n \n print (\"stored_credentials %s\" % (stored_credentials))\n atvs = await pyatv.scan(loop, identifier=id)\n if not atvs:\n print(\"No Apple TV found with the specified identifier\")\n await sendCommand(websocket, \"connection_failure\")\n return\n \n atv = atvs[0]\n \n # Set up credentials on the scanned config\n for protocol, credentials in stored_credentials.items():\n print (\"Setting protocol %s with credentials %s\" % (str(protocol), credentials))\n atv.set_credentials(protocol, credentials)\n \n try:\n # Connect to device\n device = await pyatv.connect(atv, loop)\n remote = device.remote_control\n active_device = device\n active_remote = remote\n \n # Store config for potential reconnection (only after successful connection)\n global current_config\n current_config = atv\n \n # Set up keyboard listener\n kblistener = ATVKeyboardListener()\n device.keyboard.listener = kblistener\n \n # Set up connection and power listeners (only after successful connection)\n device.listener = ATVConnectionListener()\n device.power.listener = ATVPowerListener()\n \n await sendCommand(websocket, \"connected\")\n \n except Exception as ex:\n print(f\"Failed to connect: {ex}\")\n await sendCommand(websocket, \"connection_failure\")\n \n if cmd == \"is_connected\":\n # Simple check if we have active device and remote\n ic = \"true\" if (active_remote and active_device) else \"false\"\n await sendCommand(websocket, \"is_connected\", ic)\n \n if cmd == \"ping_device\":\n # Simple ping command - just check if we have an active connection\n if active_remote and active_device:\n await sendCommand(websocket, \"ping_result\", \"connected\")\n else:\n await sendCommand(websocket, \"ping_result\", \"not_connected\")\n \n if cmd == \"key\":\n if not active_remote or not active_device:\n await sendCommand(websocket, \"command_failed\", \"not_connected\")\n return\n \n valid_keys = ['play_pause', 'left', 'right', 'down', 'up', 'select', 'menu', 'top_menu', 'home', 'home_hold', 'skip_backward', 'skip_forward', 'volume_up', 'volume_down']\n no_action_keys = ['volume_up', 'volume_down', 'play_pause', 'home_hold']\n #taction = InputAction[\"SingleTap\"]\n taction = False\n key = data\n if not isinstance(data, str):\n key = data['key']\n taction = InputAction[data['taction']]\n \n if key in valid_keys:\n try:\n if key in no_action_keys or (not taction):\n r = await getattr(active_remote, key)()\n else:\n r = await getattr(active_remote, key)(taction)\n #print (r)\n except Exception as ex:\n print(f\"Command execution failed: {ex}\", flush=True)\n await sendCommand(websocket, \"command_failed\", str(ex))\n\nasync def close_active_device():\n global active_device, active_remote\n try:\n if active_device:\n await active_device.close()\n active_device = False\n active_remote = False\n except Exception as ex:\n print (\"Error closing active_device: %s\" %(ex))\n\nasync def reset_globals():\n global scan_lookup, pairing_atv, active_pairing, active_device, active_remote, active_ws\n global current_config\n print (\"Resetting global variables\")\n scan_lookup = {}\n \n pairing_atv = False\n active_pairing = False\n active_device = False\n active_remote = False\n active_ws = False\n current_config = None # Clear config to prevent unwanted reconnection attempts\n\nkeep_running = True\n\n\nasync def check_exit_file():\n global keep_running\n if os.path.exists('stopserver'):\n os.unlink('stopserver')\n\n while keep_running:\n await asyncio.sleep(0.5)\n fe = os.path.exists('stopserver')\n txt = \"found\" if fe else \"not found\"\n #print (\"stopserver %s\" % (txt), flush=True)\n if fe:\n print (\"exiting\")\n keep_running = False\n os.unlink('stopserver')\n sys.exit(0)\n\n\nasync def ws_main(websocket):\n global active_ws\n active_ws = websocket\n \n try:\n async for message in websocket:\n try:\n j = json.loads(message)\n except Exception as ex:\n print (\"Error parsing message: %s\\n%s\" % (str(ex), message))\n continue\n \n await parseRequest(j, websocket)\n except Exception as ex:\n # Handle various connection closed scenarios\n if \"ConnectionClosed\" in str(type(ex)) or \"connection closed\" in str(ex).lower():\n print(\"WebSocket connection closed\", flush=True)\n else:\n print(f\"WebSocket error: {ex}\", flush=True)\n finally:\n # Clean up when client disconnects but keep Apple TV connection alive\n if active_ws == websocket:\n active_ws = False\n # Don't close the Apple TV connection - keep it alive for reconnecting clients\n\nasync def main(port):\n global keep_running\n width = 80\n txt = \"%s WebSocket - ATV Server\" % (my_name)\n print (\"=\"*width)\n print (txt.center(width))\n print (\"=\"*width, flush=True)\n task = asyncio.create_task(check_exit_file())\n\n async with websockets.serve(ws_main, \"localhost\", port):\n try:\n await asyncio.Future() # run forever\n except Exception as ex:\n print (ex)\n sys.exit(0)\n\n\n\nif __name__ == \"__main__\":\n args = sys.argv[1:]\n port = default_port\n if len(args) > 0:\n if args[0] in [\"-h\", \"--help\", \"-?\", \"/?\"]:\n print (\"Usage: %s (port_number)\\n\\n Port number by default is %d\" % (my_name, default_port))\n port = int(args[0])\n\n asyncio.set_event_loop(loop)\n loop.run_until_complete(main(port))\n\n", 3 | "start_server.bat": "@echo off\r\nset INSTALL_LOG=atv_pip_install.log\r\nset MY_PATH=%~dp0\r\ncd /d %MY_PATH%\r\n\r\nif not exist env (\r\n echo ATVRemote - Python install started %DATE% %TIME% >> %INSTALL_LOG%\r\n echo > setting_up_python\r\n \r\n REM Check if uv is installed\r\n where uv >nul 2>&1\r\n if not errorlevel 1 (\r\n echo Using uv for virtual environment setup >> %INSTALL_LOG%\r\n uv venv env >> %INSTALL_LOG% 2>&1\r\n call env\\Scripts\\activate.bat\r\n if exist requirements.txt (\r\n echo Installing from requirements.txt >> %INSTALL_LOG%\r\n uv pip install -r requirements.txt >> %INSTALL_LOG% 2>&1\r\n ) else (\r\n echo Installing websockets and pyatv >> %INSTALL_LOG%\r\n uv pip install websockets pyatv >> %INSTALL_LOG% 2>&1\r\n )\r\n ) else (\r\n echo Using standard Python venv >> %INSTALL_LOG%\r\n python -m venv env >> %INSTALL_LOG% 2>&1\r\n call env\\Scripts\\activate.bat\r\n python -m pip install --upgrade pip >> %INSTALL_LOG% 2>&1\r\n if exist requirements.txt (\r\n echo Installing from requirements.txt >> %INSTALL_LOG%\r\n python -m pip install -r requirements.txt >> %INSTALL_LOG% 2>&1\r\n ) else (\r\n echo Installing websockets and pyatv >> %INSTALL_LOG%\r\n python -m pip install websockets pyatv >> %INSTALL_LOG% 2>&1\r\n )\r\n \r\n )\r\n\r\n echo ATVRemote - Python install ended %DATE% %TIME% >> %INSTALL_LOG%\r\n echo ================================================== >> %INSTALL_LOG%\r\n) else (\r\n call env\\Scripts\\activate.bat\r\n)\r\n\r\n:kill_proc\r\nfor /f \"tokens=2 delims= \" %%A in ('tasklist /FI \"IMAGENAME eq python.exe\" /NH') do (\r\n tasklist /FI \"WINDOWTITLE eq wsserver.py\" | findstr wsserver.py >nul\r\n if not errorlevel 1 (\r\n echo Killing %%A\r\n taskkill /PID %%A /F\r\n )\r\n)\r\nif exist setting_up_python del setting_up_python\r\npython wsserver.py\r\n", 4 | "start_server.sh": "#!/bin/bash\nINSTALL_LOG=\"atv_pip_install.log\"\nMY_PATH=$(dirname \"$0\")\ncd \"$MY_PATH\"\nif [[ ! -d env ]]; then\n dt=$(date)\n echo \"ATVRemote - Python install started $dt\" >> $INSTALL_LOG\n touch setting_up_python\n \n # Check if uv is installed (it's faster in my testing)\n if command -v uv &> /dev/null; then\n echo \"Using uv for virtual environment setup\" >> $INSTALL_LOG\n uv venv env | tee -a $INSTALL_LOG\n source env/bin/activate\n if [[ -f requirements.txt ]]; then\n echo \"Installing from requirements.txt\" >> $INSTALL_LOG\n uv pip install -r requirements.txt | tee -a $INSTALL_LOG\n else\n echo \"Installing websockets and pyatv\" >> $INSTALL_LOG\n uv pip install websockets pyatv | tee -a $INSTALL_LOG\n fi\n \n else\n echo \"Using standard Python venv\" >> $INSTALL_LOG\n python3 -m venv env | tee -a $INSTALL_LOG\n source env/bin/activate\n python -m pip install --upgrade pip | tee -a $INSTALL_LOG\n if [[ -f requirements.txt ]]; then\n echo \"Installing from requirements.txt\" >> $INSTALL_LOG\n python -m pip install -r requirements.txt | tee -a $INSTALL_LOG\n else\n echo \"Installing websockets and pyatv\" >> $INSTALL_LOG\n python -m pip install websockets pyatv | tee -a $INSTALL_LOG\n fi\n fi\n \n dt=$(date)\n echo \"ATVRemote - Python install ended $dt\" >> $INSTALL_LOG\n echo \"==================================================\" >> $INSTALL_LOG\nelse\n source env/bin/activate\nfi\n\nfunction kill_proc () {\n for p in $(ps ax | grep -v grep | grep wsserver.py | awk '{print $1}'); do\n echo \"Killing $p\"\n kill $1 $p\n done\n}\nkill_proc\nkill_proc \"-9\"\n[[ -f setting_up_python ]] && rm setting_up_python\npython wsserver.py" 5 | }; 6 | 7 | exports.files = files; 8 | -------------------------------------------------------------------------------- /app/js/tippy-headless.umd.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],t):(e=e||self).tippy=t(e.Popper)}(this,(function(e){"use strict";var t={passive:!0,capture:!0};function n(e,t,n){if(Array.isArray(e)){var r=e[t];return null==r?Array.isArray(n)?n[t]:n:r}return e}function r(e,t){var n={}.toString.call(e);return 0===n.indexOf("[object")&&n.indexOf(t+"]")>-1}function i(e,t){return"function"==typeof e?e.apply(void 0,t):e}function o(e,t){return 0===t?e:function(r){clearTimeout(n),n=setTimeout((function(){e(r)}),t)};var n}function a(e,t){var n=Object.assign({},e);return t.forEach((function(e){delete n[e]})),n}function s(e){return[].concat(e)}function u(e,t){-1===e.indexOf(t)&&e.push(t)}function c(e){return e.split("-")[0]}function p(e){return[].slice.call(e)}function f(){return document.createElement("div")}function l(e){return["Element","Fragment"].some((function(t){return r(e,t)}))}function d(e){return r(e,"MouseEvent")}function v(e){return!(!e||!e._tippy||e._tippy.reference!==e)}function g(e){return l(e)?[e]:function(e){return r(e,"NodeList")}(e)?p(e):Array.isArray(e)?e:p(document.querySelectorAll(e))}function m(e,t){e.forEach((function(e){e&&(e.style.transitionDuration=t+"ms")}))}function h(e,t){e.forEach((function(e){e&&e.setAttribute("data-state",t)}))}function b(e){var t,n=s(e)[0];return(null==n||null==(t=n.ownerDocument)?void 0:t.body)?n.ownerDocument:document}function y(e,t,n){var r=t+"EventListener";["transitionend","webkitTransitionEnd"].forEach((function(t){e[r](t,n)}))}var E={isTouch:!1},w=0;function T(){E.isTouch||(E.isTouch=!0,window.performance&&document.addEventListener("mousemove",O))}function O(){var e=performance.now();e-w<20&&(E.isTouch=!1,document.removeEventListener("mousemove",O)),w=e}function C(){var e=document.activeElement;if(v(e)){var t=e._tippy;e.blur&&!t.state.isVisible&&e.blur()}}var x="undefined"!=typeof window&&"undefined"!=typeof document?navigator.userAgent:"",A=/MSIE |Trident\//.test(x),D=Object.assign({appendTo:function(){return document.body},aria:{content:"auto",expanded:"auto"},delay:0,duration:[300,250],getReferenceClientRect:null,hideOnClick:!0,ignoreAttributes:!1,interactive:!1,interactiveBorder:2,interactiveDebounce:0,moveTransition:"",offset:[0,10],onAfterUpdate:function(){},onBeforeUpdate:function(){},onCreate:function(){},onDestroy:function(){},onHidden:function(){},onHide:function(){},onMount:function(){},onShow:function(){},onShown:function(){},onTrigger:function(){},onUntrigger:function(){},onClickOutside:function(){},placement:"top",plugins:[],popperOptions:{},render:null,showOnCreate:!1,touch:!0,trigger:"mouseenter focus",triggerTarget:null},{animateFill:!1,followCursor:!1,inlinePositioning:!1,sticky:!1},{},{allowHTML:!1,animation:"fade",arrow:!0,content:"",inertia:!1,maxWidth:350,role:"tooltip",theme:"",zIndex:9999}),L=Object.keys(D);function k(e){var t=(e.plugins||[]).reduce((function(t,n){var r=n.name,i=n.defaultValue;return r&&(t[r]=void 0!==e[r]?e[r]:i),t}),{});return Object.assign({},e,{},t)}function R(e,t){var n=Object.assign({},t,{content:i(t.content,[e])},t.ignoreAttributes?{}:function(e,t){return(t?Object.keys(k(Object.assign({},D,{plugins:t}))):L).reduce((function(t,n){var r=(e.getAttribute("data-tippy-"+n)||"").trim();if(!r)return t;if("content"===n)t[n]=r;else try{t[n]=JSON.parse(r)}catch(e){t[n]=r}return t}),{})}(e,t.plugins));return n.aria=Object.assign({},D.aria,{},n.aria),n.aria={expanded:"auto"===n.aria.expanded?t.interactive:n.aria.expanded,content:"auto"===n.aria.content?t.interactive?null:"describedby":n.aria.content},n}function P(e){var t=e.firstElementChild,n=p(t.children);return{box:t,content:n.find((function(e){return e.classList.contains("tippy-content")})),arrow:n.find((function(e){return e.classList.contains("tippy-arrow")||e.classList.contains("tippy-svg-arrow")})),backdrop:n.find((function(e){return e.classList.contains("tippy-backdrop")}))}}var V=1,j=[],M=[];function I(r,a){var l,v,g,w,T,O,C,x,L,I=R(r,Object.assign({},D,{},k((l=a,Object.keys(l).reduce((function(e,t){return void 0!==l[t]&&(e[t]=l[t]),e}),{}))))),S=!1,B=!1,U=!1,H=!1,N=[],_=o(be,I.interactiveDebounce),F=V++,X=(L=I.plugins).filter((function(e,t){return L.indexOf(e)===t})),Y={id:F,reference:r,popper:f(),popperInstance:null,props:I,state:{isEnabled:!0,isVisible:!1,isDestroyed:!1,isMounted:!1,isShown:!1},plugins:X,clearDelayTimeouts:function(){clearTimeout(v),clearTimeout(g),cancelAnimationFrame(w)},setProps:function(e){if(Y.state.isDestroyed)return;ie("onBeforeUpdate",[Y,e]),me();var t=Y.props,n=R(r,Object.assign({},Y.props,{},e,{ignoreAttributes:!0}));Y.props=n,ge(),t.interactiveDebounce!==n.interactiveDebounce&&(se(),_=o(be,n.interactiveDebounce));t.triggerTarget&&!n.triggerTarget?s(t.triggerTarget).forEach((function(e){e.removeAttribute("aria-expanded")})):n.triggerTarget&&r.removeAttribute("aria-expanded");ae(),re(),$&&$(t,n);Y.popperInstance&&(Te(),Ce().forEach((function(e){requestAnimationFrame(e._tippy.popperInstance.forceUpdate)})));ie("onAfterUpdate",[Y,e])},setContent:function(e){Y.setProps({content:e})},show:function(){var e=Y.state.isVisible,t=Y.state.isDestroyed,r=!Y.state.isEnabled,o=E.isTouch&&!Y.props.touch,a=n(Y.props.duration,0,D.duration);if(e||t||r||o)return;if(Z().hasAttribute("disabled"))return;if(ie("onShow",[Y],!1),!1===Y.props.onShow(Y))return;Y.state.isVisible=!0,Q()&&(z.style.visibility="visible");re(),fe(),Y.state.isMounted||(z.style.transition="none");if(Q()){var s=te(),c=s.box,p=s.content;m([c,p],0)}C=function(){var e;if(Y.state.isVisible&&!H){if(H=!0,z.offsetHeight,z.style.transition=Y.props.moveTransition,Q()&&Y.props.animation){var t=te(),n=t.box,r=t.content;m([n,r],a),h([n,r],"visible")}oe(),ae(),u(M,Y),null==(e=Y.popperInstance)||e.forceUpdate(),Y.state.isMounted=!0,ie("onMount",[Y]),Y.props.animation&&Q()&&function(e,t){de(e,t)}(a,(function(){Y.state.isShown=!0,ie("onShown",[Y])}))}},function(){var e,t=Y.props.appendTo,n=Z();e=Y.props.interactive&&t===D.appendTo||"parent"===t?n.parentNode:i(t,[n]);e.contains(z)||e.appendChild(z);Te()}()},hide:function(){var e=!Y.state.isVisible,t=Y.state.isDestroyed,r=!Y.state.isEnabled,i=n(Y.props.duration,1,D.duration);if(e||t||r)return;if(ie("onHide",[Y],!1),!1===Y.props.onHide(Y))return;Y.state.isVisible=!1,Y.state.isShown=!1,H=!1,S=!1,Q()&&(z.style.visibility="hidden");if(se(),le(),re(),Q()){var o=te(),a=o.box,s=o.content;Y.props.animation&&(m([a,s],i),h([a,s],"hidden"))}oe(),ae(),Y.props.animation?Q()&&function(e,t){de(e,(function(){!Y.state.isVisible&&z.parentNode&&z.parentNode.contains(z)&&t()}))}(i,Y.unmount):Y.unmount()},hideWithInteractivity:function(e){ee().addEventListener("mousemove",_),u(j,_),_(e)},enable:function(){Y.state.isEnabled=!0},disable:function(){Y.hide(),Y.state.isEnabled=!1},unmount:function(){Y.state.isVisible&&Y.hide();if(!Y.state.isMounted)return;Oe(),Ce().forEach((function(e){e._tippy.unmount()})),z.parentNode&&z.parentNode.removeChild(z);M=M.filter((function(e){return e!==Y})),Y.state.isMounted=!1,ie("onHidden",[Y])},destroy:function(){if(Y.state.isDestroyed)return;Y.clearDelayTimeouts(),Y.unmount(),me(),delete r._tippy,Y.state.isDestroyed=!0,ie("onDestroy",[Y])}};if(!I.render)return Y;var q=I.render(Y),z=q.popper,$=q.onUpdate;z.setAttribute("data-tippy-root",""),z.id="tippy-"+Y.id,Y.popper=z,r._tippy=Y,z._tippy=Y;var W=X.map((function(e){return e.fn(Y)})),J=r.hasAttribute("aria-expanded");return ge(),ae(),re(),ie("onCreate",[Y]),I.showOnCreate&&xe(),z.addEventListener("mouseenter",(function(){Y.props.interactive&&Y.state.isVisible&&Y.clearDelayTimeouts()})),z.addEventListener("mouseleave",(function(e){Y.props.interactive&&Y.props.trigger.indexOf("mouseenter")>=0&&(ee().addEventListener("mousemove",_),_(e))})),Y;function G(){var e=Y.props.touch;return Array.isArray(e)?e:[e,0]}function K(){return"hold"===G()[0]}function Q(){var e;return!!(null==(e=Y.props.render)?void 0:e.$$tippy)}function Z(){return x||r}function ee(){var e=Z().parentNode;return e?b(e):document}function te(){return P(z)}function ne(e){return Y.state.isMounted&&!Y.state.isVisible||E.isTouch||T&&"focus"===T.type?0:n(Y.props.delay,e?0:1,D.delay)}function re(){z.style.pointerEvents=Y.props.interactive&&Y.state.isVisible?"":"none",z.style.zIndex=""+Y.props.zIndex}function ie(e,t,n){var r;(void 0===n&&(n=!0),W.forEach((function(n){n[e]&&n[e].apply(void 0,t)})),n)&&(r=Y.props)[e].apply(r,t)}function oe(){var e=Y.props.aria;if(e.content){var t="aria-"+e.content,n=z.id;s(Y.props.triggerTarget||r).forEach((function(e){var r=e.getAttribute(t);if(Y.state.isVisible)e.setAttribute(t,r?r+" "+n:n);else{var i=r&&r.replace(n,"").trim();i?e.setAttribute(t,i):e.removeAttribute(t)}}))}}function ae(){!J&&Y.props.aria.expanded&&s(Y.props.triggerTarget||r).forEach((function(e){Y.props.interactive?e.setAttribute("aria-expanded",Y.state.isVisible&&e===Z()?"true":"false"):e.removeAttribute("aria-expanded")}))}function se(){ee().removeEventListener("mousemove",_),j=j.filter((function(e){return e!==_}))}function ue(e){if(!(E.isTouch&&(U||"mousedown"===e.type)||Y.props.interactive&&z.contains(e.target))){if(Z().contains(e.target)){if(E.isTouch)return;if(Y.state.isVisible&&Y.props.trigger.indexOf("click")>=0)return}else ie("onClickOutside",[Y,e]);!0===Y.props.hideOnClick&&(Y.clearDelayTimeouts(),Y.hide(),B=!0,setTimeout((function(){B=!1})),Y.state.isMounted||le())}}function ce(){U=!0}function pe(){U=!1}function fe(){var e=ee();e.addEventListener("mousedown",ue,!0),e.addEventListener("touchend",ue,t),e.addEventListener("touchstart",pe,t),e.addEventListener("touchmove",ce,t)}function le(){var e=ee();e.removeEventListener("mousedown",ue,!0),e.removeEventListener("touchend",ue,t),e.removeEventListener("touchstart",pe,t),e.removeEventListener("touchmove",ce,t)}function de(e,t){var n=te().box;function r(e){e.target===n&&(y(n,"remove",r),t())}if(0===e)return t();y(n,"remove",O),y(n,"add",r),O=r}function ve(e,t,n){void 0===n&&(n=!1),s(Y.props.triggerTarget||r).forEach((function(r){r.addEventListener(e,t,n),N.push({node:r,eventType:e,handler:t,options:n})}))}function ge(){var e;K()&&(ve("touchstart",he,{passive:!0}),ve("touchend",ye,{passive:!0})),(e=Y.props.trigger,e.split(/\s+/).filter(Boolean)).forEach((function(e){if("manual"!==e)switch(ve(e,he),e){case"mouseenter":ve("mouseleave",ye);break;case"focus":ve(A?"focusout":"blur",Ee);break;case"focusin":ve("focusout",Ee)}}))}function me(){N.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,i=e.options;t.removeEventListener(n,r,i)})),N=[]}function he(e){var t,n=!1;if(Y.state.isEnabled&&!we(e)&&!B){var r="focus"===(null==(t=T)?void 0:t.type);T=e,x=e.currentTarget,ae(),!Y.state.isVisible&&d(e)&&j.forEach((function(t){return t(e)})),"click"===e.type&&(Y.props.trigger.indexOf("mouseenter")<0||S)&&!1!==Y.props.hideOnClick&&Y.state.isVisible?n=!0:xe(e),"click"===e.type&&(S=!n),n&&!r&&Ae(e)}}function be(e){var t=e.target,n=Z().contains(t)||z.contains(t);"mousemove"===e.type&&n||function(e,t){var n=t.clientX,r=t.clientY;return e.every((function(e){var t=e.popperRect,i=e.popperState,o=e.props.interactiveBorder,a=c(i.placement),s=i.modifiersData.offset;if(!s)return!0;var u="bottom"===a?s.top.y:0,p="top"===a?s.bottom.y:0,f="right"===a?s.left.x:0,l="left"===a?s.right.x:0,d=t.top-r+u>o,v=r-t.bottom-p>o,g=t.left-n+f>o,m=n-t.right-l>o;return d||v||g||m}))}(Ce().concat(z).map((function(e){var t,n=null==(t=e._tippy.popperInstance)?void 0:t.state;return n?{popperRect:e.getBoundingClientRect(),popperState:n,props:I}:null})).filter(Boolean),e)&&(se(),Ae(e))}function ye(e){we(e)||Y.props.trigger.indexOf("click")>=0&&S||(Y.props.interactive?Y.hideWithInteractivity(e):Ae(e))}function Ee(e){Y.props.trigger.indexOf("focusin")<0&&e.target!==Z()||Y.props.interactive&&e.relatedTarget&&z.contains(e.relatedTarget)||Ae(e)}function we(e){return!!E.isTouch&&K()!==e.type.indexOf("touch")>=0}function Te(){Oe();var t=Y.props,n=t.popperOptions,i=t.placement,o=t.offset,a=t.getReferenceClientRect,s=t.moveTransition,u=Q()?P(z).arrow:null,c=a?{getBoundingClientRect:a,contextElement:a.contextElement||Z()}:r,p=[{name:"offset",options:{offset:o}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5}},{name:"computeStyles",options:{adaptive:!s}},{name:"$$tippy",enabled:!0,phase:"beforeWrite",requires:["computeStyles"],fn:function(e){var t=e.state;if(Q()){var n=te().box;["placement","reference-hidden","escaped"].forEach((function(e){"placement"===e?n.setAttribute("data-placement",t.placement):t.attributes.popper["data-popper-"+e]?n.setAttribute("data-"+e,""):n.removeAttribute("data-"+e)})),t.attributes.popper={}}}}];Q()&&u&&p.push({name:"arrow",options:{element:u,padding:3}}),p.push.apply(p,(null==n?void 0:n.modifiers)||[]),Y.popperInstance=e.createPopper(c,z,Object.assign({},n,{placement:i,onFirstUpdate:C,modifiers:p}))}function Oe(){Y.popperInstance&&(Y.popperInstance.destroy(),Y.popperInstance=null)}function Ce(){return p(z.querySelectorAll("[data-tippy-root]"))}function xe(e){Y.clearDelayTimeouts(),e&&ie("onTrigger",[Y,e]),fe();var t=ne(!0),n=G(),r=n[0],i=n[1];E.isTouch&&"hold"===r&&i&&(t=i),t?v=setTimeout((function(){Y.show()}),t):Y.show()}function Ae(e){if(Y.clearDelayTimeouts(),ie("onUntrigger",[Y,e]),Y.state.isVisible){if(!(Y.props.trigger.indexOf("mouseenter")>=0&&Y.props.trigger.indexOf("click")>=0&&["mouseleave","mousemove"].indexOf(e.type)>=0&&S)){var t=ne(!1);t?g=setTimeout((function(){Y.state.isVisible&&Y.hide()}),t):w=requestAnimationFrame((function(){Y.hide()}))}}else le()}}function S(e,n){void 0===n&&(n={});var r=D.plugins.concat(n.plugins||[]);document.addEventListener("touchstart",T,t),window.addEventListener("blur",C);var i=Object.assign({},n,{plugins:r}),o=g(e).reduce((function(e,t){var n=t&&I(t,i);return n&&e.push(n),e}),[]);return l(e)?o[0]:o}S.defaultProps=D,S.setDefaultProps=function(e){Object.keys(e).forEach((function(t){D[t]=e[t]}))},S.currentInput=E;var B={mouseover:"mouseenter",focusin:"focus",click:"click"};var U={name:"animateFill",defaultValue:!1,fn:function(e){var t;if(!(null==(t=e.props.render)?void 0:t.$$tippy))return{};var n=P(e.popper),r=n.box,i=n.content,o=e.props.animateFill?function(){var e=f();return e.className="tippy-backdrop",h([e],"hidden"),e}():null;return{onCreate:function(){o&&(r.insertBefore(o,r.firstElementChild),r.setAttribute("data-animatefill",""),r.style.overflow="hidden",e.setProps({arrow:!1,animation:"shift-away"}))},onMount:function(){if(o){var e=r.style.transitionDuration,t=Number(e.replace("ms",""));i.style.transitionDelay=Math.round(t/10)+"ms",o.style.transitionDuration=e,h([o],"visible")}},onShow:function(){o&&(o.style.transitionDuration="0ms")},onHide:function(){o&&h([o],"hidden")}}}};var H={clientX:0,clientY:0},N=[];function _(e){var t=e.clientX,n=e.clientY;H={clientX:t,clientY:n}}var F={name:"followCursor",defaultValue:!1,fn:function(e){var t=e.reference,n=b(e.props.triggerTarget||t),r=!1,i=!1,o=!0,a=e.props;function s(){return"initial"===e.props.followCursor&&e.state.isVisible}function u(){n.addEventListener("mousemove",f)}function c(){n.removeEventListener("mousemove",f)}function p(){r=!0,e.setProps({getReferenceClientRect:null}),r=!1}function f(n){var r=!n.target||t.contains(n.target),i=e.props.followCursor,o=n.clientX,a=n.clientY,s=t.getBoundingClientRect(),u=o-s.left,c=a-s.top;!r&&e.props.interactive||e.setProps({getReferenceClientRect:function(){var e=t.getBoundingClientRect(),n=o,r=a;"initial"===i&&(n=e.left+u,r=e.top+c);var s="horizontal"===i?e.top:r,p="vertical"===i?e.right:n,f="horizontal"===i?e.bottom:r,l="vertical"===i?e.left:n;return{width:p-l,height:f-s,top:s,right:p,bottom:f,left:l}}})}function l(){e.props.followCursor&&(N.push({instance:e,doc:n}),function(e){e.addEventListener("mousemove",_)}(n))}function v(){0===(N=N.filter((function(t){return t.instance!==e}))).filter((function(e){return e.doc===n})).length&&function(e){e.removeEventListener("mousemove",_)}(n)}return{onCreate:l,onDestroy:v,onBeforeUpdate:function(){a=e.props},onAfterUpdate:function(t,n){var o=n.followCursor;r||void 0!==o&&a.followCursor!==o&&(v(),o?(l(),!e.state.isMounted||i||s()||u()):(c(),p()))},onMount:function(){e.props.followCursor&&!i&&(o&&(f(H),o=!1),s()||u())},onTrigger:function(e,t){d(t)&&(H={clientX:t.clientX,clientY:t.clientY}),i="focus"===t.type},onHidden:function(){e.props.followCursor&&(p(),c(),o=!0)}}}};var X={name:"inlinePositioning",defaultValue:!1,fn:function(e){var t,n=e.reference;var r=-1,i=!1,o={name:"tippyInlinePositioning",enabled:!0,phase:"afterWrite",fn:function(i){var o=i.state;e.props.inlinePositioning&&(t!==o.placement&&e.setProps({getReferenceClientRect:function(){return function(e){return function(e,t,n,r){if(n.length<2||null===e)return t;if(2===n.length&&r>=0&&n[0].left>n[1].right)return n[r]||t;switch(e){case"top":case"bottom":var i=n[0],o=n[n.length-1],a="top"===e,s=i.top,u=o.bottom,c=a?i.left:o.left,p=a?i.right:o.right;return{top:s,bottom:u,left:c,right:p,width:p-c,height:u-s};case"left":case"right":var f=Math.min.apply(Math,n.map((function(e){return e.left}))),l=Math.max.apply(Math,n.map((function(e){return e.right}))),d=n.filter((function(t){return"left"===e?t.left===f:t.right===l})),v=d[0].top,g=d[d.length-1].bottom;return{top:v,bottom:g,left:f,right:l,width:l-f,height:g-v};default:return t}}(c(e),n.getBoundingClientRect(),p(n.getClientRects()),r)}(o.placement)}}),t=o.placement)}};function a(){var t;i||(t=function(e,t){var n;return{popperOptions:Object.assign({},e.popperOptions,{modifiers:[].concat(((null==(n=e.popperOptions)?void 0:n.modifiers)||[]).filter((function(e){return e.name!==t.name})),[t])})}}(e.props,o),i=!0,e.setProps(t),i=!1)}return{onCreate:a,onAfterUpdate:a,onTrigger:function(t,n){if(d(n)){var i=p(e.reference.getClientRects()),o=i.find((function(e){return e.left-2<=n.clientX&&e.right+2>=n.clientX&&e.top-2<=n.clientY&&e.bottom+2>=n.clientY}));r=i.indexOf(o)}},onUntrigger:function(){r=-1}}}};var Y={name:"sticky",defaultValue:!1,fn:function(e){var t=e.reference,n=e.popper;function r(t){return!0===e.props.sticky||e.props.sticky===t}var i=null,o=null;function a(){var s=r("reference")?(e.popperInstance?e.popperInstance.state.elements.reference:t).getBoundingClientRect():null,u=r("popper")?n.getBoundingClientRect():null;(s&&q(i,s)||u&&q(o,u))&&e.popperInstance&&e.popperInstance.update(),i=s,o=u,e.state.isMounted&&requestAnimationFrame(a)}return{onMount:function(){e.props.sticky&&a()}}}};function q(e,t){return!e||!t||(e.top!==t.top||e.right!==t.right||e.bottom!==t.bottom||e.left!==t.left)}return S.setDefaultProps({plugins:[U,F,X,Y],animation:!1}),S.createSingleton=function(e,t){void 0===t&&(t={});var n,r=e,i=[],o=t.overrides,s=[];function u(){i=r.map((function(e){return e.reference}))}function c(e){r.forEach((function(t){e?t.enable():t.disable()}))}function p(e){return r.map((function(t){var r=t.setProps;return t.setProps=function(i){r(i),t.reference===n&&e.setProps(i)},function(){t.setProps=r}}))}c(!1),u();var l={fn:function(){return{onDestroy:function(){c(!0)},onTrigger:function(e,t){var a=t.currentTarget,s=i.indexOf(a);if(a!==n){n=a;var u=(o||[]).concat("content").reduce((function(e,t){return e[t]=r[s].props[t],e}),{});e.setProps(Object.assign({},u,{getReferenceClientRect:"function"==typeof u.getReferenceClientRect?u.getReferenceClientRect:function(){return a.getBoundingClientRect()}}))}}}}},d=S(f(),Object.assign({},a(t,["overrides"]),{plugins:[l].concat(t.plugins||[]),triggerTarget:i})),v=d.setProps;return d.setProps=function(e){o=e.overrides||o,v(e)},d.setInstances=function(e){c(!0),s.forEach((function(e){return e()})),r=e,c(!1),u(),p(d),d.setProps({triggerTarget:i})},s=p(d),d},S.delegate=function(e,t){var n=[],r=[],i=!1,o=t.target,u=a(t,["target"]),c=Object.assign({},u,{trigger:"manual",touch:!1}),p=Object.assign({},u,{showOnCreate:!0}),f=S(e,c);function l(e){if(e.target&&!i){var n=e.target.closest(o);if(n){var a=n.getAttribute("data-tippy-trigger")||t.trigger||D.trigger;if(!n._tippy&&!("touchstart"===e.type&&"boolean"==typeof p.touch||"touchstart"!==e.type&&a.indexOf(B[e.type])<0)){var s=S(n,p);s&&(r=r.concat(s))}}}}function d(e,t,r,i){void 0===i&&(i=!1),e.addEventListener(t,r,i),n.push({node:e,eventType:t,handler:r,options:i})}return s(f).forEach((function(e){var t=e.destroy,o=e.enable,a=e.disable;e.destroy=function(e){void 0===e&&(e=!0),e&&r.forEach((function(e){e.destroy()})),r=[],n.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,i=e.options;t.removeEventListener(n,r,i)})),n=[],t()},e.enable=function(){o(),r.forEach((function(e){return e.enable()})),i=!1},e.disable=function(){a(),r.forEach((function(e){return e.disable()})),i=!0},function(e){var t=e.reference;d(t,"touchstart",l),d(t,"mouseover",l),d(t,"focusin",l),d(t,"click",l)}(e)})),f},S.hideAll=function(e){var t=void 0===e?{}:e,n=t.exclude,r=t.duration;M.forEach((function(e){var t=!1;if(n&&(t=v(n)?e.reference===n:e.popper===n.popper),!t){var i=e.props.duration;e.setProps({duration:r}),e.hide(),e.state.isDestroyed||e.setProps({duration:i})}}))},S.roundArrow='',S})); 2 | //# sourceMappingURL=tippy-headless.umd.min.js.map 3 | -------------------------------------------------------------------------------- /app/css/select2-inverted.css: -------------------------------------------------------------------------------- 1 | .select2-container { 2 | box-sizing: border-box; 3 | display: inline-block; 4 | margin: 0; 5 | position: relative; 6 | vertical-align: middle 7 | } 8 | 9 | .select2-container .select2-selection--single { 10 | box-sizing: border-box; 11 | cursor: pointer; 12 | display: block; 13 | height: 28px; 14 | user-select: none; 15 | -webkit-user-select: none 16 | } 17 | 18 | .select2-container .select2-selection--single .select2-selection__rendered { 19 | display: block; 20 | padding-left: 8px; 21 | padding-right: 20px; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | white-space: nowrap 25 | } 26 | 27 | .select2-container .select2-selection--single .select2-selection__clear { 28 | background-color: transparent; 29 | border: none; 30 | font-size: 1em 31 | } 32 | 33 | .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { 34 | padding-right: 8px; 35 | padding-left: 20px 36 | } 37 | 38 | .select2-container .select2-selection--multiple { 39 | box-sizing: border-box; 40 | cursor: pointer; 41 | display: block; 42 | min-height: 32px; 43 | user-select: none; 44 | -webkit-user-select: none 45 | } 46 | 47 | .select2-container .select2-selection--multiple .select2-selection__rendered { 48 | display: inline; 49 | list-style: none; 50 | padding: 0 51 | } 52 | 53 | .select2-container .select2-selection--multiple .select2-selection__clear { 54 | background-color: transparent; 55 | border: none; 56 | font-size: 1em 57 | } 58 | 59 | .select2-container .select2-search--inline .select2-search__field { 60 | box-sizing: border-box; 61 | border: none; 62 | font-size: 100%; 63 | margin-top: 5px; 64 | margin-left: 5px; 65 | padding: 0 66 | } 67 | 68 | .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { 69 | -webkit-appearance: none 70 | } 71 | 72 | .select2-dropdown { 73 | background-color: white; 74 | border: 1px solid #555; 75 | border-radius: 4px; 76 | box-sizing: border-box; 77 | display: block; 78 | position: absolute; 79 | left: -100000px; 80 | width: 100%; 81 | z-index: 1051 82 | } 83 | 84 | .select2-results { 85 | display: block 86 | } 87 | 88 | .select2-results__options { 89 | list-style: none; 90 | margin: 0; 91 | padding: 0 92 | } 93 | 94 | .select2-results__option { 95 | padding: 6px; 96 | user-select: none; 97 | -webkit-user-select: none 98 | } 99 | 100 | .select2-results__option--selectable { 101 | cursor: pointer 102 | } 103 | 104 | .select2-container--open .select2-dropdown { 105 | left: 0 106 | } 107 | 108 | .select2-container--open .select2-dropdown--above { 109 | border-bottom: none; 110 | border-bottom-left-radius: 0; 111 | border-bottom-right-radius: 0 112 | } 113 | 114 | .select2-container--open .select2-dropdown--below { 115 | border-top: none; 116 | border-top-left-radius: 0; 117 | border-top-right-radius: 0 118 | } 119 | 120 | .select2-search--dropdown { 121 | display: block; 122 | padding: 4px 123 | } 124 | 125 | .select2-search--dropdown .select2-search__field { 126 | padding: 4px; 127 | width: 100%; 128 | box-sizing: border-box 129 | } 130 | 131 | .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { 132 | -webkit-appearance: none 133 | } 134 | 135 | .select2-search--dropdown.select2-search--hide { 136 | display: none 137 | } 138 | 139 | .select2-close-mask { 140 | border: 0; 141 | margin: 0; 142 | padding: 0; 143 | display: block; 144 | position: fixed; 145 | left: 0; 146 | top: 0; 147 | min-height: 100%; 148 | min-width: 100%; 149 | height: auto; 150 | width: auto; 151 | opacity: 0; 152 | z-index: 99; 153 | background-color: #000; 154 | filter: alpha(opacity=0) 155 | } 156 | 157 | .select2-hidden-accessible { 158 | border: 0 !important; 159 | clip: rect(0 0 0 0) !important; 160 | -webkit-clip-path: inset(50%) !important; 161 | clip-path: inset(50%) !important; 162 | height: 1px !important; 163 | overflow: hidden !important; 164 | padding: 0 !important; 165 | position: absolute !important; 166 | width: 1px !important; 167 | white-space: nowrap !important 168 | } 169 | 170 | .select2-container--default .select2-selection--single { 171 | background-color: #000; 172 | border: 1px solid #555; 173 | border-radius: 4px 174 | } 175 | 176 | .select2-container--default .select2-selection--single .select2-selection__rendered { 177 | color: #BBB; 178 | line-height: 28px 179 | } 180 | 181 | .select2-container--default .select2-selection--single .select2-selection__clear { 182 | cursor: pointer; 183 | float: right; 184 | font-weight: bold; 185 | height: 26px; 186 | margin-right: 20px; 187 | padding-right: 0px 188 | } 189 | 190 | .select2-container--default .select2-selection--single .select2-selection__placeholder { 191 | color: #666 192 | } 193 | 194 | .select2-container--default .select2-selection--single .select2-selection__arrow { 195 | height: 26px; 196 | position: absolute; 197 | top: 1px; 198 | right: 1px; 199 | width: 20px 200 | } 201 | 202 | .select2-container--default .select2-selection--single .select2-selection__arrow b { 203 | border-color: #777 transparent transparent transparent; 204 | border-style: solid; 205 | border-width: 5px 4px 0 4px; 206 | height: 0; 207 | left: 50%; 208 | margin-left: -4px; 209 | margin-top: -2px; 210 | position: absolute; 211 | top: 50%; 212 | width: 0 213 | } 214 | 215 | .select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { 216 | float: left 217 | } 218 | 219 | .select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { 220 | left: 1px; 221 | right: auto 222 | } 223 | 224 | .select2-container--default.select2-container--disabled .select2-selection--single { 225 | background-color: #111; 226 | cursor: default 227 | } 228 | 229 | .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { 230 | display: none 231 | } 232 | 233 | .select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { 234 | border-color: transparent transparent #777 transparent; 235 | border-width: 0 4px 5px 4px 236 | } 237 | 238 | .select2-container--default .select2-selection--multiple { 239 | background-color: white; 240 | border: 1px solid #555; 241 | border-radius: 4px; 242 | cursor: text; 243 | padding-bottom: 5px; 244 | padding-right: 5px 245 | } 246 | 247 | .select2-container--default .select2-selection--multiple .select2-selection__clear { 248 | cursor: pointer; 249 | float: right; 250 | font-weight: bold; 251 | height: 20px; 252 | margin-right: 10px; 253 | margin-top: 5px; 254 | padding: 1px 255 | } 256 | 257 | .select2-container--default .select2-selection--multiple .select2-selection__choice { 258 | background-color: #1b1b1b; 259 | border: 1px solid #555; 260 | border-radius: 4px; 261 | display: inline-block; 262 | margin-left: 5px; 263 | margin-top: 5px; 264 | padding: 0 265 | } 266 | 267 | .select2-container--default .select2-selection--multiple .select2-selection__choice__display { 268 | cursor: default; 269 | padding-left: 2px; 270 | padding-right: 5px 271 | } 272 | 273 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { 274 | background-color: transparent; 275 | border: none; 276 | border-right: 1px solid #555; 277 | border-top-left-radius: 4px; 278 | border-bottom-left-radius: 4px; 279 | color: #666; 280 | cursor: pointer; 281 | font-size: 1em; 282 | font-weight: bold; 283 | padding: 0 4px 284 | } 285 | 286 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover, 287 | .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus { 288 | background-color: #0e0e0e; 289 | color: #CCC; 290 | outline: none 291 | } 292 | 293 | .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { 294 | margin-left: 5px; 295 | margin-right: auto 296 | } 297 | 298 | .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display { 299 | padding-left: 5px; 300 | padding-right: 2px 301 | } 302 | 303 | .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { 304 | border-left: 1px solid #555; 305 | border-right: none; 306 | border-top-left-radius: 0; 307 | border-bottom-left-radius: 0; 308 | border-top-right-radius: 4px; 309 | border-bottom-right-radius: 4px 310 | } 311 | 312 | .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear { 313 | float: left; 314 | margin-left: 10px; 315 | margin-right: auto 316 | } 317 | 318 | .select2-container--default.select2-container--focus .select2-selection--multiple { 319 | border: solid black 1px; 320 | outline: 0 321 | } 322 | 323 | .select2-container--default.select2-container--disabled .select2-selection--multiple { 324 | background-color: #111; 325 | cursor: default 326 | } 327 | 328 | .select2-container--default.select2-container--disabled .select2-selection__choice__remove { 329 | display: none 330 | } 331 | 332 | .select2-container--default.select2-container--open.select2-container--above .select2-selection--single, 333 | .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { 334 | border-top-left-radius: 0; 335 | border-top-right-radius: 0 336 | } 337 | 338 | .select2-container--default.select2-container--open.select2-container--below .select2-selection--single, 339 | .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { 340 | border-bottom-left-radius: 0; 341 | border-bottom-right-radius: 0 342 | } 343 | 344 | .select2-container--default .select2-search--dropdown .select2-search__field { 345 | border: 1px solid #555 346 | } 347 | 348 | .select2-container--default .select2-search--inline .select2-search__field { 349 | background: transparent; 350 | border: none; 351 | outline: 0; 352 | box-shadow: none; 353 | -webkit-appearance: textfield 354 | } 355 | 356 | .select2-container--default .select2-results>.select2-results__options { 357 | max-height: 200px; 358 | overflow-y: auto 359 | } 360 | 361 | .select2-container--default .select2-results__option .select2-results__option { 362 | padding-left: 1em 363 | } 364 | 365 | .select2-container--default .select2-results__option .select2-results__option .select2-results__group { 366 | padding-left: 0 367 | } 368 | 369 | .select2-container--default .select2-results__option .select2-results__option .select2-results__option { 370 | margin-left: -1em; 371 | padding-left: 2em 372 | } 373 | 374 | .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { 375 | margin-left: -2em; 376 | padding-left: 3em 377 | } 378 | 379 | .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { 380 | margin-left: -3em; 381 | padding-left: 4em 382 | } 383 | 384 | .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { 385 | margin-left: -4em; 386 | padding-left: 5em 387 | } 388 | 389 | .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { 390 | margin-left: -5em; 391 | padding-left: 6em 392 | } 393 | 394 | .select2-container--default .select2-results__option--group { 395 | padding: 0 396 | } 397 | 398 | .select2-container--default .select2-results__option--disabled { 399 | color: #666 400 | } 401 | 402 | .select2-container--default .select2-results__option--selected { 403 | background-color: #222 404 | } 405 | 406 | .select2-container--default .select2-results__option--highlighted.select2-results__option--selectable { 407 | background-color: #5897fb; 408 | color: white 409 | } 410 | 411 | .select2-container--default .select2-results__group { 412 | cursor: default; 413 | display: block; 414 | padding: 6px 415 | } 416 | 417 | .select2-container--classic .select2-selection--single { 418 | background-color: #080808; 419 | border: 1px solid #555; 420 | border-radius: 4px; 421 | outline: 0; 422 | background-image: -webkit-linear-gradient(top, #000 50%, #111 100%); 423 | background-image: -o-linear-gradient(top, #000 50%, #111 100%); 424 | background-image: linear-gradient(to bottom, #000 50%, #111 100%); 425 | background-repeat: repeat-x; 426 | filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0) 427 | } 428 | 429 | .select2-container--classic .select2-selection--single:focus { 430 | border: 1px solid #5897fb 431 | } 432 | 433 | .select2-container--classic .select2-selection--single .select2-selection__rendered { 434 | color: #BBB; 435 | line-height: 28px 436 | } 437 | 438 | .select2-container--classic .select2-selection--single .select2-selection__clear { 439 | cursor: pointer; 440 | float: right; 441 | font-weight: bold; 442 | height: 26px; 443 | margin-right: 20px 444 | } 445 | 446 | .select2-container--classic .select2-selection--single .select2-selection__placeholder { 447 | color: #666 448 | } 449 | 450 | .select2-container--classic .select2-selection--single .select2-selection__arrow { 451 | background-color: #222; 452 | border: none; 453 | border-left: 1px solid #555; 454 | border-top-right-radius: 4px; 455 | border-bottom-right-radius: 4px; 456 | height: 26px; 457 | position: absolute; 458 | top: 1px; 459 | right: 1px; 460 | width: 20px; 461 | background-image: -webkit-linear-gradient(top, #111 50%, #333 100%); 462 | background-image: -o-linear-gradient(top, #111 50%, #333 100%); 463 | background-image: linear-gradient(to bottom, #111 50%, #333 100%); 464 | background-repeat: repeat-x; 465 | filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0) 466 | } 467 | 468 | .select2-container--classic .select2-selection--single .select2-selection__arrow b { 469 | border-color: #777 transparent transparent transparent; 470 | border-style: solid; 471 | border-width: 5px 4px 0 4px; 472 | height: 0; 473 | left: 50%; 474 | margin-left: -4px; 475 | margin-top: -2px; 476 | position: absolute; 477 | top: 50%; 478 | width: 0 479 | } 480 | 481 | .select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { 482 | float: left 483 | } 484 | 485 | .select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { 486 | border: none; 487 | border-right: 1px solid #555; 488 | border-radius: 0; 489 | border-top-left-radius: 4px; 490 | border-bottom-left-radius: 4px; 491 | left: 1px; 492 | right: auto 493 | } 494 | 495 | .select2-container--classic.select2-container--open .select2-selection--single { 496 | border: 1px solid #5897fb 497 | } 498 | 499 | .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { 500 | background: transparent; 501 | border: none 502 | } 503 | 504 | .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { 505 | border-color: transparent transparent #777 transparent; 506 | border-width: 0 4px 5px 4px 507 | } 508 | 509 | .select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { 510 | border-top: none; 511 | border-top-left-radius: 0; 512 | border-top-right-radius: 0; 513 | background-image: -webkit-linear-gradient(top, #000 0%, #111 50%); 514 | background-image: -o-linear-gradient(top, #000 0%, #111 50%); 515 | background-image: linear-gradient(to bottom, #000 0%, #111 50%); 516 | background-repeat: repeat-x; 517 | filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0) 518 | } 519 | 520 | .select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { 521 | border-bottom: none; 522 | border-bottom-left-radius: 0; 523 | border-bottom-right-radius: 0; 524 | background-image: -webkit-linear-gradient(top, #111 50%, #000 100%); 525 | background-image: -o-linear-gradient(top, #111 50%, #000 100%); 526 | background-image: linear-gradient(to bottom, #111 50%, #000 100%); 527 | background-repeat: repeat-x; 528 | filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0) 529 | } 530 | 531 | .select2-container--classic .select2-selection--multiple { 532 | background-color: white; 533 | border: 1px solid #555; 534 | border-radius: 4px; 535 | cursor: text; 536 | outline: 0; 537 | padding-bottom: 5px; 538 | padding-right: 5px 539 | } 540 | 541 | .select2-container--classic .select2-selection--multiple:focus { 542 | border: 1px solid #5897fb 543 | } 544 | 545 | .select2-container--classic .select2-selection--multiple .select2-selection__clear { 546 | display: none 547 | } 548 | 549 | .select2-container--classic .select2-selection--multiple .select2-selection__choice { 550 | background-color: #1b1b1b; 551 | border: 1px solid #555; 552 | border-radius: 4px; 553 | display: inline-block; 554 | margin-left: 5px; 555 | margin-top: 5px; 556 | padding: 0 557 | } 558 | 559 | .select2-container--classic .select2-selection--multiple .select2-selection__choice__display { 560 | cursor: default; 561 | padding-left: 2px; 562 | padding-right: 5px 563 | } 564 | 565 | .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { 566 | background-color: transparent; 567 | border: none; 568 | border-top-left-radius: 4px; 569 | border-bottom-left-radius: 4px; 570 | color: #777; 571 | cursor: pointer; 572 | font-size: 1em; 573 | font-weight: bold; 574 | padding: 0 4px 575 | } 576 | 577 | .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { 578 | color: #AAA; 579 | outline: none 580 | } 581 | 582 | .select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { 583 | margin-left: 5px; 584 | margin-right: auto 585 | } 586 | 587 | .select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display { 588 | padding-left: 5px; 589 | padding-right: 2px 590 | } 591 | 592 | .select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { 593 | border-top-left-radius: 0; 594 | border-bottom-left-radius: 0; 595 | border-top-right-radius: 4px; 596 | border-bottom-right-radius: 4px 597 | } 598 | 599 | .select2-container--classic.select2-container--open .select2-selection--multiple { 600 | border: 1px solid #5897fb 601 | } 602 | 603 | .select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { 604 | border-top: none; 605 | border-top-left-radius: 0; 606 | border-top-right-radius: 0 607 | } 608 | 609 | .select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { 610 | border-bottom: none; 611 | border-bottom-left-radius: 0; 612 | border-bottom-right-radius: 0 613 | } 614 | 615 | .select2-container--classic .select2-search--dropdown .select2-search__field { 616 | border: 1px solid #555; 617 | outline: 0 618 | } 619 | 620 | .select2-container--classic .select2-search--inline .select2-search__field { 621 | outline: 0; 622 | box-shadow: none 623 | } 624 | 625 | .select2-container--classic .select2-dropdown { 626 | background-color: #000; 627 | border: 1px solid transparent 628 | } 629 | 630 | .select2-container--classic .select2-dropdown--above { 631 | border-bottom: none 632 | } 633 | 634 | .select2-container--classic .select2-dropdown--below { 635 | border-top: none 636 | } 637 | 638 | .select2-container--classic .select2-results>.select2-results__options { 639 | max-height: 200px; 640 | overflow-y: auto 641 | } 642 | 643 | .select2-container--classic .select2-results__option--group { 644 | padding: 0 645 | } 646 | 647 | .select2-container--classic .select2-results__option--disabled { 648 | color: grey 649 | } 650 | 651 | .select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable { 652 | background-color: #3875d7; 653 | color: #000 654 | } 655 | 656 | .select2-container--classic .select2-results__group { 657 | cursor: default; 658 | display: block; 659 | padding: 6px 660 | } 661 | 662 | .select2-container--classic.select2-container--open .select2-dropdown { 663 | border-color: #5897fb 664 | } 665 | 666 | .select2-results { 667 | background-color: #333 668 | } -------------------------------------------------------------------------------- /app/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@electron/remote': 12 | specifier: ^2.1.2 13 | version: 2.1.3(electron@32.3.3) 14 | bluebird: 15 | specifier: ^3.7.2 16 | version: 3.7.2 17 | electron-positioner: 18 | specifier: ^4.1.0 19 | version: 4.1.0 20 | jquery: 21 | specifier: ^3.5.1 22 | version: 3.7.1 23 | menubar: 24 | specifier: ^9.0.2 25 | version: 9.5.1(electron@32.3.3) 26 | ws: 27 | specifier: ^8.3.0 28 | version: 8.18.3 29 | 30 | packages: 31 | 32 | '@electron/get@2.0.3': 33 | resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} 34 | engines: {node: '>=12'} 35 | 36 | '@electron/remote@2.1.3': 37 | resolution: {integrity: sha512-XlpxC8S4ttj/v2d+PKp9na/3Ev8bV7YWNL7Cw5b9MAWgTphEml7iYgbc7V0r9D6yDOfOkj06bchZgOZdlWJGNA==} 38 | peerDependencies: 39 | electron: '>= 13.0.0' 40 | 41 | '@sindresorhus/is@4.6.0': 42 | resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} 43 | engines: {node: '>=10'} 44 | 45 | '@szmarczak/http-timer@4.0.6': 46 | resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} 47 | engines: {node: '>=10'} 48 | 49 | '@types/cacheable-request@6.0.3': 50 | resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} 51 | 52 | '@types/http-cache-semantics@4.0.4': 53 | resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} 54 | 55 | '@types/keyv@3.1.4': 56 | resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} 57 | 58 | '@types/node@20.19.7': 59 | resolution: {integrity: sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==} 60 | 61 | '@types/responselike@1.0.3': 62 | resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} 63 | 64 | '@types/yauzl@2.10.3': 65 | resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} 66 | 67 | bluebird@3.7.2: 68 | resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} 69 | 70 | boolean@3.2.0: 71 | resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} 72 | deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. 73 | 74 | buffer-crc32@0.2.13: 75 | resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} 76 | 77 | cacheable-lookup@5.0.4: 78 | resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} 79 | engines: {node: '>=10.6.0'} 80 | 81 | cacheable-request@7.0.4: 82 | resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} 83 | engines: {node: '>=8'} 84 | 85 | clone-response@1.0.3: 86 | resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} 87 | 88 | debug@4.4.1: 89 | resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 90 | engines: {node: '>=6.0'} 91 | peerDependencies: 92 | supports-color: '*' 93 | peerDependenciesMeta: 94 | supports-color: 95 | optional: true 96 | 97 | decompress-response@6.0.0: 98 | resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} 99 | engines: {node: '>=10'} 100 | 101 | defer-to-connect@2.0.1: 102 | resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} 103 | engines: {node: '>=10'} 104 | 105 | define-data-property@1.1.4: 106 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 107 | engines: {node: '>= 0.4'} 108 | 109 | define-properties@1.2.1: 110 | resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} 111 | engines: {node: '>= 0.4'} 112 | 113 | detect-node@2.1.0: 114 | resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} 115 | 116 | electron-positioner@4.1.0: 117 | resolution: {integrity: sha512-726DfbI9ZNoCg+Fcu6XLuTKTnzf+6nFqv7h+K/V6Ug7IbaPMI7s9S8URnGtWFCy5N5PL4HSzRFF2mXuinftDdg==} 118 | 119 | electron@32.3.3: 120 | resolution: {integrity: sha512-7FT8tDg+MueAw8dBn5LJqDvlM4cZkKJhXfgB3w7P5gvSoUQVAY6LIQcXJxgL+vw2rIRY/b9ak7ZBFbCMF2Bk4w==} 121 | engines: {node: '>= 12.20.55'} 122 | hasBin: true 123 | 124 | end-of-stream@1.4.5: 125 | resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} 126 | 127 | env-paths@2.2.1: 128 | resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} 129 | engines: {node: '>=6'} 130 | 131 | es-define-property@1.0.1: 132 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 133 | engines: {node: '>= 0.4'} 134 | 135 | es-errors@1.3.0: 136 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 137 | engines: {node: '>= 0.4'} 138 | 139 | es6-error@4.1.1: 140 | resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} 141 | 142 | escape-string-regexp@4.0.0: 143 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 144 | engines: {node: '>=10'} 145 | 146 | extract-zip@2.0.1: 147 | resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} 148 | engines: {node: '>= 10.17.0'} 149 | hasBin: true 150 | 151 | fd-slicer@1.1.0: 152 | resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} 153 | 154 | fs-extra@8.1.0: 155 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 156 | engines: {node: '>=6 <7 || >=8'} 157 | 158 | get-stream@5.2.0: 159 | resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} 160 | engines: {node: '>=8'} 161 | 162 | global-agent@3.0.0: 163 | resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} 164 | engines: {node: '>=10.0'} 165 | 166 | globalthis@1.0.4: 167 | resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} 168 | engines: {node: '>= 0.4'} 169 | 170 | gopd@1.2.0: 171 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 172 | engines: {node: '>= 0.4'} 173 | 174 | got@11.8.6: 175 | resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} 176 | engines: {node: '>=10.19.0'} 177 | 178 | graceful-fs@4.2.11: 179 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 180 | 181 | has-property-descriptors@1.0.2: 182 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 183 | 184 | http-cache-semantics@4.2.0: 185 | resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} 186 | 187 | http2-wrapper@1.0.3: 188 | resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} 189 | engines: {node: '>=10.19.0'} 190 | 191 | jquery@3.7.1: 192 | resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} 193 | 194 | json-buffer@3.0.1: 195 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 196 | 197 | json-stringify-safe@5.0.1: 198 | resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} 199 | 200 | jsonfile@4.0.0: 201 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 202 | 203 | keyv@4.5.4: 204 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 205 | 206 | lowercase-keys@2.0.0: 207 | resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} 208 | engines: {node: '>=8'} 209 | 210 | matcher@3.0.0: 211 | resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} 212 | engines: {node: '>=10'} 213 | 214 | menubar@9.5.1: 215 | resolution: {integrity: sha512-swfgKal+DTgJINay36X+LGBSqyFKS4d9FyJ2w0s/4MtO7/UGplEZqluLTnq4xgLNxNjMWhXycOELP+rRYpTagA==} 216 | peerDependencies: 217 | electron: '>=9.0.0 <33.0.0' 218 | 219 | mimic-response@1.0.1: 220 | resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} 221 | engines: {node: '>=4'} 222 | 223 | mimic-response@3.1.0: 224 | resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} 225 | engines: {node: '>=10'} 226 | 227 | ms@2.1.3: 228 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 229 | 230 | normalize-url@6.1.0: 231 | resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} 232 | engines: {node: '>=10'} 233 | 234 | object-keys@1.1.1: 235 | resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} 236 | engines: {node: '>= 0.4'} 237 | 238 | once@1.4.0: 239 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 240 | 241 | p-cancelable@2.1.1: 242 | resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} 243 | engines: {node: '>=8'} 244 | 245 | pend@1.2.0: 246 | resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 247 | 248 | progress@2.0.3: 249 | resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} 250 | engines: {node: '>=0.4.0'} 251 | 252 | pump@3.0.3: 253 | resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} 254 | 255 | quick-lru@5.1.1: 256 | resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} 257 | engines: {node: '>=10'} 258 | 259 | resolve-alpn@1.2.1: 260 | resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} 261 | 262 | responselike@2.0.1: 263 | resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} 264 | 265 | roarr@2.15.4: 266 | resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} 267 | engines: {node: '>=8.0'} 268 | 269 | semver-compare@1.0.0: 270 | resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} 271 | 272 | semver@6.3.1: 273 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 274 | hasBin: true 275 | 276 | semver@7.7.2: 277 | resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} 278 | engines: {node: '>=10'} 279 | hasBin: true 280 | 281 | serialize-error@7.0.1: 282 | resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} 283 | engines: {node: '>=10'} 284 | 285 | sprintf-js@1.1.3: 286 | resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} 287 | 288 | sumchecker@3.0.1: 289 | resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} 290 | engines: {node: '>= 8.0'} 291 | 292 | type-fest@0.13.1: 293 | resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} 294 | engines: {node: '>=10'} 295 | 296 | undici-types@6.21.0: 297 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 298 | 299 | universalify@0.1.2: 300 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 301 | engines: {node: '>= 4.0.0'} 302 | 303 | wrappy@1.0.2: 304 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 305 | 306 | ws@8.18.3: 307 | resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} 308 | engines: {node: '>=10.0.0'} 309 | peerDependencies: 310 | bufferutil: ^4.0.1 311 | utf-8-validate: '>=5.0.2' 312 | peerDependenciesMeta: 313 | bufferutil: 314 | optional: true 315 | utf-8-validate: 316 | optional: true 317 | 318 | yauzl@2.10.0: 319 | resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 320 | 321 | snapshots: 322 | 323 | '@electron/get@2.0.3': 324 | dependencies: 325 | debug: 4.4.1 326 | env-paths: 2.2.1 327 | fs-extra: 8.1.0 328 | got: 11.8.6 329 | progress: 2.0.3 330 | semver: 6.3.1 331 | sumchecker: 3.0.1 332 | optionalDependencies: 333 | global-agent: 3.0.0 334 | transitivePeerDependencies: 335 | - supports-color 336 | 337 | '@electron/remote@2.1.3(electron@32.3.3)': 338 | dependencies: 339 | electron: 32.3.3 340 | 341 | '@sindresorhus/is@4.6.0': {} 342 | 343 | '@szmarczak/http-timer@4.0.6': 344 | dependencies: 345 | defer-to-connect: 2.0.1 346 | 347 | '@types/cacheable-request@6.0.3': 348 | dependencies: 349 | '@types/http-cache-semantics': 4.0.4 350 | '@types/keyv': 3.1.4 351 | '@types/node': 20.19.7 352 | '@types/responselike': 1.0.3 353 | 354 | '@types/http-cache-semantics@4.0.4': {} 355 | 356 | '@types/keyv@3.1.4': 357 | dependencies: 358 | '@types/node': 20.19.7 359 | 360 | '@types/node@20.19.7': 361 | dependencies: 362 | undici-types: 6.21.0 363 | 364 | '@types/responselike@1.0.3': 365 | dependencies: 366 | '@types/node': 20.19.7 367 | 368 | '@types/yauzl@2.10.3': 369 | dependencies: 370 | '@types/node': 20.19.7 371 | optional: true 372 | 373 | bluebird@3.7.2: {} 374 | 375 | boolean@3.2.0: 376 | optional: true 377 | 378 | buffer-crc32@0.2.13: {} 379 | 380 | cacheable-lookup@5.0.4: {} 381 | 382 | cacheable-request@7.0.4: 383 | dependencies: 384 | clone-response: 1.0.3 385 | get-stream: 5.2.0 386 | http-cache-semantics: 4.2.0 387 | keyv: 4.5.4 388 | lowercase-keys: 2.0.0 389 | normalize-url: 6.1.0 390 | responselike: 2.0.1 391 | 392 | clone-response@1.0.3: 393 | dependencies: 394 | mimic-response: 1.0.1 395 | 396 | debug@4.4.1: 397 | dependencies: 398 | ms: 2.1.3 399 | 400 | decompress-response@6.0.0: 401 | dependencies: 402 | mimic-response: 3.1.0 403 | 404 | defer-to-connect@2.0.1: {} 405 | 406 | define-data-property@1.1.4: 407 | dependencies: 408 | es-define-property: 1.0.1 409 | es-errors: 1.3.0 410 | gopd: 1.2.0 411 | optional: true 412 | 413 | define-properties@1.2.1: 414 | dependencies: 415 | define-data-property: 1.1.4 416 | has-property-descriptors: 1.0.2 417 | object-keys: 1.1.1 418 | optional: true 419 | 420 | detect-node@2.1.0: 421 | optional: true 422 | 423 | electron-positioner@4.1.0: {} 424 | 425 | electron@32.3.3: 426 | dependencies: 427 | '@electron/get': 2.0.3 428 | '@types/node': 20.19.7 429 | extract-zip: 2.0.1 430 | transitivePeerDependencies: 431 | - supports-color 432 | 433 | end-of-stream@1.4.5: 434 | dependencies: 435 | once: 1.4.0 436 | 437 | env-paths@2.2.1: {} 438 | 439 | es-define-property@1.0.1: 440 | optional: true 441 | 442 | es-errors@1.3.0: 443 | optional: true 444 | 445 | es6-error@4.1.1: 446 | optional: true 447 | 448 | escape-string-regexp@4.0.0: 449 | optional: true 450 | 451 | extract-zip@2.0.1: 452 | dependencies: 453 | debug: 4.4.1 454 | get-stream: 5.2.0 455 | yauzl: 2.10.0 456 | optionalDependencies: 457 | '@types/yauzl': 2.10.3 458 | transitivePeerDependencies: 459 | - supports-color 460 | 461 | fd-slicer@1.1.0: 462 | dependencies: 463 | pend: 1.2.0 464 | 465 | fs-extra@8.1.0: 466 | dependencies: 467 | graceful-fs: 4.2.11 468 | jsonfile: 4.0.0 469 | universalify: 0.1.2 470 | 471 | get-stream@5.2.0: 472 | dependencies: 473 | pump: 3.0.3 474 | 475 | global-agent@3.0.0: 476 | dependencies: 477 | boolean: 3.2.0 478 | es6-error: 4.1.1 479 | matcher: 3.0.0 480 | roarr: 2.15.4 481 | semver: 7.7.2 482 | serialize-error: 7.0.1 483 | optional: true 484 | 485 | globalthis@1.0.4: 486 | dependencies: 487 | define-properties: 1.2.1 488 | gopd: 1.2.0 489 | optional: true 490 | 491 | gopd@1.2.0: 492 | optional: true 493 | 494 | got@11.8.6: 495 | dependencies: 496 | '@sindresorhus/is': 4.6.0 497 | '@szmarczak/http-timer': 4.0.6 498 | '@types/cacheable-request': 6.0.3 499 | '@types/responselike': 1.0.3 500 | cacheable-lookup: 5.0.4 501 | cacheable-request: 7.0.4 502 | decompress-response: 6.0.0 503 | http2-wrapper: 1.0.3 504 | lowercase-keys: 2.0.0 505 | p-cancelable: 2.1.1 506 | responselike: 2.0.1 507 | 508 | graceful-fs@4.2.11: {} 509 | 510 | has-property-descriptors@1.0.2: 511 | dependencies: 512 | es-define-property: 1.0.1 513 | optional: true 514 | 515 | http-cache-semantics@4.2.0: {} 516 | 517 | http2-wrapper@1.0.3: 518 | dependencies: 519 | quick-lru: 5.1.1 520 | resolve-alpn: 1.2.1 521 | 522 | jquery@3.7.1: {} 523 | 524 | json-buffer@3.0.1: {} 525 | 526 | json-stringify-safe@5.0.1: 527 | optional: true 528 | 529 | jsonfile@4.0.0: 530 | optionalDependencies: 531 | graceful-fs: 4.2.11 532 | 533 | keyv@4.5.4: 534 | dependencies: 535 | json-buffer: 3.0.1 536 | 537 | lowercase-keys@2.0.0: {} 538 | 539 | matcher@3.0.0: 540 | dependencies: 541 | escape-string-regexp: 4.0.0 542 | optional: true 543 | 544 | menubar@9.5.1(electron@32.3.3): 545 | dependencies: 546 | electron: 32.3.3 547 | electron-positioner: 4.1.0 548 | 549 | mimic-response@1.0.1: {} 550 | 551 | mimic-response@3.1.0: {} 552 | 553 | ms@2.1.3: {} 554 | 555 | normalize-url@6.1.0: {} 556 | 557 | object-keys@1.1.1: 558 | optional: true 559 | 560 | once@1.4.0: 561 | dependencies: 562 | wrappy: 1.0.2 563 | 564 | p-cancelable@2.1.1: {} 565 | 566 | pend@1.2.0: {} 567 | 568 | progress@2.0.3: {} 569 | 570 | pump@3.0.3: 571 | dependencies: 572 | end-of-stream: 1.4.5 573 | once: 1.4.0 574 | 575 | quick-lru@5.1.1: {} 576 | 577 | resolve-alpn@1.2.1: {} 578 | 579 | responselike@2.0.1: 580 | dependencies: 581 | lowercase-keys: 2.0.0 582 | 583 | roarr@2.15.4: 584 | dependencies: 585 | boolean: 3.2.0 586 | detect-node: 2.1.0 587 | globalthis: 1.0.4 588 | json-stringify-safe: 5.0.1 589 | semver-compare: 1.0.0 590 | sprintf-js: 1.1.3 591 | optional: true 592 | 593 | semver-compare@1.0.0: 594 | optional: true 595 | 596 | semver@6.3.1: {} 597 | 598 | semver@7.7.2: 599 | optional: true 600 | 601 | serialize-error@7.0.1: 602 | dependencies: 603 | type-fest: 0.13.1 604 | optional: true 605 | 606 | sprintf-js@1.1.3: 607 | optional: true 608 | 609 | sumchecker@3.0.1: 610 | dependencies: 611 | debug: 4.4.1 612 | transitivePeerDependencies: 613 | - supports-color 614 | 615 | type-fest@0.13.1: 616 | optional: true 617 | 618 | undici-types@6.21.0: {} 619 | 620 | universalify@0.1.2: {} 621 | 622 | wrappy@1.0.2: {} 623 | 624 | ws@8.18.3: {} 625 | 626 | yauzl@2.10.0: 627 | dependencies: 628 | buffer-crc32: 0.2.13 629 | fd-slicer: 1.1.0 630 | --------------------------------------------------------------------------------