Screen orientation, Keep Display on (in Android Client)

This commit is contained in:
Robin E. R. Davies
2025-07-19 00:06:17 -04:00
parent 818a08824c
commit 9b6798134d
15 changed files with 559 additions and 51 deletions
Binary file not shown.
+1
View File
@@ -36,6 +36,7 @@ header_pages:
theme: minima theme: minima
plugins: plugins:
- jekyll-feed - jekyll-feed
- jekyll-seo-tag
include: include:
- .well-known - .well-known
+1
View File
@@ -313,6 +313,7 @@ int main(int argc, char *argv[])
sigset_t sigSet; sigset_t sigSet;
int s; int s;
sigemptyset(&sigSet); sigemptyset(&sigSet);
sigaddset(&sigSet, SIGINT); sigaddset(&sigSet, SIGINT);
sigaddset(&sigSet, SIGTERM); sigaddset(&sigSet, SIGTERM);
sigaddset(&sigSet, SIGUSR1); sigaddset(&sigSet, SIGUSR1);
+16 -16
View File
@@ -101,22 +101,22 @@
// Run the function when the window loads // Run the function when the window loads
window.addEventListener('load', removeHashOnLoad); window.addEventListener('load', removeHashOnLoad);
// used by Android client to perform focus scrolling on keyboard open.
function getFocusedElementBounds() { // function getFocusedElementBounds() {
let focusedElement = document.activeElement; // let focusedElement = document.activeElement;
if (focusedElement && focusedElement !== document.body) { // if (focusedElement && focusedElement !== document.body) {
let rect = focusedElement.getBoundingClientRect(); // let rect = focusedElement.getBoundingClientRect();
return { // return {
top: rect.top, // top: rect.top,
left: rect.left, // left: rect.left,
right: rect.right, // right: rect.right,
bottom: rect.bottom, // bottom: rect.bottom,
windowWidth: window.innerWidth, // windowWidth: window.innerWidth,
windowHeight: window.innerHeight, // windowHeight: window.innerHeight,
}; // };
} // }
return null; // return null;
} // }
</script> </script>
+21 -1
View File
@@ -32,6 +32,11 @@ export interface AndroidHostInterface {
setThemePreference(theme: number): void; setThemePreference(theme: number): void;
getThemePreference(): number; getThemePreference(): number;
isDarkTheme?: ()=> boolean; isDarkTheme?: ()=> boolean;
setServerVersion(serverVersion: string) : void;
setKeepScreenOn(keepScreenOn: boolean): void;
getKeepScreenOn(): boolean;
setScreenOrientation(orientation: number): void;
getScreenOrientation(): number;
}; };
export class FakeAndroidHost implements AndroidHostInterface export class FakeAndroidHost implements AndroidHostInterface
@@ -40,7 +45,7 @@ export class FakeAndroidHost implements AndroidHostInterface
return true; return true;
} }
getHostVersion(): string { getHostVersion(): string {
return "Fake Android 1.0"; return "Fake Host: v1.1.26";
} }
chooseNewDevice(): void { chooseNewDevice(): void {
@@ -64,6 +69,21 @@ export class FakeAndroidHost implements AndroidHostInterface
getThemePreference(): number { getThemePreference(): number {
return this.theme; return this.theme;
} }
setServerVersion(_serverVersion: string): void {
// No-op for fake host
}
private keepScreenOn = false;
setKeepScreenOn(keepScreenOn: boolean): void {this.keepScreenOn = keepScreenOn; };
getKeepScreenOn(): boolean { return this.keepScreenOn; };
private screenOrientation = 0;
setScreenOrientation(_orientation: number): void {
this.screenOrientation = _orientation;
}
getScreenOrientation(): number {
return this.screenOrientation;
}
} }
export function isAndroidHosted(): boolean { export function isAndroidHosted(): boolean {
return ((window as any).AndroidHost as AndroidHostInterface) !== undefined; return ((window as any).AndroidHost as AndroidHostInterface) !== undefined;
+1 -1
View File
@@ -1000,7 +1000,7 @@ export
<ListItemIcon > <ListItemIcon >
<VolunteerActivismIcon className={classes.menuIcon} color="inherit" /> <VolunteerActivismIcon className={classes.menuIcon} color="inherit" />
</ListItemIcon> </ListItemIcon>
<ListItemText primary='Donations' /> <ListItemText primary='Sponsorship' />
</ListItemButton> </ListItemButton>
</List> </List>
+49 -8
View File
@@ -23,8 +23,9 @@ import { createStyles } from './WithStyles';
// import Tone3000Dialog from './Tone3000Dialog'; // import Tone3000Dialog from './Tone3000Dialog';
import Tone3000HelpDialog from './Tone3000HelpDialog'; import Tone3000HelpDialog from './Tone3000HelpDialog';
import GuitarMLHelpDialog from './GuitarMlHelpDialog';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import Link from '@mui/material/Link'; import LinkEx from './LinkEx';
import DraggableButtonBase from './DraggableButtonBase'; import DraggableButtonBase from './DraggableButtonBase';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
@@ -69,6 +70,7 @@ import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile"; const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
const ToobMlModelFileUrl = "http://two-play.com/plugins/toob-ml#modelFile";
const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_THRESHOLD = 48;
@@ -168,7 +170,8 @@ export interface FilePropertyDialogState {
multiSelect: boolean, multiSelect: boolean,
selectedFiles: string[], selectedFiles: string[],
//openTone3000Dialog: boolean, //openTone3000Dialog: boolean,
openTone3000Help: boolean openTone3000Help: boolean,
openGuitarMlHelp: boolean
}; };
@@ -241,7 +244,8 @@ export default withStyles(
multiSelect: false, multiSelect: false,
selectedFiles: [], selectedFiles: [],
//openTone3000Dialog: false, //openTone3000Dialog: false,
openTone3000Help: false openTone3000Help: false,
openGuitarMlHelp: false
}; };
this.requestScroll = true; this.requestScroll = true;
} }
@@ -1057,6 +1061,15 @@ export default withStyles(
// } // }
handleGuitarMlHelp(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
e.preventDefault();
this.setState({ openGuitarMlHelp: true });
}
handleTone3000Help(e: React.MouseEvent<HTMLButtonElement>) { handleTone3000Help(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@@ -1068,6 +1081,7 @@ export default withStyles(
render() { render() {
const isTracksDirectory = this.isTracksDirectory(); const isTracksDirectory = this.isTracksDirectory();
const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl; const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl;
const isToobMLModelFile = this.props.fileProperty.patchProperty === ToobMlModelFileUrl;
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
let columnWidth = this.state.columnWidth; let columnWidth = this.state.columnWidth;
@@ -1487,17 +1501,16 @@ export default withStyles(
</DialogContent> </DialogContent>
{(!this.state.reordering && !this.state.multiSelect) && ( {(!this.state.reordering && !this.state.multiSelect) && (
<> <>
<DialogActions style={{ justifyContent: "stretch" }}> <DialogActions style={{ justifyContent: "stretch", width: "100%" }}>
{this.state.windowWidth > 500 ? ( <div style={{display: "flex", flexFlow: "column nowrap", width: "100%", alignItems: "stretch", }}>
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "column nowrap" }}>
{isToobNamModelFile && ( {isToobNamModelFile && (
<div style={{ <div style={{
display: "flex", flexFlow: "row nowrap", justifyContent: "center", display: "flex", flexFlow: "row nowrap", justifyContent: "center",
alignItems: "center", width: "100%" alignItems: "center", width: "100%"
}}> }}>
<Typography variant="body2" > <Typography variant="body2" >
Download model files from <Link Download model files from <LinkEx
href="https://www.tone3000.com/search" target="_blank">TONE3000</Link> href="https://www.tone3000.com/search" target="_blank">TONE3000</LinkEx>
</Typography> </Typography>
<IconButtonEx tooltip="Help" <IconButtonEx tooltip="Help"
onClick={(e) => { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }} onClick={(e) => { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }}
@@ -1507,6 +1520,27 @@ export default withStyles(
</div> </div>
)} )}
{isToobMLModelFile && (
<div style={{
display: "flex", flexFlow: "row nowrap", justifyContent: "center", width: "100%",
alignItems: "center"
}}>
<Typography variant="body2" >
Download model files from <LinkEx
href="https://github.com/GuitarML/ToneLibrary/releases/download/v1.0/Proteus_Tone_Packs.zip" target="_blank">GuitarML</LinkEx>
</Typography>
<IconButtonEx tooltip="Help"
onClick={(e) => { this.handleGuitarMlHelp(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }}
>
<HelpOutlineIcon />
</IconButtonEx>
</div>
)}
{this.state.windowWidth > 500 ? (
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "column nowrap" }}>
<div style={{ flex: "1 1 100%", display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}> <div style={{ flex: "1 1 100%", display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
<IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary" <IconButtonEx style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
@@ -1587,6 +1621,7 @@ export default withStyles(
</div> </div>
</div> </div>
)} )}
</div>
</DialogActions> </DialogActions>
</> </>
)} )}
@@ -1702,6 +1737,12 @@ export default withStyles(
onClose={() => this.setState({ openTone3000Help: false })} onClose={() => this.setState({ openTone3000Help: false })}
/> />
)} )}
{this.state.openGuitarMlHelp && (
<GuitarMLHelpDialog
open={this.state.openGuitarMlHelp}
onClose={() => this.setState({ openGuitarMlHelp: false })}
/>
)}
</DialogEx> </DialogEx>
); );
} }
+76
View File
@@ -0,0 +1,76 @@
import DialogEx from "./DialogEx";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import Toolbar from "@mui/material/Toolbar";
import IconButtonEx from "./IconButtonEx";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
function GuitarMlHelpDialog(props: {
open: boolean;
onClose: () => void;
}) {
const { open, onClose } = props;
return (
<DialogEx
tag="tone3000-help"
fullWidth={true}
maxWidth="sm"
onEnterKey={() => { onClose(); }}
onClose={() => { onClose(); }}
open={open}>
<DialogTitle >
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography id="tone3000-auth-dialog-title" noWrap component="div" sx={{ flexGrow: 1 }}>
TONE3000 Help
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "start" }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{
maxWidth: 500, marginLeft: "auto", marginRight: "auto",
display: "flex", flexFlow: "column nowrap", gap: 16, alignItems: "start",
}}>
<Typography variant="body1" component="div" display="block" >
The <Link href="https://www.tone3000.com" target="_blank">GuitarML Tone Library Project</Link> provides
a substantial collection of high-quality neural amp models for use in plugins that use the ML
model file format.
</Typography>
<Typography variant="body1" component="div" display="block">
To use GuitarML models with TooB ML, you must first download the collection of models from the Guitar ML website
by clicking on the link. This should download a file named "Proteus_Tone_Packs.zip" into your browser's Downloads directory.
You must then upload the downloaded zip file, found in your browser's Download directory to the
Pipedal server using the <em>Upload</em> button in PiPedal's file browser dialog.
PiPedal automatically extracts bundles of model files from zip archives, so manual extraction is unnecessary.
</Typography>
<Typography variant="body1" component="div" display="block">
Please consider <Link href="https://www.patreon.com/GuitarML" target="_blank">sponsoring the GuitarML project</Link> in order to support the ongoing development of the Tone Library.
</Typography>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
</DialogContent>
</DialogEx>
);
}
export default GuitarMlHelpDialog;
+2 -2
View File
@@ -70,9 +70,9 @@ export default class JackStatusView extends React.Component<JackStatusViewProps,
render() { render() {
return ( return (
<div style={{ <div style={{
position: "absolute", right: 30, bottom: 0, height: 30, width: 200, position: "absolute", right: 30, bottom: 2,left: 30, height: 30,
paddingRight: 20, paddingBottom: 6, paddingRight: 20, paddingBottom: 6,
textAlign: "right", opacity: 0.7, whiteSpace: "nowrap", fontSize: 12, zIndex: 10, fontWeight: 900 textAlign: "center", opacity: 0.7, whiteSpace: "nowrap", fontSize: 12, zIndex: 10, fontWeight: 900
}}> }}>
{JackHostStatus.getDisplayView("",this.state.jackStatus) } {JackHostStatus.getDisplayView("",this.state.jackStatus) }
</div> </div>
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* 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 Link, {LinkProps} from '@mui/material/Link';
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
export interface LinkExProps extends LinkProps {
}
function LinkEx(props: LinkExProps) {
const { href, ...rest } = props;
function handleClick(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) {
const model:PiPedalModel = PiPedalModelFactory.getInstance();
// Prevent default behavior if needed
if (model.isAndroidHosted()) {
model.androidHost?.launchExternalUrl(href as string);
} else {
window.open(href as string, '_blank', 'noopener,noreferrer');
}
event.preventDefault();
event.stopPropagation();
}
return (
<Link onClick={(e: React.MouseEvent<HTMLSpanElement, MouseEvent>)=>{ handleClick(e); }} component="span"
{...rest} />
)
}
export default LinkEx;
+17 -3
View File
@@ -79,6 +79,7 @@ interface PerformanceViewState {
presets: PresetIndex; presets: PresetIndex;
banks: BankIndex; banks: BankIndex;
showSnapshotEditor: boolean; showSnapshotEditor: boolean;
showStatusMonitor: boolean;
snapshotEditorIndex: number; snapshotEditorIndex: number;
presetModified: boolean; presetModified: boolean;
} }
@@ -100,15 +101,24 @@ export const PerformanceView =
banks: this.model.banks.get(), banks: this.model.banks.get(),
wrapSelects: false, wrapSelects: false,
showSnapshotEditor: false, showSnapshotEditor: false,
showStatusMonitor: this.model.showStatusMonitor.get(),
snapshotEditorIndex: 0, snapshotEditorIndex: 0,
presetModified: this.model.presetChanged.get() presetModified: this.model.presetChanged.get()
}; };
this.onPresetsChanged = this.onPresetsChanged.bind(this); this.onPresetsChanged = this.onPresetsChanged.bind(this);
this.onBanksChanged = this.onBanksChanged.bind(this); this.onBanksChanged = this.onBanksChanged.bind(this);
this.onPresetChangedChanged = this.onPresetChangedChanged.bind(this); this.onPresetChangedChanged = this.onPresetChangedChanged.bind(this);
this.showStatusMonitorHandler = this.showStatusMonitorHandler.bind(this);
} }
showStatusMonitorHandler() {
this.setState({
showStatusMonitor: this.model.showStatusMonitor.get()
});
}
getTag() { return "performView"} getTag() { return "performView"}
isOpen() { return this.props.open } isOpen() { return this.props.open }
onDialogStackClose() { onDialogStackClose() {
@@ -156,6 +166,8 @@ export const PerformanceView =
this.model.presets.addOnChangedHandler(this.onPresetsChanged); this.model.presets.addOnChangedHandler(this.onPresetsChanged);
this.model.banks.addOnChangedHandler(this.onBanksChanged); this.model.banks.addOnChangedHandler(this.onBanksChanged);
this.model.presetChanged.addOnChangedHandler(this.onPresetChangedChanged) this.model.presetChanged.addOnChangedHandler(this.onPresetChangedChanged)
this.model.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
this.setState({ this.setState({
presets: this.model.presets.get(), presets: this.model.presets.get(),
banks: this.model.banks.get(), banks: this.model.banks.get(),
@@ -173,6 +185,8 @@ export const PerformanceView =
this.model.presetChanged.removeOnChangedHandler(this.onPresetChangedChanged) this.model.presetChanged.removeOnChangedHandler(this.onPresetChangedChanged)
this.model.presets.removeOnChangedHandler(this.onPresetsChanged); this.model.presets.removeOnChangedHandler(this.onPresetsChanged);
this.model.banks.removeOnChangedHandler(this.onBanksChanged); this.model.banks.removeOnChangedHandler(this.onBanksChanged);
this.model.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
this.mounted = false; this.mounted = false;
this.updateHooks(); this.updateHooks();
@@ -394,9 +408,9 @@ export const PerformanceView =
</div> </div>
{!this.state.showSnapshotEditor && ( {!this.state.showSnapshotEditor && this.state.showStatusMonitor && (
<JackStatusView />) <JackStatusView />
} )}
</div> </div>
{this.state.showSnapshotEditor && ( {this.state.showSnapshotEditor && (
<SnapshotEditor snapshotIndex={this.state.snapshotEditorIndex} <SnapshotEditor snapshotIndex={this.state.snapshotEditorIndex}
+93
View File
@@ -25,6 +25,7 @@ import ObservableEvent from './ObservableEvent';
import { ObservableProperty } from './ObservableProperty'; import { ObservableProperty } from './ObservableProperty';
import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard' import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard'
import PluginClass from './PluginClass'; import PluginClass from './PluginClass';
import ScreenOrientation from './ScreenOrientation';
import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket'; import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket';
import { nullCast } from './Utility' import { nullCast } from './Utility'
import { JackConfiguration, JackChannelSelection } from './Jack'; import { JackConfiguration, JackChannelSelection } from './Jack';
@@ -82,6 +83,59 @@ export enum ReconnectReason {
HotspotChanging HotspotChanging
}; };
export class HostVersion {
constructor(hostVersion: string) {
this.versionString = hostVersion;
let pos = hostVersion.indexOf(":");
let remainder: string;
if (pos >= 0) {
this.hostName = hostVersion.substring(0, pos).trim();
remainder = hostVersion.substring(pos + 1).trim();
} else {
this.hostName = "Unknown Host";
remainder = "v0.0.0";
}
const args = remainder.split(","); // make make provisions for more.
if (args.length >= 1)
{
let versionString = args[0].trim();
if (versionString.startsWith('v')) {
versionString = versionString.substring(1).trim();
}
let releaseTypePos = versionString.indexOf("-");
if (releaseTypePos >= 0) {
this.releaseType = versionString.substring(releaseTypePos + 1).trim();
versionString = versionString.substring(0, releaseTypePos).trim();
}
else {
this.releaseType = "Release";
}
const parts = args[0].split(".");
if (parts.length >= 3) {
this.majorVersion = parseInt(parts[0]);
this.minorVersion = parseInt(parts[1]);
this.buildNumber = parseInt(parts[2]);
}
}
}
lessThan(major: number, minor: number, build: number): boolean {
if (this.majorVersion < major) return true;
if (this.majorVersion > major) return false;
if (this.minorVersion < minor) return true;
if (this.minorVersion > minor) return false;
return this.buildNumber < build;
}
versionString: string = "";
hostName: string = "";
releaseType: string = "";
majorVersion: number = 0;
minorVersion: number = 0;
buildNumber: number = 0;
}
export type PedalboardItemEnabledChangeCallback = (instanceId: number, isEnabled: boolean) => void; export type PedalboardItemEnabledChangeCallback = (instanceId: number, isEnabled: boolean) => void;
interface PedalboardItemEnabledChangeItem { interface PedalboardItemEnabledChangeItem {
handle: number; handle: number;
@@ -440,6 +494,12 @@ export class PiPedalModel //implements PiPedalModel
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false); hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
canKeepScreenOn: boolean = false;
keepScreenOn: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
canSetScreenOrientation: boolean = false;
screenOrientation: ObservableProperty<ScreenOrientation> = new ObservableProperty<ScreenOrientation>(ScreenOrientation.SystemDefault);
hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false); hasWifiDevice: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>(); onSnapshotModified: ObservableEvent<SnapshotModifiedEvent> = new ObservableEvent<SnapshotModifiedEvent>();
@@ -504,6 +564,7 @@ export class PiPedalModel //implements PiPedalModel
} }
androidHost?: AndroidHostInterface; androidHost?: AndroidHostInterface;
hostVersion?: HostVersion;
constructor() { constructor() {
this.androidHost = (window as any).AndroidHost as AndroidHostInterface; this.androidHost = (window as any).AndroidHost as AndroidHostInterface;
@@ -1295,6 +1356,17 @@ export class PiPedalModel //implements PiPedalModel
.then((succeeded) => { .then((succeeded) => {
if (succeeded) { if (succeeded) {
this.state.set(State.Ready); this.state.set(State.Ready);
if (this.androidHost) {
this.hostVersion = new HostVersion(this.androidHost.getHostVersion());
if (!this.hostVersion.lessThan(1,1,16))
{
this.androidHost.setServerVersion(this.serverVersion?.serverVersion??"");
this.canKeepScreenOn = true;
this.keepScreenOn.set(this.androidHost.getKeepScreenOn())
this.canSetScreenOrientation = true;
this.screenOrientation.set(this.androidHost.getScreenOrientation())
}
}
} }
}) })
.catch((error) => { .catch((error) => {
@@ -3412,6 +3484,27 @@ export class PiPedalModel //implements PiPedalModel
return false; return false;
} }
setKeepScreenOn(keepScreenOn: boolean): void {
if (this.canKeepScreenOn) {
this.keepScreenOn.set(keepScreenOn);
if (this.androidHost) {
this.androidHost.setKeepScreenOn(keepScreenOn);
}
}
}
getKeepScreenOn(): boolean {
return this.keepScreenOn.get();
}
getScreenOrientation(): ScreenOrientation {
return this.screenOrientation.get();
}
setScreenOrientation(value: ScreenOrientation) {
if (this.canSetScreenOrientation) {
this.screenOrientation.set(value);
this.androidHost?.setScreenOrientation(value as number);
}
}
}; };
let instance: PiPedalModel | undefined = undefined; let instance: PiPedalModel | undefined = undefined;
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* 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.
*/
enum ScreenOrientation {
// values must match Pipedal Remote ScreenOrientation.java
SystemDefault = 0,
Landscape = 1,
Portrait = 2
}
export default ScreenOrientation;
@@ -0,0 +1,74 @@
// Copyright (c) 2022 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.
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import DialogEx from './DialogEx';
import { ListItemButton } from '@mui/material';
import ScreenOrientation from './ScreenOrientation';
export interface SelectScreenOrientationDialogProps {
open: boolean;
screenOrientation: ScreenOrientation;
onClose: (screenOrientation: ScreenOrientation | null) => void;
}
function SelectScreenOrientationDialog(props: SelectScreenOrientationDialogProps) {
//const classes = useStyles();
const { onClose, screenOrientation, open } = props;
const handleCancel = () => {
onClose(null);
}
const handleOk = (value: ScreenOrientation) => {
onClose(value);
};
const handleListItemClick = (value: ScreenOrientation) => {
handleOk(value);
};
return (
<DialogEx tag="channels" onClose={handleCancel} aria-labelledby="select-channels-title" open={open}
onEnterKey={() => { }}
>
<List style={{ marginLeft: 0, marginRight: 0 }}>
<ListItemButton onClick={() => handleListItemClick(ScreenOrientation.SystemDefault)} key={"0"}
selected={screenOrientation === ScreenOrientation.SystemDefault} >
<ListItemText primary={"System default"} />
</ListItemButton>
<ListItemButton onClick={() => handleListItemClick(ScreenOrientation.Portrait)} key={"1"}
selected={screenOrientation === ScreenOrientation.Portrait} >
<ListItemText primary={"Portrait"} />
</ListItemButton>
<ListItemButton onClick={() => handleListItemClick(ScreenOrientation.Landscape)} key={"2"}
selected={screenOrientation === ScreenOrientation.Landscape} >
<ListItemText primary={"Landscape"} />
</ListItemButton>
</List>
</DialogEx>
);
}
export default SelectScreenOrientationDialog;
+106 -1
View File
@@ -57,6 +57,8 @@ import WithStyles from './WithStyles';
import { canScaleWindow, getWindowScaleOptions, getWindowScaleText, setWindowScale, getWindowScale } from './WindowScale'; import { canScaleWindow, getWindowScaleOptions, getWindowScaleText, setWindowScale, getWindowScale } from './WindowScale';
import OptionsDialog from './OptionsDialog'; import OptionsDialog from './OptionsDialog';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import ScreenOrientation from './ScreenOrientation';
import SelectScreenOrientationDialog from './SelectScreenOrientationDialog';
interface SettingsDialogProps extends WithStyles<typeof styles> { interface SettingsDialogProps extends WithStyles<typeof styles> {
@@ -74,6 +76,8 @@ interface SettingsDialogState {
jackSettings: JackChannelSelection; jackSettings: JackChannelSelection;
jackServerSettings: JackServerSettings; jackServerSettings: JackServerSettings;
alsaSequencerConfiguration: AlsaSequencerConfiguration; alsaSequencerConfiguration: AlsaSequencerConfiguration;
keepScreenOn: boolean;
screenOrientation: ScreenOrientation;
jackStatus?: JackHostStatus; jackStatus?: JackHostStatus;
governorSettings: GovernorSettings; governorSettings: GovernorSettings;
@@ -88,6 +92,7 @@ interface SettingsDialogState {
showGovernorSettingsDialog: boolean; showGovernorSettingsDialog: boolean;
showInputSelectDialog: boolean; showInputSelectDialog: boolean;
showOutputSelectDialog: boolean; showOutputSelectDialog: boolean;
showScreenOrientationDialog: boolean;
showMidiSelectDialog: boolean; showMidiSelectDialog: boolean;
showThemeSelectDialog: boolean; showThemeSelectDialog: boolean;
showJackServerSettingsDialog: boolean; showJackServerSettingsDialog: boolean;
@@ -191,13 +196,15 @@ const SettingsDialog = withStyles(
wifiDirectConfigSettings: this.model.wifiDirectConfigSettings.get(), wifiDirectConfigSettings: this.model.wifiDirectConfigSettings.get(),
governorSettings: this.model.governorSettings.get(), governorSettings: this.model.governorSettings.get(),
continueDisabled: true, continueDisabled: true,
keepScreenOn: this.model.keepScreenOn.get(),
screenOrientation: this.model.getScreenOrientation(),
showWindowScaleDialog: false, showWindowScaleDialog: false,
showWifiConfigDialog: false, showWifiConfigDialog: false,
showWifiDirectConfigDialog: false, showWifiDirectConfigDialog: false,
showGovernorSettingsDialog: false, showGovernorSettingsDialog: false,
showInputSelectDialog: false, showInputSelectDialog: false,
showOutputSelectDialog: false, showOutputSelectDialog: false,
showScreenOrientationDialog: false,
showMidiSelectDialog: false, showMidiSelectDialog: false,
showThemeSelectDialog: false, showThemeSelectDialog: false,
showJackServerSettingsDialog: false, showJackServerSettingsDialog: false,
@@ -219,9 +226,19 @@ const SettingsDialog = withStyles(
this.handleConnectionStateChanged = this.handleConnectionStateChanged.bind(this); this.handleConnectionStateChanged = this.handleConnectionStateChanged.bind(this);
this.handleShowStatusMonitorChanged = this.handleShowStatusMonitorChanged.bind(this); this.handleShowStatusMonitorChanged = this.handleShowStatusMonitorChanged.bind(this);
this.handleHasWifiChanged = this.handleHasWifiChanged.bind(this); this.handleHasWifiChanged = this.handleHasWifiChanged.bind(this);
this.handleKeepScreenOnChanged = this.handleKeepScreenOnChanged.bind(this);
this.handleScreenOrientationChanged = this.handleScreenOrientationChanged.bind(this);
} }
handleScreenOrientationChanged(newValue: ScreenOrientation) {
this.setState({
screenOrientation: newValue
});
}
handleKeepScreenOnChanged(newValue: boolean) {
this.setState({ keepScreenOn: newValue });
}
handleHasWifiChanged(newValue: boolean) { handleHasWifiChanged(newValue: boolean) {
this.setState({ hasWifiDevice: newValue }); this.setState({ hasWifiDevice: newValue });
} }
@@ -337,6 +354,8 @@ const SettingsDialog = withStyles(
if (active !== this.active) { if (active !== this.active) {
this.active = active; this.active = active;
if (active) { if (active) {
this.model.keepScreenOn.addOnChangedHandler(this.handleKeepScreenOnChanged);
this.model.screenOrientation.addOnChangedHandler(this.handleScreenOrientationChanged);
this.model.hasWifiDevice.addOnChangedHandler(this.handleHasWifiChanged); this.model.hasWifiDevice.addOnChangedHandler(this.handleHasWifiChanged);
this.model.state.addOnChangedHandler(this.handleConnectionStateChanged); this.model.state.addOnChangedHandler(this.handleConnectionStateChanged);
this.model.showStatusMonitor.addOnChangedHandler(this.handleShowStatusMonitorChanged); this.model.showStatusMonitor.addOnChangedHandler(this.handleShowStatusMonitorChanged);
@@ -377,6 +396,8 @@ const SettingsDialog = withStyles(
} }
this.model.state.removeOnChangedHandler(this.handleConnectionStateChanged); this.model.state.removeOnChangedHandler(this.handleConnectionStateChanged);
this.model.showStatusMonitor.removeOnChangedHandler(this.handleShowStatusMonitorChanged); this.model.showStatusMonitor.removeOnChangedHandler(this.handleShowStatusMonitorChanged);
this.model.keepScreenOn.removeOnChangedHandler(this.handleKeepScreenOnChanged);
this.model.screenOrientation.removeOnChangedHandler(this.handleScreenOrientationChanged);
this.model.jackConfiguration.removeOnChangedHandler(this.handleJackConfigurationChanged); this.model.jackConfiguration.removeOnChangedHandler(this.handleJackConfigurationChanged);
this.model.alsaSequencerConfiguration.removeOnChangedHandler(this.handleAlsaSequencerConfigurationChanged); this.model.alsaSequencerConfiguration.removeOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
@@ -441,6 +462,28 @@ const SettingsDialog = withStyles(
showOutputSelectDialog: false showOutputSelectDialog: false
}); });
} }
handleScreenOrientation() {
this.setState({
showScreenOrientationDialog: true,
});
}
getOrientationText() {
switch (this.state.screenOrientation) {
case ScreenOrientation.Portrait:
return "Portrait";
case ScreenOrientation.Landscape:
return "Landscape";
default:
return "System default";
}
}
handleScreenOrientationDialogResult(orientation: ScreenOrientation | null): void {
if (orientation !== null) {
this.setState({ screenOrientation: orientation });
this.model.setScreenOrientation(orientation);
}
this.setState({ showScreenOrientationDialog: false });
}
handleOutputSelection() { handleOutputSelection() {
this.setState({ this.setState({
@@ -553,6 +596,8 @@ const SettingsDialog = withStyles(
let selectedChannels: string[] = this.state.showInputSelectDialog ? this.state.jackSettings.inputAudioPorts : this.state.jackSettings.outputAudioPorts; let selectedChannels: string[] = this.state.showInputSelectDialog ? this.state.jackSettings.inputAudioPorts : this.state.jackSettings.outputAudioPorts;
let disableShutdown = this.state.shuttingDown || this.state.restarting; let disableShutdown = this.state.shuttingDown || this.state.restarting;
let canKeepScreenOn = this.model.canKeepScreenOn;
return ( return (
<DialogEx tag="settings" fullScreen open={this.props.open} <DialogEx tag="settings" fullScreen open={this.props.open}
onClose={() => { this.props.onClose() }} TransitionComponent={Transition} onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
@@ -730,6 +775,51 @@ const SettingsDialog = withStyles(
</Typography> </Typography>
</div> </div>
</ButtonBase> </ButtonBase>
{canKeepScreenOn &&
(
<ButtonBase
className={classes.setting}
onClick={() => {
this.model.setKeepScreenOn(!this.state.keepScreenOn)
}} >
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<div style={{
width: "100%", display: "flex", flexDirection: "row", flexWrap: "nowrap",
alignItems: "center", maxWidth: 400
}}>
<div style={{ flex: "1 1 auto" }}>
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
Keep screen on.</Typography>
</div>
<div style={{ flex: "0 0 auto" }}>
<Switch
checked={this.state.keepScreenOn}
onChange={
(e) => { this.model.setKeepScreenOn(e.target.checked); }
}
/>
</div>
</div>
</div>
</ButtonBase>
)
}
{this.model.canSetScreenOrientation && (
<ButtonBase className={classes.setting} onClick={() => this.handleScreenOrientation()}
>
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography display="block" variant="body2" noWrap>Display Orientation</Typography>
<Typography display="block" variant="caption" color="textSecondary" noWrap>{
this.getOrientationText()
}</Typography>
</div>
</ButtonBase>
)
}
{(canScaleWindow()) && {(canScaleWindow()) &&
( (
@@ -908,6 +998,21 @@ const SettingsDialog = withStyles(
) )
} }
{
this.state.showScreenOrientationDialog &&
(
<SelectScreenOrientationDialog
open={this.state.showScreenOrientationDialog}
onClose={(screenOrientation: ScreenOrientation | null) => {
this.handleScreenOrientationDialogResult(screenOrientation);
}}
screenOrientation={this.state.screenOrientation}
/>
)
}
{ {
(this.state.showGovernorSettingsDialog) && (this.state.showGovernorSettingsDialog) &&
( (