"use strict"; import React, { Component } from "react"; import { AirPlayAuthCredentials } from "../../lib/auth" import * as base64 from "../../lib/base64"; import * as devices from "./devices"; const _ = browser.i18n.getMessage; interface AirPlayDeviceProps { data: devices.Device; onRemove: (device: devices.Device) => void; onRegenCredentials: (device: devices.Device) => void; onPairCredentials: (device: devices.Device) => void; } const AirPlayDevice = (props: AirPlayDeviceProps) => { const clientSk = base64.encode(props.data.credentials.clientSk); const clientPk = base64.encode(props.data.credentials.clientPk); const pairedStatusClassName = !props.data.isPaired ? "device__paired-status" : "device__paired-status device__paired-status--paired"; function copyCredentials () { navigator.clipboard.writeText( ` Client ID: ${props.data.credentials.clientId}\n` + `Private Key: ${clientSk}\n` + ` Public Key: ${clientPk}`); } return (
{ props.data.isPaired || }
{ props.data.name } { props.data.isPaired ? _("optionsAirPlayDevicePairedStatusPaired") : _("optionsAirPlayDevicePairedStatusUnpaired") }
{ props.data.address }
{ _("optionsAirPlayDeviceCredentials") }
{ _("optionsAirPlayDeviceCredentialsClientId") } { props.data.credentials.clientId }
{ _("optionsAirPlayDeviceCredentialsPrivateKey") } { clientSk }
{ _("optionsAirPlayDeviceCredentialsPublicKey") } { clientPk }
); }; interface AirPlayDeviceManagerProps {} interface AirPlayDeviceManagerState { hasLoaded: boolean; isFormValid: boolean; devices: devices.Device[]; newDeviceName: string; newDeviceAddress: string; newDeviceAddressSuggestion: string; } export default class AirPlayDeviceManager extends Component< AirPlayDeviceManagerProps, AirPlayDeviceManagerState> { constructor (props: AirPlayDeviceManagerProps) { super(props); this.onFormSubmit = this.onFormSubmit.bind(this); this.onFormInput = this.onFormInput.bind(this); this.onDeviceAdd = this.onDeviceAdd.bind(this); this.onDeviceRemove = this.onDeviceRemove.bind(this); this.onDeviceRegenCredentials = this.onDeviceRegenCredentials.bind(this); this.onDevicePairCredentials = this.onDevicePairCredentials.bind(this); this.onNewDeviceNameChange = this.onNewDeviceNameChange.bind(this); this.onNewDeviceAddressChange = this.onNewDeviceAddressChange.bind(this); this.state = { hasLoaded: false , isFormValid: false , devices: [] , newDeviceName: "" , newDeviceAddress: "" , newDeviceAddressSuggestion: "" }; } public render () { if (!this.state.hasLoaded) { return; } return (
); } public async componentDidMount () { this.setState({ hasLoaded: true , devices: await devices.getAll() }); } private onFormSubmit (ev: React.FormEvent) { ev.preventDefault(); if (ev.currentTarget.reportValidity()) { this.onDeviceAdd(); } } private onFormInput (ev: React.ChangeEvent) { this.setState({ isFormValid: ev.currentTarget.reportValidity() }); } private async onDeviceAdd () { const device: devices.Device = { name: this.state.newDeviceName , address: this.state.newDeviceAddress , credentials: new AirPlayAuthCredentials() , isPaired: false }; // Use generated suggested address if user left it blank if (!this.state.newDeviceAddress) { device.address = this.state.newDeviceAddressSuggestion; } await devices.add(device); this.setState({ devices: await devices.getAll() }); } private onDeviceRemove (device: devices.Device) { this.setState(state => ({ devices: state.devices.filter(d => d.name !== device.name) })); devices.remove(device); } private onDeviceRegenCredentials (device: devices.Device) { this.setState(state => { devices: state.devices.map(d => { /** * Generate and set new credentials with an empty * AirPlayAuthCredentials call. Then, since the new credentials * aren't paired, set isPaired to false. */ if (d.name === device.name) { d.credentials = new AirPlayAuthCredentials(); d.isPaired = false; } return d; }); }); } private onDevicePairCredentials () {} /** * Each time the new device name field is changed, provided the * user has not already set an address in the address field, * make a best-guess at what the actual address is and set the * placeholder attribute of the address field to display it as * indication. * * If, once the new device form is submitted, the address field * was left blank, the suggested address is used instead. */ private onNewDeviceNameChange (ev: React.ChangeEvent) { this.setState({ newDeviceName: ev.target.value }); if (!this.state.newDeviceAddress) { /** * Use the same-ish formatting rules as macOS for service * names: * - Consecutive whitespace is replaced by a single space. No * need for tabs or newlines. * - All remaining spaces are replaced by hyphens. * - Any characters that aren't alpha-numerics or hyphens are * removed. */ const formattedName = ev.target.value .replace(/\s{2,}/g, " ") .replace(/ /g, "-") .replace(/[^a-zA-Z0-9-]/g, ""); // Set new suggestion this.setState({ newDeviceAddressSuggestion: `${formattedName}.local` }); } } private onNewDeviceAddressChange (ev: React.ChangeEvent) { this.setState({ newDeviceAddress: ev.target.value }); } }