This commit is contained in:
Robin E.R. Davies
2026-02-04 15:05:37 -05:00
parent f0c3995a8f
commit e5beb97466
35 changed files with 3815 additions and 317 deletions
+1730 -3
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -25,6 +25,9 @@
"react-virtualized-auto-sizer": "^1.0.25",
"react-window": "^1.8.11",
"rehype-external-links": "^3.0.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"tss-react": "^4.9.15"
},
"devDependencies": {
+24 -8
View File
@@ -1,26 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tone3000 Download Received</title>
</head>
<body>
<script>
// Parse URL parameters
const urlParams = new URLSearchParams(window.location.search);
const toneUrl = urlParams.get('tone_url');
// Send the data back to the opener window
if (window.opener) {
window.opener.postMessage({
type: 'tone3000Download',
toneUrl: toneUrl
}, '*');
}
// Close the window after sending the data
var popupWindow = null;;
try {
// Send the data back to the opener window
if (window.opener) {
popupWindow =
window.opener.postMessage({
type: 'tone3000Download',
toneUrl: toneUrl,
popupX: window.screenX,
popupY: window.screenY,
popupWidth: window.innerWidth,
popupHeight: window.innerHeight
}, '*');
} else {
alert("No opener window found.");
}
}
catch (e) {
// Close the window after sending the data
alert(e.message);
}
window.close();
</script>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="37.637" cy="28.806" r="28.276"/>
<g>
<path d="M37.443-3.5c8.988,0,16.57,3.085,22.742,9.257C66.393,11.967,69.5,19.548,69.5,28.5c0,8.991-3.049,16.476-9.145,22.456
C53.879,57.319,46.242,60.5,37.443,60.5c-8.649,0-16.153-3.144-22.514-9.43C8.644,44.784,5.5,37.262,5.5,28.5
c0-8.761,3.144-16.342,9.429-22.742C21.101-0.415,28.604-3.5,37.443-3.5z M37.557,2.272c-7.276,0-13.428,2.553-18.457,7.657
c-5.22,5.334-7.829,11.525-7.829,18.572c0,7.086,2.59,13.22,7.77,18.398c5.181,5.182,11.352,7.771,18.514,7.771
c7.123,0,13.334-2.607,18.629-7.828c5.029-4.838,7.543-10.952,7.543-18.343c0-7.276-2.553-13.465-7.656-18.571
C50.967,4.824,44.795,2.272,37.557,2.272z M46.129,20.557v13.085h-3.656v15.542h-9.944V33.643h-3.656V20.557
c0-0.572,0.2-1.057,0.599-1.457c0.401-0.399,0.887-0.6,1.457-0.6h13.144c0.533,0,1.01,0.2,1.428,0.6
C45.918,19.5,46.129,19.986,46.129,20.557z M33.042,12.329c0-3.008,1.485-4.514,4.458-4.514s4.457,1.504,4.457,4.514
c0,2.971-1.486,4.457-4.457,4.457S33.042,15.3,33.042,12.329z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="37.785" cy="28.501" r="28.836"/>
<path d="M37.441-3.5c8.951,0,16.572,3.125,22.857,9.372c3.008,3.009,5.295,6.448,6.857,10.314
c1.561,3.867,2.344,7.971,2.344,12.314c0,4.381-0.773,8.486-2.314,12.313c-1.543,3.828-3.82,7.21-6.828,10.143
c-3.123,3.085-6.666,5.448-10.629,7.086c-3.961,1.638-8.057,2.457-12.285,2.457s-8.276-0.808-12.143-2.429
c-3.866-1.618-7.333-3.961-10.4-7.027c-3.067-3.066-5.4-6.524-7-10.372S5.5,32.767,5.5,28.5c0-4.229,0.809-8.295,2.428-12.2
c1.619-3.905,3.972-7.4,7.057-10.486C21.08-0.394,28.565-3.5,37.441-3.5z M37.557,2.272c-7.314,0-13.467,2.553-18.458,7.657
c-2.515,2.553-4.448,5.419-5.8,8.6c-1.354,3.181-2.029,6.505-2.029,9.972c0,3.429,0.675,6.734,2.029,9.913
c1.353,3.183,3.285,6.021,5.8,8.516c2.514,2.496,5.351,4.399,8.515,5.715c3.161,1.314,6.476,1.971,9.943,1.971
c3.428,0,6.75-0.665,9.973-1.999c3.219-1.335,6.121-3.257,8.713-5.771c4.99-4.876,7.484-10.99,7.484-18.344
c0-3.543-0.648-6.895-1.943-10.057c-1.293-3.162-3.18-5.98-5.654-8.458C50.984,4.844,44.795,2.272,37.557,2.272z M37.156,23.187
l-4.287,2.229c-0.458-0.951-1.019-1.619-1.685-2c-0.667-0.38-1.286-0.571-1.858-0.571c-2.856,0-4.286,1.885-4.286,5.657
c0,1.714,0.362,3.084,1.085,4.113c0.724,1.029,1.791,1.544,3.201,1.544c1.867,0,3.181-0.915,3.944-2.743l3.942,2
c-0.838,1.563-2,2.791-3.486,3.686c-1.484,0.896-3.123,1.343-4.914,1.343c-2.857,0-5.163-0.875-6.915-2.629
c-1.752-1.752-2.628-4.19-2.628-7.313c0-3.048,0.886-5.466,2.657-7.257c1.771-1.79,4.009-2.686,6.715-2.686
C32.604,18.558,35.441,20.101,37.156,23.187z M55.613,23.187l-4.229,2.229c-0.457-0.951-1.02-1.619-1.686-2
c-0.668-0.38-1.307-0.571-1.914-0.571c-2.857,0-4.287,1.885-4.287,5.657c0,1.714,0.363,3.084,1.086,4.113
c0.723,1.029,1.789,1.544,3.201,1.544c1.865,0,3.18-0.915,3.941-2.743l4,2c-0.875,1.563-2.057,2.791-3.541,3.686
c-1.486,0.896-3.105,1.343-4.857,1.343c-2.896,0-5.209-0.875-6.941-2.629c-1.736-1.752-2.602-4.19-2.602-7.313
c0-3.048,0.885-5.466,2.658-7.257c1.77-1.79,4.008-2.686,6.713-2.686C51.117,18.558,53.938,20.101,55.613,23.187z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42
c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157
c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z
M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136
c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532
C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396
c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465
c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5
c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259
c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061
c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994
c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516
c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628
C18.092,8.818,24.252,6.259,31.567,6.259z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="37.47" cy="28.736" r="29.471"/>
<g>
<path d="M37.442-3.5c8.99,0,16.571,3.085,22.743,9.256C66.393,11.928,69.5,19.509,69.5,28.5c0,8.992-3.048,16.476-9.145,22.458
C53.88,57.32,46.241,60.5,37.442,60.5c-8.686,0-16.19-3.162-22.513-9.485C8.644,44.728,5.5,37.225,5.5,28.5
c0-8.762,3.144-16.343,9.429-22.743C21.1-0.414,28.604-3.5,37.442-3.5z M12.7,19.872c-0.952,2.628-1.429,5.505-1.429,8.629
c0,7.086,2.59,13.22,7.77,18.4c5.219,5.144,11.391,7.715,18.514,7.715c7.201,0,13.409-2.608,18.63-7.829
c1.867-1.79,3.332-3.657,4.398-5.602l-12.056-5.371c-0.421,2.02-1.439,3.667-3.057,4.942c-1.622,1.276-3.535,2.011-5.744,2.2
v4.915h-3.714v-4.915c-3.543-0.036-6.782-1.312-9.714-3.827l4.4-4.457c2.094,1.942,4.476,2.913,7.143,2.913
c1.104,0,2.048-0.246,2.83-0.743c0.78-0.494,1.172-1.312,1.172-2.457c0-0.801-0.287-1.448-0.858-1.943l-3.085-1.315l-3.771-1.715
l-5.086-2.229L12.7,19.872z M37.557,2.214c-7.276,0-13.428,2.571-18.457,7.714c-1.258,1.258-2.439,2.686-3.543,4.287L27.786,19.7
c0.533-1.676,1.542-3.019,3.029-4.028c1.484-1.009,3.218-1.571,5.2-1.686V9.071h3.715v4.915c2.934,0.153,5.6,1.143,8,2.971
l-4.172,4.286c-1.793-1.257-3.619-1.885-5.486-1.885c-0.991,0-1.876,0.191-2.656,0.571c-0.781,0.381-1.172,1.029-1.172,1.943
c0,0.267,0.095,0.533,0.285,0.8l4.057,1.83l2.8,1.257l5.144,2.285l16.397,7.314c0.535-2.248,0.801-4.533,0.801-6.857
c0-7.353-2.552-13.543-7.656-18.573C51.005,4.785,44.831,2.214,37.557,2.214z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64.000977px" height="64px" viewBox="0 0 64.000977 64" enable-background="new 0 0 64.000977 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="32.064453" cy="31.788086" r="29.012695"/>
<g>
<path d="M31.943848,0C40.896484,0,48.476562,3.105469,54.6875,9.314453C60.894531,15.486328,64.000977,23.045898,64.000977,32
s-3.048828,16.457031-9.145508,22.513672C48.417969,60.837891,40.779297,64,31.942871,64
c-8.648926,0-16.152832-3.142578-22.513672-9.429688C3.144043,48.286133,0,40.761719,0,32.000977
c0-8.723633,3.144043-16.285156,9.429199-22.68457C15.640137,3.105469,23.14502,0,31.943848,0z M32.060547,5.771484
c-7.275391,0-13.429688,2.570312-18.458496,7.714844C8.381836,18.783203,5.772949,24.954102,5.772949,32
c0,7.125,2.589844,13.256836,7.77002,18.400391c5.181152,5.181641,11.352051,7.770508,18.515625,7.770508
c7.123047,0,13.332031-2.608398,18.626953-7.828125C55.713867,45.466797,58.228516,39.353516,58.228516,32
c0-7.3125-2.553711-13.484375-7.65625-18.513672C45.504883,8.341797,39.333984,5.771484,32.060547,5.771484z M44.117188,24.456055
v5.485352H20.859863v-5.485352H44.117188z M44.117188,34.743164v5.481445H20.859863v-5.481445H44.117188z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="36.944" cy="28.631" r="29.105"/>
<g>
<path d="M37.443-3.5c8.951,0,16.531,3.105,22.742,9.315C66.393,11.987,69.5,19.548,69.5,28.5c0,8.954-3.049,16.457-9.145,22.514
C53.918,57.338,46.279,60.5,37.443,60.5c-8.649,0-16.153-3.143-22.514-9.429C8.644,44.786,5.5,37.264,5.5,28.501
c0-8.723,3.144-16.285,9.429-22.685C21.138-0.395,28.643-3.5,37.443-3.5z M37.557,2.272c-7.276,0-13.428,2.572-18.457,7.715
c-5.22,5.296-7.829,11.467-7.829,18.513c0,7.125,2.59,13.257,7.77,18.4c5.181,5.182,11.352,7.771,18.514,7.771
c7.123,0,13.334-2.609,18.629-7.828c5.029-4.876,7.543-10.99,7.543-18.343c0-7.313-2.553-13.485-7.656-18.513
C51.004,4.842,44.832,2.272,37.557,2.272z M23.271,23.985c0.609-3.924,2.189-6.962,4.742-9.114
c2.552-2.152,5.656-3.228,9.314-3.228c5.027,0,9.029,1.62,12,4.856c2.971,3.238,4.457,7.391,4.457,12.457
c0,4.915-1.543,9-4.627,12.256c-3.088,3.256-7.086,4.886-12.002,4.886c-3.619,0-6.743-1.085-9.371-3.257
c-2.629-2.172-4.209-5.257-4.743-9.257H31.1c0.19,3.886,2.533,5.829,7.029,5.829c2.246,0,4.057-0.972,5.428-2.914
c1.373-1.942,2.059-4.534,2.059-7.771c0-3.391-0.629-5.971-1.885-7.743c-1.258-1.771-3.066-2.657-5.43-2.657
c-4.268,0-6.667,1.885-7.2,5.656h2.343l-6.342,6.343l-6.343-6.343L23.271,23.985L23.271,23.985z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+44
View File
@@ -0,0 +1,44 @@
<html>
<head>
<title>Test Tone3000 Select</title>
<script>
window.addEventListener("message", (event) => {
localStorage.setItem("t3000popupX", event.data.popupX);
localStorage.setItem("t3000popupY", event.data.popupY);
localStorage.setItem("t3000popupWidth", event.data.popupWidth);
localStorage.setItem("t3000popupHeight", event.data.popupHeight);
alert("Message from popup:\n\n" +
JSON.stringify(event.data));
});
var popup = null;
function onPopupClick() {
let savedWidth = localStorage.getItem("t3000popupWidth") ?? window.innerWidth *0.8;
let savedHeight = localStorage.getItem("t3000popupHeight") ?? window.innerHeight *0.8;
let savedX = localStorage.getItem("t3000popupX") || 40;
let savedY = localStorage.getItem("t3000popupY") || 40;
if (savedX < 0) savedX = 0;
if (savedY < 0) savedY = 0;
if (savedWidth < 640) savedWidth = 640;
if (savedHeight < 480) savedHeight = 480;
let popupWin =
window.open(
"https://www.tone3000.com/api/v1/select?app_id=pipedal_app&redirect_url=http://localhost:5173/public/handleTone3000download.html",
"tone3000SelectPopup",
`screenX=${savedX},screenY=${savedY},innerWidth=${savedWidth},innerHeight=${savedHeight},scrollbars=yes`
);
}
</script>
</head>
<body>
<h1>Test Tone3000 Select Page</h1>
<p>This is a test page for Tone3000 select functionality.</p>
<Button onclick="onPopupClick()">Open Tone 3000</Button>
</body>
</html>
+68 -3
View File
@@ -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) {
+10 -5
View File
@@ -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 = "<none>";
} else {
value = this.fileNameOnly(this.state.value);
value = safeFilenameDecode(this.fileNameOnly(this.state.value));
toolTipText = value;
}
}
@@ -233,7 +237,7 @@ const FilePropertyControl =
{/* CONTROL SECTION */}
<div className={classes.midSection} style={{ width: "100%", paddingLeft: 8 }}>
<ToolTipEx title={toolTipText}>
<ButtonBase style={{ width: "100%", borderRadius: "4px 4px 0px 0px", overflow: "hidden" }} onClick={() => { this.onFileClick() }} >
<div style={{
width: "100%", background:
@@ -249,6 +253,7 @@ const FilePropertyControl =
<div style={{ height: "1px", width: "100%", background: this.props.theme.palette.text.secondary }}>&nbsp;</div>
</div>
</ButtonBase>
</ToolTipEx>
</div>
{/* LABEL/EDIT SECTION*/}
+10 -18
View File
@@ -21,8 +21,6 @@
import React from 'react';
import { createStyles } from './WithStyles';
import Tone3000Dialog from './Tone3000Dialog';
import TextInfoDialog from './TextInfoDialog';
import Tone3000HelpDialog from './Tone3000HelpDialog';
import GuitarMLHelpDialog from './GuitarMlHelpDialog';
@@ -69,6 +67,7 @@ import OkCancelDialog from './OkCancelDialog';
import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
import { Tone3000DownloadDialog } from './Tone3000Dialog';
const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile";
@@ -705,7 +704,7 @@ export default withStyles(
if (this.state.previousSelection == selectedItem) {
return;
}
if (!this.isLicenseFile(selectedItem) && !this.isFolderArtwork(selectedItem)) {
if (!this.isTextFile(selectedItem) && !this.isFolderArtwork(selectedItem)) {
this.props.onApply(fileProperty, selectedItem);
}
this.setState({ previousSelection: selectedItem });
@@ -1071,9 +1070,7 @@ export default withStyles(
handleTone3000Dialog(e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
e.preventDefault();
this.setState({ openTone3000Dialog: true });
}
@@ -1743,14 +1740,13 @@ export default withStyles(
)
}
{this.state.openTone3000Dialog && (
<Tone3000Dialog
open={this.state.openTone3000Dialog}
onCancel={() => this.setState({ openTone3000Dialog: false })}
onDownload={(toneUrl: string) => {
alert("Donload tone url: " + toneUrl);
<Tone3000DownloadDialog
onClose={() => this.setState({ openTone3000Dialog: false })}
downloadPath={this.state.currentDirectory}
onDownloadComplete={() => {
this.setState({ openTone3000Dialog: false });
}
}
this.requestFiles(this.state.navDirectory);
}}
/>
)}
{this.state.openTone3000Help && (
@@ -1774,15 +1770,11 @@ export default withStyles(
</DialogEx>
);
}
isLicenseFile(fileName: string) {
isTextFile(fileName: string) {
let extension = pathExtension(fileName);
if (extension === ".txt" || extension === ".md") {
return true;
}
let fileNameOnly = pathFileNameOnly(fileName);
if (fileNameOnly.toUpperCase() === "LICENSE" || fileNameOnly.toUpperCase() === "README") {
return true;
}
return false;
}
@@ -1801,7 +1793,7 @@ export default withStyles(
this.requestFiles(this.state.selectedFile);
this.setState({ navDirectory: this.state.selectedFile });
} else {
if (this.isLicenseFile(this.state.selectedFile)) {
if (this.isTextFile(this.state.selectedFile)) {
this.handleShowTextFile(this.state.selectedFile);
} else {
this.props.onOk(this.props.fileProperty, this.state.selectedFile);
+77 -12
View File
@@ -27,6 +27,7 @@ import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard
import PluginClass from './PluginClass';
import ScreenOrientation from './ScreenOrientation';
import PiPedalSocket, { PiPedalMessageHeader } from './PiPedalSocket';
import {Tone3000DownloadHandler} from './Tone3000Dialog';
import { nullCast } from './Utility'
import { JackConfiguration, JackChannelSelection } from './Jack';
import { BankIndex } from './Banks';
@@ -60,7 +61,7 @@ export enum State {
HotspotChanging,
};
function getErrorMessage(error: any) {
export function getErrorMessage(error: any) {
if (error instanceof Error) {
return (error as Error).message;
}
@@ -494,7 +495,6 @@ export class PiPedalModel //implements PiPedalModel
static getInstance(): PiPedalModel {
return PiPedalModelFactory.getInstance();
}
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
canKeepScreenOn: boolean = false;
keepScreenOn: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
@@ -552,6 +552,8 @@ export class PiPedalModel //implements PiPedalModel
uiPluginsByUri: Map<string, UiPlugin> = new Map<string, UiPlugin>();
private tone3000DownloadHandler: Tone3000DownloadHandler | null = null;
svgImgUrl(svgImage: string): string {
// return this.varServerUrl + "img/" + svgImage;
return "img/" + svgImage;
@@ -651,6 +653,53 @@ export class PiPedalModel //implements PiPedalModel
return true;
}
async downloadModelsFromTone3000(
tone3000DownloadUrl: string,
downloadPath: string
): Promise<void> {
try {
if (!this.webSocket) {
return;
}
this.webSocket.send("downloadModelsFromTone3000",
{
downloadPath: downloadPath,
tone3000Url: tone3000DownloadUrl
});
} catch (error) {
this.showAlert(getErrorMessage(error));
}
}
showTone3000DownloadDialog(
downloadPath: string,
onClosed: () => void,
onDownloadStarted: () => void,
onDownloadComplete: () => void
): void {
if (this.tone3000DownloadHandler === null) {
this.tone3000DownloadHandler = new Tone3000DownloadHandler(this);
}
this.tone3000DownloadHandler.launchTone3000Dialog(
downloadPath,
onClosed,
(errorMessage: string) => {
this.showAlert(errorMessage);
onClosed();
},
onDownloadStarted,
onDownloadComplete
);
};
closeTone3000DownloadDialog(): void {
if (this.tone3000DownloadHandler) {
this.tone3000DownloadHandler.closeTone3000Dialog();
}
}
private updateEnabledItems(pedalboard: Pedalboard) {
for (let item of pedalboard.itemsGenerator()) {
this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled);
@@ -862,10 +911,7 @@ export class PiPedalModel //implements PiPedalModel
} else if (message === "onErrorMessage") {
this.showAlert(body as string);
} else if (message == "onTone3000AuthChanged") {
this.hasTone3000Auth.set(body as boolean);
}
else if (message === "onLv2PluginsChanging") {
} else if (message === "onLv2PluginsChanging") {
this.onLv2PluginsChanging();
} else if (message === "onUpdateStatusChanged") {
let updateStatus = new UpdateStatus().deserialize(body);
@@ -1256,9 +1302,6 @@ export class PiPedalModel //implements PiPedalModel
this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(
await this.getWebSocket().request<any>("getAlsaSequencerConfiguration")
));
this.hasTone3000Auth.set(
await this.getWebSocket().request<boolean>("getHasTone3000Auth")
);
this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request<any>("getBankIndex")));
this.favorites.set(await this.getWebSocket().request<FavoritesList>("getFavorites"));
@@ -1539,8 +1582,7 @@ export class PiPedalModel //implements PiPedalModel
this.webSocket?.send("setSnapshot", index);
}
private pruneSnapshotValues(pedalboard: Pedalboard)
{
private pruneSnapshotValues(pedalboard: Pedalboard) {
let validPluginIds: Set<number> = new Set<number>();
let it = pedalboard.itemsGenerator();
@@ -1572,7 +1614,7 @@ export class PiPedalModel //implements PiPedalModel
pedalboard.selectedSnapshot = selectedSnapshot;
}
this.pruneSnapshotValues(pedalboard);
this.setModelPedalboard(pedalboard);
this.webSocket?.send("setSnapshots", { snapshots: pedalboard.snapshots, selectedSnapshot: selectedSnapshot });
@@ -2752,6 +2794,29 @@ export class PiPedalModel //implements PiPedalModel
document.body.removeChild(link);
}
async uploadTone3000File(url: string) : Promise<boolean> {
try {
let response = await fetch(
url,
{
method: "GET",
}
);
let json = await response.json();
if (json.success === true) {
return true;
}
if (json.errorMessage !== undefined)
{
throw new Error(json.errorMessage);
}
throw new Error("Invalid server response.");
} catch (error) {
throw new Error("Upload to PiPedal server failed. " + getErrorMessage(error));
}
}
uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
let result = new Promise<string>((resolve, reject) => {
try {
+50
View File
@@ -0,0 +1,50 @@
.text-info-dialog-content #user-content-tone3000_float {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: justify;
margin-top: 8px;
margin-bottom: 32px;
}
.text-info-dialog-content #user-content-tone3000_float_content {
flex-grow: 1;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-between;
}
.text-info-dialog-content #user-content-tone3000_title {
margin-top: 0;
margin-bottom: 12px;
padding: 0;
}
.text-info-dialog-content #user-content-tone3000_subtitle {
font-size: 0.8em;
margin-top: 0px;
margin-bottom: 0px;
padding: 0;
}
.text-info-dialog-content #user-content-cc_img {
width: 24px;
height: 24px;
}
.text-info-dialog-content #user-content-tone3000_thumbnail {
width: 130px;
height: auto;
object-fit: contain;
margin-right: 24px;
border: 1px solid var(--pd-border-color);
border-radius: 6px;
align-self: flex-start;
}
@media (max-width: 600px) {
.text-info-dialog-content #user-content-tone3000_thumbnail {
width: 80px;
margin-right: 20px;
}
}
+12 -3
View File
@@ -20,6 +20,7 @@
import React, { SyntheticEvent } from 'react';
import './TextInfoDialog.css';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
@@ -27,7 +28,10 @@ import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, {defaultSchema} from 'rehype-sanitize';
import rehypeExternalLinks from 'rehype-external-links'
import remarkGfm from 'remark-gfm';
import { PiPedalError } from './PiPedalError';
import DialogEx from './DialogEx';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
@@ -139,7 +143,6 @@ const TextInfoDialog = class extends ResizeResponsiveComponent<TextInfoDialogPro
onEnterKey={() => { this.props.onClose() }}
style={{ userSelect: "none" }}
>
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
<div style={{ flex: "0 0 auto" }}>
<AppBar style={{
@@ -168,8 +171,14 @@ const TextInfoDialog = class extends ResizeResponsiveComponent<TextInfoDialogPro
display: "flex", flexDirection: "row", flexWrap: "nowrap", alignItems: "start",
}}>
<div style={{ flex: "1 1 0px" }} />
<div style={{ flex: "1 1 auto",maxWidth: 650 }} >
<ReactMarkdown rehypePlugins={[[rehypeExternalLinks, {target: '_blank'}]]}>
<div className="text-info-dialog-content" style={{ flex: "1 1 auto",maxWidth: 650 }} >
<ReactMarkdown rehypePlugins={
[
[remarkGfm],
[rehypeExternalLinks, {target: '_blank'}],
rehypeRaw,
[rehypeSanitize, defaultSchema]
]}>
{this.state.textFileContent}
</ReactMarkdown>
</div>
+244 -108
View File
@@ -1,125 +1,261 @@
import { JSX } from "@emotion/react/jsx-dev-runtime";
import { PiPedalModel, getErrorMessage } from "./PiPedalModel";
import DialogEx from "./DialogEx";
import DialogContent from "@mui/material/DialogContent";
import DialogAction from "@mui/material/DialogActions";
import { Button, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import LinearProgress from "@mui/material/LinearProgress";
import React from 'react';
import AppBar from '@mui/material/AppBar';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import DialogEx from './DialogEx';
import Toolbar from '@mui/material/Toolbar';
import IconButtonEx from './IconButtonEx';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
export type OnTone3000DownloadHandler = (tone3000DownloadUrl: string) => void;
// used to check for online status.
let TONE3000_PING_URL = "https://www.tone3000.com/robots.txt";
export class Tone3000DownloadHandler {
export interface Tone3000DialogProps {
open: boolean;
onCancel: () => void;
onDownload: (toneUrl: string) => void;
}
private async handleTone3000Download(tone3000DownloadUrl: string): Promise<void> {
try {
// Dialog can close. We'll use the following await to manage
// lifecycle from here onward.
this.stopTone3000DialogMonitor();
interface Tone3000AuthState {
authToken: string;
refreshToken: string;
expiry: number;
};
// function getAuthToken(): Tone3000AuthState | null {
// let authToken = window.localStorage.getItem("tone300_auth_token");
// if (authToken === null) {
// return null;
// }
// let refreshToken = window.localStorage.getItem("tone3000_refresh_token");
// if (refreshToken === null) {
// return null;
// }
// let expiry = window.localStorage.getTime("tone3000_auth_expiry");
// return { authToken: authToken, refreshToken: refreshToken, expiry: expiry };
// }
// function setAuthToken(authToken: string, refreshToken: string, expirySeconds: number) {
// window.localStorage.setItem("tone3000_auth_token", authToken);
// window.localStorage.setItem("tone3000_refresh_token", refreshToken);
// window.localStorage.setItem("tone3000_auth_expiry", String(Date.now() + (expirySeconds * 1000)));
// }
// enum AuthState {
// OnBoarding,
// TokenExpired,
// Authenticated
// }
function Tone3000Dialog(props: Tone3000DialogProps) {
const { onCancel,onDownload } = props;
function RedirectUrl() {
let baseUrl = new URL(window.location.href);
let result = "http://" + baseUrl.hostname + ":" + baseUrl.port + "/public/handleTone3000download.html";
return result;
}
const appId = 'pipedal_app';
const redirectUrl = RedirectUrl();
let selectUrl = `https://www.tone3000.com/api/v1/select?app_id=${appId}&redirect_url=${redirectUrl}`;
const model: PiPedalModel = PiPedalModelFactory.getInstance();
if (model) {
this.handleTone3000DownloadComplete();
await this.model.downloadModelsFromTone3000(tone3000DownloadUrl,this.downloadPath);
return;
} catch (e) {
this.handleTone3000DownloadError(getErrorMessage(e));
return;
}
}
React.useEffect(()=> {
private messageEventListener: ((event: MessageEvent) => void) | null = null;
let messagehandler = (event: MessageEvent) => {
// Handle messages received from the iframe
if (event.origin !== "https://www.tone3000.com") {
private model: PiPedalModel;
public constructor(model: PiPedalModel) {
this.model = model;
this.messageEventListener = (event: MessageEvent) => {
if (event.data.type !== "tone3000Download") {
return;
}
const data = event.data;
if (data.type === "tone3000_selection") {
onDownload(data.toneUrl);
this.handleTone3000Download(event.data.toneUrl as string);
};
window.addEventListener("message", this.messageEventListener);
}
public dispose(): void {
if (this.messageEventListener) {
window.removeEventListener("message", this.messageEventListener);
this.messageEventListener = null;
}
}
private popupWindow: Window | null = null;
private appId: string = "pipedal_app";
private redirectUrl(): string {
let hostname = window.location.hostname;
let port = window.location.port;
let protocol = window.location.protocol;
let serverUrl: string;
if (protocol === "http:" && (port === "80" || port === "")) {
serverUrl = `${protocol}//${hostname}`;
} else if (protocol === "https:" && (port === "443" || port === "")) {
serverUrl = `${protocol}//${hostname}`;
} else {
serverUrl = `${protocol}//${hostname}:${port}`;
}
return `${serverUrl}/public/handleTone3000download.html`;
}
private tone3000SelectUrl(): string {
return `https://www.tone3000.com/api/v1/select?app_id=${this.appId}&redirect_url=${this.redirectUrl()}`;
}
private handleTone3000DialogClosed() {
let t = this.onClosedCallback;
this.onClosedCallback = undefined;
this.onErrorCallback = undefined;
this.onDownloadCompleteCallback = undefined;
if (t) {
t();
}
this.stopTone3000DialogMonitor();
}
private handleTone3000Downloadstarted() {
if (this.onDownloadStartedCallback) {
this.onDownloadStartedCallback();
}
}
private handleTone3000DownloadComplete() {
let t = this.onDownloadCompleteCallback;
this.onClosedCallback = undefined;
this.onErrorCallback = undefined;
this.onDownloadCompleteCallback = undefined;
if (t) {
t();
}
this.stopTone3000DialogMonitor();
}
private handleTone3000DownloadError(errorMessage: string) {
let t = this.onErrorCallback;
this.onClosedCallback = undefined;
this.onErrorCallback = undefined;
this.onDownloadCompleteCallback = undefined;
if (t) {
t(errorMessage);
}
this.stopTone3000DialogMonitor();
}
private tone3000DialogInterval: number | undefined = undefined;
private stopTone3000DialogMonitor() {
if (this.tone3000DialogInterval !== undefined) {
clearInterval(this.tone3000DialogInterval);
this.tone3000DialogInterval = undefined;
}
}
private startTone3000DialogMonitor() {
this.tone3000DialogInterval = setInterval(() => {
if (this.popupWindow == null || this.popupWindow.closed) {
this.stopTone3000DialogMonitor();
this.popupWindow = null;
this.handleTone3000DialogClosed();
}
}, 500);
}
private downloadPath: string = "";
private onClosedCallback: (() => void) | undefined = undefined;
private onErrorCallback: ((errorMessage: string) => void) | undefined = undefined;
private onDownloadStartedCallback: (() => void) | undefined = undefined;
private onDownloadCompleteCallback: (() => void) | undefined = undefined;
public launchTone3000Dialog(
downloadPath: string,
onClosed: () => void,
onError: (errorMessage: string) => void,
onDownloadStarted: () => void,
onDownloadComplete: () => void
) {
this.closeTone3000Dialog();
this.downloadPath = downloadPath;
this.onClosedCallback = onClosed;
this.onDownloadCompleteCallback = onDownloadComplete;
this.onDownloadStartedCallback = onDownloadStarted;
this.onErrorCallback = onError;
fetch(TONE3000_PING_URL, { cache: "no-cache" })
.then((response) => {
if (!response.ok) {
this.handleTone3000DownloadError(`Unable to connect to Tone3000: ${response.status}${response.statusText}`);
return;
}
// online
let popupWidth = Math.floor(window.innerWidth * 0.8);
let popupHeight = Math.floor(window.innerHeight * 0.8);
if (window.innerWidth < 410) {
popupWidth = 400;
}
this.popupWindow =
window.open(
this.tone3000SelectUrl(),
"popup",
`innerWidth=${popupWidth},innerHeight=${popupHeight},scrollbars=yes`
);
this.startTone3000DialogMonitor();
if (!this.popupWindow) {
console.error("Failed to open Tone3000 dialog popup window.");
this.handleTone3000DownloadError("Cannot open popup window.");
return;
}
})
.catch((error) => {
// offline
this.handleTone3000DownloadError("Not online. Unable to connect to Tone3000 servers.");
return;
});
}
public closeTone3000Dialog(): void {
this.handleTone3000DialogClosed();
this.stopTone3000DialogMonitor();
if (this.popupWindow) {
try {
let t = this.popupWindow;
this.popupWindow = null;
t.close();
} catch (e) {
console.error("Error closing Tone3000 dialog popup:", e);
}
}
window.addEventListener("message", messagehandler);
return () => {
window.removeEventListener("message", messagehandler);
}
});
return (
<DialogEx open={props.open} onClose={()=>{ onCancel()}} fullScreen={true} tag="tone3000Dlg" onEnterKey={()=>{}} >
<div style={{display: "flex", flexDirection: "column", width: "100%", height: "100%"}}>
<div style={{ flex: "0 0 auto" }}>
<AppBar style={{
position: 'relative',
top: 0, left: 0
}} >
<Toolbar>
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={()=> onCancel()}
aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
<Typography noWrap variant="h6"
sx={{ marginLeft: 2, flex: 1 }}
>
Tone3000 Direct Download
</Typography>
</Toolbar>
</AppBar>
</div>
<iframe style={{flex: "1 1 1px", border: "none"}}
src={ selectUrl}
>
</iframe>
</div>
</DialogEx>
);
}
}
export default Tone3000Dialog;
export interface Tone3000DownloadDialogProps {
onClose: () => void,
onDownloadComplete: () => void,
downloadPath: string
}
export function Tone3000DownloadDialog(props: Tone3000DownloadDialogProps): JSX.Element {
const model = PiPedalModel.getInstance();
let { onClose, onDownloadComplete, downloadPath } = props;
let [downloading, setDownloading] = useState(false);
useEffect(() => {
model.showTone3000DownloadDialog(
downloadPath,
() => {
onClose();
},
() => {
setDownloading(true);
},
() => {
onDownloadComplete();
}
);
return () => {
model.closeTone3000DownloadDialog();
};
});
return (
<DialogEx tag="tone3000-download"
open={true}
onClose={() => {
onClose();
}}
onEnterKey={() => { }}
>
<DialogContent>
<Typography variant="body2">Downloading from Tone3000...</Typography>
{downloading &&
<div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<LinearProgress style={{ marginTop: 16, flexGrow: 1 }} />
</div>
}
</DialogContent>
<DialogAction>
{!downloading && (
<Button
onClick={() => {
onClose();
}}>
Cancel
</Button>
)}
</DialogAction>
</DialogEx>
);
}