ToobPlayer UI. .mdata file handling.

This commit is contained in:
Robin E. R. Davies
2025-06-19 16:04:32 -04:00
parent bf9846e37f
commit f35ee9330d
29 changed files with 671 additions and 425 deletions
+35 -1
View File
@@ -21,7 +21,7 @@
* SOFTWARE.
*/
import { pathConcat, pathParentDirectory } from "./FileUtils";
import { pathConcat, pathParentDirectory, pathFileName } from "./FileUtils";
import { PiPedalModel } from "./PiPedalModel";
export class ThumbnailType {
@@ -32,6 +32,40 @@ export class ThumbnailType {
};
const fileVersionRegex = /\((\d+)\)\.[0-9a-zA-Z]*$/;
function getVersionSuffix(filePath: string): string | null {
const match = filePath.match(fileVersionRegex);
return match ? match[1] : null;
}
export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string {
if (!metadata) {
return pathFileName(pathname);
}
if (metadata.title === "") {
return pathFileName(pathname);
}
let trackDisplay = "";
if (metadata.track > 0) {
if (metadata.track >= 1000) {
trackDisplay = (metadata.track % 1000).toString() + "/" + Math.floor(metadata.track / 1000) + ". ";
} else {
trackDisplay = metadata.track.toString() + ". ";
}
}
let result = trackDisplay + metadata.title;
if (result === "") {
result = pathFileName(pathname);
}
let versionSuffix = getVersionSuffix(pathname);
if (versionSuffix) {
result += " (" + versionSuffix + ")";
}
return result;
}
export default class AudioFileMetadata {
//AudioFileMetadata&operator=(const AudioFileMetadata&) = default;
deserialize(o: any) {
+13 -13
View File
@@ -18,7 +18,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React, { Component } from 'react';
import IconButton from '@mui/material/IconButton';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { BankIndexEntry, BankIndex } from './Banks';
@@ -422,17 +422,17 @@ const BankDialog = withStyles(
<div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()}
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
<Typography noWrap variant="h6" className={classes.dialogTitle}>
Banks
</Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
<IconButtonEx tooltip="Edit" color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon />
</IconButton>
</IconButtonEx>
</Toolbar>
</AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
@@ -440,14 +440,14 @@ const BankDialog = withStyles(
>
<Toolbar>
{(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon />
</IconButton>
</IconButtonEx>
) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
)}
<Typography noWrap variant="h6" className={classes.dialogTitle}>
@@ -477,12 +477,12 @@ const BankDialog = withStyles(
}
}
/>
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
</IconButtonEx>
<IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon />
</IconButton>
</IconButtonEx>
<Menu
id="more-menu"
anchorEl={this.state.moreMenuAnchorEl}
+4 -1
View File
@@ -42,7 +42,10 @@ function ButtonEx(props: ButtonExProps) {
placement="top-start" arrow
enterDelay={1500} enterNextDelay={1500}
>
<Button {...extra} style={style} />
<span>
{/* Using span to prevent tooltip from disappearing when button is disabled */}
<Button {...extra} style={style} />
</span>
</Tooltip>
);
}
+8 -26
View File
@@ -62,7 +62,7 @@ import UploadFileDialog from './UploadFileDialog';
import OkCancelDialog from './OkCancelDialog';
import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri } from './AudioFileMetadata';
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
interface Point {
x: number;
@@ -95,6 +95,7 @@ const audioFileExtensions: { [name: string]: boolean } = {
};
function screenToClient(element: HTMLDivElement, point: Point): Point {
const dpr = (window.devicePixelRatio || 1) as number;;
let rect = element.getBoundingClientRect();
@@ -650,15 +651,18 @@ export default withStyles(
}
getCompactTrackTitle(fileEntry: FileEntry): string {
let metadata = fileEntry.metadata;
let title = getTrackTitle(fileEntry.pathname, metadata);
if (!metadata) {
return "#error";
return title;
}
let title = this.getTrackTitle(fileEntry);
if (metadata.album !== "") {
title += " (" + metadata.album + ")";
}
return title;
}
getAlbumTitle(fileEntry: FileEntry): string {
let artist = fileEntry.metadata?.artist || "";
if (artist == "") {
@@ -668,28 +672,6 @@ export default withStyles(
let joiner = (artist !== "" && album !== "") ? " - " : "";
return album + joiner + artist;
}
getTrackTitle(fileEntry: FileEntry): string {
if (!fileEntry.metadata) {
return pathFileName(fileEntry.pathname);
}
if (fileEntry.metadata.title === "") {
return pathFileName(fileEntry.pathname);
}
let metadata = fileEntry.metadata;
let trackDisplay = "";
if (metadata.track > 0) {
if (metadata.track >= 1000) {
trackDisplay = (metadata.track % 1000).toString() + "/" + Math.floor(metadata.track / 1000) + ". ";
} else {
trackDisplay = metadata.track.toString() + ". ";
}
}
let result = trackDisplay + metadata.title;
if (result === "") {
result = pathFileName(fileEntry.pathname);
}
return result;
}
getTrackThumbnail(fileEntry: FileEntry): string {
return getAlbumArtUri(this.model, fileEntry.metadata, fileEntry.pathname);
}
@@ -1100,7 +1082,7 @@ export default withStyles(
className={classes.secondaryText}
variant="body2"
style={{ flex: "0 0 auto", textOverflow: "ellipsis", textAlign: "left", marginBottom: 4 }}>
{this.getTrackTitle(value)}
{getTrackTitle(value.pathname, value.metadata)}
</Typography>
<Typography noWrap className={classes.secondaryText}
variant="body2"
+11 -4
View File
@@ -42,6 +42,7 @@ import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
export enum State {
@@ -2445,10 +2446,13 @@ export class PiPedalModel //implements PiPedalModel
let downloadUrl = this.varServerUrl + "downloadMediaFile?path=" + encodeURIComponent(filePath);
// download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement;
let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = downloadUrl;
link.setAttribute("download", "");
link.target = "_blank";
link.setAttribute("download", pathFileName(filePath));
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
download(targetType: string, instanceId: number | string): void {
@@ -2457,10 +2461,13 @@ export class PiPedalModel //implements PiPedalModel
// window.open(url, "_blank");
// download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement;
let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = url;
link.setAttribute("download", "");
link.target = "_blank";
link.download = "download.piPreset";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
+21 -22
View File
@@ -450,13 +450,13 @@ const PluginControlView =
/>
));
}
private ixKey: number = 1;
private ixKey: number = 1;
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
let symbol = uiControl.symbol;
if (!uiControl.is_input) {
return (
<PluginOutputControl key={uiControl.symbol } instanceId={this.props.instanceId} uiControl={uiControl} />
<PluginOutputControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} />
);
}
@@ -472,7 +472,7 @@ const PluginControlView =
throw new PiPedalStateError("Missing control value.");
}
return ((
<PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
<PluginControl key={"ppc" + uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
@@ -498,7 +498,7 @@ const PluginControlView =
let previousControl = controls[controls.length - 1];
if (!(previousControl instanceof ControlGroup)) {
let pair = (
<div key={"k"+this.ixKey++} className={classes.controlPair}>
<div key={"k" + this.ixKey++} className={classes.controlPair}>
{previousControl as ReactNode}
{newControl}
</div>
@@ -508,7 +508,7 @@ const PluginControlView =
// (e.g.. inserting at position 4 still places the extended control after four previous controls
controls.push((
<div key={"k"+this.ixKey++} className={classes.controlSpacer} />
<div key={"k" + this.ixKey++} className={classes.controlSpacer} />
));
} else {
controls.push(newControl);
@@ -686,7 +686,7 @@ const PluginControlView =
let item = controlGroup.controls[j];
controls.push(
(
<div key={"ctl" + (this.controlKeyIndex++)} className={classes.controlPadding}>
<div key={"ctlx" + (this.controlKeyIndex++)} className={classes.controlPadding}>
{item}
</div>
@@ -695,7 +695,7 @@ const PluginControlView =
}
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div key={"ctlx" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div className={classes.portGroupTitle}>
<Tooltip title={controlGroup.name}
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
@@ -713,21 +713,20 @@ const PluginControlView =
));
} else {
if (this.fullScreen())
{
result.push(
<div style={{position: "relative", width: "100%", height:"100%"}}
>
{node as ReactNode}
</div>
);
} else {
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
{node as ReactNode}
</div>
));
}
if (this.fullScreen()) {
result.push(
<div key={"fullScreen" + this.props.instanceId} style={{ position: "relative", width: "100%", height: "100%" }}
>
{node as ReactNode}
</div>
);
} else {
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
{node as ReactNode}
</div>
));
}
}
}
+15 -14
View File
@@ -20,10 +20,11 @@
import React, { SyntheticEvent,Component } from 'react';
import { css } from '@emotion/react';
import {isDarkMode} from './DarkMode';
import IconButton from '@mui/material/IconButton';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
import Button from "@mui/material/Button";
import Button from '@mui/material/Button';
import ButtonEx from './ButtonEx';
import ButtonBase from "@mui/material/ButtonBase";
import DialogEx from './DialogEx';
import AppBar from '@mui/material/AppBar';
@@ -351,17 +352,17 @@ const PresetDialog = withStyles(
<div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()}
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
<Typography variant="h6" className={classes.dialogTitle}>
Presets
</Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
<IconButtonEx tooltip="Edit"color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon />
</IconButton>
</IconButtonEx>
</Toolbar>
</AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
@@ -369,14 +370,14 @@ const PresetDialog = withStyles(
>
<Toolbar>
{(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon />
</IconButton>
</IconButtonEx>
) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
)}
<Typography variant="h6" className={classes.dialogTitle}>
@@ -402,12 +403,12 @@ const PresetDialog = withStyles(
}
}
/>
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
</IconButtonEx>
<IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon />
</IconButton>
</IconButtonEx>
<Menu
id="more-menu"
anchorEl={this.state.moreMenuAnchorEl}
+29 -14
View File
@@ -45,6 +45,8 @@ import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat';
import Timebase, { LoopParameters, TimebaseUnits } from './Timebase';
import ControlSlider from './ControlSlider';
import { getTrackTitle } from './AudioFileMetadata';
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
@@ -320,19 +322,9 @@ export default function ToobPlayerControl(
}
model.getAudioFileMetadata(path)
.then((metadata) => {
let strTrack: string = "";
if (metadata.track > 0) {
let disc = (metadata.track / 1000);
if (disc >= 1) {
strTrack = (metadata.track % 1000).toString() + '/' + Math.floor(disc).toString();
} else {
strTrack = metadata.track.toString();
}
strTrack += ". ";
}
let coverArtUri = getAlbumArtUri(model, metadata, path);
setCoverArt(coverArtUri);
setTitle(strTrack + metadata.title);
setTitle(getTrackTitle(path, metadata));
setAlbum(metadata.album);
setArtist(metadata.artist);
setAlbumArtist(metadata.albumArtist);
@@ -419,6 +411,8 @@ export default function ToobPlayerControl(
);
}
function FilePanel() {
let textColor = pluginState == PluginState.Error ? "error" : "textPrimary";
return (
<ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }}
onClick={() => { SelectFile() }}
@@ -442,10 +436,10 @@ export default function ToobPlayerControl(
</Typography>
) : (
<div>
<Typography variant="body1" noWrap>
<Typography variant="body1" color={textColor} noWrap>
{titleLine}
</Typography>
<Typography variant="body2" noWrap sx={{}}>
<Typography variant="body2" color={textColor} noWrap sx={{}}>
{albumLine}
</Typography>
</div>
@@ -483,6 +477,27 @@ export default function ToobPlayerControl(
function onLoopPropertyChanged(loopSettingsJson: string) {
try {
if (loopSettingsJson === "") {
setTimebase(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
})
let loopParameters = {
start: 0.0,
loopEnable: false,
loopStart: 0.0,
loopEnd: 0.0
};
setLoopParameters(loopParameters);
setStart(0.0);
setLoopEnable(false);
setLoopStart(0.0);
setLoopEnd(0.0);
return;
}
let atomObject = JSON.parse(loopSettingsJson);
let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters;
let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);;
@@ -827,7 +842,7 @@ export default function ToobPlayerControl(
playing={!paused}
onPreview={() => { handlePreview(); }}
value={loopParameters}
onCancelPlaying= {()=> { handleCancelPlaying();}}
onCancelPlaying={() => { handleCancelPlaying(); }}
timebase={timebase}
onTimebaseChange={(newTimebase) => {
setTimebase(newTimebase);
+4 -3
View File
@@ -110,14 +110,14 @@ const ToobPlayerView =
for (let mixControl of mixPanel.controls) {
extraControls.push(
(
<div key={"k"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
<div key={"kExtra"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
{mixControl}
</div>
));
}
let panel = (
<ToobPlayerControl key="MusicPlayer" instanceId={this.props.instanceId} extraControls={extraControls} />
<ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
);
let result: (React.ReactNode | ControlGroup)[] = [];
@@ -128,6 +128,7 @@ const ToobPlayerView =
render() {
return (
<PluginControlView
key={"ToobPlayerView" + this.props.instanceId}
instanceId={this.props.instanceId}
item={this.props.item}
customization={this}
@@ -145,7 +146,7 @@ class ToobPlayerViewFactory implements IControlViewFactory {
uri: string = "http://two-play.com/plugins/toob-player";
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
return (<ToobPlayerView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
return (<ToobPlayerView key={"ppmv"+pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
}