├── release └── .gitignore ├── .gitignore ├── com.dflydev.streamdeck.restreamio.sdPlugin ├── public │ ├── dist │ │ └── .gitignore │ ├── icons │ │ └── caret.svg │ ├── plugin.html │ ├── property-inspector.html │ └── sdpi.css ├── icons │ ├── action.png │ ├── action@2x.png │ ├── actionimage.png │ ├── channel-off.png │ ├── channel-on.png │ ├── plugin-icon.png │ ├── category-icon.png │ ├── channel-on@2x.png │ ├── actionimage@2x.png │ ├── category-icon@2x.png │ ├── channel-off@2x.png │ ├── channel-unknown.png │ ├── plugin-icon@2x.png │ ├── channel-unknown@2x.png │ ├── channel-action-image.png │ └── channel-action-image@2x.png └── manifest.json ├── .github ├── FUNDING.yml └── workflows │ └── pre-release.yml ├── tsconfig.eslint.json ├── .eslintignore ├── .prettierignore ├── .env.example ├── .prettierrc.json ├── .editorconfig ├── src ├── settings │ ├── global-settings.ts │ ├── settings.ts │ ├── managed-settings.ts │ ├── managed-global-settings.ts │ └── deferred-global-settings-requests.ts ├── actions │ ├── action.ts │ └── channel-action.ts ├── restreamio-plugin.ts ├── restreamio │ ├── types.ts │ └── restreamio-client.ts └── restreamio-property-inspector.ts ├── tsconfig.json ├── LICENSE.txt ├── package.json ├── .eslintrc.json └── README.md /release/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .env 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/public/dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [simensen, dflydev] 2 | open_collective: dflydev 3 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /com.dflydev.streamdeck.restreamio.sdPlugin/public/dist/ 3 | /node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /com.dflydev.streamdeck.restreamio.sdPlugin/public/dist/ 3 | /node_modules/ 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | STREAMDECK_RESTREAMIO_LOGIN_URL=https://example.test/login 2 | STREAMDECK_RESTREAMIO_REFRESH_URL=https://example.test/refresh 3 | -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/action.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/action@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/action@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/actionimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/actionimage.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-off.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-on.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/plugin-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/plugin-icon.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/category-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/category-icon.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-on@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-on@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/actionimage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/actionimage@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/category-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/category-icon@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-off@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-off@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-unknown.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/plugin-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/plugin-icon@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-unknown@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-unknown@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-action-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-action-image.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-action-image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dflydev/streamdeck-restreamio/main/com.dflydev.streamdeck.restreamio.sdPlugin/icons/channel-action-image@2x.png -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/public/icons/caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } 12 | -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/public/plugin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.dflydev.streamdeck.restreamio 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | 17 | [*.{js,ts,json}] 18 | indent_size = 2 19 | 20 | [*.vue] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /src/settings/global-settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RestreamioAccountCollection, 3 | RestreamioStreamingPlatform, 4 | } from '../restreamio/types' 5 | 6 | export function createEmptyGlobalSettings(): GlobalSettings { 7 | return { 8 | restreamioAccounts: {}, 9 | restreamioStreamingPlatforms: [], 10 | } 11 | } 12 | 13 | export interface GlobalSettings { 14 | restreamioAccounts: RestreamioAccountCollection 15 | restreamioStreamingPlatforms: RestreamioStreamingPlatform[] 16 | } 17 | -------------------------------------------------------------------------------- /src/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import {RestreamioAccountId, RestreamioChannelId} from '../restreamio/types' 2 | 3 | export function createEmptySettings(): Settings { 4 | return new (class implements Settings { 5 | restreamioAccountId: null 6 | restreamioChannelId: null 7 | name: null 8 | platform: null 9 | })() 10 | } 11 | 12 | export interface Settings { 13 | restreamioAccountId: RestreamioAccountId | null 14 | restreamioChannelId: RestreamioChannelId | null 15 | name: string | null 16 | platform: string | null 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES6", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "declaration": true, 9 | "sourceMap": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "strictPropertyInitialization": false, 12 | "skipLibCheck": true, 13 | "outDir": "./build", 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "lib": [ 17 | "dom", 18 | "ESNext" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "**/*.spec.ts" 27 | ], 28 | "extends": "@tsconfig/recommended/tsconfig.json" 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/action.ts: -------------------------------------------------------------------------------- 1 | import {RestreamioPlugin} from '../restreamio-plugin' 2 | import {ManagedGlobalSettings} from '../settings/managed-global-settings' 3 | import {ManagedSettings} from '../settings/managed-settings' 4 | import {RestreamioClient} from '../restreamio/restreamio-client' 5 | import {StreamDeckAction} from 'streamdeck-typescript' 6 | 7 | export abstract class Action extends StreamDeckAction< 8 | RestreamioPlugin, 9 | TheAction 10 | > { 11 | protected restreamioClient(context?: string): RestreamioClient { 12 | return RestreamioClient.fromSettingsManager( 13 | this.plugin.settingsManager, 14 | context, 15 | ) 16 | } 17 | 18 | protected get globalSettings(): ManagedGlobalSettings { 19 | return new ManagedGlobalSettings(this.plugin.settingsManager) 20 | } 21 | 22 | protected settings(context: string): ManagedSettings { 23 | return new ManagedSettings(this.plugin.settingsManager, context) 24 | } 25 | 26 | constructor(protected plugin: RestreamioPlugin, actionName: string) { 27 | super(plugin, actionName) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 dflydev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Actions": [ 3 | { 4 | "Icon": "icons/channel-action-image", 5 | "Name": "Channel", 6 | "States": [ 7 | { 8 | "Name": "On", 9 | "Image": "icons/channel-on", 10 | "TitleAlignment": "bottom", 11 | "FontSize": "11" 12 | }, 13 | { 14 | "Name": "Off", 15 | "Image": "icons/channel-off", 16 | "TitleAlignment": "bottom", 17 | "FontSize": "11" 18 | } 19 | ], 20 | "Tooltip": "Toggle channel off/on", 21 | "UUID": "com.dflydev.streamdeck.restreamio.channel" 22 | } 23 | ], 24 | "SDKVersion": 2, 25 | "Author": "dflydev", 26 | "Category": "Restream.io", 27 | "CategoryIcon": "icons/category-icon", 28 | "CodePath": "public/plugin.html", 29 | "PropertyInspectorPath": "public/property-inspector.html", 30 | "Description": "Control and view stats from Restream.io", 31 | "Name": "Restream.io plugin", 32 | "Icon": "icons/category-icon", 33 | "URL": "https://www.elgato.com/en/gaming/stream-deck", 34 | "Version": "1.0.0", 35 | "OS": [ 36 | { 37 | "Platform": "mac", 38 | "MinimumVersion" : "10.11" 39 | }, 40 | { 41 | "Platform": "windows", 42 | "MinimumVersion" : "10" 43 | } 44 | ], 45 | "Software": 46 | { 47 | "MinimumVersion" : "4.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/public/property-inspector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.dflydev.streamdeck.restreamio.pi 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
Account
13 | 14 |
15 |
16 | 20 | 24 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dflydev/streamdeck-restreamio", 3 | "version": "0.0.0", 4 | "description": "Restream.io plugin for Stream Deck", 5 | "repository": "https://github.com/dflydev/streamdeck-restreamio", 6 | "scripts": { 7 | "browserify": "browserify -t @sethvincent/dotenvify build/restreamio-plugin.js > com.dflydev.streamdeck.restreamio.sdPlugin/public/dist/restreamio-plugin-bundle.js && browserify -t @sethvincent/dotenvify build/restreamio-property-inspector.js > com.dflydev.streamdeck.restreamio.sdPlugin/public/dist/restreamio-property-inspector-bundle.js", 8 | "build": "npm run tsc && npm run browserify", 9 | "lint": "eslint src/**/*.ts", 10 | "fix": "eslint --fix src/**/*.ts", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "tsc": "tsc -p tsconfig.json", 13 | "watch": "tsc-watch --onSuccess \"npm run browserify\"" 14 | }, 15 | "author": "Beau Simensen (https://beausimensen.com)", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@sethvincent/dotenvify": "^1.0.4", 19 | "@tsconfig/recommended": "^1.0.1", 20 | "@types/node": "^14.14.31", 21 | "@typescript-eslint/eslint-plugin": "^4.15.2", 22 | "@typescript-eslint/parser": "^4.15.2", 23 | "axios": "^0.21.1", 24 | "axios-auth-refresh": "^3.1.0", 25 | "browserify": "^17.0.0", 26 | "dotenv": "^8.2.0", 27 | "eslint": "^7.21.0", 28 | "eslint-plugin-github": "^4.1.1", 29 | "prettier": "^2.2.1", 30 | "retry-axios": "^2.4.0", 31 | "streamdeck-typescript": "^3.0.0", 32 | "tsc-watch": "^4.2.9", 33 | "typescript": "^4.2.2", 34 | "watchify": "^4.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/restreamio-plugin.ts: -------------------------------------------------------------------------------- 1 | import {SDOnActionEvent, StreamDeckPluginHandler} from 'streamdeck-typescript' 2 | import {ChannelAction} from './actions/channel-action' 3 | import {RestreamioClient} from './restreamio/restreamio-client' 4 | import {ManagedGlobalSettings} from './settings/managed-global-settings' 5 | import { 6 | DeferredFunction, 7 | DeferredGlobalSettingsRequests, 8 | } from './settings/deferred-global-settings-requests' 9 | 10 | export class RestreamioPlugin extends StreamDeckPluginHandler { 11 | protected deferredGlobalSettingsRequests: DeferredGlobalSettingsRequests 12 | 13 | constructor() { 14 | super() 15 | this.deferredGlobalSettingsRequests = new DeferredGlobalSettingsRequests( 16 | this.settingsManager, 17 | ) 18 | new ChannelAction(this, 'com.dflydev.streamdeck.restreamio.channel') 19 | } 20 | 21 | get managedGlobalSettings(): ManagedGlobalSettings { 22 | return new ManagedGlobalSettings(this.settingsManager) 23 | } 24 | 25 | @SDOnActionEvent('setupReady') 26 | private async onSetupReady(): Promise { 27 | const restreamioStreamingPlatforms = await RestreamioClient.fromSettingsManager( 28 | this.settingsManager, 29 | ).getStreamingPlatforms() 30 | 31 | this.managedGlobalSettings.setRestreamioStreamingPlatforms( 32 | restreamioStreamingPlatforms, 33 | ) 34 | 35 | this.deferredGlobalSettingsRequests.handle() 36 | } 37 | 38 | withGlobalSettings(context: string, fnc: DeferredFunction): void { 39 | this.deferredGlobalSettingsRequests.deferOrNow( 40 | this.globalSettingsReady, 41 | fnc, 42 | context, 43 | ) 44 | } 45 | } 46 | 47 | new RestreamioPlugin() 48 | -------------------------------------------------------------------------------- /src/restreamio/types.ts: -------------------------------------------------------------------------------- 1 | export interface RestreamioAccountCollection { 2 | [key: string]: RestreamioAccount 3 | } 4 | 5 | export interface RestreamioAccount { 6 | profile: RestreamioProfile 7 | token: RestreamioToken 8 | } 9 | 10 | export interface RestreamioProfile { 11 | id: RestreamioAccountId 12 | email: RestreamioEmail 13 | username: RestreamioUsername 14 | } 15 | 16 | export type RestreamioRefreshedTokenCallback = ( 17 | restreamioToken: RestreamioToken, 18 | ) => void 19 | export type RestreamioChannelId = number 20 | 21 | export type RestreamioAccountId = number 22 | export type RestreamioEmail = string 23 | export type RestreamioUsername = string 24 | 25 | export type RestreamioStreamingPlatformId = number 26 | 27 | export type RestreamioStreamingPlatformImageUrl = string 28 | 29 | export interface RestreamioStreamingPlatformImage { 30 | svg: RestreamioStreamingPlatformImageUrl 31 | png: RestreamioStreamingPlatformImageUrl 32 | } 33 | 34 | export interface RestreamioStreamingPlatform { 35 | id: RestreamioStreamingPlatformId 36 | name: string 37 | url: string 38 | image: RestreamioStreamingPlatformImage 39 | altImage: RestreamioStreamingPlatformImage 40 | } 41 | 42 | export interface RestreamioToken { 43 | tokenType: RestreamioTokenType 44 | tokenScope: RestreamioTokenScope 45 | accessToken: RestreamioAccessToken 46 | accessTokenEpochExpirationTime: RestreamioAccessTokenEpochExpirationTime 47 | refreshToken: RestreamioRefreshToken 48 | refreshTokenEpochExpirationTime: RestreamioRefreshTokenEpochExpirationTime 49 | } 50 | 51 | export type RestreamioEpochExpirationTime = bigint 52 | 53 | export type RestreamioTokenType = string 54 | export type RestreamioTokenScope = string 55 | 56 | export type RestreamioAccessToken = string 57 | export type RestreamioAccessTokenEpochExpirationTime = RestreamioEpochExpirationTime 58 | 59 | export type RestreamioRefreshToken = string 60 | export type RestreamioRefreshTokenEpochExpirationTime = RestreamioEpochExpirationTime 61 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: "pre-release" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | pre-release: 10 | name: "Pre Release" 11 | runs-on: "macos-latest" 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v2 19 | 20 | - name: Get npm cache directory 21 | id: npm_cache 22 | run: | 23 | echo "::set-output name=dir::$(npm config get cache)" 24 | 25 | - name: Cache npm cache 26 | uses: actions/cache@v1 27 | with: 28 | path: ${{ steps.npm_cache.outputs.dir }} 29 | key: ${{ runner.os }}-npm-cache-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-npm-cache- 32 | 33 | - name: Cache npm dependencies 34 | uses: actions/cache@v1 35 | with: 36 | path: node_modules 37 | key: ${{ runner.os }}-npm-node-modules-${{ hashFiles('**/package-lock.json') }} 38 | restore-keys: | 39 | ${{ runner.os }}-npm-node-modules- 40 | 41 | - run: npm install 42 | 43 | - run: npm run lint 44 | 45 | - name: Prepare the environment 46 | run: cp .env.example .env 47 | 48 | - run: npm run build 49 | env: 50 | STREAMDECK_RESTREAMIO_LOGIN_URL: ${{ secrets.STREAMDECK_RESTREAMIO_LOGIN_URL }} 51 | STREAMDECK_RESTREAMIO_REFRESH_URL: ${{ secrets.STREAMDECK_RESTREAMIO_REFRESH_URL }} 52 | 53 | - name: Copy additional assets 54 | run: | 55 | cp LICENSE.txt README.md com.dflydev.streamdeck.restreamio.sdPlugin 56 | 57 | - name: StreamDeck Distribution Tool 58 | uses: AdamCarballo/streamdeck-distribution-tool@v1 59 | with: 60 | input: com.dflydev.streamdeck.restreamio.sdPlugin 61 | output: release 62 | 63 | - uses: "marvinpinto/action-automatic-releases@latest" 64 | with: 65 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 66 | automatic_release_tag: "latest" 67 | prerelease: true 68 | title: "Latest Build" 69 | files: | 70 | release/com.dflydev.streamdeck.restreamio.streamDeckPlugin 71 | -------------------------------------------------------------------------------- /src/settings/managed-settings.ts: -------------------------------------------------------------------------------- 1 | import {RestreamioAccountId, RestreamioChannelId} from '../restreamio/types' 2 | import {SettingsManager} from 'streamdeck-typescript' 3 | import {createEmptySettings, Settings} from './settings' 4 | 5 | export class ManagedSettings implements Settings { 6 | private context: string 7 | private settingsManager: SettingsManager 8 | 9 | private get rawSettings(): Settings { 10 | const rawSettings = this.settingsManager.getContextSettings( 11 | this.context, 12 | ) 13 | 14 | if (!rawSettings) { 15 | return createEmptySettings() 16 | } 17 | 18 | return rawSettings 19 | } 20 | 21 | get restreamioAccountId(): RestreamioAccountId | null { 22 | return this.rawSettings.restreamioAccountId 23 | } 24 | 25 | get restreamioChannelId(): RestreamioChannelId | null { 26 | return this.rawSettings.restreamioChannelId 27 | } 28 | 29 | get name(): string | null { 30 | return this.rawSettings.name 31 | } 32 | 33 | get platform(): string | null { 34 | return this.rawSettings.platform 35 | } 36 | 37 | constructor(settingsManager: SettingsManager, context: string) { 38 | this.settingsManager = settingsManager 39 | this.context = context 40 | } 41 | 42 | assignRestreamioAccountId(restreamioAccountId: RestreamioAccountId): void { 43 | this.settingsManager.setContextSettingsAttributes(this.context, { 44 | restreamioAccountId, 45 | }) 46 | } 47 | 48 | assignRestreamioChannelId( 49 | restreamioChannelId: RestreamioChannelId, 50 | platform: string, 51 | name: string, 52 | ): void { 53 | this.settingsManager.setContextSettingsAttributes(this.context, { 54 | restreamioChannelId, 55 | platform, 56 | name, 57 | }) 58 | } 59 | 60 | assignRestreamioChannelPlatform(platform: string): void { 61 | this.settingsManager.setContextSettingsAttributes(this.context, { 62 | platform, 63 | }) 64 | } 65 | 66 | assignRestreamioChannelName(name: string): void { 67 | this.settingsManager.setContextSettingsAttributes(this.context, { 68 | name, 69 | }) 70 | } 71 | 72 | unassignRestreamioAccount(): void { 73 | this.settingsManager.setContextSettingsAttributes(this.context, { 74 | restreamioAccountId: null, 75 | restreamioChannelId: null, 76 | platform: null, 77 | name: null, 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/settings/managed-global-settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RestreamioAccount, 3 | RestreamioAccountCollection, 4 | RestreamioAccountId, 5 | RestreamioStreamingPlatform, 6 | RestreamioStreamingPlatformId, 7 | } from '../restreamio/types' 8 | import {SettingsManager} from 'streamdeck-typescript' 9 | import {createEmptyGlobalSettings, GlobalSettings} from './global-settings' 10 | 11 | export class ManagedGlobalSettings implements GlobalSettings { 12 | private settingsManager: SettingsManager 13 | 14 | get rawGlobalSettings(): GlobalSettings { 15 | const rawGlobalSettings = this.settingsManager.getGlobalSettings() as GlobalSettings 16 | 17 | if (!rawGlobalSettings) { 18 | return createEmptyGlobalSettings() 19 | } 20 | 21 | return rawGlobalSettings 22 | } 23 | 24 | get restreamioAccounts(): RestreamioAccountCollection { 25 | return this.rawGlobalSettings.restreamioAccounts 26 | } 27 | 28 | get restreamioStreamingPlatforms(): RestreamioStreamingPlatform[] { 29 | return this.rawGlobalSettings.restreamioStreamingPlatforms 30 | } 31 | 32 | constructor(settingsManager: SettingsManager) { 33 | this.settingsManager = settingsManager 34 | } 35 | 36 | setRestreamioStreamingPlatforms( 37 | restreamioStreamingPlatforms: RestreamioStreamingPlatform[], 38 | ): void { 39 | this.settingsManager.setGlobalSettingsAttributes({ 40 | restreamioStreamingPlatforms, 41 | }) 42 | } 43 | 44 | registerRestreamioAccount( 45 | restreamioAccount: RestreamioAccount, 46 | ): RestreamioAccountId { 47 | this.settingsManager.setGlobalSettings({ 48 | ...this.rawGlobalSettings, 49 | restreamioAccounts: { 50 | ...this.rawGlobalSettings.restreamioAccounts, 51 | [restreamioAccount.profile.id]: restreamioAccount, 52 | }, 53 | }) 54 | 55 | return restreamioAccount.profile.id 56 | } 57 | 58 | getRestreamioAccounts(): RestreamioAccount[] { 59 | if (!this.rawGlobalSettings.restreamioAccounts) { 60 | return [] 61 | } 62 | 63 | return Object.keys(this.rawGlobalSettings.restreamioAccounts).map( 64 | k => this.rawGlobalSettings.restreamioAccounts[k], 65 | ) 66 | } 67 | 68 | getRestreamioStreamingPlatformById( 69 | restreamStreamingPlatformId: RestreamioStreamingPlatformId, 70 | ): RestreamioStreamingPlatform | undefined { 71 | return this.rawGlobalSettings.restreamioStreamingPlatforms.find( 72 | platform => platform.id === restreamStreamingPlatformId, 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/settings/deferred-global-settings-requests.ts: -------------------------------------------------------------------------------- 1 | import {ManagedGlobalSettings} from './managed-global-settings' 2 | import {ManagedSettings} from './managed-settings' 3 | import {SettingsManager} from 'streamdeck-typescript' 4 | 5 | export type DeferredFunctionWithoutContext = ( 6 | managedGlobalSettings: ManagedGlobalSettings, 7 | ) => void 8 | 9 | export type DeferredFunctionWithContext = ( 10 | managedGlobalSettings: ManagedGlobalSettings, 11 | managedSeetings: ManagedSettings, 12 | ) => void 13 | 14 | export type DeferredFunction = 15 | | DeferredFunctionWithoutContext 16 | | DeferredFunctionWithContext 17 | 18 | interface DeferredRequest { 19 | fnc: DeferredFunction 20 | context?: string 21 | } 22 | 23 | export class DeferredGlobalSettingsRequests { 24 | private deferredGlobalSettingsRequests: DeferredRequest[] = [] 25 | private settingsManager: SettingsManager 26 | 27 | constructor(settingsManager: SettingsManager) { 28 | this.settingsManager = settingsManager 29 | } 30 | 31 | handle(): void { 32 | while (this.deferredGlobalSettingsRequests.length > 0) { 33 | const deferredRequest = this.deferredGlobalSettingsRequests.shift() 34 | 35 | if (!deferredRequest) { 36 | continue 37 | } 38 | 39 | if (deferredRequest.context) { 40 | this.withGlobalSettingsResolvedForContext( 41 | deferredRequest.fnc, 42 | deferredRequest.context, 43 | ) 44 | } else { 45 | this.withGlobalSettingsResolved( 46 | deferredRequest.fnc as DeferredFunctionWithoutContext, 47 | ) 48 | } 49 | } 50 | } 51 | 52 | deferOrNow(isReady: boolean, fnc: DeferredFunction, context?: string): void { 53 | if (isReady) { 54 | if (context) { 55 | this.withGlobalSettingsResolvedForContext(fnc, context) 56 | } else { 57 | this.withGlobalSettingsResolved(fnc as DeferredFunctionWithoutContext) 58 | } 59 | } else { 60 | this.defer(fnc, context) 61 | } 62 | } 63 | 64 | defer(fnc: DeferredFunction, context?: string): void { 65 | this.deferredGlobalSettingsRequests.push({fnc, context}) 66 | } 67 | 68 | private withGlobalSettingsResolved( 69 | fnc: DeferredFunctionWithoutContext, 70 | ): void { 71 | fnc(new ManagedGlobalSettings(this.settingsManager)) 72 | } 73 | 74 | private withGlobalSettingsResolvedForContext( 75 | fnc: DeferredFunctionWithContext, 76 | context: string, 77 | ): void { 78 | fnc( 79 | new ManagedGlobalSettings(this.settingsManager), 80 | new ManagedSettings(this.settingsManager, context ?? ''), 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@typescript-eslint"], 3 | "extends": [ 4 | "plugin:github/browser", 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 9, 11 | "sourceType": "module", 12 | "project": "./tsconfig.eslint.json" 13 | }, 14 | "rules": { 15 | "eslint-comments/no-use": "off", 16 | "import/no-namespace": "off", 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": "error", 19 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 20 | "@typescript-eslint/no-require-imports": "error", 21 | "@typescript-eslint/array-type": "error", 22 | "@typescript-eslint/await-thenable": "error", 23 | //"@typescript-eslint/ban-ts-ignore": "error", 24 | "camelcase": "off", 25 | "@typescript-eslint/naming-convention": "error", 26 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 27 | "@typescript-eslint/func-call-spacing": ["error", "never"], 28 | "@typescript-eslint/no-array-constructor": "error", 29 | "@typescript-eslint/no-empty-interface": "error", 30 | "@typescript-eslint/no-explicit-any": "error", 31 | "@typescript-eslint/no-extraneous-class": "error", 32 | "@typescript-eslint/no-for-in-array": "error", 33 | "@typescript-eslint/no-inferrable-types": "error", 34 | "@typescript-eslint/no-misused-new": "error", 35 | "@typescript-eslint/no-namespace": "error", 36 | "@typescript-eslint/no-non-null-assertion": "warn", 37 | //"@typescript-eslint/no-object-literal-type-assertion": "error", 38 | "@typescript-eslint/no-unnecessary-qualifier": "error", 39 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 40 | "@typescript-eslint/no-useless-constructor": "error", 41 | "@typescript-eslint/no-var-requires": "error", 42 | "@typescript-eslint/prefer-for-of": "warn", 43 | "@typescript-eslint/prefer-function-type": "warn", 44 | "@typescript-eslint/prefer-includes": "error", 45 | //"@typescript-eslint/prefer-interface": "error", 46 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 47 | "@typescript-eslint/promise-function-async": "error", 48 | "@typescript-eslint/require-array-sort-compare": "error", 49 | "@typescript-eslint/restrict-plus-operands": "error", 50 | "semi": "off", 51 | "@typescript-eslint/semi": ["error", "never"], 52 | "@typescript-eslint/type-annotation-spacing": "error", 53 | "@typescript-eslint/unbound-method": "error" 54 | }, 55 | "env": { 56 | "node": true, 57 | "es6": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🔌 Stream Deck Restream.io plugin

2 |

3 | 4 | Documentation 5 | 6 | 7 | Maintenance 8 | 9 | 10 | License: MIT 11 | 12 | 13 | Twitter: dflydev 14 | 15 | 16 | Twitter: beausimensen 17 | 18 |

19 | 20 | > Integrate [Elgato Stream Deck](https://www.elgato.com/en/gaming/stream-deck) with [Restream.io](https://restream.io). 21 | 22 | ## 🚀 Quick start 23 | 24 | [Download the latest release](https://github.com/dflydev/streamdeck-restreamio/releases/download/latest/com.dflydev.streamdeck.restreamio.streamDeckPlugin) from GitHub. 25 | 26 | ## Author 27 | 28 | 👥 **dflydev** 29 | 30 | * Website: [dflydev.com](https://dflydev.com?utm_source=streamdeck-restreamio&utm_medium=readme-author) 31 | * Twitter: [@dflydev](https://twitter.com/dflydev) 32 | * Github: [@dflydev](https://github.com/dflydev) 33 | 34 | 👤 **Beau Simensen** 35 | 36 | * Website: [beausimensen.com](https://beausimensen.com?utm_source=streamdeck-restreamio&utm_medium=readme-author) 37 | * Twitter: [@dflydev](https://twitter.com/beausimensen) 38 | * Github: [@simensen](https://github.com/simensen) 39 | 40 | ## 🤝 Contributing 41 | 42 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/dflydev/streamdeck-restreamio/issues). 43 | 44 | 45 | ## ❤️ Support the development 46 | 47 | Give a ⭐️ if this project helped you! 48 | 49 | Did this project save you time? Did this project increase your productivity? Did this project solve a problem for you? Did this project make your life easier? Please also consider donating or buying a license! 50 | 51 | * [Sponsor Beau Simensen on GitHub](https://github.com/sponsors/simensen) 52 | * [Sponsor dflydev on GitHub](https://github.com/sponsors/dflydev) 53 | * [Sponsor dflydev on OpenCollecctive](https://opencollective.com/dflydev) 54 | 55 | 56 | ## 📝 License 57 | 58 | Copyright © 2021 [dflydev](https://github.com/dflydev).
59 | This project is [MIT](https://github.com/dflydev/streamdeck-restreamio/blob/master/LICENSE) licensed. 60 | -------------------------------------------------------------------------------- /src/actions/channel-action.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DidReceiveSettingsEvent, 3 | KeyUpEvent, 4 | SDOnActionEvent, 5 | StateType, 6 | WillAppearEvent, 7 | WillDisappearEvent, 8 | } from 'streamdeck-typescript' 9 | import {RestreamioPlugin} from '../restreamio-plugin' 10 | import {Action} from './action' 11 | import {Settings} from '../settings/settings' 12 | 13 | export class ChannelAction extends Action { 14 | private channels: Set 15 | 16 | constructor(plugin: RestreamioPlugin, actionName: string) { 17 | super(plugin, actionName) 18 | this.channels = new Set() 19 | } 20 | 21 | @SDOnActionEvent('willAppear') 22 | private onWillAppear({ 23 | context, 24 | payload: {isInMultiAction}, 25 | }: WillAppearEvent): void { 26 | if (!isInMultiAction) { 27 | this.channels.add(context) 28 | } 29 | 30 | this.plugin.setState(StateType.OFF, context) 31 | this.updateUi(context) 32 | 33 | this.plugin.withGlobalSettings(context, () => this.getChannelState(context)) 34 | } 35 | 36 | @SDOnActionEvent('willDisappear') 37 | private onWillDisappear({ 38 | context, 39 | payload: {isInMultiAction}, 40 | }: WillDisappearEvent): void { 41 | if (!isInMultiAction) { 42 | this.channels.delete(context) 43 | } 44 | } 45 | 46 | private getChannelState(context: string): void { 47 | this.restreamioClient(context) 48 | .getChannel() 49 | .then(channelData => { 50 | const channel = channelData as {active: boolean} 51 | this.plugin.setState( 52 | channel.active ? StateType.ON : StateType.OFF, 53 | context, 54 | ) 55 | }) 56 | } 57 | 58 | @SDOnActionEvent('didReceiveSettings') 59 | private onDidReceiveSettings({context}: DidReceiveSettingsEvent): void { 60 | this.updateUi(context) 61 | } 62 | 63 | @SDOnActionEvent('keyUp') 64 | private async onKeyUp({ 65 | context, 66 | payload: {settings, isInMultiAction, userDesiredState, state}, 67 | }: KeyUpEvent): Promise { 68 | if (isInMultiAction) { 69 | if (userDesiredState === StateType.ON) { 70 | await this.restreamioClient(context).enableChannel() 71 | this.setAllChannelActionsTo(context, settings, StateType.ON) 72 | } else { 73 | await this.restreamioClient(context).disableChannel() 74 | this.setAllChannelActionsTo(context, settings, StateType.OFF) 75 | } 76 | } else { 77 | if (state === StateType.OFF) { 78 | try { 79 | await this.restreamioClient(context).enableChannel() 80 | this.plugin.showOk(context) 81 | this.setAllChannelActionsTo(context, settings, StateType.ON) 82 | } catch (e) { 83 | console.log('Could not enable channel', e) 84 | this.plugin.setState(StateType.OFF, context) 85 | this.plugin.showAlert(context) 86 | } 87 | } 88 | if (state === StateType.ON) { 89 | try { 90 | await this.restreamioClient(context).disableChannel() 91 | this.plugin.showOk(context) 92 | this.setAllChannelActionsTo(context, settings, StateType.OFF) 93 | } catch (e) { 94 | console.log('Could not disable channel', e) 95 | this.plugin.setState(StateType.ON, context) 96 | this.plugin.showAlert(context) 97 | } 98 | } 99 | } 100 | 101 | return Promise.resolve() 102 | } 103 | 104 | private setAllChannelActionsTo( 105 | context: string, 106 | settings: Settings, 107 | desiredState: StateType, 108 | ): void { 109 | const ourContext = context 110 | this.channels.forEach((context: string) => { 111 | if (ourContext === context) { 112 | return 113 | } 114 | 115 | const theirSettings = this.settings(context) 116 | 117 | if (theirSettings.restreamioAccountId !== settings.restreamioAccountId) { 118 | return 119 | } 120 | 121 | if (theirSettings.restreamioChannelId !== settings.restreamioChannelId) { 122 | return 123 | } 124 | 125 | this.plugin.setState(desiredState, context) 126 | }) 127 | } 128 | 129 | private async updateUi(context: string): Promise { 130 | if ( 131 | !this.settings(context).restreamioAccountId || 132 | !this.settings(context).restreamioChannelId 133 | ) { 134 | return Promise.resolve() 135 | } 136 | 137 | const platform = this.settings(context).platform 138 | const name = this.settings(context).name 139 | 140 | this.plugin.setTitle(`${name}\n${platform}`, context) 141 | 142 | return Promise.resolve() 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/restreamio/restreamio-client.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosInstance, AxiosRequestConfig} from 'axios' 2 | import {ManagedGlobalSettings} from '../settings/managed-global-settings' 3 | import {ManagedSettings} from '../settings/managed-settings' 4 | import {RestreamioStreamingPlatform, RestreamioToken} from './types' 5 | import {SettingsManager} from 'streamdeck-typescript' 6 | import createAuthRefreshInterceptor from 'axios-auth-refresh' 7 | import * as rax from 'retry-axios' 8 | 9 | export class RestreamioClient { 10 | private managedGlobalSettings: ManagedGlobalSettings 11 | private managedSettings?: ManagedSettings 12 | private authenticatedAxios: AxiosInstance 13 | private unAuthenticatedAxios: AxiosInstance 14 | 15 | static fromSettingsManager( 16 | settingsManager: SettingsManager, 17 | context?: string, 18 | ): RestreamioClient { 19 | return new RestreamioClient( 20 | new ManagedGlobalSettings(settingsManager), 21 | context ? new ManagedSettings(settingsManager, context) : undefined, 22 | ) 23 | } 24 | 25 | constructor( 26 | managedGlobalSettings: ManagedGlobalSettings, 27 | managedSettings?: ManagedSettings, 28 | ) { 29 | this.managedGlobalSettings = managedGlobalSettings 30 | this.managedSettings = managedSettings 31 | this.authenticatedAxios = this.createAuthenticatedAxios() 32 | this.unAuthenticatedAxios = RestreamioClient.createUnauthenticatedAxios() 33 | } 34 | 35 | private static createUnauthenticatedAxios(): AxiosInstance { 36 | const unauthenticatedAxios = axios.create() 37 | 38 | unauthenticatedAxios.defaults.raxConfig = { 39 | instance: unauthenticatedAxios, 40 | } 41 | 42 | rax.attach(unauthenticatedAxios) 43 | 44 | return unauthenticatedAxios 45 | } 46 | 47 | private createAuthenticatedAxios(): AxiosInstance { 48 | const authenticatedAxios = axios.create() 49 | 50 | authenticatedAxios.interceptors.request.use(request => { 51 | const token = this.getToken() 52 | 53 | if (!token) { 54 | return request 55 | } 56 | 57 | request.headers['Authorization'] = `Bearer ${token.accessToken}` 58 | 59 | return request 60 | }) 61 | 62 | createAuthRefreshInterceptor(authenticatedAxios, async failedRequest => { 63 | const formData = new FormData() 64 | formData.append('refresh_token', this.getToken()?.refreshToken ?? '') 65 | const refreshUrl = process.env.STREAMDECK_RESTREAMIO_REFRESH_URL 66 | 67 | if (!refreshUrl) { 68 | throw new Error('Refresh URL is not defined') 69 | } 70 | 71 | return axios 72 | .post(refreshUrl, formData) 73 | .then(async tokenRefreshResponse => { 74 | this.managedGlobalSettings.registerRestreamioAccount( 75 | tokenRefreshResponse.data, 76 | ) 77 | failedRequest.response.config.headers[ 78 | 'Authorization' 79 | ] = `Bearer ${tokenRefreshResponse.data.token.accessToken}` 80 | 81 | return Promise.resolve() 82 | }) 83 | }) 84 | 85 | authenticatedAxios.defaults.raxConfig = { 86 | instance: authenticatedAxios, 87 | } 88 | 89 | rax.attach(authenticatedAxios) 90 | 91 | return authenticatedAxios 92 | } 93 | 94 | async getChannel(): Promise { 95 | if (!this.managedSettings?.restreamioChannelId) { 96 | return {active: false} 97 | } 98 | 99 | return this.get( 100 | `https://api.restream.io/v2/user/channel/${this.managedSettings?.restreamioChannelId}`, 101 | ) 102 | } 103 | 104 | async enableChannel(): Promise { 105 | if (!this.managedSettings?.restreamioChannelId) { 106 | return 107 | } 108 | 109 | return await this.patch( 110 | `https://api.restream.io/v2/user/channel/${this.managedSettings?.restreamioChannelId}`, 111 | { 112 | active: true, 113 | }, 114 | ) 115 | } 116 | 117 | async disableChannel(): Promise { 118 | if (!this.managedSettings?.restreamioChannelId) { 119 | return 120 | } 121 | 122 | return await this.patch( 123 | `https://api.restream.io/v2/user/channel/${this.managedSettings?.restreamioChannelId}`, 124 | { 125 | active: false, 126 | }, 127 | ) 128 | } 129 | 130 | async getChannels(): Promise { 131 | return this.get('https://api.restream.io/v2/user/channel/all') 132 | } 133 | 134 | private getToken(): RestreamioToken | null { 135 | if (!this.managedSettings?.restreamioAccountId) { 136 | return null 137 | } 138 | 139 | return this.managedGlobalSettings.restreamioAccounts[ 140 | this.managedSettings.restreamioAccountId 141 | ].token 142 | } 143 | 144 | async getStreamingPlatforms(): Promise { 145 | const options = { 146 | method: 'GET', 147 | url: 'https://api.restream.io/v2/platform/all', 148 | } as AxiosRequestConfig 149 | 150 | return await axios.request(options).then(response => { 151 | return response.data 152 | }) 153 | } 154 | 155 | private async get(url: string): Promise { 156 | const options = { 157 | method: 'GET', 158 | url, 159 | } as AxiosRequestConfig 160 | 161 | return await this.authenticatedAxios 162 | .request(options) 163 | .then(function (response) { 164 | return response.data 165 | }) 166 | } 167 | 168 | private async patch(url: string, payload: unknown): Promise { 169 | const options = { 170 | method: 'PATCH', 171 | url, 172 | data: payload, 173 | } as AxiosRequestConfig 174 | 175 | return await this.authenticatedAxios.request(options) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/restreamio-property-inspector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DidReceiveSettingsEvent, 3 | SDOnPiEvent, 4 | StreamDeckPropertyInspectorHandler, 5 | } from 'streamdeck-typescript' 6 | import {GlobalSettings} from './settings/global-settings' 7 | import {ManagedGlobalSettings} from './settings/managed-global-settings' 8 | import {ManagedSettings} from './settings/managed-settings' 9 | import {RestreamioAccount} from './restreamio/types' 10 | import {Settings} from './settings/settings' 11 | import {RestreamioClient} from './restreamio/restreamio-client' 12 | import { 13 | DeferredFunction, 14 | DeferredGlobalSettingsRequests, 15 | } from './settings/deferred-global-settings-requests' 16 | import Timeout = NodeJS.Timeout 17 | 18 | class RestreamioPropertyInspector extends StreamDeckPropertyInspectorHandler< 19 | Settings, 20 | GlobalSettings 21 | > { 22 | private deferredGlobalSettingsRequests: DeferredGlobalSettingsRequests 23 | private restreamioAccountIdElement: HTMLSelectElement 24 | private restreamioChannelIdElement: HTMLSelectElement 25 | private restreamioChannelElement: HTMLDivElement 26 | private restreamioChannelPlatformWrapperElement: HTMLDivElement 27 | private restreamioChannelNameWrapperElement: HTMLDivElement 28 | private restreamioChannelPlatformFieldElement: HTMLInputElement 29 | private restreamioChannelNameFieldElement: HTMLInputElement 30 | 31 | private authWindow: Window | null 32 | 33 | private updateUiTimeoutId: Timeout 34 | 35 | constructor() { 36 | super() 37 | this.deferredGlobalSettingsRequests = new DeferredGlobalSettingsRequests( 38 | this.settingsManager, 39 | ) 40 | } 41 | 42 | @SDOnPiEvent('documentLoaded') 43 | private onDocumentLoaded(): void { 44 | window.addEventListener('message', e => { 45 | this.withGlobalSettings( 46 | (globalSettings: ManagedGlobalSettings, settings: ManagedSettings) => { 47 | const restreamioAccountId = globalSettings.registerRestreamioAccount( 48 | e.data, 49 | ) 50 | settings.assignRestreamioAccountId(restreamioAccountId) 51 | 52 | if (this.authWindow) { 53 | this.authWindow.close() 54 | 55 | this.authWindow = null 56 | } 57 | 58 | this.updateUi() 59 | }, 60 | ) 61 | }) 62 | 63 | this.restreamioAccountIdElement = document.getElementById( 64 | 'restreamio_account_id', 65 | ) as HTMLSelectElement 66 | this.restreamioAccountIdElement.onchange = () => { 67 | const selectedValue = this.restreamioAccountIdElement.value 68 | 69 | if (selectedValue === '__NEW__') { 70 | return 71 | } 72 | 73 | this.withGlobalSettings( 74 | (globalSettings: ManagedGlobalSettings, settings: ManagedSettings) => { 75 | if (selectedValue) { 76 | settings.assignRestreamioAccountId(Number(selectedValue)) 77 | } else { 78 | settings.unassignRestreamioAccount() 79 | } 80 | 81 | this.updateUi() 82 | }, 83 | ) 84 | } 85 | 86 | this.restreamioAccountIdElement.onclick = () => { 87 | if (this.restreamioAccountIdElement.value !== '__NEW__') { 88 | return 89 | } 90 | 91 | const externalUrl = process.env.STREAMDECK_RESTREAMIO_LOGIN_URL 92 | 93 | this.authWindow = window.open(externalUrl, 'Reastream.io') 94 | } 95 | 96 | this.restreamioChannelElement = document.getElementById( 97 | 'restreamio_channel', 98 | ) as HTMLDivElement 99 | this.restreamioChannelPlatformWrapperElement = document.getElementById( 100 | 'restreamio_channel_platform_wrapper', 101 | ) as HTMLDivElement 102 | this.restreamioChannelNameWrapperElement = document.getElementById( 103 | 'restreamio_channel_name_wrapper', 104 | ) as HTMLDivElement 105 | this.restreamioChannelIdElement = document.getElementById( 106 | 'restreamio_channel_id', 107 | ) as HTMLSelectElement 108 | this.restreamioChannelPlatformFieldElement = document.getElementById( 109 | 'restreamio_channel_platform', 110 | ) as HTMLInputElement 111 | this.restreamioChannelNameFieldElement = document.getElementById( 112 | 'restreamio_channel_name', 113 | ) as HTMLInputElement 114 | this.restreamioChannelIdElement.onchange = () => { 115 | const selectedValue = this.restreamioChannelIdElement.value 116 | 117 | this.withGlobalSettings( 118 | (globalSettings: ManagedGlobalSettings, settings: ManagedSettings) => { 119 | const label = 120 | this.restreamioChannelIdElement.selectedOptions.item(0)?.text || 121 | 'Unknown – Unknown' 122 | const splitLabel = label.split(' – ') 123 | const platform = splitLabel.shift() 124 | const name = splitLabel.shift() 125 | settings.assignRestreamioChannelId( 126 | Number(selectedValue), 127 | platform ?? 'Unknown', 128 | name ?? 'Unknown', 129 | ) 130 | 131 | this.updateUi() 132 | }, 133 | ) 134 | } 135 | 136 | this.restreamioChannelNameFieldElement.onchange = () => { 137 | this.withGlobalSettings( 138 | (globalSettings: ManagedGlobalSettings, settings: ManagedSettings) => { 139 | settings.assignRestreamioChannelName( 140 | this.restreamioChannelNameFieldElement.value, 141 | ) 142 | }, 143 | ) 144 | } 145 | 146 | this.restreamioChannelPlatformFieldElement.onchange = () => { 147 | this.withGlobalSettings( 148 | (globalSettings: ManagedGlobalSettings, settings: ManagedSettings) => { 149 | settings.assignRestreamioChannelPlatform( 150 | this.restreamioChannelPlatformFieldElement.value, 151 | ) 152 | }, 153 | ) 154 | } 155 | 156 | this.updateUi() 157 | } 158 | 159 | @SDOnPiEvent('globalSettingsAvailable') 160 | private async onGlobalSettingsAvailable(): Promise { 161 | this.deferredGlobalSettingsRequests.handle() 162 | } 163 | 164 | @SDOnPiEvent('didReceiveSettings') 165 | private async onDidReceiveSettings({ 166 | payload: {settings}, 167 | }: DidReceiveSettingsEvent): Promise { 168 | this.updateUi() 169 | } 170 | 171 | @SDOnPiEvent('didReceiveGlobalSettings') 172 | private onDidReceiveGlobalSettings(): void { 173 | this.updateUi() 174 | } 175 | 176 | withGlobalSettings(fnc: DeferredFunction): void { 177 | this.deferredGlobalSettingsRequests.deferOrNow( 178 | this.globalSettingsReady, 179 | fnc, 180 | this.actionInfo.context, 181 | ) 182 | } 183 | 184 | private updateUi(): void { 185 | this.withGlobalSettings( 186 | async ( 187 | globalSettings: ManagedGlobalSettings, 188 | settings: ManagedSettings, 189 | ) => { 190 | clearTimeout(this.updateUiTimeoutId) 191 | this.updateUiTimeoutId = setTimeout(async () => { 192 | this.updateRestreamioAccountUi(globalSettings, settings) 193 | await this.updateRestreamioChannelUi(globalSettings, settings) 194 | }, 100) 195 | }, 196 | ) 197 | } 198 | 199 | private async updateRestreamioAccountUi( 200 | globalSettings: ManagedGlobalSettings, 201 | settings: ManagedSettings, 202 | ): Promise { 203 | while (this.restreamioAccountIdElement.options.length > 0) { 204 | this.restreamioAccountIdElement.options.remove(0) 205 | } 206 | 207 | let anAccountIsSelected = false 208 | 209 | globalSettings 210 | .getRestreamioAccounts() 211 | .forEach((restreamioAccount: RestreamioAccount) => { 212 | const option = document.createElement('option') 213 | option.value = restreamioAccount.profile.id.toString() 214 | option.text = restreamioAccount.profile.username 215 | if (restreamioAccount.profile.id === settings.restreamioAccountId) { 216 | option.selected = true 217 | 218 | anAccountIsSelected = true 219 | } 220 | this.restreamioAccountIdElement.options.add(option) 221 | }) 222 | 223 | const blankOption = document.createElement('option') 224 | 225 | if (!anAccountIsSelected) { 226 | blankOption.selected = !anAccountIsSelected 227 | } 228 | 229 | this.restreamioAccountIdElement.options.add(blankOption) 230 | 231 | const newOption = document.createElement('option') 232 | newOption.value = '__NEW__' 233 | newOption.text = 'Add new account' 234 | 235 | this.restreamioAccountIdElement.options.add(newOption) 236 | } 237 | 238 | private async updateRestreamioChannelUi( 239 | globalSettings: ManagedGlobalSettings, 240 | settings: ManagedSettings, 241 | ): Promise { 242 | const restreamioClient = new RestreamioClient(globalSettings, settings) 243 | 244 | let channels: any[] = [] 245 | 246 | if (settings.restreamioAccountId) { 247 | try { 248 | channels = (await restreamioClient.getChannels()) as any[] 249 | } catch (e) { 250 | channels = [] 251 | } 252 | } 253 | 254 | while (this.restreamioChannelIdElement.options.length > 0) { 255 | this.restreamioChannelIdElement.options.remove(0) 256 | } 257 | 258 | let aChannelIsSelected = false 259 | 260 | channels.forEach((channel: any) => { 261 | const option = document.createElement('option') 262 | const platform = globalSettings.getRestreamioStreamingPlatformById( 263 | channel.streamingPlatformId, 264 | ) 265 | option.value = channel.id 266 | option.text = `${platform ? platform.name : 'Unknown'} – ${ 267 | channel.displayName 268 | }` 269 | if (channel.id === settings.restreamioChannelId) { 270 | option.selected = true 271 | 272 | aChannelIsSelected = true 273 | } 274 | this.restreamioChannelIdElement.options.add(option) 275 | }) 276 | 277 | if (!aChannelIsSelected) { 278 | const blankOption = document.createElement('option') 279 | blankOption.selected = true 280 | this.restreamioChannelIdElement.options.add(blankOption) 281 | } 282 | 283 | if (this.restreamioChannelIdElement.options.length === 1) { 284 | this.restreamioChannelElement.style.display = 'none' 285 | } else { 286 | this.restreamioChannelElement.style.display = 'flex' 287 | } 288 | 289 | if (aChannelIsSelected) { 290 | this.restreamioChannelPlatformFieldElement.value = settings.platform ?? '' 291 | this.restreamioChannelNameFieldElement.value = settings.name ?? '' 292 | 293 | this.restreamioChannelPlatformWrapperElement.style.display = 'flex' 294 | this.restreamioChannelNameWrapperElement.style.display = 'flex' 295 | } else { 296 | this.restreamioChannelPlatformWrapperElement.style.display = 'none' 297 | this.restreamioChannelNameWrapperElement.style.display = 'none' 298 | } 299 | } 300 | } 301 | 302 | new RestreamioPropertyInspector() 303 | -------------------------------------------------------------------------------- /com.dflydev.streamdeck.restreamio.sdPlugin/public/sdpi.css: -------------------------------------------------------------------------------- 1 | html { 2 | --sdpi-bgcolor: #2D2D2D; 3 | --sdpi-background: #3D3D3D; 4 | --sdpi-color: #d8d8d8; 5 | --sdpi-bordercolor: #3a3a3a; 6 | --sdpi-borderradius: 0px; 7 | --sdpi-width: 224px; 8 | --sdpi-fontweight: 600; 9 | --sdpi-letterspacing: -0.25pt; 10 | height: 100%; 11 | width: 100%; 12 | overflow: hidden; 13 | } 14 | 15 | html, body { 16 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 17 | font-size: 9pt; 18 | background-color: var(--sdpi-bgcolor); 19 | color: #9a9a9a; 20 | } 21 | 22 | body { 23 | height: 100%; 24 | padding: 0; 25 | overflow-x: hidden; 26 | overflow-y: auto; 27 | margin: 0; 28 | -webkit-overflow-scrolling: touch; 29 | -webkit-text-size-adjust: 100%; 30 | -webkit-font-smoothing: antialiased; 31 | } 32 | 33 | mark { 34 | background-color: var(--sdpi-bgcolor); 35 | color: var(--sdpi-color); 36 | } 37 | 38 | .hidden { 39 | display: none; 40 | } 41 | 42 | hr, hr2 { 43 | -webkit-margin-before: 1em; 44 | -webkit-margin-after: 1em; 45 | border-style: none; 46 | background: var(--sdpi-background); 47 | height: 1px; 48 | } 49 | 50 | hr2, 51 | .sdpi-heading { 52 | display: flex; 53 | flex-basis: 100%; 54 | align-items: center; 55 | color: inherit; 56 | font-size: 9pt; 57 | margin: 8px 0px; 58 | } 59 | 60 | .sdpi-heading::before, 61 | .sdpi-heading::after { 62 | content: ""; 63 | flex-grow: 1; 64 | background: var(--sdpi-background); 65 | height: 1px; 66 | font-size: 0px; 67 | line-height: 0px; 68 | margin: 0px 16px; 69 | } 70 | 71 | hr2 { 72 | height: 2px; 73 | } 74 | 75 | hr, hr2 { 76 | margin-left:16px; 77 | margin-right:16px; 78 | } 79 | 80 | .sdpi-item-value, 81 | option, 82 | input, 83 | select, 84 | button { 85 | font-size: 10pt; 86 | font-weight: var(--sdpi-fontweight); 87 | letter-spacing: var(--sdpi-letterspacing); 88 | } 89 | 90 | 91 | 92 | .win .sdpi-item-value, 93 | .win option, 94 | .win input, 95 | .win select, 96 | .win button { 97 | font-size: 11px; 98 | font-style: normal; 99 | letter-spacing: inherit; 100 | font-weight: 100; 101 | } 102 | 103 | .win button { 104 | font-size: 12px; 105 | } 106 | 107 | ::-webkit-progress-value, 108 | meter::-webkit-meter-optimum-value { 109 | border-radius: 2px; 110 | /* background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); */ 111 | } 112 | 113 | ::-webkit-progress-bar, 114 | meter::-webkit-meter-bar { 115 | border-radius: 3px; 116 | background: var(--sdpi-background); 117 | } 118 | 119 | ::-webkit-progress-bar:active, 120 | meter::-webkit-meter-bar:active { 121 | border-radius: 3px; 122 | background: #222222; 123 | } 124 | ::-webkit-progress-value:active, 125 | meter::-webkit-meter-optimum-value:active { 126 | background: #99f; 127 | } 128 | 129 | progress, 130 | progress.sdpi-item-value { 131 | min-height: 5px !important; 132 | height: 5px; 133 | background-color: #303030; 134 | } 135 | 136 | progress { 137 | margin-top: 8px !important; 138 | margin-bottom: 8px !important; 139 | } 140 | 141 | .full progress, 142 | progress.full { 143 | margin-top: 3px !important; 144 | } 145 | 146 | ::-webkit-progress-inner-element { 147 | background-color: transparent; 148 | } 149 | 150 | 151 | .sdpi-item[type="progress"] { 152 | margin-top: 4px !important; 153 | margin-bottom: 12px; 154 | min-height: 15px; 155 | } 156 | 157 | .sdpi-item-child.full:last-child { 158 | margin-bottom: 4px; 159 | } 160 | 161 | .tabs { 162 | /** 163 | * Setting display to flex makes this container lay 164 | * out its children using flexbox, the exact same 165 | * as in the above "Stepper input" example. 166 | */ 167 | display: flex; 168 | 169 | border-bottom: 1px solid #D7DBDD; 170 | } 171 | 172 | .tab { 173 | cursor: pointer; 174 | padding: 5px 30px; 175 | color: #16a2d7; 176 | font-size: 9pt; 177 | border-bottom: 2px solid transparent; 178 | } 179 | 180 | .tab.is-tab-selected { 181 | border-bottom-color: #4ebbe4; 182 | } 183 | 184 | select { 185 | -webkit-appearance: none; 186 | -moz-appearance: none; 187 | -o-appearance: none; 188 | appearance: none; 189 | background: url(./icons/caret.svg) no-repeat 97% center; 190 | } 191 | 192 | label.sdpi-file-label, 193 | input[type="button"], 194 | input[type="submit"], 195 | input[type="reset"], 196 | input[type="file"], 197 | input[type=file]::-webkit-file-upload-button, 198 | button, 199 | select { 200 | color: var(--sdpi-color); 201 | border: 1pt solid #303030; 202 | font-size: 8pt; 203 | background-color: var(--sdpi-background); 204 | border-radius: var(--sdpi-borderradius); 205 | } 206 | 207 | label.sdpi-file-label, 208 | input[type="button"], 209 | input[type="submit"], 210 | input[type="reset"], 211 | input[type="file"], 212 | input[type=file]::-webkit-file-upload-button, 213 | button { 214 | border: 1pt solid var(--sdpi-color); 215 | border-radius: var(--sdpi-borderradius); 216 | min-height: 23px !important; 217 | height: 23px !important; 218 | margin-right: 8px; 219 | } 220 | 221 | input[type=number]::-webkit-inner-spin-button, 222 | input[type=number]::-webkit-outer-spin-button { 223 | -webkit-appearance: none; 224 | margin: 0; 225 | } 226 | 227 | input[type="file"] { 228 | border-radius: var(--sdpi-borderradius); 229 | max-width: 220px; 230 | } 231 | 232 | option { 233 | height: 1.5em; 234 | padding: 4px; 235 | } 236 | 237 | /* SDPI */ 238 | 239 | .sdpi-wrapper { 240 | overflow-x: hidden; 241 | } 242 | 243 | .sdpi-item { 244 | display: flex; 245 | flex-direction: row; 246 | min-height: 32px; 247 | align-items: center; 248 | margin-top: 2px; 249 | max-width: 344px; 250 | } 251 | 252 | .sdpi-item:first-child { 253 | margin-top:1px; 254 | } 255 | 256 | .sdpi-item:last-child { 257 | margin-bottom: 0px; 258 | } 259 | 260 | .sdpi-item > *:not(.sdpi-item-label):not(meter):not(details) { 261 | min-height: 26px; 262 | padding: 0px 4px 0px 4px; 263 | } 264 | 265 | .sdpi-item > *:not(.sdpi-item-label.empty):not(meter) { 266 | min-height: 26px; 267 | padding: 0px 4px 0px 4px; 268 | } 269 | 270 | 271 | .sdpi-item-group { 272 | padding: 0 !important; 273 | } 274 | 275 | meter.sdpi-item-value { 276 | margin-left: 6px; 277 | } 278 | 279 | .sdpi-item[type="group"] { 280 | display: block; 281 | margin-top: 12px; 282 | margin-bottom: 12px; 283 | /* border: 1px solid white; */ 284 | flex-direction: unset; 285 | text-align: left; 286 | } 287 | 288 | .sdpi-item[type="group"] > .sdpi-item-label, 289 | .sdpi-item[type="group"].sdpi-item-label { 290 | width: 96%; 291 | text-align: left; 292 | font-weight: 700; 293 | margin-bottom: 4px; 294 | padding-left: 4px; 295 | } 296 | 297 | dl, 298 | ul, 299 | ol { 300 | -webkit-margin-before: 0px; 301 | -webkit-margin-after: 4px; 302 | -webkit-padding-start: 1em; 303 | max-height: 90px; 304 | overflow-y: scroll; 305 | cursor: pointer; 306 | user-select: none; 307 | } 308 | 309 | table.sdpi-item-value, 310 | dl.sdpi-item-value, 311 | ul.sdpi-item-value, 312 | ol.sdpi-item-value { 313 | -webkit-margin-before: 4px; 314 | -webkit-margin-after: 8px; 315 | -webkit-padding-start: 1em; 316 | width: var(--sdpi-width); 317 | text-align: center; 318 | } 319 | 320 | table > caption { 321 | margin: 2px; 322 | } 323 | 324 | .list, 325 | .sdpi-item[type="list"] { 326 | align-items: baseline; 327 | } 328 | 329 | .sdpi-item-label { 330 | text-align: right; 331 | flex: none; 332 | width: 94px; 333 | padding-right: 4px; 334 | font-weight: 600; 335 | -webkit-user-select: none; 336 | } 337 | 338 | .win .sdpi-item-label, 339 | .sdpi-item-label > small{ 340 | font-weight: normal; 341 | } 342 | 343 | .sdpi-item-label:after { 344 | content: ": "; 345 | } 346 | 347 | .sdpi-item-label.empty:after { 348 | content: ""; 349 | } 350 | 351 | .sdpi-test, 352 | .sdpi-item-value { 353 | flex: 1 0 0; 354 | /* flex-grow: 1; 355 | flex-shrink: 0; */ 356 | margin-right: 14px; 357 | margin-left: 4px; 358 | justify-content: space-evenly; 359 | } 360 | 361 | canvas.sdpi-item-value { 362 | max-width: 144px; 363 | max-height: 144px; 364 | width: 144px; 365 | height: 144px; 366 | margin: 0 auto; 367 | cursor: pointer; 368 | } 369 | 370 | input.sdpi-item-value { 371 | margin-left: 5px; 372 | } 373 | 374 | .sdpi-item-value button, 375 | button.sdpi-item-value { 376 | margin-left: 7px; 377 | margin-right: 19px; 378 | } 379 | 380 | .sdpi-item-value.range { 381 | margin-left: 0px; 382 | } 383 | 384 | table, 385 | dl.sdpi-item-value, 386 | ul.sdpi-item-value, 387 | ol.sdpi-item-value, 388 | .sdpi-item-value > dl, 389 | .sdpi-item-value > ul, 390 | .sdpi-item-value > ol 391 | { 392 | list-style-type: none; 393 | list-style-position: outside; 394 | margin-left: -4px; 395 | margin-right: -4px; 396 | padding: 4px; 397 | border: 1px solid var(--sdpi-bordercolor); 398 | } 399 | 400 | dl.sdpi-item-value, 401 | ul.sdpi-item-value, 402 | ol.sdpi-item-value, 403 | .sdpi-item-value > ol { 404 | list-style-type: none; 405 | list-style-position: inside; 406 | margin-left: 5px; 407 | margin-right: 12px; 408 | padding: 4px !important; 409 | } 410 | 411 | ol.sdpi-item-value, 412 | .sdpi-item-value > ol[listtype="none"] { 413 | list-style-type: none; 414 | } 415 | ol.sdpi-item-value[type="decimal"], 416 | .sdpi-item-value > ol[type="decimal"] { 417 | list-style-type: decimal; 418 | } 419 | 420 | ol.sdpi-item-value[type="decimal-leading-zero"], 421 | .sdpi-item-value > ol[type="decimal-leading-zero"] { 422 | list-style-type: decimal-leading-zero; 423 | } 424 | 425 | ol.sdpi-item-value[type="lower-alpha"], 426 | .sdpi-item-value > ol[type="lower-alpha"] { 427 | list-style-type: lower-alpha; 428 | } 429 | 430 | ol.sdpi-item-value[type="upper-alpha"], 431 | .sdpi-item-value > ol[type="upper-alpha"] { 432 | list-style-type: upper-alpha; 433 | } 434 | 435 | ol.sdpi-item-value[type="upper-roman"], 436 | .sdpi-item-value > ol[type="upper-roman"] { 437 | list-style-type: upper-roman; 438 | } 439 | 440 | ol.sdpi-item-value[type="lower-roman"], 441 | .sdpi-item-value > ol[type="lower-roman"] { 442 | list-style-type: upper-roman; 443 | } 444 | 445 | tr:nth-child(even), 446 | .sdpi-item-value > ul > li:nth-child(even), 447 | .sdpi-item-value > ol > li:nth-child(even), 448 | li:nth-child(even) { 449 | background-color: rgba(0,0,0,.2) 450 | } 451 | 452 | td:hover, 453 | .sdpi-item-value > ul > li:hover:nth-child(even), 454 | .sdpi-item-value > ol > li:hover:nth-child(even), 455 | li:hover:nth-child(even), 456 | li:hover { 457 | background-color: rgba(255,255,255,.1); 458 | } 459 | 460 | td.selected, 461 | td.selected:hover, 462 | li.selected:hover, 463 | li.selected { 464 | color: white; 465 | background-color: #77f; 466 | } 467 | 468 | tr { 469 | border: 1px solid var(--sdpi-bordercolor); 470 | } 471 | 472 | td { 473 | border-right: 1px solid var(--sdpi-bordercolor); 474 | -webkit-user-select: none; 475 | } 476 | 477 | tr:last-child, 478 | td:last-child { 479 | border: none; 480 | } 481 | 482 | .sdpi-item-value.select, 483 | .sdpi-item-value > select { 484 | margin-right: 13px; 485 | margin-left: 4px; 486 | } 487 | 488 | .sdpi-item-child, 489 | .sdpi-item-group > .sdpi-item > input[type="color"] { 490 | margin-top: 0.4em; 491 | margin-right: 4px; 492 | } 493 | 494 | .full, 495 | .full *, 496 | .sdpi-item-value.full, 497 | .sdpi-item-child > full > *, 498 | .sdpi-item-child.full, 499 | .sdpi-item-child.full > *, 500 | .full > .sdpi-item-child, 501 | .full > .sdpi-item-child > *{ 502 | display: flex; 503 | flex: 1 1 0; 504 | margin-bottom: 4px; 505 | margin-left: 0px; 506 | width: 100%; 507 | 508 | justify-content: space-evenly; 509 | } 510 | 511 | .sdpi-item-group > .sdpi-item > input[type="color"] { 512 | margin-top: 0px; 513 | } 514 | 515 | ::-webkit-calendar-picker-indicator:focus, 516 | input[type=file]::-webkit-file-upload-button:focus, 517 | button:focus, 518 | textarea:focus, 519 | input:focus, 520 | select:focus, 521 | option:focus, 522 | details:focus, 523 | summary:focus, 524 | .custom-select select { 525 | outline: none; 526 | } 527 | 528 | summary { 529 | cursor: default; 530 | -webkit-user-select: none; 531 | } 532 | 533 | .pointer, 534 | summary .pointer { 535 | cursor: pointer; 536 | } 537 | 538 | details.message { 539 | padding: 4px 18px 4px 12px; 540 | } 541 | 542 | details.message summary { 543 | font-size: 10pt; 544 | font-weight: 600; 545 | min-height: 18px; 546 | } 547 | 548 | details.message:first-child { 549 | margin-top: 4px; 550 | margin-left: 0; 551 | padding-left: 106px; 552 | } 553 | 554 | details.message h1 { 555 | text-align: left; 556 | } 557 | 558 | .message > summary::-webkit-details-marker { 559 | display: none; 560 | } 561 | 562 | .info20, 563 | .question, 564 | .caution, 565 | .info { 566 | background-repeat: no-repeat; 567 | background-position: 70px center; 568 | } 569 | 570 | .info20 { 571 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z M10,8 C8.8954305,8 8,8.84275812 8,9.88235294 L8,16.1176471 C8,17.1572419 8.8954305,18 10,18 C11.1045695,18 12,17.1572419 12,16.1176471 L12,9.88235294 C12,8.84275812 11.1045695,8 10,8 Z M10,3 C8.8954305,3 8,3.88165465 8,4.96923077 L8,5.03076923 C8,6.11834535 8.8954305,7 10,7 C11.1045695,7 12,6.11834535 12,5.03076923 L12,4.96923077 C12,3.88165465 11.1045695,3 10,3 Z'/%3E%3C/svg%3E%0A"); 572 | } 573 | 574 | .info { 575 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M10,8 C9.44771525,8 9,8.42137906 9,8.94117647 L9,14.0588235 C9,14.5786209 9.44771525,15 10,15 C10.5522847,15 11,14.5786209 11,14.0588235 L11,8.94117647 C11,8.42137906 10.5522847,8 10,8 Z M10,5 C9.44771525,5 9,5.44082732 9,5.98461538 L9,6.01538462 C9,6.55917268 9.44771525,7 10,7 C10.5522847,7 11,6.55917268 11,6.01538462 L11,5.98461538 C11,5.44082732 10.5522847,5 10,5 Z'/%3E%3C/svg%3E%0A"); 576 | } 577 | 578 | .info2 { 579 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath fill='%23999' d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M7.5,2 C6.67157287,2 6,2.66124098 6,3.47692307 L6,3.52307693 C6,4.33875902 6.67157287,5 7.5,5 C8.32842705,5 9,4.33875902 9,3.52307693 L9,3.47692307 C9,2.66124098 8.32842705,2 7.5,2 Z M5,6 L5,7.02155172 L6,7 L6,12 L5,12.0076778 L5,13 L10,13 L10,12 L9,12.0076778 L9,6 L5,6 Z'/%3E%3C/svg%3E%0A"); 580 | } 581 | 582 | .sdpi-more-info { 583 | background-image: linear-gradient(to right, #00000000 0%,#00000040 80%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpolygon fill='%23999' points='4 7 8 7 8 5 12 8 8 11 8 9 4 9'/%3E%3C/svg%3E%0A"); 584 | } 585 | .caution { 586 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M9.03952676,0.746646542 C9.57068894,-0.245797319 10.4285735,-0.25196227 10.9630352,0.746646542 L19.7705903,17.2030214 C20.3017525,18.1954653 19.8777595,19 18.8371387,19 L1.16542323,19 C0.118729947,19 -0.302490098,18.2016302 0.231971607,17.2030214 L9.03952676,0.746646542 Z M10,2.25584053 L1.9601405,17.3478261 L18.04099,17.3478261 L10,2.25584053 Z M10,5.9375 C10.531043,5.9375 10.9615385,6.37373537 10.9615385,6.91185897 L10.9615385,11.6923077 C10.9615385,12.2304313 10.531043,12.6666667 10,12.6666667 C9.46895697,12.6666667 9.03846154,12.2304313 9.03846154,11.6923077 L9.03846154,6.91185897 C9.03846154,6.37373537 9.46895697,5.9375 10,5.9375 Z M10,13.4583333 C10.6372516,13.4583333 11.1538462,13.9818158 11.1538462,14.6275641 L11.1538462,14.6641026 C11.1538462,15.3098509 10.6372516,15.8333333 10,15.8333333 C9.36274837,15.8333333 8.84615385,15.3098509 8.84615385,14.6641026 L8.84615385,14.6275641 C8.84615385,13.9818158 9.36274837,13.4583333 10,13.4583333 Z'/%3E%3C/svg%3E%0A"); 587 | } 588 | 589 | .question { 590 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A"); 591 | } 592 | 593 | 594 | .sdpi-more-info { 595 | position: fixed; 596 | left: 0px; 597 | right: 0px; 598 | bottom: 0px; 599 | min-height:16px; 600 | padding-right: 16px; 601 | text-align: right; 602 | -webkit-touch-callout: none; 603 | cursor: pointer; 604 | user-select: none; 605 | background-position: right center; 606 | background-repeat: no-repeat; 607 | border-radius: var(--sdpi-borderradius); 608 | text-decoration: none; 609 | color: var(--sdpi-color); 610 | } 611 | 612 | .sdpi-more-info-button { 613 | display: flex; 614 | align-self: right; 615 | margin-left: auto; 616 | position: fixed; 617 | right: 17px; 618 | bottom: 0px; 619 | } 620 | 621 | details a { 622 | background-position: right !important; 623 | min-height: 24px; 624 | display: inline-block; 625 | line-height: 24px; 626 | padding-right: 28px; 627 | } 628 | input:not([type="range"]), 629 | textarea { 630 | -webkit-appearance: none; 631 | background: var(--sdpi-background); 632 | color: var(--sdpi-color); 633 | font-weight: normal; 634 | font-size: 9pt; 635 | border: none; 636 | margin-top: 2px; 637 | margin-bottom: 2px; 638 | } 639 | 640 | textarea + label { 641 | display: flex; 642 | justify-content: flex-end 643 | } 644 | input[type="radio"], 645 | input[type="checkbox"] { 646 | display: none; 647 | } 648 | input[type="radio"] + label, 649 | input[type="checkbox"] + label { 650 | font-size: 9pt; 651 | color: var(--sdpi-color); 652 | font-weight: normal; 653 | margin-right: 8px; 654 | -webkit-user-select: none; 655 | } 656 | 657 | input[type="radio"] + label:after, 658 | input[type="checkbox"] + label:after { 659 | content: " " !important; 660 | } 661 | 662 | .sdpi-item[type="radio"] > .sdpi-item-value, 663 | .sdpi-item[type="checkbox"] > .sdpi-item-value { 664 | padding-top: 2px; 665 | } 666 | 667 | .sdpi-item[type="checkbox"] > .sdpi-item-value > * { 668 | margin-top: 4px; 669 | } 670 | 671 | .sdpi-item[type="checkbox"] .sdpi-item-child, 672 | .sdpi-item[type="radio"] .sdpi-item-child { 673 | display: inline-block; 674 | } 675 | 676 | .sdpi-item[type="range"] .sdpi-item-value, 677 | .sdpi-item[type="meter"] .sdpi-item-child, 678 | .sdpi-item[type="progress"] .sdpi-item-child { 679 | display: flex; 680 | } 681 | 682 | .sdpi-item[type="range"] .sdpi-item-value { 683 | min-height: 26px; 684 | } 685 | 686 | .sdpi-item[type="range"] .sdpi-item-value span, 687 | .sdpi-item[type="meter"] .sdpi-item-child span, 688 | .sdpi-item[type="progress"] .sdpi-item-child span { 689 | margin-top: -2px; 690 | min-width: 8px; 691 | text-align: right; 692 | user-select: none; 693 | cursor: pointer; 694 | } 695 | 696 | .sdpi-item[type="range"] .sdpi-item-value span { 697 | margin-top: 7px; 698 | text-align: right; 699 | } 700 | 701 | span + input[type="range"] { 702 | display: flex; 703 | max-width: 168px; 704 | 705 | } 706 | 707 | .sdpi-item[type="range"] .sdpi-item-value span:first-child, 708 | .sdpi-item[type="meter"] .sdpi-item-child span:first-child, 709 | .sdpi-item[type="progress"] .sdpi-item-child span:first-child { 710 | margin-right: 4px; 711 | } 712 | 713 | .sdpi-item[type="range"] .sdpi-item-value span:last-child, 714 | .sdpi-item[type="meter"] .sdpi-item-child span:last-child, 715 | .sdpi-item[type="progress"] .sdpi-item-child span:last-child { 716 | margin-left: 4px; 717 | } 718 | 719 | .reverse { 720 | transform: rotate(180deg); 721 | } 722 | 723 | .sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child { 724 | margin-left: -10px; 725 | } 726 | 727 | .sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child { 728 | margin-left: -14px; 729 | } 730 | 731 | .sdpi-item[type="radio"] > .sdpi-item-value > * { 732 | margin-top: 2px; 733 | } 734 | 735 | details { 736 | padding: 8px 18px 8px 12px; 737 | min-width: 86px; 738 | } 739 | 740 | details > h4 { 741 | border-bottom: 1px solid var(--sdpi-bordercolor); 742 | } 743 | 744 | legend { 745 | display: none; 746 | } 747 | .sdpi-item-value > textarea { 748 | padding: 0px; 749 | width: 227px; 750 | margin-left: 1px; 751 | } 752 | 753 | input[type="radio"] + label span, 754 | input[type="checkbox"] + label span { 755 | display: inline-block; 756 | width: 16px; 757 | height: 16px; 758 | margin: 2px 4px 2px 0; 759 | border-radius: 3px; 760 | vertical-align: middle; 761 | background: var(--sdpi-background); 762 | cursor: pointer; 763 | border: 1px solid rgb(0,0,0,.2); 764 | } 765 | 766 | input[type="radio"] + label span { 767 | border-radius: 100%; 768 | } 769 | 770 | input[type="radio"]:checked + label span, 771 | input[type="checkbox"]:checked + label span { 772 | background-color: #77f; 773 | background-image: url(check.svg); 774 | background-repeat: no-repeat; 775 | background-position: center center; 776 | border: 1px solid rgb(0,0,0,.4); 777 | } 778 | 779 | input[type="radio"]:active:checked + label span, 780 | input[type="radio"]:active + label span, 781 | input[type="checkbox"]:active:checked + label span, 782 | input[type="checkbox"]:active + label span { 783 | background-color: #303030; 784 | } 785 | 786 | input[type="radio"]:checked + label span { 787 | background-image: url(rcheck.svg); 788 | } 789 | 790 | 791 | /* 792 | input[type="radio"] + label span { 793 | background: url(buttons.png) -38px top no-repeat; 794 | } 795 | 796 | input[type="radio"]:checked + label span { 797 | background: url(buttons.png) -57px top no-repeat; 798 | } 799 | */ 800 | 801 | input[type="range"] { 802 | width: var(--sdpi-width); 803 | height: 30px; 804 | overflow: hidden; 805 | cursor: pointer; 806 | background: transparent !important; 807 | } 808 | 809 | .sdpi-item > input[type="range"] { 810 | margin-left: 8px; 811 | max-width: var(--sdpi-width); 812 | width: var(--sdpi-width); 813 | padding: 0px; 814 | } 815 | 816 | /* 817 | input[type="range"], 818 | input[type="range"]::-webkit-slider-runnable-track, 819 | input[type="range"]::-webkit-slider-thumb { 820 | -webkit-appearance: none; 821 | } 822 | */ 823 | 824 | input[type="range"]::-webkit-slider-runnable-track { 825 | height: 5px; 826 | background: #979797; 827 | border-radius: 3px; 828 | padding:0px !important; 829 | border: 1px solid var(--sdpi-background); 830 | } 831 | 832 | input[type="range"]::-webkit-slider-thumb { 833 | position: relative; 834 | -webkit-appearance: none; 835 | background-color: var(--sdpi-color); 836 | width: 12px; 837 | height: 12px; 838 | border-radius: 20px; 839 | margin-top: -5px; 840 | border: none; 841 | 842 | } 843 | input[type="range" i]{ 844 | margin: 0; 845 | } 846 | 847 | input[type="range"]::-webkit-slider-thumb::before { 848 | position: absolute; 849 | content: ""; 850 | height: 5px; /* equal to height of runnable track or 1 less */ 851 | width: 500px; /* make this bigger than the widest range input element */ 852 | left: -502px; /* this should be -2px - width */ 853 | top: 8px; /* don't change this */ 854 | background: #77f; 855 | } 856 | 857 | input[type="color"] { 858 | min-width: 32px; 859 | min-height: 32px; 860 | width: 32px; 861 | height: 32px; 862 | padding: 0; 863 | background-color: var(--sdpi-bgcolor); 864 | flex: none; 865 | } 866 | 867 | ::-webkit-color-swatch { 868 | min-width: 24px; 869 | } 870 | 871 | textarea { 872 | height: 3em; 873 | word-break: break-word; 874 | line-height: 1.5em; 875 | } 876 | 877 | .textarea { 878 | padding: 0px !important; 879 | } 880 | 881 | textarea { 882 | width: 221px; /*98%;*/ 883 | height: 96%; 884 | min-height: 6em; 885 | resize: none; 886 | border-radius: var(--sdpi-borderradius); 887 | } 888 | 889 | /* CAROUSEL */ 890 | 891 | .sdpi-item[type="carousel"]{ 892 | 893 | } 894 | 895 | .sdpi-item.card-carousel-wrapper, 896 | .sdpi-item > .card-carousel-wrapper { 897 | padding: 0; 898 | } 899 | 900 | 901 | .card-carousel-wrapper { 902 | display: flex; 903 | align-items: center; 904 | justify-content: center; 905 | margin: 12px auto; 906 | color: #666a73; 907 | } 908 | 909 | .card-carousel { 910 | display: flex; 911 | justify-content: center; 912 | width: 278px; 913 | } 914 | .card-carousel--overflow-container { 915 | overflow: hidden; 916 | } 917 | .card-carousel--nav__left, 918 | .card-carousel--nav__right { 919 | /* display: inline-block; */ 920 | width: 12px; 921 | height: 12px; 922 | border-top: 2px solid #42b883; 923 | border-right: 2px solid #42b883; 924 | cursor: pointer; 925 | margin: 0 4px; 926 | transition: transform 150ms linear; 927 | } 928 | .card-carousel--nav__left[disabled], 929 | .card-carousel--nav__right[disabled] { 930 | opacity: 0.2; 931 | border-color: black; 932 | } 933 | .card-carousel--nav__left { 934 | transform: rotate(-135deg); 935 | } 936 | .card-carousel--nav__left:active { 937 | transform: rotate(-135deg) scale(0.85); 938 | } 939 | .card-carousel--nav__right { 940 | transform: rotate(45deg); 941 | } 942 | .card-carousel--nav__right:active { 943 | transform: rotate(45deg) scale(0.85); 944 | } 945 | .card-carousel-cards { 946 | display: flex; 947 | transition: transform 150ms ease-out; 948 | transform: translatex(0px); 949 | } 950 | .card-carousel-cards .card-carousel--card { 951 | margin: 0 5px; 952 | cursor: pointer; 953 | /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ 954 | background-color: #fff; 955 | border-radius: 4px; 956 | z-index: 3; 957 | } 958 | .xxcard-carousel-cards .card-carousel--card:first-child { 959 | margin-left: 0; 960 | } 961 | .xxcard-carousel-cards .card-carousel--card:last-child { 962 | margin-right: 0; 963 | } 964 | .card-carousel-cards .card-carousel--card img { 965 | vertical-align: bottom; 966 | border-top-left-radius: 4px; 967 | border-top-right-radius: 4px; 968 | transition: opacity 150ms linear; 969 | width: 60px; 970 | } 971 | .card-carousel-cards .card-carousel--card img:hover { 972 | opacity: 0.5; 973 | } 974 | .card-carousel-cards .card-carousel--card--footer { 975 | border-top: 0; 976 | max-width: 80px; 977 | overflow: hidden; 978 | display: flex; 979 | height: 100%; 980 | flex-direction: column; 981 | } 982 | .card-carousel-cards .card-carousel--card--footer p { 983 | padding: 3px 0; 984 | margin: 0; 985 | margin-bottom: 2px; 986 | font-size: 15px; 987 | font-weight: 500; 988 | color: #2c3e50; 989 | } 990 | .card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) { 991 | font-size: 12px; 992 | font-weight: 300; 993 | padding: 6px; 994 | color: #666a73; 995 | } 996 | 997 | 998 | h1 { 999 | font-size: 1.3em; 1000 | font-weight: 500; 1001 | text-align: center; 1002 | margin-bottom: 12px; 1003 | } 1004 | 1005 | ::-webkit-datetime-edit { 1006 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 1007 | background: url(elg_calendar_inv.svg) no-repeat left center; 1008 | padding-right: 1em; 1009 | padding-left: 25px; 1010 | background-position: 4px 0px; 1011 | } 1012 | ::-webkit-datetime-edit-fields-wrapper { 1013 | 1014 | } 1015 | ::-webkit-datetime-edit-text { padding: 0 0.3em; } 1016 | ::-webkit-datetime-edit-month-field { } 1017 | ::-webkit-datetime-edit-day-field {} 1018 | ::-webkit-datetime-edit-year-field {} 1019 | ::-webkit-inner-spin-button { 1020 | 1021 | /* display: none; */ 1022 | } 1023 | ::-webkit-calendar-picker-indicator { 1024 | background: transparent; 1025 | font-size: 17px; 1026 | } 1027 | 1028 | ::-webkit-calendar-picker-indicator:focus { 1029 | background-color: rgba(0,0,0,0.2); 1030 | } 1031 | 1032 | input[type="date"] { 1033 | -webkit-align-items: center; 1034 | display: -webkit-inline-flex; 1035 | font-family: monospace; 1036 | overflow: hidden; 1037 | padding: 0; 1038 | -webkit-padding-start: 1px; 1039 | } 1040 | 1041 | input::-webkit-datetime-edit { 1042 | -webkit-flex: 1; 1043 | -webkit-user-modify: read-only !important; 1044 | display: inline-block; 1045 | min-width: 0; 1046 | overflow: hidden; 1047 | } 1048 | 1049 | /* 1050 | input::-webkit-datetime-edit-fields-wrapper { 1051 | -webkit-user-modify: read-only !important; 1052 | display: inline-block; 1053 | padding: 1px 0; 1054 | white-space: pre; 1055 | 1056 | } 1057 | */ 1058 | 1059 | /* 1060 | input[type="date"] { 1061 | background-color: red; 1062 | outline: none; 1063 | } 1064 | 1065 | input[type="date"]::-webkit-clear-button { 1066 | font-size: 18px; 1067 | height: 30px; 1068 | position: relative; 1069 | } 1070 | 1071 | input[type="date"]::-webkit-inner-spin-button { 1072 | height: 28px; 1073 | } 1074 | 1075 | input[type="date"]::-webkit-calendar-picker-indicator { 1076 | font-size: 15px; 1077 | } */ 1078 | 1079 | input[type="file"] { 1080 | opacity: 0; 1081 | display: none; 1082 | } 1083 | 1084 | .sdpi-item > input[type="file"] { 1085 | opacity: 1; 1086 | display: flex; 1087 | } 1088 | 1089 | input[type="file"] + span { 1090 | display: flex; 1091 | flex: 0 1 auto; 1092 | background-color: #0000ff50; 1093 | } 1094 | 1095 | label.sdpi-file-label { 1096 | cursor: pointer; 1097 | user-select: none; 1098 | display: inline-block; 1099 | min-height: 21px !important; 1100 | height: 21px !important; 1101 | line-height: 20px; 1102 | padding: 0px 4px; 1103 | margin: auto; 1104 | margin-right: 0px; 1105 | float:right; 1106 | } 1107 | 1108 | .sdpi-file-label > label:active, 1109 | .sdpi-file-label.file:active, 1110 | label.sdpi-file-label:active, 1111 | label.sdpi-file-info:active, 1112 | input[type="file"]::-webkit-file-upload-button:active, 1113 | button:active { 1114 | background-color: var(--sdpi-color); 1115 | color:#303030; 1116 | } 1117 | 1118 | 1119 | input:required:invalid, input:focus:invalid { 1120 | background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center; 1121 | } 1122 | 1123 | input:required:valid { 1124 | background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center; 1125 | } 1126 | 1127 | .tooltip, 1128 | :tooltip, 1129 | :title { 1130 | color: yellow; 1131 | } 1132 | 1133 | [title]:hover { 1134 | display: flex; 1135 | align-items: center; 1136 | justify-content: center; 1137 | } 1138 | 1139 | [title]:hover::after { 1140 | content: ''; 1141 | position: absolute; 1142 | bottom: -1000px; 1143 | left: 8px; 1144 | display: none; 1145 | color: #fff; 1146 | border: 8px solid transparent; 1147 | border-bottom: 8px solid #000; 1148 | } 1149 | [title]:hover::before { 1150 | content: attr(title); 1151 | display: flex; 1152 | justify-content: center; 1153 | align-self: center; 1154 | padding: 6px 12px; 1155 | border-radius: 5px; 1156 | background: rgba(0,0,0,0.8); 1157 | color: var(--sdpi-color); 1158 | font-size: 9pt; 1159 | font-family: sans-serif; 1160 | opacity: 1; 1161 | position: absolute; 1162 | height: auto; 1163 | /* width: 50%; 1164 | left: 35%; */ 1165 | text-align: center; 1166 | bottom: 2px; 1167 | z-index: 100; 1168 | box-shadow: 0px 3px 6px rgba(0, 0, 0, .5); 1169 | /* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); */ 1170 | } 1171 | 1172 | .sdpi-item-group.file { 1173 | width: 232px; 1174 | display: flex; 1175 | align-items: center; 1176 | } 1177 | 1178 | .sdpi-file-info { 1179 | overflow-wrap: break-word; 1180 | word-wrap: break-word; 1181 | hyphens: auto; 1182 | 1183 | min-width: 132px; 1184 | max-width: 144px; 1185 | max-height: 32px; 1186 | margin-top: 0px; 1187 | margin-left: 5px; 1188 | display: inline-block; 1189 | overflow: hidden; 1190 | padding: 6px 4px; 1191 | background-color: var(--sdpi-background); 1192 | } 1193 | 1194 | 1195 | ::-webkit-scrollbar { 1196 | width: 8px; 1197 | } 1198 | 1199 | ::-webkit-scrollbar-track { 1200 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 1201 | } 1202 | 1203 | ::-webkit-scrollbar-thumb { 1204 | background-color: #999999; 1205 | outline: 1px solid slategrey; 1206 | border-radius: 8px; 1207 | } 1208 | 1209 | a { 1210 | color: #7397d2; 1211 | } 1212 | 1213 | .testcontainer { 1214 | display: flex; 1215 | background-color: #0000ff20; 1216 | max-width: 400px; 1217 | height: 200px; 1218 | align-content: space-evenly; 1219 | } 1220 | 1221 | input[type=range] { 1222 | -webkit-appearance: none; 1223 | /* background-color: green; */ 1224 | height:6px; 1225 | margin-top: 12px; 1226 | z-index: 0; 1227 | overflow: visible; 1228 | } 1229 | 1230 | /* 1231 | input[type="range"]::-webkit-slider-thumb { 1232 | -webkit-appearance: none; 1233 | background-color: var(--sdpi-color); 1234 | width: 12px; 1235 | height: 12px; 1236 | border-radius: 20px; 1237 | margin-top: -6px; 1238 | border: none; 1239 | } */ 1240 | 1241 | :-webkit-slider-thumb { 1242 | -webkit-appearance: none; 1243 | background-color: var(--sdpi-color); 1244 | width: 16px; 1245 | height: 16px; 1246 | border-radius: 20px; 1247 | margin-top: -6px; 1248 | border: 1px solid #999999; 1249 | } 1250 | 1251 | .sdpi-item[type="range"] .sdpi-item-group { 1252 | display: flex; 1253 | flex-direction: column; 1254 | } 1255 | 1256 | .xxsdpi-item[type="range"] .sdpi-item-group input { 1257 | max-width: 204px; 1258 | } 1259 | 1260 | .sdpi-item[type="range"] .sdpi-item-group span { 1261 | margin-left: 0px !important; 1262 | } 1263 | 1264 | .sdpi-item[type="range"] .sdpi-item-group > .sdpi-item-child { 1265 | display: flex; 1266 | flex-direction: row; 1267 | } 1268 | 1269 | :disabled { 1270 | color: #993333; 1271 | } 1272 | 1273 | select, 1274 | select option { 1275 | color: var(--sdpi-color); 1276 | } 1277 | 1278 | select.disabled, 1279 | select option:disabled { 1280 | color: #fd9494; 1281 | font-style: italic; 1282 | } 1283 | 1284 | .runningAppsContainer { 1285 | display: none; 1286 | } 1287 | 1288 | /* debug 1289 | div { 1290 | background-color: rgba(64,128,255,0.2); 1291 | } 1292 | */ 1293 | 1294 | .min80 > .sdpi-item-child { 1295 | min-width: 80px; 1296 | } 1297 | 1298 | .min100 > .sdpi-item-child { 1299 | min-width: 100px; 1300 | } 1301 | 1302 | .min120 > .sdpi-item-child { 1303 | min-width: 120px; 1304 | } 1305 | 1306 | .min140 > .sdpi-item-child { 1307 | min-width: 140px; 1308 | } 1309 | 1310 | .min160 > .sdpi-item-child { 1311 | min-width: 160px; 1312 | } 1313 | 1314 | .min200 > .sdpi-item-child { 1315 | min-width: 200px; 1316 | } 1317 | 1318 | .max40 { 1319 | flex-basis: 40%; 1320 | flex-grow: 0; 1321 | } 1322 | 1323 | .max30 { 1324 | flex-basis: 30%; 1325 | flex-grow: 0; 1326 | } 1327 | 1328 | .max20 { 1329 | flex-basis: 20%; 1330 | flex-grow: 0; 1331 | } 1332 | 1333 | .up20 { 1334 | margin-top: -20px; 1335 | } 1336 | 1337 | .alignCenter { 1338 | align-items: center; 1339 | } 1340 | 1341 | .alignTop { 1342 | align-items: flex-start; 1343 | } 1344 | 1345 | .alignBaseline { 1346 | align-items: baseline; 1347 | } 1348 | 1349 | .noMargins, 1350 | .noMargins *, 1351 | .noInnerMargins * { 1352 | margin: 0; 1353 | padding: 0; 1354 | } 1355 | 1356 | 1357 | /** 1358 | input[type=range].vVertical { 1359 | -webkit-appearance: none; 1360 | background-color: green; 1361 | margin-left: -60px; 1362 | width: 100px; 1363 | height:6px; 1364 | margin-top: 0px; 1365 | transform:rotate(90deg); 1366 | z-index: 0; 1367 | overflow: visible; 1368 | } 1369 | 1370 | input[type=range].vHorizon { 1371 | -webkit-appearance: none; 1372 | background-color: pink; 1373 | height: 10px; 1374 | width:200px; 1375 | 1376 | } 1377 | 1378 | .test2 { 1379 | background-color: #00ff0020; 1380 | display: flex; 1381 | } 1382 | 1383 | 1384 | .vertical.sdpi-item[type="range"] .sdpi-item-value { 1385 | display: block; 1386 | } 1387 | 1388 | 1389 | .vertical.sdpi-item:first-child, 1390 | .vertical { 1391 | margin-top: 12px; 1392 | margin-bottom: 16px; 1393 | } 1394 | .vertical > .sdpi-item-value { 1395 | margin-right: 16px; 1396 | } 1397 | 1398 | .vertical .sdpi-item-group { 1399 | width: 100%; 1400 | display: flex; 1401 | justify-content: space-evenly; 1402 | } 1403 | 1404 | .vertical input[type=range] { 1405 | height: 100px; 1406 | width: 21px; 1407 | -webkit-appearance: slider-vertical; 1408 | display: flex; 1409 | flex-flow: column; 1410 | } 1411 | 1412 | .vertical input[type="range"]::-webkit-slider-runnable-track { 1413 | height: auto; 1414 | width: 5px; 1415 | } 1416 | 1417 | .vertical input[type="range"]::-webkit-slider-thumb { 1418 | margin-top: 0px; 1419 | margin-left: -6px; 1420 | } 1421 | 1422 | .vertical .sdpi-item-value { 1423 | flex-flow: column; 1424 | align-items: flex-start; 1425 | } 1426 | 1427 | .vertical.sdpi-item[type="range"] .sdpi-item-value { 1428 | align-items: center; 1429 | margin-right: 16px; 1430 | text-align: center; 1431 | } 1432 | 1433 | .vertical.sdpi-item[type="range"] .sdpi-item-value span, 1434 | .vertical input[type="range"] .sdpi-item-value span { 1435 | text-align: center; 1436 | margin: 4px 0px; 1437 | } 1438 | */ 1439 | 1440 | /* 1441 | .file { 1442 | box-sizing: border-box; 1443 | display: block; 1444 | overflow: hidden; 1445 | padding: 10px; 1446 | position: relative; 1447 | text-indent: 100%; 1448 | white-space: nowrap; 1449 | height: 190px; 1450 | width: 160px; 1451 | } 1452 | .file::before { 1453 | content: ""; 1454 | display: block; 1455 | position: absolute; 1456 | top: 10px; 1457 | left: 10px; 1458 | height: 170px; 1459 | width: 140px; 1460 | } 1461 | .file::after { 1462 | content: ""; 1463 | height: 90px; 1464 | width: 90px; 1465 | position: absolute; 1466 | right: 0; 1467 | bottom: 0; 1468 | overflow: visible; 1469 | } 1470 | 1471 | .list--files { 1472 | display: flex; 1473 | flex-wrap: wrap; 1474 | justify-content: center; 1475 | margin: auto; 1476 | padding: 30px 0; 1477 | width: 630px; 1478 | } 1479 | .list--files > li { 1480 | margin: 0; 1481 | padding: 15px; 1482 | } 1483 | 1484 | .type-document::before { 1485 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNzBweCIgdmlld0JveD0iMCAwIDE0MCAxNzAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0E3QTlBQyIgZD0iTTAsMHYxNzBoMTQwVjBIMHogTTEzMCwxNjBIMTBWMTBoMTIwVjE2MHogTTExMCw0MEgzMFYzMGg4MFY0MHogTTExMCw2MEgzMFY1MGg4MFY2MHogTTExMCw4MEgzMFY3MGg4MFY4MHoNCiAgIE0xMTAsMTAwSDMwVjkwaDgwVjEwMHogTTExMCwxMjBIMzB2LTEwaDgwVjEyMHogTTkwLDE0MEgzMHYtMTBoNjBWMTQweiIvPg0KPC9zdmc+); 1486 | } 1487 | 1488 | .type-image { 1489 | height: 160px; 1490 | } 1491 | .type-image::before { 1492 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNDBweCIgdmlld0JveD0iMCAwIDE0MCAxNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE0MCAxNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxwYXRoIGZpbGw9IiNBN0E5QUMiIGQ9Ik0wLDB2MTQwaDE0MFYwSDB6IE0xMzAsMTMwSDEwVjEwaDEyMFYxMzB6Ii8+DQogIDxwb2x5Z29uIGZpbGw9IiNFNkU3RTgiIHBvaW50cz0iOTAsMTEwIDQwLDQwIDEwLDgwIDEwLDEzMCA5MCwxMzAgICIvPg0KICA8cG9seWdvbiBmaWxsPSIjRDFEM0Q0IiBwb2ludHM9IjEwLDEzMCA1MCw5MCA2MCwxMDAgMTAwLDYwIDEzMCwxMzAgICIvPg0KPC9nPg0KPC9zdmc+); 1493 | height: 140px; 1494 | } 1495 | 1496 | .state-synced::after { 1497 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzAwQTY1MSIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0yMCw0NUwyMCw0NWMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAuMSwxMC4xYzIuNywyLjcsNy4yLDIuNyw5LjksMEw3MCwzNWMyLjgtMi44LDIuOC03LjIsMC0xMGwwLDANCiAgICBjLTIuOC0yLjgtNy4yLTIuOC0xMCwwTDM1LDUwbC01LTVDMjcuMiw0Mi4yLDIyLjgsNDIuMiwyMCw0NXoiLz4NCjwvZz4NCjwvc3ZnPg==); 1498 | } 1499 | 1500 | .state-deleted::after { 1501 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iI0VEMUMyNCIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik02NSwyNUw2NSwyNWMtMi44LTIuOC03LjItMi44LTEwLDBMNDUsMzVMMzUsMjVjLTIuOC0yLjgtNy4yLTIuOC0xMCwwbDAsMGMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAsMTANCiAgICBMMjUsNTVjLTIuOCwyLjgtMi44LDcuMiwwLDEwbDAsMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwxMC0xMGwxMCwxMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwwLDBjMi44LTIuOCwyLjgtNy4yLDAtMTBMNTUsNDVsMTAtMTANCiAgICBDNjcuOCwzMi4yLDY3LjgsMjcuOCw2NSwyNXoiLz4NCjwvZz4NCjwvc3ZnPg==); 1502 | } 1503 | .state-deleted::before { 1504 | opacity: .25; 1505 | } 1506 | 1507 | .state-locked::after { 1508 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzU4NTk1QiIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxyZWN0IHg9IjIwIiB5PSI0MCIgZmlsbD0iI0ZGRkZGRiIgd2lkdGg9IjUwIiBoZWlnaHQ9IjMwIi8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0zMi41LDQ2LjVjLTIuOCwwLTUtMi4yLTUtNVYyOWMwLTkuNiw3LjktMTcuNSwxNy41LTE3LjVTNjIuNSwxOS40LDYyLjUsMjljMCwyLjgtMi4yLDUtNSw1cy01LTIuMi01LTUNCiAgICBjMC00LjEtMy40LTcuNS03LjUtNy41cy03LjUsMy40LTcuNSw3LjV2MTIuNUMzNy41LDQ0LjMsMzUuMyw0Ni41LDMyLjUsNDYuNXoiLz4NCjwvZz4NCjwvc3ZnPg==); 1509 | } 1510 | 1511 | 1512 | 1513 | html { 1514 | --fheight: 95px; 1515 | --fwidth: 80px; 1516 | --fspacing: 5px; 1517 | --ftotalwidth: 315px; 1518 | --bgsize: 50%; 1519 | --bgsize2: cover; 1520 | --bgsize3: contain; 1521 | } 1522 | 1523 | ul { 1524 | list-style: none; 1525 | } 1526 | 1527 | 1528 | .file { 1529 | height: var(--fheight); 1530 | width: var(--fwidth); 1531 | } 1532 | .file::before { 1533 | content: ""; 1534 | display: block; 1535 | position: absolute; 1536 | top: var(--fspacing); 1537 | left: var(--fspacing); 1538 | height: calc(var(--fheight) - var(--fspacing)*2); 1539 | width: calc(var(--fwidth) - var(--fspacing)*2); 1540 | } 1541 | .file::after { 1542 | content: ""; 1543 | height: calc(var(--fheight)/2); 1544 | width: calc(var(--fheight)/2); 1545 | position: absolute; 1546 | right: 0; 1547 | bottom: 0; 1548 | overflow: visible; 1549 | } 1550 | 1551 | .list--files { 1552 | display: flex; 1553 | flex-wrap: wrap; 1554 | justify-content: center; 1555 | margin: auto; 1556 | padding: calc(var(--fspacing)*3) 0; 1557 | width: var(--ftotalwidth); 1558 | } 1559 | .list--files > li { 1560 | margin: 0; 1561 | padding: var(--fspacing); 1562 | } 1563 | 1564 | .type-document::before { 1565 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNzBweCIgdmlld0JveD0iMCAwIDE0MCAxNzAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZmlsbD0iI0E3QTlBQyIgZD0iTTAsMHYxNzBoMTQwVjBIMHogTTEzMCwxNjBIMTBWMTBoMTIwVjE2MHogTTExMCw0MEgzMFYzMGg4MFY0MHogTTExMCw2MEgzMFY1MGg4MFY2MHogTTExMCw4MEgzMFY3MGg4MFY4MHoNCiAgIE0xMTAsMTAwSDMwVjkwaDgwVjEwMHogTTExMCwxMjBIMzB2LTEwaDgwVjEyMHogTTkwLDE0MEgzMHYtMTBoNjBWMTQweiIvPg0KPC9zdmc+); 1566 | height: calc(var(--fheight) - var(--fspacing)*2); 1567 | background-size: var(--bgsize2); 1568 | background-repeat: no-repeat; 1569 | } 1570 | 1571 | .type-image { 1572 | height: var(--fwidth); 1573 | height: calc(var(--fheight) - var(--fspacing)*2); 1574 | } 1575 | .type-image::before { 1576 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDBweCIgaGVpZ2h0PSIxNDBweCIgdmlld0JveD0iMCAwIDE0MCAxNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE0MCAxNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxwYXRoIGZpbGw9IiNBN0E5QUMiIGQ9Ik0wLDB2MTQwaDE0MFYwSDB6IE0xMzAsMTMwSDEwVjEwaDEyMFYxMzB6Ii8+DQogIDxwb2x5Z29uIGZpbGw9IiNFNkU3RTgiIHBvaW50cz0iOTAsMTEwIDQwLDQwIDEwLDgwIDEwLDEzMCA5MCwxMzAgICIvPg0KICA8cG9seWdvbiBmaWxsPSIjRDFEM0Q0IiBwb2ludHM9IjEwLDEzMCA1MCw5MCA2MCwxMDAgMTAwLDYwIDEzMCwxMzAgICIvPg0KPC9nPg0KPC9zdmc+); 1577 | height: calc(var(--fheight) - var(--fspacing)*2); 1578 | background-size: var(--bgsize3); 1579 | background-repeat: no-repeat; 1580 | } 1581 | 1582 | .state-synced::after { 1583 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzAwQTY1MSIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0yMCw0NUwyMCw0NWMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAuMSwxMC4xYzIuNywyLjcsNy4yLDIuNyw5LjksMEw3MCwzNWMyLjgtMi44LDIuOC03LjIsMC0xMGwwLDANCiAgICBjLTIuOC0yLjgtNy4yLTIuOC0xMCwwTDM1LDUwbC01LTVDMjcuMiw0Mi4yLDIyLjgsNDIuMiwyMCw0NXoiLz4NCjwvZz4NCjwvc3ZnPg==); 1584 | background-size: var(--bgsize); 1585 | background-repeat: no-repeat; 1586 | background-position: bottom right; 1587 | } 1588 | 1589 | .state-deleted::after { 1590 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iI0VEMUMyNCIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik02NSwyNUw2NSwyNWMtMi44LTIuOC03LjItMi44LTEwLDBMNDUsMzVMMzUsMjVjLTIuOC0yLjgtNy4yLTIuOC0xMCwwbDAsMGMtMi44LDIuOC0yLjgsNy4yLDAsMTBsMTAsMTANCiAgICBMMjUsNTVjLTIuOCwyLjgtMi44LDcuMiwwLDEwbDAsMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwxMC0xMGwxMCwxMGMyLjgsMi44LDcuMiwyLjgsMTAsMGwwLDBjMi44LTIuOCwyLjgtNy4yLDAtMTBMNTUsNDVsMTAtMTANCiAgICBDNjcuOCwzMi4yLDY3LjgsMjcuOCw2NSwyNXoiLz4NCjwvZz4NCjwvc3ZnPg==); 1591 | background-size: var(--bgsize); 1592 | background-repeat: no-repeat; 1593 | background-position: bottom right; 1594 | } 1595 | .state-deleted::before { 1596 | opacity: .25; 1597 | } 1598 | 1599 | .state-locked::after { 1600 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczphPSJodHRwOi8vbnMuYWRvYmUuY29tL0Fkb2JlU1ZHVmlld2VyRXh0ZW5zaW9ucy8zLjAvIg0KICAgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI5MHB4IiBoZWlnaHQ9IjkwcHgiIHZpZXdCb3g9IjAgMCA5MCA5MCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgOTAgOTAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQogIDxjaXJjbGUgZmlsbD0iIzU4NTk1QiIgY3g9IjQ1IiBjeT0iNDUiIHI9IjQ1Ii8+DQogIDxyZWN0IHg9IjIwIiB5PSI0MCIgZmlsbD0iI0ZGRkZGRiIgd2lkdGg9IjUwIiBoZWlnaHQ9IjMwIi8+DQogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0zMi41LDQ2LjVjLTIuOCwwLTUtMi4yLTUtNVYyOWMwLTkuNiw3LjktMTcuNSwxNy41LTE3LjVTNjIuNSwxOS40LDYyLjUsMjljMCwyLjgtMi4yLDUtNSw1cy01LTIuMi01LTUNCiAgICBjMC00LjEtMy40LTcuNS03LjUtNy41cy03LjUsMy40LTcuNSw3LjV2MTIuNUMzNy41LDQ0LjMsMzUuMyw0Ni41LDMyLjUsNDYuNXoiLz4NCjwvZz4NCjwvc3ZnPg==); 1601 | background-size: var(--bgsize); 1602 | background-repeat: no-repeat; 1603 | background-position: bottom right; 1604 | } 1605 | */ --------------------------------------------------------------------------------