\n"
+ << mdDate(download.updated_at())
+ << " "
+ << mdUser(download.user())
+ << "
\n"
+ << " " << mdSizes(download.sizes()) << ", "
+ << mdSizes(download.sizes()) << ", "
+ << mdGear(download.gear()) << ", "
+ << mdPlatform(download.platform())
+ << "
\n"
+ << " License: xxx" << mdLicense(download.license()) << "\n"
+ << "
This is a test page for Tone3000 select functionality.
+ + + + \ No newline at end of file diff --git a/vite/src/pipedal/AudioFileMetadata.tsx b/vite/src/pipedal/AudioFileMetadata.tsx index d38d435..28ff765 100644 --- a/vite/src/pipedal/AudioFileMetadata.tsx +++ b/vite/src/pipedal/AudioFileMetadata.tsx @@ -38,13 +38,78 @@ function getVersionSuffix(filePath: string): string | null { return match ? match[1] : null; } +export function safeFilenameDecode(filename: string): string { + let originalFilename = filename; + let pos = filename.indexOf('%'); + if (pos < 0) { + return filename; + } + let result = ""; + while (true) { + if (pos < 0) { + result += filename; + return result; + } + result += filename.substring(0, pos); + if (pos+3 >= filename.length) { + // invalid encoding. just use the original + return originalFilename; + } + let hex = filename.charAt(pos+1) + filename.charAt(pos+2); + let byte = parseInt(hex, 16); + + // Handle UTF-8 to UTF-16 conversion + if (byte < 0x80) { + // Single-byte character (ASCII) + result += String.fromCharCode(byte); + filename = filename.substring(pos+3); + } else if ((byte & 0xE0) === 0xC0) { + // 2-byte sequence + if (pos+6 > filename.length || filename.charAt(pos+3) !== '%') { + return originalFilename; // invalid encoding + } + let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16); + let codePoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F); + result += String.fromCharCode(codePoint); + filename = filename.substring(pos+6); + } else if ((byte & 0xF0) === 0xE0) { + // 3-byte sequence + if (pos+9 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%') { + return originalFilename; // invalid encoding + } + let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16); + let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16); + let codePoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F); + result += String.fromCharCode(codePoint); + filename = filename.substring(pos+9); + } else if ((byte & 0xF8) === 0xF0) { + // 4-byte sequence (surrogate pair needed) + if (pos+12 > filename.length || filename.charAt(pos+3) !== '%' || filename.charAt(pos+6) !== '%' || filename.charAt(pos+9) !== '%') { + return originalFilename; // invalid encoding + } + let byte2 = parseInt(filename.charAt(pos+4) + filename.charAt(pos+5), 16); + let byte3 = parseInt(filename.charAt(pos+7) + filename.charAt(pos+8), 16); + let byte4 = parseInt(filename.charAt(pos+10) + filename.charAt(pos+11), 16); + let codePoint = ((byte & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F); + // Convert to UTF-16 surrogate pair + codePoint -= 0x10000; + result += String.fromCharCode(0xD800 + (codePoint >> 10)); + result += String.fromCharCode(0xDC00 + (codePoint & 0x3FF)); + filename = filename.substring(pos+12); + } else { + // Invalid UTF-8 start byte + return originalFilename; + } + pos = filename.indexOf('%'); + } +} export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string { if (!metadata) { - return pathFileName(pathname); + return safeFilenameDecode(pathFileName(pathname)); } if (metadata.title === "") { - return pathFileName(pathname); + return safeFilenameDecode(pathFileName(pathname)); } let trackDisplay = ""; if (metadata.track > 0) { @@ -56,7 +121,7 @@ export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | nu } let result = trackDisplay + metadata.title; if (result === "") { - result = pathFileName(pathname); + result = safeFilenameDecode(pathFileName(pathname)); } let versionSuffix = getVersionSuffix(pathname); if (versionSuffix) { diff --git a/vite/src/pipedal/FilePropertyControl.tsx b/vite/src/pipedal/FilePropertyControl.tsx index 5419933..df6be1a 100644 --- a/vite/src/pipedal/FilePropertyControl.tsx +++ b/vite/src/pipedal/FilePropertyControl.tsx @@ -25,6 +25,7 @@ import { Component, SyntheticEvent } from 'react'; import { Theme } from '@mui/material/styles'; import { css } from '@emotion/react'; +import ToolTipEx from './ToolTipEx'; import { UiFileProperty } from './Lv2Plugin'; import Typography from '@mui/material/Typography'; @@ -33,15 +34,16 @@ import ButtonBase from '@mui/material/ButtonBase' import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { PedalboardItem } from './Pedalboard'; import { isDarkMode } from './DarkMode'; - - -export const StandardItemSize = { width: 80, height: 140 } +import {safeFilenameDecode} from './AudioFileMetadata'; import { withStyles } from "tss-react/mui"; import WithStyles from './WithStyles'; import { withTheme } from './WithStyles'; + +export const StandardItemSize = { width: 80, height: 140 } + const styles = (theme: Theme) => { return { frame: css({ @@ -209,11 +211,13 @@ const FilePropertyControl = let fileProperty = this.props.fileProperty; let value = "\u00A0"; + let toolTipText: string | null = null; if (this.state.hasValue) { if (this.state.value.length === 0) { value = "