Update dialog
This commit is contained in:
@@ -21,6 +21,7 @@ add_custom_command(
|
||||
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
|
||||
|
||||
DEPENDS
|
||||
src/Updater.tsx
|
||||
src/OkCancelDialog.tsx
|
||||
src/PluginControl.tsx
|
||||
src/svg/ic_save_bank_as.svg
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"socket_server_port": 80,
|
||||
"socket_server_port": 8080,
|
||||
"socket_server_address": "*",
|
||||
"debug": true,
|
||||
"max_upload_size": 536870912,
|
||||
|
||||
@@ -184,7 +184,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
|
||||
let addressKey = 0;
|
||||
return (
|
||||
<DialogEx tag="about" fullScreen open={this.props.open}
|
||||
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
|
||||
@@ -228,7 +228,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
{this.model.isAndroidHosted() && (
|
||||
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
||||
Copyright © 2022 Robin Davies.
|
||||
Copyright © 2022-2024 Robin Davies.
|
||||
</Typography>
|
||||
)}
|
||||
<Divider />
|
||||
@@ -239,7 +239,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
||||
{
|
||||
this.model.serverVersion?.webAddresses.map((address) =>
|
||||
(
|
||||
<Typography display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
||||
<Typography key={addressKey++} display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
||||
{address}
|
||||
</Typography>
|
||||
))
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface AndroidHostInterface {
|
||||
getHostVersion() : string;
|
||||
chooseNewDevice() : void;
|
||||
setDisconnected(isDisconnected: boolean): void;
|
||||
|
||||
launchExternalUrl(url:string): boolean;
|
||||
};
|
||||
|
||||
export class FakeAndroidHost implements AndroidHostInterface
|
||||
@@ -47,4 +47,8 @@ export class FakeAndroidHost implements AndroidHostInterface
|
||||
}
|
||||
showSponsorship() : void { }
|
||||
|
||||
}
|
||||
launchExternalUrl(url:string): boolean
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+48
-15
@@ -48,7 +48,7 @@ import SettingsDialog from './SettingsDialog';
|
||||
import AboutDialog from './AboutDialog';
|
||||
import BankDialog from './BankDialog';
|
||||
|
||||
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo } from './PiPedalModel';
|
||||
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo,wantsLoadingScreen } from './PiPedalModel';
|
||||
import ZoomedUiControl from './ZoomedUiControl'
|
||||
import MainPage from './MainPage';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
@@ -60,6 +60,7 @@ import RenameDialog from './RenameDialog';
|
||||
import JackStatusView from './JackStatusView';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import UpdateDialog from './UpdateDialog';
|
||||
|
||||
import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg';
|
||||
import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg';
|
||||
@@ -270,6 +271,7 @@ type AppState = {
|
||||
tinyToolBar: boolean;
|
||||
alertDialogOpen: boolean;
|
||||
alertDialogMessage: string;
|
||||
updateDialogOpen: boolean;
|
||||
isSettingsDialogOpen: boolean;
|
||||
onboarding: boolean;
|
||||
isDebug: boolean;
|
||||
@@ -324,6 +326,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
alertDialogMessage: "",
|
||||
presetName: this.model_.presets.get().getSelectedText(),
|
||||
isSettingsDialogOpen: false,
|
||||
updateDialogOpen: false,
|
||||
onboarding: false,
|
||||
isDebug: true,
|
||||
presetChanged: this.model_.presets.get().presetChanged,
|
||||
@@ -339,8 +342,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
|
||||
};
|
||||
|
||||
this.promptForUpdateHandler = this.promptForUpdateHandler.bind(this);
|
||||
this.errorChangeHandler_ = this.setErrorMessage.bind(this);
|
||||
this.unmountListener = this.unmountListener.bind(this);
|
||||
this.beforeUnloadListener = this.beforeUnloadListener.bind(this);
|
||||
this.unloadListener = this.unloadListener.bind(this);
|
||||
this.stateChangeHandler_ = this.setDisplayState.bind(this);
|
||||
this.presetChangedHandler = this.presetChangedHandler.bind(this);
|
||||
this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this);
|
||||
@@ -510,18 +515,26 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
this.setState({ isFullScreen: !this.state.isFullScreen });
|
||||
}
|
||||
|
||||
private unmountListener(e: Event) {
|
||||
if (this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) {
|
||||
e.preventDefault();
|
||||
(e as any).returnValue = "Are you sure you want to leave this page?";
|
||||
return "Are you sure you want to leave this page?";
|
||||
}
|
||||
private unloadListener(e: Event) {
|
||||
this.model_.close();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private beforeUnloadListener(e: Event) {
|
||||
this.model_.close();
|
||||
return undefined;
|
||||
}
|
||||
promptForUpdateHandler(newValue: boolean) {
|
||||
if (this.state.updateDialogOpen !== newValue)
|
||||
{
|
||||
this.setState({updateDialogOpen: newValue});
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
|
||||
super.componentDidMount();
|
||||
window.addEventListener("beforeunload", this.unmountListener);
|
||||
window.addEventListener("beforeunload", this.beforeUnloadListener);
|
||||
window.addEventListener("unload", this.unloadListener);
|
||||
|
||||
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
|
||||
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
|
||||
@@ -529,6 +542,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler);
|
||||
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
||||
this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
|
||||
this.model_.promptForUpdate.addOnChangedHandler(this.promptForUpdateHandler);
|
||||
this.alertMessageChangedHandler();
|
||||
}
|
||||
|
||||
@@ -550,14 +564,17 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
window.removeEventListener("beforeunload", this.unmountListener);
|
||||
window.removeEventListener("beforeunload", this.beforeUnloadListener);
|
||||
|
||||
this.model_.promptForUpdate.removeOnChangedHandler(this.promptForUpdateHandler);
|
||||
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
|
||||
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
|
||||
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
|
||||
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
|
||||
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
|
||||
|
||||
this.model_.close();
|
||||
|
||||
}
|
||||
|
||||
alertMessageChangedHandler() {
|
||||
@@ -661,6 +678,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
return "Applying\u00A0changes...";
|
||||
case State.ReloadingPlugins:
|
||||
return "Reloading\u00A0plugins...";
|
||||
case State.DownloadingUpdate:
|
||||
return "Downloading update...";
|
||||
case State.InstallingUpdate:
|
||||
return "Installing update....";
|
||||
default:
|
||||
return "Reconnecting...";
|
||||
}
|
||||
@@ -840,7 +861,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
</div>
|
||||
</main>
|
||||
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
|
||||
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
||||
{ (this.state.aboutDialogOpen)&&
|
||||
(
|
||||
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
||||
)}
|
||||
<SettingsDialog
|
||||
open={this.state.isSettingsDialogOpen}
|
||||
onboarding={this.state.onboarding}
|
||||
@@ -872,6 +896,16 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
|
||||
}
|
||||
/>
|
||||
{
|
||||
(this.state.updateDialogOpen)
|
||||
&& (
|
||||
<UpdateDialog open={true} />
|
||||
)
|
||||
}
|
||||
{this.state.showStatusMonitor && (<JackStatusView />)}
|
||||
|
||||
|
||||
|
||||
<DialogEx
|
||||
tag="Alert"
|
||||
open={this.state.alertDialogOpen}
|
||||
@@ -893,14 +927,13 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
{this.state.showStatusMonitor && (<JackStatusView />)}
|
||||
|
||||
|
||||
<div className={classes.errorContent} style={{
|
||||
display: (
|
||||
this.state.displayState === State.Reconnecting
|
||||
|| this.state.displayState === State.ApplyingChanges
|
||||
|| this.state.displayState === State.ReloadingPlugins)
|
||||
wantsLoadingScreen(this.state.displayState)
|
||||
? "block" : "none"
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className={classes.errorContentMask} />
|
||||
|
||||
+319
-123
@@ -20,7 +20,7 @@
|
||||
import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin';
|
||||
|
||||
import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError';
|
||||
|
||||
import { UpdateStatus, UpdatePolicyT} from './Updater';
|
||||
import { ObservableProperty } from './ObservableProperty';
|
||||
import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'
|
||||
import PluginClass from './PluginClass';
|
||||
@@ -38,7 +38,7 @@ import GovernorSettings from './GovernorSettings';
|
||||
import WifiChannel from './WifiChannel';
|
||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||
import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode';
|
||||
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
|
||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||
|
||||
|
||||
@@ -49,13 +49,21 @@ export enum State {
|
||||
Background,
|
||||
Reconnecting,
|
||||
ApplyingChanges,
|
||||
ReloadingPlugins
|
||||
ReloadingPlugins,
|
||||
DownloadingUpdate,
|
||||
InstallingUpdate,
|
||||
};
|
||||
|
||||
export function wantsLoadingScreen(state: State)
|
||||
{
|
||||
return state >= State.Reconnecting;
|
||||
}
|
||||
|
||||
export enum ReconnectReason {
|
||||
Disconnected,
|
||||
LoadingSettings,
|
||||
ReloadingPlugins,
|
||||
Updating,
|
||||
};
|
||||
|
||||
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||
@@ -369,6 +377,9 @@ export class PiPedalModel //implements PiPedalModel
|
||||
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
|
||||
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
|
||||
|
||||
promptForUpdate: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||
updateStatus: ObservableProperty<UpdateStatus> = new ObservableProperty<UpdateStatus>(new UpdateStatus());
|
||||
|
||||
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
||||
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
||||
|
||||
@@ -431,23 +442,32 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this);
|
||||
}
|
||||
|
||||
onSocketReconnecting(retry: number, maxRetries: number): void {
|
||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
||||
expectDisconnect(reason: ReconnectReason)
|
||||
{
|
||||
this.reconnectReason = reason;
|
||||
}
|
||||
|
||||
onSocketReconnecting(retry: number, maxRetries: number): boolean {
|
||||
if (this.isClosed) return false;
|
||||
if (this.visibilityState.get() === VisibilityState.Hidden) return false;
|
||||
//if (retry !== 0) {
|
||||
switch (this.reconnectReason)
|
||||
{
|
||||
case ReconnectReason.Disconnected:
|
||||
default:
|
||||
this.setState(State.Reconnecting);
|
||||
break;
|
||||
case ReconnectReason.LoadingSettings:
|
||||
this.setState(State.ApplyingChanges);
|
||||
break;
|
||||
case ReconnectReason.ReloadingPlugins:
|
||||
this.setState(State.ReloadingPlugins);
|
||||
break;
|
||||
switch (this.reconnectReason) {
|
||||
case ReconnectReason.Disconnected:
|
||||
default:
|
||||
this.setState(State.Reconnecting);
|
||||
break;
|
||||
case ReconnectReason.LoadingSettings:
|
||||
this.setState(State.ApplyingChanges);
|
||||
break;
|
||||
case ReconnectReason.ReloadingPlugins:
|
||||
this.setState(State.ReloadingPlugins);
|
||||
break;
|
||||
case ReconnectReason.Updating:
|
||||
this.setState(State.InstallingUpdate);
|
||||
break;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -486,17 +506,17 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
else if (message === "onOutputVolumeChanged") {
|
||||
let value = body as number;
|
||||
this._setOutputVolume(value,false);
|
||||
this._setOutputVolume(value, false);
|
||||
}
|
||||
else if (message === "onInputVolumeChanged") {
|
||||
let value = body as number;
|
||||
this._setInputVolume(value,false);
|
||||
this._setInputVolume(value, false);
|
||||
} else if (message === "onLv2StateChanged") {
|
||||
|
||||
let instanceId = body.instanceId as number;
|
||||
let state = body.state as [boolean,any];
|
||||
let state = body.state as [boolean, any];
|
||||
|
||||
this.onLv2StateChanged(instanceId,state);
|
||||
this.onLv2StateChanged(instanceId, state);
|
||||
} else if (message === "onVst3ControlChanged") {
|
||||
let controlChangedBody = body as Vst3ControlChangedBody;
|
||||
this._setVst3PedalboardControlValue(
|
||||
@@ -613,18 +633,114 @@ export class PiPedalModel //implements PiPedalModel
|
||||
} else if (message === "onSystemMidiBindingsChanged") {
|
||||
let bindings = MidiBinding.deserialize_array(body);
|
||||
this.systemMidiBindings.set(bindings);
|
||||
} else if (message === "onErrorMessage")
|
||||
{
|
||||
} else if (message === "onErrorMessage") {
|
||||
this.showAlert(body as string);
|
||||
|
||||
}
|
||||
else if (message === "onLv2PluginsChanging")
|
||||
{
|
||||
}
|
||||
else if (message === "onLv2PluginsChanging") {
|
||||
this.onLv2PluginsChanging();
|
||||
} else if (message === "onUpdateStatusChanged") {
|
||||
let updateStatus = new UpdateStatus().deserialize(body);
|
||||
this.onUpdateStatusChanged(updateStatus);
|
||||
}
|
||||
}
|
||||
onLv2PluginsChanging() : void {
|
||||
this.reconnectReason = ReconnectReason.ReloadingPlugins;
|
||||
|
||||
private updateLaterTimeout?: NodeJS.Timeout = undefined;
|
||||
|
||||
private clearPromptForUpdateTimer()
|
||||
{
|
||||
if (this.updateLaterTimeout)
|
||||
{
|
||||
clearTimeout(this.updateLaterTimeout);
|
||||
this.updateLaterTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private setPromptForUpdateTimer(when: Date)
|
||||
{
|
||||
let ms = when.getTime()-Date.now();
|
||||
this.updateLaterTimeout = setTimeout(
|
||||
()=>{
|
||||
this.updateLaterTimeout = undefined;
|
||||
// make the server do a fresh check
|
||||
this.getUpdateStatus()
|
||||
.then(
|
||||
()=>{
|
||||
this.updatePromptForUpdate();
|
||||
})
|
||||
.catch((e)=>{
|
||||
|
||||
});
|
||||
},
|
||||
ms);
|
||||
}
|
||||
|
||||
|
||||
private updatePromptForUpdate() {
|
||||
this.clearPromptForUpdateTimer();
|
||||
|
||||
let stateEnabled = true; // must be present to accept alerts. this.state.get() === State.Ready;
|
||||
let timeEnabled = false;
|
||||
|
||||
let updateLaterTime = this.getUpdateTime();
|
||||
if (updateLaterTime == null) {
|
||||
timeEnabled = true;
|
||||
} else {
|
||||
let nDate = Date.now();
|
||||
|
||||
let now: Date = new Date(nDate);
|
||||
let tomorrow: Date = new Date(nDate + 86400000);
|
||||
|
||||
timeEnabled = (updateLaterTime < now || updateLaterTime >= tomorrow)
|
||||
if (!timeEnabled)
|
||||
{
|
||||
this.setPromptForUpdateTimer(updateLaterTime);
|
||||
}
|
||||
}
|
||||
let updateStatus = this.updateStatus.get();
|
||||
let statusEnabled = updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable;
|
||||
|
||||
if (stateEnabled && timeEnabled && statusEnabled)
|
||||
{
|
||||
this.showUpdateDialogValue = true;
|
||||
}
|
||||
this.promptForUpdate.set(this.showUpdateDialogValue);
|
||||
|
||||
if (stateEnabled && statusEnabled && !timeEnabled && updateLaterTime)
|
||||
{
|
||||
this.setPromptForUpdateTimer(updateLaterTime);
|
||||
}
|
||||
}
|
||||
private showUpdateDialogValue: boolean = false;
|
||||
|
||||
showUpdateDialog(show: boolean = true) {
|
||||
if (this.showUpdateDialogValue !== show)
|
||||
{
|
||||
this.showUpdateDialogValue = show;
|
||||
if (show)
|
||||
{
|
||||
this.forceUpdateCheck();
|
||||
}
|
||||
this.updatePromptForUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateStatusChanged(updateStatus: UpdateStatus): void {
|
||||
let current = this.updateStatus.get();
|
||||
if (!current.equals(updateStatus)) {
|
||||
this.updateStatus.set(updateStatus);
|
||||
if (current.currentVersion.length !== 0 && current.currentVersion !== updateStatus.currentVersion) {
|
||||
// !! Server has been updated!!!
|
||||
this.reloadPage();
|
||||
}
|
||||
if (updateStatus.getActiveRelease().updateAvailable) {
|
||||
this.promptForUpdate.set(true);
|
||||
}
|
||||
this.updatePromptForUpdate()
|
||||
}
|
||||
}
|
||||
onLv2PluginsChanging(): void {
|
||||
this.expectDisconnect(ReconnectReason.ReloadingPlugins);
|
||||
// this.webSocket?.reconnect(); // let the server do it for us.
|
||||
|
||||
}
|
||||
@@ -639,19 +755,21 @@ export class PiPedalModel //implements PiPedalModel
|
||||
setState(state: State) {
|
||||
if (this.state.get() !== state) {
|
||||
this.state.set(state);
|
||||
this.updatePromptForUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
validatePlugins(plugin: PluginClass) {
|
||||
validatePluginClasses(plugin: PluginClass) {
|
||||
if (plugin.plugin_type === PluginType.None) {
|
||||
console.log("Error: No plugin type for uri '" + plugin.uri + "'");
|
||||
}
|
||||
for (let i = 0; i < plugin.children.length; ++i) {
|
||||
this.validatePlugins(plugin.children[i]);
|
||||
this.validatePluginClasses(plugin.children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private reconnectReason: ReconnectReason = ReconnectReason.Disconnected;
|
||||
|
||||
isReloading(): boolean {
|
||||
@@ -681,13 +799,17 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.androidHost?.setDisconnected(false);
|
||||
}
|
||||
|
||||
this.reconnectReason = ReconnectReason.Disconnected;
|
||||
this.expectDisconnect(ReconnectReason.Disconnected); // the next expected disconnect will be an actual disconnect.
|
||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
||||
|
||||
// reload state, but not configuration.
|
||||
this.getWebSocket().request<number>("hello")
|
||||
.then(clientId => {
|
||||
this.clientId = clientId;
|
||||
return this.getUpdateStatus(); // detects whether server has been upgraded.
|
||||
})
|
||||
.then((updateStatus) => {
|
||||
|
||||
return this.getWebSocket().request<any>("plugins");
|
||||
})
|
||||
.then(data => {
|
||||
@@ -774,7 +896,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
}
|
||||
|
||||
maxFileUploadSize: number = 512*1024*1024;
|
||||
maxFileUploadSize: number = 512 * 1024 * 1024;
|
||||
maxPresetUploadSize: number = 1024 * 1024;
|
||||
debug: boolean = false;
|
||||
|
||||
@@ -795,7 +917,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.androidHost = new FakeAndroidHost();
|
||||
}
|
||||
this.debug = !!data.debug;
|
||||
let { socket_server_port, socket_server_address,max_upload_size } = data;
|
||||
let { socket_server_port, socket_server_address, max_upload_size } = data;
|
||||
if ((!socket_server_address) || socket_server_address === "*") {
|
||||
socket_server_address = window.location.hostname;
|
||||
}
|
||||
@@ -825,6 +947,9 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return false;
|
||||
})
|
||||
.then(() => {
|
||||
return this.getUpdateStatus();
|
||||
})
|
||||
.then((updateStatus) => {
|
||||
const isoRequest = new Request('iso_codes.json');
|
||||
return fetch(isoRequest);
|
||||
})
|
||||
@@ -868,7 +993,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
.then((data) => {
|
||||
|
||||
this.plugin_classes.set(new PluginClass().deserialize(data));
|
||||
this.validatePlugins(this.plugin_classes.get());
|
||||
this.validatePluginClasses(this.plugin_classes.get());
|
||||
|
||||
return this.getWebSocket().request<PresetIndex>("getPresets");
|
||||
})
|
||||
@@ -1131,7 +1256,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
}
|
||||
|
||||
private onLv2StateChanged(instanceId: number, state: [boolean,any]): void {
|
||||
private onLv2StateChanged(instanceId: number, state: [boolean, any]): void {
|
||||
let item = this.pedalboard.get().getItem(instanceId);
|
||||
item.lv2State = state;
|
||||
for (let item of this.stateChangedListeners) {
|
||||
@@ -1182,33 +1307,31 @@ export class PiPedalModel //implements PiPedalModel
|
||||
this.webSocket?.send("setPatchProperty", body);
|
||||
}
|
||||
|
||||
setInputVolume(volume_db: number) : void {
|
||||
this._setInputVolume(volume_db,true);
|
||||
setInputVolume(volume_db: number): void {
|
||||
this._setInputVolume(volume_db, true);
|
||||
}
|
||||
previewInputVolume(volume_db: number) : void {
|
||||
nullCast(this.webSocket).send("previewInputVolume",volume_db);
|
||||
previewInputVolume(volume_db: number): void {
|
||||
nullCast(this.webSocket).send("previewInputVolume", volume_db);
|
||||
|
||||
}
|
||||
previewOutputVolume(volume_db: number) : void {
|
||||
nullCast(this.webSocket).send("previewOutputVolume",volume_db);
|
||||
previewOutputVolume(volume_db: number): void {
|
||||
nullCast(this.webSocket).send("previewOutputVolume", volume_db);
|
||||
|
||||
}
|
||||
|
||||
private _setInputVolume(volume_db: number, notifyServer: boolean) : void {
|
||||
let changed: boolean = false;
|
||||
private _setInputVolume(volume_db: number, notifyServer: boolean): void {
|
||||
let changed: boolean = false;
|
||||
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (pedalboard.input_volume_db !== volume_db)
|
||||
{
|
||||
if (pedalboard.input_volume_db !== volume_db) {
|
||||
let newPedalboard = pedalboard.clone();
|
||||
newPedalboard.input_volume_db = volume_db;
|
||||
this.pedalboard.set(newPedalboard);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
if (changed) {
|
||||
if (notifyServer) {
|
||||
nullCast(this.webSocket).send("setInputVolume",volume_db);
|
||||
nullCast(this.webSocket).send("setInputVolume", volume_db);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
||||
@@ -1220,26 +1343,23 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
}
|
||||
|
||||
setOutputVolume(volume_db: number, notifyServer: boolean) : void
|
||||
{
|
||||
this._setOutputVolume(volume_db,true);
|
||||
setOutputVolume(volume_db: number, notifyServer: boolean): void {
|
||||
this._setOutputVolume(volume_db, true);
|
||||
}
|
||||
|
||||
private _setOutputVolume(volume_db: number, notifyServer: boolean) : void {
|
||||
let changed: boolean = false;
|
||||
private _setOutputVolume(volume_db: number, notifyServer: boolean): void {
|
||||
let changed: boolean = false;
|
||||
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (pedalboard.output_volume_db !== volume_db)
|
||||
{
|
||||
if (pedalboard.output_volume_db !== volume_db) {
|
||||
let newPedalboard = pedalboard.clone();
|
||||
newPedalboard.output_volume_db = volume_db;
|
||||
this.pedalboard.set(newPedalboard);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
if (changed) {
|
||||
if (notifyServer) {
|
||||
nullCast(this.webSocket).send("setOutputVolume",volume_db);
|
||||
nullCast(this.webSocket).send("setOutputVolume", volume_db);
|
||||
}
|
||||
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
||||
let item = this._controlValueChangeItems[i];
|
||||
@@ -1256,15 +1376,13 @@ export class PiPedalModel //implements PiPedalModel
|
||||
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
||||
let newPedalboard = pedalboard.clone();
|
||||
|
||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
|
||||
{
|
||||
this._setInputVolume(value,notifyServer);
|
||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
|
||||
this._setInputVolume(value, notifyServer);
|
||||
return;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
||||
{
|
||||
this._setOutputVolume(value,notifyServer);
|
||||
} else if (instanceId === Pedalboard.END_CONTROL) {
|
||||
this._setOutputVolume(value, notifyServer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let item = newPedalboard.getItem(instanceId);
|
||||
let changed = item.setControlValue(key, value);
|
||||
|
||||
@@ -1365,7 +1483,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
while (true) {
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
let item = v.value;
|
||||
let item = v.value;
|
||||
if (item.instanceId === itemId) {
|
||||
item.deserialize(new PedalboardItem()); // skeezy way to re-initialize.
|
||||
item.instanceId = ++newPedalboard.nextInstanceId;
|
||||
@@ -1395,16 +1513,14 @@ export class PiPedalModel //implements PiPedalModel
|
||||
// mouse is down. Don't update EVERYBODY, but we must change
|
||||
// the control on the running audio plugin.
|
||||
// TODO: respect "expensive" port attribute.
|
||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
|
||||
{
|
||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
|
||||
this.previewInputVolume(value);
|
||||
return;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
||||
{
|
||||
} else if (instanceId === Pedalboard.END_CONTROL) {
|
||||
this.previewOutputVolume(value);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this._setServerControl("previewControl", instanceId, key, value);
|
||||
}
|
||||
|
||||
@@ -1530,13 +1646,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
addPedalboardItem(instanceId: number, append: boolean): number {
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (instanceId === Pedalboard.START_CONTROL && append)
|
||||
{
|
||||
if (instanceId === Pedalboard.START_CONTROL && append) {
|
||||
instanceId = pedalboard.items[0].instanceId;
|
||||
append = false;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL && !append)
|
||||
{
|
||||
instanceId = pedalboard.items[pedalboard.items.length-1].instanceId;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL && !append) {
|
||||
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
||||
append = true;
|
||||
}
|
||||
let newPedalboard = pedalboard.clone();
|
||||
@@ -1555,14 +1669,12 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
addPedalboardSplitItem(instanceId: number, append: boolean): number {
|
||||
let pedalboard = this.pedalboard.get();
|
||||
|
||||
if (instanceId === Pedalboard.START_CONTROL && append)
|
||||
{
|
||||
|
||||
if (instanceId === Pedalboard.START_CONTROL && append) {
|
||||
instanceId = pedalboard.items[0].instanceId;
|
||||
append = false;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL && !append)
|
||||
{
|
||||
instanceId = pedalboard.items[pedalboard.items.length-1].instanceId;
|
||||
} else if (instanceId === Pedalboard.END_CONTROL && !append) {
|
||||
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
||||
append = true;
|
||||
}
|
||||
|
||||
@@ -1615,7 +1727,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
|
||||
|
||||
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> {
|
||||
// default behaviour is to save after the currently selected preset.
|
||||
if (saveAfterInstanceId === -1) {
|
||||
@@ -1636,21 +1747,94 @@ export class PiPedalModel //implements PiPedalModel
|
||||
});
|
||||
}
|
||||
|
||||
getUpdateStatus(): Promise<UpdateStatus> {
|
||||
return new Promise<UpdateStatus>(
|
||||
(accept, reject) => {
|
||||
nullCast(this.webSocket)
|
||||
.request<UpdateStatus>('getUpdateStatus')
|
||||
.then((result) => {
|
||||
let updateStatus = new UpdateStatus().deserialize(result);
|
||||
this.onUpdateStatusChanged(updateStatus);
|
||||
accept(result);
|
||||
}).catch(
|
||||
(e) => {
|
||||
reject(e);
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
launchExternalUrl(url: string) {
|
||||
if (this.isAndroidHosted()) {
|
||||
try {
|
||||
this.androidHost?.launchExternalUrl(url);
|
||||
} catch (e) {
|
||||
// if they haven't updated their client yet, just don't do it.
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
window.open(url, "_blank")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
updateLater(delayMs: number) {
|
||||
let futureDate = new Date(Date.now() + delayMs);
|
||||
localStorage.setItem('nextUpdateTime', futureDate.toISOString());
|
||||
|
||||
this.updatePromptForUpdate();
|
||||
}
|
||||
|
||||
updateNow(): Promise<void> {
|
||||
return new Promise<void>(
|
||||
(accept,reject) => {
|
||||
let updateStatus = this.updateStatus.get();
|
||||
if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable)
|
||||
{
|
||||
this.setState(State.DownloadingUpdate);
|
||||
let url = updateStatus.getActiveRelease().updateUrl;
|
||||
nullCast(this.webSocket)
|
||||
.request<void>('updateNow', url)
|
||||
.then(()=> {
|
||||
this.setState(State.InstallingUpdate);
|
||||
accept();
|
||||
})
|
||||
.catch(
|
||||
(e: any)=>{
|
||||
this.setState(State.Ready); // TODO: hopefully we haven't had an intermediate disconnect.
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
reject(new Error("Invalid update request."));
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getUpdateTime(): Date | null {
|
||||
let item = localStorage.getItem('nextUpdateTime');
|
||||
if (item) {
|
||||
return new Date(item);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// deprecated.
|
||||
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
||||
return nullCast(this.webSocket)
|
||||
.request<string[]>('requestFileList', piPedalFileProperty);
|
||||
}
|
||||
requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
|
||||
requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
|
||||
return nullCast(this.webSocket)
|
||||
.request<FileEntry[]>('requestFileList2',
|
||||
{relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty}
|
||||
{ relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty }
|
||||
);
|
||||
}
|
||||
|
||||
deleteUserFile(fileName: string) : Promise<boolean>
|
||||
{
|
||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName);
|
||||
|
||||
deleteUserFile(fileName: string): Promise<boolean> {
|
||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile', fileName);
|
||||
}
|
||||
|
||||
|
||||
@@ -1718,7 +1902,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
|
||||
setJackSettings(jackSettings: JackChannelSelection): void {
|
||||
this.reconnectReason = ReconnectReason.LoadingSettings;
|
||||
this.expectDisconnect(ReconnectReason.LoadingSettings);
|
||||
this.webSocket?.send("setJackSettings", jackSettings);
|
||||
}
|
||||
|
||||
@@ -1823,7 +2007,15 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private isClosed = false;
|
||||
close() {
|
||||
if (!this.isClosed)
|
||||
{
|
||||
this.isClosed = true;
|
||||
this.webSocket?.close();
|
||||
this.webSocket = undefined;
|
||||
}
|
||||
}
|
||||
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
||||
let result = new Promise<boolean>((resolve, reject) => {
|
||||
if (this.webSocket) {
|
||||
@@ -2107,7 +2299,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
||||
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
||||
let result = new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
if (file.size > this.maxFileUploadSize) {
|
||||
@@ -2117,17 +2309,15 @@ export class PiPedalModel //implements PiPedalModel
|
||||
let parsedUrl = new URL(url);
|
||||
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
|
||||
let query = parsedUrl.search;
|
||||
if (query.length === 0)
|
||||
{
|
||||
if (query.length === 0) {
|
||||
query += '?';
|
||||
} else {
|
||||
query += '&';
|
||||
}
|
||||
if (!fileNameOnly)
|
||||
{
|
||||
if (!fileNameOnly) {
|
||||
reject("Invalid filename.");
|
||||
}
|
||||
query += "filename=" + encodeURIComponent(fileNameOnly??"");
|
||||
query += "filename=" + encodeURIComponent(fileNameOnly ?? "");
|
||||
parsedUrl.search = query;
|
||||
url = parsedUrl.toString();
|
||||
fetch(
|
||||
@@ -2146,7 +2336,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
reject("Upload failed. " + response.statusText);
|
||||
return;
|
||||
} else {
|
||||
return response.json();
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((json) => {
|
||||
@@ -2254,8 +2444,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
let ws = this.webSocket;
|
||||
if (!ws)
|
||||
{
|
||||
if (!ws) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
@@ -2271,8 +2460,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
});
|
||||
});
|
||||
}
|
||||
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
||||
{
|
||||
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
|
||||
let ws = this.webSocket;
|
||||
@@ -2295,8 +2483,8 @@ export class PiPedalModel //implements PiPedalModel
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty) : Promise<FilePropertyDirectoryTree> {
|
||||
|
||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty): Promise<FilePropertyDirectoryTree> {
|
||||
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
||||
let ws = this.webSocket;
|
||||
if (!ws) {
|
||||
@@ -2306,15 +2494,14 @@ export class PiPedalModel //implements PiPedalModel
|
||||
ws.request<FilePropertyDirectoryTree>(
|
||||
"getFilePropertyDirectoryTree",
|
||||
uiFileProperty
|
||||
).then((result)=> {
|
||||
).then((result) => {
|
||||
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
||||
{
|
||||
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
|
||||
let ws = this.webSocket;
|
||||
@@ -2441,11 +2628,11 @@ export class PiPedalModel //implements PiPedalModel
|
||||
.catch((err) => {
|
||||
//resolve();
|
||||
});
|
||||
resolve();
|
||||
resolve();
|
||||
|
||||
});
|
||||
this.reconnectReason = ReconnectReason.LoadingSettings;
|
||||
this.webSocket?.reconnect(); // close immediately, and wait for recoonnect.
|
||||
this.expectDisconnect(ReconnectReason.LoadingSettings);
|
||||
// this.webSocket?.reconnect(); // avoid races by letting the server do it for us.
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2524,8 +2711,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
++ix;
|
||||
if (ix >= uiPlugin.controls.length) return;
|
||||
while (!uiPlugin.controls[ix].is_input)
|
||||
{
|
||||
while (!uiPlugin.controls[ix].is_input) {
|
||||
++ix;
|
||||
if (ix >= uiPlugin.controls.length) return;
|
||||
}
|
||||
@@ -2558,8 +2744,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
--ix;
|
||||
if (ix < 0) return;
|
||||
while (!uiPlugin.controls[ix].is_input)
|
||||
{
|
||||
while (!uiPlugin.controls[ix].is_input) {
|
||||
--ix;
|
||||
if (ix < 0) return;
|
||||
}
|
||||
@@ -2590,6 +2775,20 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
|
||||
setUpdatePolicy(updatePolicy: UpdatePolicyT) : void {
|
||||
let iPolicy = updatePolicy as number;
|
||||
if (this.webSocket) {
|
||||
this.webSocket.send("setUpdatePolicy", iPolicy);
|
||||
}
|
||||
|
||||
}
|
||||
forceUpdateCheck() {
|
||||
if (this.webSocket) {
|
||||
this.webSocket.send("forceUpdateCheck");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
preloadImages(imageList: string): void {
|
||||
let imageNames = imageList.split(';');
|
||||
for (let i = 0; i < imageNames.length; ++i) {
|
||||
@@ -2619,8 +2818,7 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
|
||||
// returns the ID of the new preset.
|
||||
newPresetItem(createAfter: number): Promise<number>
|
||||
{
|
||||
newPresetItem(createAfter: number): Promise<number> {
|
||||
return nullCast(this.webSocket).request<number>("newPreset");
|
||||
}
|
||||
|
||||
@@ -2630,14 +2828,13 @@ export class PiPedalModel //implements PiPedalModel
|
||||
|
||||
setTheme(value: ColorTheme) {
|
||||
|
||||
if (this.getTheme() !== value)
|
||||
{
|
||||
if (this.getTheme() !== value) {
|
||||
setColorScheme(value);
|
||||
setTimeout(()=>{
|
||||
setTimeout(() => {
|
||||
this.reloadPage();
|
||||
},
|
||||
200);
|
||||
}
|
||||
200);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2647,7 +2844,6 @@ export class PiPedalModel //implements PiPedalModel
|
||||
window.location.href = url;
|
||||
//window.location.reload();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let instance: PiPedalModel | undefined = undefined;
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface PiPedalSocketListener {
|
||||
onError: (message: string, exception?: Error) => void;
|
||||
onConnectionLost: () => void;
|
||||
onReconnect: () => void;
|
||||
onReconnecting: (retry: number, maxRetries: number) => void;
|
||||
onReconnecting: (retry: number, maxRetries: number) => boolean;
|
||||
};
|
||||
|
||||
class PiPedalSocket {
|
||||
@@ -183,6 +183,7 @@ class PiPedalSocket {
|
||||
|
||||
handleClose(_event: any): any {
|
||||
if (this.retrying) {
|
||||
this.close();
|
||||
// treat this as a fatal error.
|
||||
if (this.listener) {
|
||||
this.listener.onError("Server connection lost.");
|
||||
@@ -230,7 +231,9 @@ class PiPedalSocket {
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.listener.onReconnecting(this.retryCount,MAX_RETRIES);
|
||||
if (!this.listener.onReconnecting(this.retryCount,MAX_RETRIES)) {
|
||||
return;
|
||||
}
|
||||
++this.retryCount;
|
||||
|
||||
this.connectInternal_()
|
||||
@@ -262,6 +265,7 @@ class PiPedalSocket {
|
||||
this.socket.onmessage = null;
|
||||
this.socket.onopen = null;
|
||||
this.socket.close();
|
||||
this.socket = undefined;
|
||||
}
|
||||
} catch (ignored)
|
||||
{
|
||||
|
||||
@@ -393,6 +393,12 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
showThemeSelectDialog: true
|
||||
});
|
||||
}
|
||||
|
||||
handleCheckForUpdates()
|
||||
{
|
||||
this.model.showUpdateDialog();
|
||||
}
|
||||
|
||||
handleMidiMessageSettings() {
|
||||
this.setState({
|
||||
showSystemMidiBindingsDialog: true
|
||||
@@ -786,6 +792,18 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
</div>
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase
|
||||
className={classes.setting}
|
||||
onClick={() => { this.handleCheckForUpdates(); }} >
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
|
||||
Check for updates...</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
|
||||
|
||||
|
||||
<ButtonBase className={classes.setting} disabled={disableShutdown}
|
||||
onClick={() => this.handleRestart()} >
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { UpdateStatus, UpdateRelease, UpdatePolicyT,intToUpdatePolicyT } from './Updater';
|
||||
import { PiPedalModelFactory, PiPedalModel } from './PiPedalModel';
|
||||
import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
|
||||
//const UPDATE_CHECK_DELAY = 86400000; // one day in ms.
|
||||
const UPDATE_CHECK_DELAY = 30 * 1000; // testing
|
||||
const CLOSE_DELAY = 100; // ms.
|
||||
|
||||
|
||||
|
||||
export interface UpdateDialogProps {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
export interface UpdateDialogState {
|
||||
updateStatus: UpdateStatus;
|
||||
compactLandscape: boolean;
|
||||
alertDialogOpen: boolean;
|
||||
alertDialogMessage: string;
|
||||
};
|
||||
|
||||
export default class UpdateDialog extends React.Component<UpdateDialogProps, UpdateDialogState> {
|
||||
private model: PiPedalModel;
|
||||
|
||||
constructor(props: UpdateDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
let updateStatus = this.model.updateStatus.get();
|
||||
this.state = {
|
||||
updateStatus: updateStatus,
|
||||
compactLandscape: false,
|
||||
alertDialogOpen: false,
|
||||
alertDialogMessage: ""
|
||||
};
|
||||
this.onUpdateStatusChanged = this.onUpdateStatusChanged.bind(this);
|
||||
}
|
||||
|
||||
onUpdateStatusChanged(newValue: UpdateStatus)
|
||||
{
|
||||
if (!newValue.equals(this.state.updateStatus))
|
||||
{
|
||||
this.setState({updateStatus: newValue});
|
||||
}
|
||||
}
|
||||
|
||||
private mounted: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.model.updateStatus.addOnChangedHandler(this.onUpdateStatusChanged);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.updateStatus.removeOnChangedHandler(this.onUpdateStatusChanged);
|
||||
this.mounted = false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
private handleOk() {
|
||||
|
||||
}
|
||||
private handleUpdateNow() {
|
||||
this.model.updateNow()
|
||||
.then(() => { }) // all handling is done by model, so we can run ui-less.
|
||||
.catch((e: any) => {
|
||||
this.showAlert(e.toString());
|
||||
});
|
||||
}
|
||||
private updatingLater: boolean = false;
|
||||
private handleUpdateLater() {
|
||||
this.updatingLater = true;
|
||||
this.model.updateLater(UPDATE_CHECK_DELAY);
|
||||
this.model.showUpdateDialog(false); // allow close if launched from the settings window.
|
||||
}
|
||||
|
||||
private handleClose() {
|
||||
// ideally, we'd like to not close at all, but it screws up the nav backstack if we dont.
|
||||
// so, close, but do so very briefly
|
||||
if (!this.updatingLater) {
|
||||
this.model.updateLater(CLOSE_DELAY); // close and re-open immediately.
|
||||
}
|
||||
this.updatingLater = false;
|
||||
this.model.showUpdateDialog(false);
|
||||
}
|
||||
|
||||
handleCloseAlert() {
|
||||
this.setState({ alertDialogOpen: false, alertDialogMessage: "" })
|
||||
}
|
||||
|
||||
showAlert(message: string) {
|
||||
this.setState({ alertDialogOpen: true, alertDialogMessage: message });
|
||||
}
|
||||
|
||||
onViewReleaseNotes() {
|
||||
this.model.launchExternalUrl("https://rerdavies.github.io/pipedal/ReleaseNotes.html");
|
||||
}
|
||||
|
||||
upToDate(): boolean {
|
||||
let updateStatus = this.state.updateStatus;
|
||||
let updateRelease: UpdateRelease = updateStatus.getActiveRelease();
|
||||
return updateStatus.isValid && !updateRelease.updateAvailable
|
||||
}
|
||||
canUpgrade(): boolean {
|
||||
let updateStatus = this.state.updateStatus;
|
||||
let updateRelease = updateStatus.getActiveRelease();
|
||||
|
||||
return updateStatus.isValid && updateStatus.isOnline && updateRelease.updateAvailable;
|
||||
}
|
||||
|
||||
private onPolicySelected(newValue: string | number): void {
|
||||
if (newValue.toString() === "") return;
|
||||
let updatePolicy = intToUpdatePolicyT(newValue as number);
|
||||
this.model.setUpdatePolicy(updatePolicy);
|
||||
|
||||
}
|
||||
render() {
|
||||
let updateStatus = this.state.updateStatus;
|
||||
let updateRelease = updateStatus.getActiveRelease();
|
||||
let upToDate = this.upToDate();
|
||||
let canUpgrade = this.canUpgrade();
|
||||
let compact = this.state.compactLandscape;
|
||||
return (
|
||||
<DialogEx tag="update" open={this.props.open} onClose={() => { this.handleClose(); }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
{
|
||||
(!compact) &&
|
||||
(
|
||||
<DialogTitle>
|
||||
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }} >
|
||||
<Typography style={{ flexGrow: 1, flexShrink: 1, marginRight: 20 }} noWrap>PiPedal Updates</Typography>
|
||||
<Select style={{opacity: 0.6}} variant="standard" value={updateStatus.updatePolicy as number} onChange={(ev) => { this.onPolicySelected(ev.target.value); }}>
|
||||
<MenuItem value={UpdatePolicyT.ReleaseOnly as number}>Release only</MenuItem>
|
||||
<MenuItem value={UpdatePolicyT.ReleaseOrBeta as number}>Release or Beta</MenuItem>
|
||||
<MenuItem value={UpdatePolicyT.Development as number}>Development</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
{
|
||||
(!compact) &&
|
||||
(
|
||||
<Divider />
|
||||
)
|
||||
}
|
||||
|
||||
<DialogContent>
|
||||
{
|
||||
(upToDate) && (
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
|
||||
PiPedal is up to date.
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
{(canUpgrade) && (
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
|
||||
A new version of the PiPedal server is available. Would you like to update now?
|
||||
</Typography>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
<div style={{ display: "flex", flexFlow: "row noWrap" }}>
|
||||
<div>
|
||||
<Typography noWrap variant="body2" color="textSecondary" >
|
||||
Current version:
|
||||
</Typography>
|
||||
{(canUpgrade) && (
|
||||
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
|
||||
Update version:
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div style={{ flexShrink: 1, flexGrow: 1, marginLeft: 12 }}>
|
||||
<Typography noWrap variant="body2" color="textSecondary" >
|
||||
{updateStatus.currentVersionDisplayName}
|
||||
</Typography>
|
||||
{(canUpgrade) && (
|
||||
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
|
||||
{updateRelease.upgradeVersionDisplayName}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(updateStatus.errorMessage.length !== 0) &&
|
||||
(
|
||||
<Typography variant="body2" style={{ marginTop: 12 }}>
|
||||
{
|
||||
updateStatus.errorMessage
|
||||
}
|
||||
</Typography>
|
||||
|
||||
)
|
||||
}
|
||||
<Button style={{ marginLeft: 16, marginTop: 12 }} onClick={() => { this.onViewReleaseNotes(); }}>View release notes</Button>
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<DialogActions>
|
||||
{
|
||||
(canUpgrade) ?
|
||||
(
|
||||
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}>
|
||||
<Button variant="dialogSecondary" onClick={() => { this.handleUpdateLater(); }}>Update later</Button>
|
||||
<div style={{ flexGrow: 1, flexShrink: 1 }} />
|
||||
<Button variant="dialogPrimary" onClick={() => { this.handleUpdateNow(); }}>Update now</Button>
|
||||
</div>
|
||||
|
||||
|
||||
) : (
|
||||
|
||||
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}
|
||||
onClick={()=>{ this.model.showUpdateDialog(false); }}
|
||||
>
|
||||
<div style={{ flexGrow: 1, flexShrink: 1 }} />
|
||||
<Button variant="dialogPrimary">OK</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</DialogActions>
|
||||
|
||||
<DialogEx
|
||||
tag="UpdateAlert"
|
||||
open={this.state.alertDialogOpen}
|
||||
onClose={() => { this.handleCloseAlert(); }}
|
||||
aria-describedby="Alert Dialog"
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="secondaryText">
|
||||
{
|
||||
this.state.alertDialogMessage
|
||||
}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="dialogPrimary" onClick={() => { this.handleCloseAlert(); }} autoFocus>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
|
||||
</DialogEx >
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
export enum UpdatePolicyT {
|
||||
// must match declaration in Updater.hpp
|
||||
ReleaseOnly = 0,
|
||||
ReleaseOrBeta = 1,
|
||||
Development = 2,
|
||||
|
||||
};
|
||||
export function intToUpdatePolicyT(intValue: number): UpdatePolicyT {
|
||||
return (Object.values(UpdatePolicyT).find(enumValue => enumValue === intValue)) as UpdatePolicyT;
|
||||
}
|
||||
export class UpdateRelease {
|
||||
deserialize(input: any): UpdateRelease {
|
||||
this.updateAvailable = input.updateAvailable;
|
||||
this.upgradeVersion = input.upgradeVersion;
|
||||
this.upgradeVersionDisplayName = input.upgradeVersionDisplayName;
|
||||
this.assetName = input.assetName;
|
||||
this.updateUrl = input.updateUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
updateAvailable: boolean = false;
|
||||
upgradeVersion: string = ""; //just the version.
|
||||
upgradeVersionDisplayName: string = ""; // display name for the version.
|
||||
assetName: string = ""; // filename only
|
||||
updateUrl: string = ""; // url from which to download the .deb file.
|
||||
|
||||
equals(other: UpdateRelease): boolean {
|
||||
return (this.updateAvailable === other.updateAvailable) &&
|
||||
(this.upgradeVersion === other.upgradeVersion) &&
|
||||
(this.upgradeVersionDisplayName === other.upgradeVersionDisplayName) &&
|
||||
(this.assetName === other.assetName) &&
|
||||
(this.updateUrl === other.updateUrl)
|
||||
;
|
||||
}
|
||||
};
|
||||
|
||||
export class UpdateStatus {
|
||||
lastUpdateTime: number = 0;
|
||||
isValid: boolean = false;
|
||||
errorMessage: string = "";
|
||||
updatePolicy: UpdatePolicyT = UpdatePolicyT.ReleaseOrBeta;
|
||||
isOnline: boolean = false;
|
||||
currentVersion: string = "";
|
||||
currentVersionDisplayName: string = "";
|
||||
|
||||
releaseOnlyRelease: UpdateRelease = new UpdateRelease();
|
||||
releaseOrBetaRelease: UpdateRelease = new UpdateRelease();
|
||||
devRelease: UpdateRelease = new UpdateRelease();
|
||||
|
||||
deserialize(input: any): UpdateStatus {
|
||||
this.lastUpdateTime = input.lastUpdateTime;
|
||||
this.isValid = input.isValid;
|
||||
this.errorMessage = input.errorMessage;
|
||||
this.isOnline = input.isOnline;
|
||||
this.updatePolicy = intToUpdatePolicyT(input.updatePolicy);
|
||||
this.currentVersion = input.currentVersion;
|
||||
this.currentVersionDisplayName = input.currentVersionDisplayName;
|
||||
this.releaseOnlyRelease = new UpdateRelease().deserialize(input.releaseOnlyRelease);
|
||||
this.releaseOrBetaRelease = new UpdateRelease().deserialize(input.releaseOrBetaRelease);
|
||||
this.devRelease = new UpdateRelease().deserialize(input.devRelease);
|
||||
return this;
|
||||
}
|
||||
|
||||
static deserialize_array(input: any): UpdateStatus[] {
|
||||
let result: UpdateStatus[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new UpdateStatus().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getActiveRelease(): UpdateRelease {
|
||||
switch (this.updatePolicy) {
|
||||
case UpdatePolicyT.ReleaseOnly:
|
||||
return this.releaseOnlyRelease;
|
||||
case UpdatePolicyT.ReleaseOrBeta:
|
||||
return this.releaseOrBetaRelease;
|
||||
case UpdatePolicyT.Development:
|
||||
return this.devRelease;
|
||||
default:
|
||||
throw new Error("Invalid argument.");
|
||||
}
|
||||
}
|
||||
equals(other: UpdateStatus): boolean {
|
||||
return (this.isValid === other.isValid)
|
||||
&& (this.lastUpdateTime === other.lastUpdateTime)
|
||||
&& (this.errorMessage === other.errorMessage)
|
||||
&& (this.isOnline === other.isOnline)
|
||||
&& (this.currentVersion === other.currentVersion)
|
||||
&& (this.currentVersionDisplayName === other.currentVersionDisplayName)
|
||||
&& (this.releaseOnlyRelease.equals(other.releaseOnlyRelease))
|
||||
&& (this.releaseOrBetaRelease.equals(other.releaseOrBetaRelease))
|
||||
&& (this.devRelease.equals(other.devRelease))
|
||||
;
|
||||
// other properties are contractually dealt with.
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user