Skip to content

Commit

Permalink
Make Snapserver host editable
Browse files Browse the repository at this point in the history
  • Loading branch information
badaix committed Mar 18, 2024
1 parent c781261 commit 68b69ba
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 154 deletions.
2 changes: 1 addition & 1 deletion src/components/Client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function Client(props: ClientProps) {
if (!props.client.connected)
menuitems.push(<MenuItem key='Menu-Delete' onClick={() => { props.onDelete(); setAnchorEl(null); setOpen(false); }}>Delete</MenuItem>);

console.debug("Render Client " + props.client.host.name + ", id: " + props.client.id);
// console.debug("Render Client " + props.client.host.name + ", id: " + props.client.id);

return (
<Box sx={{ opacity: props.client.connected ? 1.0 : 0.5 }} >
Expand Down
2 changes: 1 addition & 1 deletion src/components/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export default function Group(props: GroupProps) {
return clients;
}

console.debug("Render Group " + props.group.id);
// console.debug("Render Group " + props.group.id);
const groupClients = [];

for (const client of getClients()) {
Expand Down
52 changes: 52 additions & 0 deletions src/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Button, Dialog, DialogActions, DialogContent, TextField, DialogTitle, MenuItem, Select, SelectChangeEvent, InputLabel, FormControl, FormControlLabel, Checkbox } from '@mui/material';
import { useState } from 'react';
import { config, Theme } from '../config.ts';

export default function SettingsDialog(props: { open: boolean, onClose: (_apply: boolean) => void }) {
const [serverurl, setServerurl] = useState(config.baseUrl);
const [theme, setTheme] = useState(config.theme);
const [showOffline, setShowOffline] = useState(config.showOffline);

function handleClose(apply: boolean) {
if (apply) {
config.baseUrl = serverurl;
config.theme = theme;
config.showOffline = showOffline;
}
props.onClose(apply);
}

return (
<div>
<Dialog open={props.open} >
<DialogTitle>Settings</DialogTitle>
<DialogContent dividers>
<TextField
autoFocus margin="dense" id="host" label="Snapserver host" type="text" fullWidth variant="standard"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { setServerurl(event.target.value as string) }}
value={serverurl}
/>
<FormControl variant="standard" fullWidth sx={{ minWidth: 100 }}>
<InputLabel id="theme-label">Theme</InputLabel>
<Select
labelId="theme-select-label"
id="demo-theme-select"
value={theme}
label="Theme"
onChange={(event: SelectChangeEvent<Theme>) => { console.log("Theme selected: " + event.target.value); setTheme(event.target.value as Theme) }}
>
<MenuItem value={Theme.System}>{Theme.System}</MenuItem>
<MenuItem value={Theme.Light}>{Theme.Light}</MenuItem>
<MenuItem value={Theme.Dark}>{Theme.Dark}</MenuItem>
</Select>
</FormControl>
<FormControlLabel sx={{ minWidth: 100, mt: 1 }} control={<Checkbox checked={showOffline} onChange={(_event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => setShowOffline(checked)} />} label="Show offline clients" />
</DialogContent>
<DialogActions>
<Button onClick={() => { handleClose(false) }}>Cancel</Button>
<Button onClick={() => { handleClose(true) }}>OK</Button>
</DialogActions>
</Dialog>
</div>
);
}
202 changes: 106 additions & 96 deletions src/components/SnapWeb.tsx

Large diffs are not rendered by default.

52 changes: 50 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,55 @@
const host = import.meta.env.VITE_APP_SNAPSERVER_HOST || window.location.host;

const keys = {
snapserver_host: "snapserver.host",
theme: "theme",
showoffline: "showoffline"
}

enum Theme {
System = "system",
Light = "light",
Dark = "dark",
}

function setPersistentValue(key: string, value: string) {
if (window.localStorage) {
window.localStorage.setItem(key, value);
}
}

function getPersistentValue(key: string, defaultValue: string = ""): string {
if (window.localStorage) {
const value = window.localStorage.getItem(key);
if (value !== null) {
return value;
}
window.localStorage.setItem(key, defaultValue);
return defaultValue;
}
return defaultValue;
}

const config = {
baseUrl: (window.location.protocol === "https:" ? "wss://" : "ws://") + host,
get baseUrl() {
return getPersistentValue(keys.snapserver_host, (window.location.protocol === "https:" ? "wss://" : "ws://") + host);
},
set baseUrl(value) {
setPersistentValue(keys.snapserver_host, value);
},
get theme() {
return getPersistentValue(keys.theme, Theme.System.toString()) as Theme;
},
set theme(value: Theme) {
setPersistentValue(keys.theme, value);
},
get showOffline() {
return getPersistentValue(keys.showoffline, String(false)) === String(true);
},
set showOffline(value: boolean) {
setPersistentValue(keys.showoffline, String(value));
}
};

export { config };

export { config, getPersistentValue, setPersistentValue, Theme };
6 changes: 1 addition & 5 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './main.css';
import { config } from "./config";
import SnapWeb from './components/SnapWeb';
import { SnapControl } from './snapcontrol';



const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);

const snapcontrol = new SnapControl(config.baseUrl);

console.log(`Welcome to ${import.meta.env.VITE_APP_NAME} ${import.meta.env.VITE_APP_VERSION}`)

root.render(
<React.StrictMode>
<SnapWeb snapcontrol={snapcontrol} />
<SnapWeb />
</React.StrictMode>
);
63 changes: 47 additions & 16 deletions src/snapcontrol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,30 +249,61 @@ namespace Snapcast {
}
}

interface OnChange { (_server: Snapcast.Server): void }
// interface OnChange { (_server: Snapcast.Server): void }
// interface OnStreamChange { (id: string): void };

class SnapControl {

constructor(baseUrl: string) {
constructor() {
this.onChange = null;
this.onConnectionChanged = null;
this.server = new Snapcast.Server();
this.baseUrl = baseUrl;
this.msg_id = 0;
this.status_req_id = -1;
this.connect();
this.timer = null;
}

private connect() {
this.connection = new WebSocket(this.baseUrl + '/jsonrpc');
this.connection.onmessage = (msg: MessageEvent) => this.onMessage(msg.data);
this.connection.onopen = () => { this.status_req_id = this.sendRequest('Server.GetStatus'); };
this.connection.onerror = (ev: Event) => { console.error('error:', ev); };
this.connection.onclose = () => {
console.info('connection lost, reconnecting in 1s');
setTimeout(() => this.connect(), 1000);
};
public connect(baseUrl: string) {
this.disconnect();
try {
this.connection = new WebSocket(baseUrl + '/jsonrpc');
this.connection.onmessage = (msg: MessageEvent) => this.onMessage(msg.data);
this.connection.onopen = () => {
this.status_req_id = this.sendRequest('Server.GetStatus');
if (this.onConnectionChanged)
this.onConnectionChanged(this, true);
};
this.connection.onerror = (ev: Event) => { console.error('error:', ev); };
this.connection.onclose = () => {
if (this.onConnectionChanged)
this.onConnectionChanged(this, false, 'Connection lost, trying to reconnect.');
console.info('connection lost, reconnecting in 1s');
this.timer = setTimeout(() => this.connect(baseUrl), 1000);
};
} catch (e) {
console.info('Exception while connecting: "' + e + '", reconnecting in 1s');
if (this.onConnectionChanged)
this.onConnectionChanged(this, false, 'Exception while connecting: "' + e + '", trying to reconnect.');
this.timer = setTimeout(() => this.connect(baseUrl), 1000);
}
}

public disconnect() {
if (this.timer)
clearTimeout(this.timer);
if (this.connection) {
this.connection.onclose = () => { };
if (this.connection.readyState === WebSocket.OPEN) {
this.connection.close();
}
}
if (this.onConnectionChanged)
this.onConnectionChanged(this, false);
}

onChange: ((_this: SnapControl, _server: Snapcast.Server) => any) | null;
onConnectionChanged: ((_this: SnapControl, _connected: boolean, _error?: string) => any) | null;

private onNotification(notification: any): boolean {
let stream!: Snapcast.Stream;
switch (notification.method) {
Expand Down Expand Up @@ -486,21 +517,21 @@ class SnapControl {
if (refresh) {
if (this.onChange) {
console.debug("onChange");
this.onChange(this.server);
this.onChange(this, this.server);
} else {
console.debug("no onChange");
}
}
}

public onChange?: OnChange;
// public onChange?: OnChange;
// public onStreamChange?: OnStreamChange;

baseUrl: string;
connection!: WebSocket;
server: Snapcast.Server;
msg_id: number;
status_req_id: number;
timer: ReturnType<typeof setTimeout> | null;
}


Expand Down
34 changes: 1 addition & 33 deletions src/snapstream.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Flac from 'libflacjs/dist/libflac.js'
import { getPersistentValue } from './config.ts'
import { AudioContext, IAudioBuffer, IAudioContext, IAudioBufferSourceNode, IGainNode } from 'standardized-audio-context'


Expand All @@ -15,39 +16,6 @@ interface IAudioContextPatched extends IAudioContext {
readonly outputLatency: number;
}

function setCookie(key: string, value: string, exdays: number = -1) {
const d = new Date();
if (exdays < 0)
exdays = 10 * 365;
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
const expires = "expires=" + d.toUTCString();
document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/";
}


function getPersistentValue(key: string, defaultValue: string = ""): string {
if (window.localStorage) {
const value = window.localStorage.getItem(key);
if (value !== null) {
return value;
}
window.localStorage.setItem(key, defaultValue);
return defaultValue;
}
// Fallback to cookies if localStorage is not available.
const name = key + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let c of ca) {
c = c.trimLeft();
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
setCookie(key, defaultValue);
return defaultValue;
}

function getChromeVersion(): number | null {
const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2]) : null;
Expand Down

0 comments on commit 68b69ba

Please sign in to comment.