Update dialog
This commit is contained in:
Vendored
+1
-1
@@ -112,7 +112,7 @@
|
|||||||
//"[json_variants]" // subtest of your choice, or none to run all of the tests.
|
//"[json_variants]" // subtest of your choice, or none to run all of the tests.
|
||||||
//"[inverting_mutex_test]"
|
//"[inverting_mutex_test]"
|
||||||
// "[utf8_to_utf32]"
|
// "[utf8_to_utf32]"
|
||||||
"[wifi_channels_test]"
|
"[updater]"
|
||||||
],
|
],
|
||||||
|
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ project(pipedal
|
|||||||
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
||||||
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
||||||
)
|
)
|
||||||
set (DISPLAY_VERSION "v1.2.41")
|
set (DISPLAY_VERSION "PiPedal v1.2.41-Release")
|
||||||
|
set (PACKAGE_ARCHITECTURE "arm64")
|
||||||
set (CMAKE_INSTALL_PREFIX "/usr/")
|
set (CMAKE_INSTALL_PREFIX "/usr/")
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|||||||
@@ -267,3 +267,42 @@ int pipedal::sysExecWait(ProcessId pid_)
|
|||||||
return exitStatus;
|
return exitStatus;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::string &args)
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
fs::path fullPath = findOnSystemPath(program);
|
||||||
|
if (!fs::exists(fullPath))
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Path does not exist. " << fullPath));
|
||||||
|
}
|
||||||
|
std::stringstream s;
|
||||||
|
s << fullPath.c_str() << " " << args << " 2>&1";
|
||||||
|
|
||||||
|
std::string fullCommand = s.str();
|
||||||
|
FILE *output = popen(fullCommand.c_str(), "r");
|
||||||
|
if (output)
|
||||||
|
{
|
||||||
|
char buffer[512];
|
||||||
|
std::stringstream ssOutput;
|
||||||
|
|
||||||
|
while (!feof(output))
|
||||||
|
{
|
||||||
|
size_t nRead = fread(buffer, sizeof(char),sizeof(buffer), output);
|
||||||
|
ssOutput.write(buffer,nRead);
|
||||||
|
if (nRead == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
int rc = pclose(output);
|
||||||
|
SysExecOutput result
|
||||||
|
{
|
||||||
|
.exitCode = rc,
|
||||||
|
.output = ssOutput.str()
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error(SS("Failed to execute command. " << fullCommand ));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,20 +20,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace pipedal {
|
namespace pipedal
|
||||||
// exec a command, returning the actual exit code (unlike execXX() or system() )
|
{
|
||||||
int sysExec(const char*szCommand);
|
// exec a command, returning the actual exit code (unlike execXX() or system() )
|
||||||
|
int sysExec(const char *szCommand);
|
||||||
|
|
||||||
// execute a command, suppressing output.
|
// execute a command, suppressing output.
|
||||||
int silentSysExec(const char *szCommand);
|
int silentSysExec(const char *szCommand);
|
||||||
|
|
||||||
|
struct SysExecOutput
|
||||||
|
{
|
||||||
|
int exitCode;
|
||||||
|
std::string output;
|
||||||
|
};
|
||||||
|
|
||||||
using ProcessId = int64_t; // platform-agnostic wrapper for pid_t;
|
SysExecOutput sysExecForOutput(const std::string& command, const std::string&args);
|
||||||
// Returns a pid or -1 on errror.
|
|
||||||
ProcessId sysExecAsync(const std::string&command);
|
|
||||||
|
|
||||||
void sysExecTerminate(ProcessId pid_,int termTimeoutMs = 1000,int killTimeoutMs = 500 ); // returns the process's exit status.
|
using ProcessId = int64_t; // platform-agnostic wrapper for pid_t;
|
||||||
int sysExecWait(ProcessId pid);
|
// Returns a pid or -1 on errror.
|
||||||
|
ProcessId sysExecAsync(const std::string &command);
|
||||||
|
|
||||||
std::string getSelfExePath();
|
void sysExecTerminate(ProcessId pid_, int termTimeoutMs = 1000, int killTimeoutMs = 500); // returns the process's exit status.
|
||||||
|
int sysExecWait(ProcessId pid);
|
||||||
|
|
||||||
|
std::string getSelfExePath();
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,7 @@ namespace pipedal
|
|||||||
public:
|
public:
|
||||||
~json_variant();
|
~json_variant();
|
||||||
json_variant();
|
json_variant();
|
||||||
|
json_variant(json_reader &reader);
|
||||||
json_variant(json_variant &&);
|
json_variant(json_variant &&);
|
||||||
json_variant(const json_variant &);
|
json_variant(const json_variant &);
|
||||||
|
|
||||||
@@ -81,9 +82,9 @@ namespace pipedal
|
|||||||
json_variant(const std::shared_ptr<json_array> &value);
|
json_variant(const std::shared_ptr<json_array> &value);
|
||||||
json_variant(json_array &&array);
|
json_variant(json_array &&array);
|
||||||
json_variant(json_object &&object);
|
json_variant(json_object &&object);
|
||||||
json_variant(const char*sz);
|
json_variant(const char *sz);
|
||||||
|
|
||||||
json_variant(const void*) = delete; // do NOT allow implicit conversion of pointers to bool
|
json_variant(const void *) = delete; // do NOT allow implicit conversion of pointers to bool
|
||||||
|
|
||||||
json_variant &operator=(json_variant &&value);
|
json_variant &operator=(json_variant &&value);
|
||||||
json_variant &operator=(const json_variant &value);
|
json_variant &operator=(const json_variant &value);
|
||||||
@@ -94,9 +95,9 @@ namespace pipedal
|
|||||||
json_variant &operator=(json_object &&value);
|
json_variant &operator=(json_object &&value);
|
||||||
json_variant &operator=(json_array &&value);
|
json_variant &operator=(json_array &&value);
|
||||||
|
|
||||||
json_variant &operator=(const char*sz) { return (*this) = std::string(sz); }
|
json_variant &operator=(const char *sz) { return (*this) = std::string(sz); }
|
||||||
|
|
||||||
json_variant &operator=(void*) = delete; // do NOT allow implicit conversion of pointers to bool
|
json_variant &operator=(void *) = delete; // do NOT allow implicit conversion of pointers to bool
|
||||||
|
|
||||||
void require_type(ContentType content_type) const
|
void require_type(ContentType content_type) const
|
||||||
{
|
{
|
||||||
@@ -145,6 +146,22 @@ namespace pipedal
|
|||||||
return content.double_value;
|
return content.double_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int8_t as_int8() const { return (int8_t)as_number(); }
|
||||||
|
|
||||||
|
uint8_t as_uint8() const { return (uint8_t)as_number(); }
|
||||||
|
|
||||||
|
int16_t as_int16() const { return (int16_t)as_number(); }
|
||||||
|
|
||||||
|
uint16_t as_uint16() const { return (uint16_t)as_number(); }
|
||||||
|
|
||||||
|
int32_t as_int32() const { return (int32_t)as_number(); }
|
||||||
|
|
||||||
|
uint32_t as_uint32() const { return (uint32_t)as_number(); }
|
||||||
|
|
||||||
|
int64_t as_int64() const { return (int64_t)as_number(); }
|
||||||
|
|
||||||
|
uint64_t as_uint64() const { return (uint64_t)as_number(); }
|
||||||
|
|
||||||
const std::string &as_string() const;
|
const std::string &as_string() const;
|
||||||
std::string &as_string();
|
std::string &as_string();
|
||||||
|
|
||||||
@@ -154,9 +171,6 @@ namespace pipedal
|
|||||||
const std::shared_ptr<json_array> &as_array() const;
|
const std::shared_ptr<json_array> &as_array() const;
|
||||||
std::shared_ptr<json_array> &as_array();
|
std::shared_ptr<json_array> &as_array();
|
||||||
|
|
||||||
template <typename U>
|
|
||||||
U &as() { static_assert("Invalid type."); }
|
|
||||||
|
|
||||||
// convenience methods for object and array manipulation.
|
// convenience methods for object and array manipulation.
|
||||||
static json_variant make_object();
|
static json_variant make_object();
|
||||||
static json_variant make_array();
|
static json_variant make_array();
|
||||||
@@ -179,6 +193,7 @@ namespace pipedal
|
|||||||
bool operator!=(const json_variant &other) const;
|
bool operator!=(const json_variant &other) const;
|
||||||
|
|
||||||
std::string to_string() const;
|
std::string to_string() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void free();
|
void free();
|
||||||
void write_double_value(json_writer &writer, double value) const;
|
void write_double_value(json_writer &writer, double value) const;
|
||||||
@@ -212,12 +227,12 @@ namespace pipedal
|
|||||||
class json_array : public JsonSerializable
|
class json_array : public JsonSerializable
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
json_array(const json_array&) { } // deleted.
|
json_array(const json_array &) {} // deleted.
|
||||||
public:
|
public:
|
||||||
using ptr = std::shared_ptr<json_array>;
|
using ptr = std::shared_ptr<json_array>;
|
||||||
|
|
||||||
json_array() { ++allocation_count_; }
|
json_array() { ++allocation_count_; }
|
||||||
json_array(json_array&&other);
|
json_array(json_array &&other);
|
||||||
~json_array() { --allocation_count_; }
|
~json_array() { --allocation_count_; }
|
||||||
|
|
||||||
json_variant &at(size_t index);
|
json_variant &at(size_t index);
|
||||||
@@ -247,7 +262,7 @@ namespace pipedal
|
|||||||
}
|
}
|
||||||
using iterator = std::vector<json_variant>::iterator;
|
using iterator = std::vector<json_variant>::iterator;
|
||||||
using const_iterator = std::vector<json_variant>::const_iterator;
|
using const_iterator = std::vector<json_variant>::const_iterator;
|
||||||
|
|
||||||
iterator begin() { return values.begin(); }
|
iterator begin() { return values.begin(); }
|
||||||
iterator end() { return values.end(); }
|
iterator end() { return values.end(); }
|
||||||
const_iterator begin() const { return values.begin(); }
|
const_iterator begin() const { return values.begin(); }
|
||||||
@@ -266,13 +281,13 @@ namespace pipedal
|
|||||||
class json_object : public JsonSerializable
|
class json_object : public JsonSerializable
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
json_object(const json_object&) { } // deleted.
|
json_object(const json_object &) {} // deleted.
|
||||||
public:
|
public:
|
||||||
using ptr = std::shared_ptr<json_object>;
|
using ptr = std::shared_ptr<json_object>;
|
||||||
|
|
||||||
json_object() { ++ allocation_count_;}
|
json_object() { ++allocation_count_; }
|
||||||
json_object(json_object&&other);
|
json_object(json_object &&other);
|
||||||
~json_object() { --allocation_count_;}
|
~json_object() { --allocation_count_; }
|
||||||
|
|
||||||
size_t size() const { return values.size(); }
|
size_t size() const { return values.size(); }
|
||||||
json_variant &at(const std::string &index);
|
json_variant &at(const std::string &index);
|
||||||
@@ -285,25 +300,24 @@ namespace pipedal
|
|||||||
bool operator!=(const json_object &other) const { return (!((*this) == other)); }
|
bool operator!=(const json_object &other) const { return (!((*this) == other)); }
|
||||||
bool contains(const std::string &index) const;
|
bool contains(const std::string &index) const;
|
||||||
|
|
||||||
|
using values_t = std::vector<std::pair<std::string, json_variant>>;
|
||||||
using values_t = std::vector< std::pair<std::string, json_variant> >;
|
|
||||||
using iterator = values_t::iterator;
|
using iterator = values_t::iterator;
|
||||||
using const_iterator = values_t::const_iterator;
|
using const_iterator = values_t::const_iterator;
|
||||||
|
|
||||||
iterator begin() { return values.begin(); }
|
iterator begin() { return values.begin(); }
|
||||||
iterator end() { return values.end(); }
|
iterator end() { return values.end(); }
|
||||||
const_iterator begin() const { return values.begin(); }
|
const_iterator begin() const { return values.begin(); }
|
||||||
const_iterator end() const { return values.end(); }
|
const_iterator end() const { return values.end(); }
|
||||||
|
|
||||||
iterator find(const std::string& key);
|
iterator find(const std::string &key);
|
||||||
const_iterator find(const std::string& key) const;
|
const_iterator find(const std::string &key) const;
|
||||||
|
|
||||||
|
|
||||||
// strictly for testing purposes. Not thread-safe.
|
// strictly for testing purposes. Not thread-safe.
|
||||||
static int64_t allocation_count()
|
static int64_t allocation_count()
|
||||||
{
|
{
|
||||||
return allocation_count_;
|
return allocation_count_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual void read_json(json_reader &reader);
|
virtual void read_json(json_reader &reader);
|
||||||
virtual void write_json(json_writer &writer) const;
|
virtual void write_json(json_writer &writer) const;
|
||||||
@@ -317,27 +331,6 @@ namespace pipedal
|
|||||||
inline std::string &json_variant::memString() { return *(std::string *)content.mem; }
|
inline std::string &json_variant::memString() { return *(std::string *)content.mem; }
|
||||||
inline const std::string &json_variant::memString() const { return *(const std::string *)content.mem; }
|
inline const std::string &json_variant::memString() const { return *(const std::string *)content.mem; }
|
||||||
|
|
||||||
template <>
|
|
||||||
inline json_null &json_variant::as<json_null>() { return as_null(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline bool &json_variant::as<bool>() { return as_bool(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline double &json_variant::as<double>() { return as_number(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::string &json_variant::as<std::string>() { return as_string(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::shared_ptr<json_object> &json_variant::as<std::shared_ptr<json_object>>() { return as_object(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::shared_ptr<json_array> &json_variant::as<std::shared_ptr<json_array>>() { return as_array(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline json_variant &json_variant::as<json_variant>() { return *this; }
|
|
||||||
|
|
||||||
inline json_variant::object_ptr &json_variant::memObject()
|
inline json_variant::object_ptr &json_variant::memObject()
|
||||||
{
|
{
|
||||||
return *(object_ptr *)content.mem;
|
return *(object_ptr *)content.mem;
|
||||||
@@ -525,22 +518,23 @@ namespace pipedal
|
|||||||
return !(*this == other);
|
return !(*this == other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Holds a string but is json_read and json_written as an unqoted json object.
|
// Holds a string but is json_read and json_written as an unqoted json object.
|
||||||
class raw_json_string: public JsonSerializable {
|
class raw_json_string : public JsonSerializable
|
||||||
public:
|
{
|
||||||
raw_json_string() { }
|
public:
|
||||||
raw_json_string(const std::string &value) : value(value) {}
|
raw_json_string() {}
|
||||||
const std::string& as_string() const { return value; }
|
raw_json_string(const std::string &value) : value(value) {}
|
||||||
|
const std::string &as_string() const { return value; }
|
||||||
|
|
||||||
void Set(const std::string&value) { this->value = value; }
|
void Set(const std::string &value) { this->value = value; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
virtual void write_json(json_writer &writer) const
|
||||||
virtual void write_json(json_writer &writer) const {
|
{
|
||||||
writer.write_raw(value.c_str());
|
writer.write_raw(value.c_str());
|
||||||
}
|
}
|
||||||
virtual void read_json(json_reader &reader) {
|
virtual void read_json(json_reader &reader)
|
||||||
|
{
|
||||||
throw std::logic_error("Not implemented.");
|
throw std::logic_error("Not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -552,6 +552,11 @@ json_variant::json_variant(const char*sz)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_variant::json_variant(json_reader&reader)
|
||||||
|
{
|
||||||
|
this->read_json(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*static*/ json_null json_null::instance;
|
/*static*/ json_null json_null::instance;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ add_custom_command(
|
|||||||
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
|
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
|
||||||
|
|
||||||
DEPENDS
|
DEPENDS
|
||||||
|
src/Updater.tsx
|
||||||
src/OkCancelDialog.tsx
|
src/OkCancelDialog.tsx
|
||||||
src/PluginControl.tsx
|
src/PluginControl.tsx
|
||||||
src/svg/ic_save_bank_as.svg
|
src/svg/ic_save_bank_as.svg
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"socket_server_port": 80,
|
"socket_server_port": 8080,
|
||||||
"socket_server_address": "*",
|
"socket_server_address": "*",
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"max_upload_size": 536870912,
|
"max_upload_size": 536870912,
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let classes = this.props.classes;
|
let classes = this.props.classes;
|
||||||
|
let addressKey = 0;
|
||||||
return (
|
return (
|
||||||
<DialogEx tag="about" fullScreen open={this.props.open}
|
<DialogEx tag="about" fullScreen open={this.props.open}
|
||||||
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
|
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
|
||||||
@@ -228,7 +228,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
{this.model.isAndroidHosted() && (
|
{this.model.isAndroidHosted() && (
|
||||||
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
||||||
Copyright © 2022 Robin Davies.
|
Copyright © 2022-2024 Robin Davies.
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -239,7 +239,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
{
|
{
|
||||||
this.model.serverVersion?.webAddresses.map((address) =>
|
this.model.serverVersion?.webAddresses.map((address) =>
|
||||||
(
|
(
|
||||||
<Typography display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
<Typography key={addressKey++} display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
||||||
{address}
|
{address}
|
||||||
</Typography>
|
</Typography>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface AndroidHostInterface {
|
|||||||
getHostVersion() : string;
|
getHostVersion() : string;
|
||||||
chooseNewDevice() : void;
|
chooseNewDevice() : void;
|
||||||
setDisconnected(isDisconnected: boolean): void;
|
setDisconnected(isDisconnected: boolean): void;
|
||||||
|
launchExternalUrl(url:string): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FakeAndroidHost implements AndroidHostInterface
|
export class FakeAndroidHost implements AndroidHostInterface
|
||||||
@@ -47,4 +47,8 @@ export class FakeAndroidHost implements AndroidHostInterface
|
|||||||
}
|
}
|
||||||
showSponsorship() : void { }
|
showSponsorship() : void { }
|
||||||
|
|
||||||
}
|
launchExternalUrl(url:string): boolean
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+48
-15
@@ -48,7 +48,7 @@ import SettingsDialog from './SettingsDialog';
|
|||||||
import AboutDialog from './AboutDialog';
|
import AboutDialog from './AboutDialog';
|
||||||
import BankDialog from './BankDialog';
|
import BankDialog from './BankDialog';
|
||||||
|
|
||||||
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo } from './PiPedalModel';
|
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo,wantsLoadingScreen } from './PiPedalModel';
|
||||||
import ZoomedUiControl from './ZoomedUiControl'
|
import ZoomedUiControl from './ZoomedUiControl'
|
||||||
import MainPage from './MainPage';
|
import MainPage from './MainPage';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
@@ -60,6 +60,7 @@ import RenameDialog from './RenameDialog';
|
|||||||
import JackStatusView from './JackStatusView';
|
import JackStatusView from './JackStatusView';
|
||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import { isDarkMode } from './DarkMode';
|
import { isDarkMode } from './DarkMode';
|
||||||
|
import UpdateDialog from './UpdateDialog';
|
||||||
|
|
||||||
import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg';
|
import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg';
|
||||||
import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg';
|
import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg';
|
||||||
@@ -270,6 +271,7 @@ type AppState = {
|
|||||||
tinyToolBar: boolean;
|
tinyToolBar: boolean;
|
||||||
alertDialogOpen: boolean;
|
alertDialogOpen: boolean;
|
||||||
alertDialogMessage: string;
|
alertDialogMessage: string;
|
||||||
|
updateDialogOpen: boolean;
|
||||||
isSettingsDialogOpen: boolean;
|
isSettingsDialogOpen: boolean;
|
||||||
onboarding: boolean;
|
onboarding: boolean;
|
||||||
isDebug: boolean;
|
isDebug: boolean;
|
||||||
@@ -324,6 +326,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
alertDialogMessage: "",
|
alertDialogMessage: "",
|
||||||
presetName: this.model_.presets.get().getSelectedText(),
|
presetName: this.model_.presets.get().getSelectedText(),
|
||||||
isSettingsDialogOpen: false,
|
isSettingsDialogOpen: false,
|
||||||
|
updateDialogOpen: false,
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
isDebug: true,
|
isDebug: true,
|
||||||
presetChanged: this.model_.presets.get().presetChanged,
|
presetChanged: this.model_.presets.get().presetChanged,
|
||||||
@@ -339,8 +342,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.promptForUpdateHandler = this.promptForUpdateHandler.bind(this);
|
||||||
this.errorChangeHandler_ = this.setErrorMessage.bind(this);
|
this.errorChangeHandler_ = this.setErrorMessage.bind(this);
|
||||||
this.unmountListener = this.unmountListener.bind(this);
|
this.beforeUnloadListener = this.beforeUnloadListener.bind(this);
|
||||||
|
this.unloadListener = this.unloadListener.bind(this);
|
||||||
this.stateChangeHandler_ = this.setDisplayState.bind(this);
|
this.stateChangeHandler_ = this.setDisplayState.bind(this);
|
||||||
this.presetChangedHandler = this.presetChangedHandler.bind(this);
|
this.presetChangedHandler = this.presetChangedHandler.bind(this);
|
||||||
this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this);
|
this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this);
|
||||||
@@ -510,18 +515,26 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
this.setState({ isFullScreen: !this.state.isFullScreen });
|
this.setState({ isFullScreen: !this.state.isFullScreen });
|
||||||
}
|
}
|
||||||
|
|
||||||
private unmountListener(e: Event) {
|
private unloadListener(e: Event) {
|
||||||
if (this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) {
|
this.model_.close();
|
||||||
e.preventDefault();
|
|
||||||
(e as any).returnValue = "Are you sure you want to leave this page?";
|
|
||||||
return "Are you sure you want to leave this page?";
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private beforeUnloadListener(e: Event) {
|
||||||
|
this.model_.close();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
promptForUpdateHandler(newValue: boolean) {
|
||||||
|
if (this.state.updateDialogOpen !== newValue)
|
||||||
|
{
|
||||||
|
this.setState({updateDialogOpen: newValue});
|
||||||
|
}
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
||||||
super.componentDidMount();
|
super.componentDidMount();
|
||||||
window.addEventListener("beforeunload", this.unmountListener);
|
window.addEventListener("beforeunload", this.beforeUnloadListener);
|
||||||
|
window.addEventListener("unload", this.unloadListener);
|
||||||
|
|
||||||
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
|
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
|
||||||
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
|
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
|
||||||
@@ -529,6 +542,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler);
|
this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler);
|
||||||
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
||||||
this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
|
this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
|
||||||
|
this.model_.promptForUpdate.addOnChangedHandler(this.promptForUpdateHandler);
|
||||||
this.alertMessageChangedHandler();
|
this.alertMessageChangedHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,14 +564,17 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
super.componentWillUnmount();
|
super.componentWillUnmount();
|
||||||
window.removeEventListener("beforeunload", this.unmountListener);
|
window.removeEventListener("beforeunload", this.beforeUnloadListener);
|
||||||
|
|
||||||
|
this.model_.promptForUpdate.removeOnChangedHandler(this.promptForUpdateHandler);
|
||||||
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
|
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
|
||||||
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
|
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
|
||||||
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
|
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
|
||||||
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
|
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
|
||||||
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
|
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
|
||||||
|
|
||||||
|
this.model_.close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alertMessageChangedHandler() {
|
alertMessageChangedHandler() {
|
||||||
@@ -661,6 +678,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
return "Applying\u00A0changes...";
|
return "Applying\u00A0changes...";
|
||||||
case State.ReloadingPlugins:
|
case State.ReloadingPlugins:
|
||||||
return "Reloading\u00A0plugins...";
|
return "Reloading\u00A0plugins...";
|
||||||
|
case State.DownloadingUpdate:
|
||||||
|
return "Downloading update...";
|
||||||
|
case State.InstallingUpdate:
|
||||||
|
return "Installing update....";
|
||||||
default:
|
default:
|
||||||
return "Reconnecting...";
|
return "Reconnecting...";
|
||||||
}
|
}
|
||||||
@@ -840,7 +861,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
|
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
|
||||||
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
{ (this.state.aboutDialogOpen)&&
|
||||||
|
(
|
||||||
|
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
||||||
|
)}
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
open={this.state.isSettingsDialogOpen}
|
open={this.state.isSettingsDialogOpen}
|
||||||
onboarding={this.state.onboarding}
|
onboarding={this.state.onboarding}
|
||||||
@@ -872,6 +896,16 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
|
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
(this.state.updateDialogOpen)
|
||||||
|
&& (
|
||||||
|
<UpdateDialog open={true} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{this.state.showStatusMonitor && (<JackStatusView />)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<DialogEx
|
<DialogEx
|
||||||
tag="Alert"
|
tag="Alert"
|
||||||
open={this.state.alertDialogOpen}
|
open={this.state.alertDialogOpen}
|
||||||
@@ -893,14 +927,13 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogEx>
|
</DialogEx>
|
||||||
{this.state.showStatusMonitor && (<JackStatusView />)}
|
|
||||||
|
|
||||||
<div className={classes.errorContent} style={{
|
<div className={classes.errorContent} style={{
|
||||||
display: (
|
display: (
|
||||||
this.state.displayState === State.Reconnecting
|
wantsLoadingScreen(this.state.displayState)
|
||||||
|| this.state.displayState === State.ApplyingChanges
|
|
||||||
|| this.state.displayState === State.ReloadingPlugins)
|
|
||||||
? "block" : "none"
|
? "block" : "none"
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={classes.errorContentMask} />
|
<div className={classes.errorContentMask} />
|
||||||
|
|||||||
+319
-123
@@ -20,7 +20,7 @@
|
|||||||
import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin';
|
import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin';
|
||||||
|
|
||||||
import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError';
|
import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError';
|
||||||
|
import { UpdateStatus, UpdatePolicyT} from './Updater';
|
||||||
import { ObservableProperty } from './ObservableProperty';
|
import { ObservableProperty } from './ObservableProperty';
|
||||||
import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'
|
import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'
|
||||||
import PluginClass from './PluginClass';
|
import PluginClass from './PluginClass';
|
||||||
@@ -38,7 +38,7 @@ import GovernorSettings from './GovernorSettings';
|
|||||||
import WifiChannel from './WifiChannel';
|
import WifiChannel from './WifiChannel';
|
||||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||||
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
|
||||||
import {ColorTheme, getColorScheme,setColorScheme} from './DarkMode';
|
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
|
||||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||||
|
|
||||||
|
|
||||||
@@ -49,13 +49,21 @@ export enum State {
|
|||||||
Background,
|
Background,
|
||||||
Reconnecting,
|
Reconnecting,
|
||||||
ApplyingChanges,
|
ApplyingChanges,
|
||||||
ReloadingPlugins
|
ReloadingPlugins,
|
||||||
|
DownloadingUpdate,
|
||||||
|
InstallingUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function wantsLoadingScreen(state: State)
|
||||||
|
{
|
||||||
|
return state >= State.Reconnecting;
|
||||||
|
}
|
||||||
|
|
||||||
export enum ReconnectReason {
|
export enum ReconnectReason {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
LoadingSettings,
|
LoadingSettings,
|
||||||
ReloadingPlugins,
|
ReloadingPlugins,
|
||||||
|
Updating,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||||
@@ -369,6 +377,9 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
|
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
|
||||||
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
|
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
|
||||||
|
|
||||||
|
promptForUpdate: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||||
|
updateStatus: ObservableProperty<UpdateStatus> = new ObservableProperty<UpdateStatus>(new UpdateStatus());
|
||||||
|
|
||||||
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
||||||
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
||||||
|
|
||||||
@@ -431,23 +442,32 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this);
|
this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSocketReconnecting(retry: number, maxRetries: number): void {
|
expectDisconnect(reason: ReconnectReason)
|
||||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
{
|
||||||
|
this.reconnectReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSocketReconnecting(retry: number, maxRetries: number): boolean {
|
||||||
|
if (this.isClosed) return false;
|
||||||
|
if (this.visibilityState.get() === VisibilityState.Hidden) return false;
|
||||||
//if (retry !== 0) {
|
//if (retry !== 0) {
|
||||||
switch (this.reconnectReason)
|
switch (this.reconnectReason) {
|
||||||
{
|
case ReconnectReason.Disconnected:
|
||||||
case ReconnectReason.Disconnected:
|
default:
|
||||||
default:
|
this.setState(State.Reconnecting);
|
||||||
this.setState(State.Reconnecting);
|
break;
|
||||||
break;
|
case ReconnectReason.LoadingSettings:
|
||||||
case ReconnectReason.LoadingSettings:
|
this.setState(State.ApplyingChanges);
|
||||||
this.setState(State.ApplyingChanges);
|
break;
|
||||||
break;
|
case ReconnectReason.ReloadingPlugins:
|
||||||
case ReconnectReason.ReloadingPlugins:
|
this.setState(State.ReloadingPlugins);
|
||||||
this.setState(State.ReloadingPlugins);
|
break;
|
||||||
break;
|
case ReconnectReason.Updating:
|
||||||
|
this.setState(State.InstallingUpdate);
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -486,17 +506,17 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
else if (message === "onOutputVolumeChanged") {
|
else if (message === "onOutputVolumeChanged") {
|
||||||
let value = body as number;
|
let value = body as number;
|
||||||
this._setOutputVolume(value,false);
|
this._setOutputVolume(value, false);
|
||||||
}
|
}
|
||||||
else if (message === "onInputVolumeChanged") {
|
else if (message === "onInputVolumeChanged") {
|
||||||
let value = body as number;
|
let value = body as number;
|
||||||
this._setInputVolume(value,false);
|
this._setInputVolume(value, false);
|
||||||
} else if (message === "onLv2StateChanged") {
|
} else if (message === "onLv2StateChanged") {
|
||||||
|
|
||||||
let instanceId = body.instanceId as number;
|
let instanceId = body.instanceId as number;
|
||||||
let state = body.state as [boolean,any];
|
let state = body.state as [boolean, any];
|
||||||
|
|
||||||
this.onLv2StateChanged(instanceId,state);
|
this.onLv2StateChanged(instanceId, state);
|
||||||
} else if (message === "onVst3ControlChanged") {
|
} else if (message === "onVst3ControlChanged") {
|
||||||
let controlChangedBody = body as Vst3ControlChangedBody;
|
let controlChangedBody = body as Vst3ControlChangedBody;
|
||||||
this._setVst3PedalboardControlValue(
|
this._setVst3PedalboardControlValue(
|
||||||
@@ -613,18 +633,114 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
} else if (message === "onSystemMidiBindingsChanged") {
|
} else if (message === "onSystemMidiBindingsChanged") {
|
||||||
let bindings = MidiBinding.deserialize_array(body);
|
let bindings = MidiBinding.deserialize_array(body);
|
||||||
this.systemMidiBindings.set(bindings);
|
this.systemMidiBindings.set(bindings);
|
||||||
} else if (message === "onErrorMessage")
|
} else if (message === "onErrorMessage") {
|
||||||
{
|
|
||||||
this.showAlert(body as string);
|
this.showAlert(body as string);
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (message === "onLv2PluginsChanging")
|
else if (message === "onLv2PluginsChanging") {
|
||||||
{
|
|
||||||
this.onLv2PluginsChanging();
|
this.onLv2PluginsChanging();
|
||||||
|
} else if (message === "onUpdateStatusChanged") {
|
||||||
|
let updateStatus = new UpdateStatus().deserialize(body);
|
||||||
|
this.onUpdateStatusChanged(updateStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLv2PluginsChanging() : void {
|
|
||||||
this.reconnectReason = ReconnectReason.ReloadingPlugins;
|
private updateLaterTimeout?: NodeJS.Timeout = undefined;
|
||||||
|
|
||||||
|
private clearPromptForUpdateTimer()
|
||||||
|
{
|
||||||
|
if (this.updateLaterTimeout)
|
||||||
|
{
|
||||||
|
clearTimeout(this.updateLaterTimeout);
|
||||||
|
this.updateLaterTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setPromptForUpdateTimer(when: Date)
|
||||||
|
{
|
||||||
|
let ms = when.getTime()-Date.now();
|
||||||
|
this.updateLaterTimeout = setTimeout(
|
||||||
|
()=>{
|
||||||
|
this.updateLaterTimeout = undefined;
|
||||||
|
// make the server do a fresh check
|
||||||
|
this.getUpdateStatus()
|
||||||
|
.then(
|
||||||
|
()=>{
|
||||||
|
this.updatePromptForUpdate();
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private updatePromptForUpdate() {
|
||||||
|
this.clearPromptForUpdateTimer();
|
||||||
|
|
||||||
|
let stateEnabled = true; // must be present to accept alerts. this.state.get() === State.Ready;
|
||||||
|
let timeEnabled = false;
|
||||||
|
|
||||||
|
let updateLaterTime = this.getUpdateTime();
|
||||||
|
if (updateLaterTime == null) {
|
||||||
|
timeEnabled = true;
|
||||||
|
} else {
|
||||||
|
let nDate = Date.now();
|
||||||
|
|
||||||
|
let now: Date = new Date(nDate);
|
||||||
|
let tomorrow: Date = new Date(nDate + 86400000);
|
||||||
|
|
||||||
|
timeEnabled = (updateLaterTime < now || updateLaterTime >= tomorrow)
|
||||||
|
if (!timeEnabled)
|
||||||
|
{
|
||||||
|
this.setPromptForUpdateTimer(updateLaterTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let updateStatus = this.updateStatus.get();
|
||||||
|
let statusEnabled = updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable;
|
||||||
|
|
||||||
|
if (stateEnabled && timeEnabled && statusEnabled)
|
||||||
|
{
|
||||||
|
this.showUpdateDialogValue = true;
|
||||||
|
}
|
||||||
|
this.promptForUpdate.set(this.showUpdateDialogValue);
|
||||||
|
|
||||||
|
if (stateEnabled && statusEnabled && !timeEnabled && updateLaterTime)
|
||||||
|
{
|
||||||
|
this.setPromptForUpdateTimer(updateLaterTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private showUpdateDialogValue: boolean = false;
|
||||||
|
|
||||||
|
showUpdateDialog(show: boolean = true) {
|
||||||
|
if (this.showUpdateDialogValue !== show)
|
||||||
|
{
|
||||||
|
this.showUpdateDialogValue = show;
|
||||||
|
if (show)
|
||||||
|
{
|
||||||
|
this.forceUpdateCheck();
|
||||||
|
}
|
||||||
|
this.updatePromptForUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateStatusChanged(updateStatus: UpdateStatus): void {
|
||||||
|
let current = this.updateStatus.get();
|
||||||
|
if (!current.equals(updateStatus)) {
|
||||||
|
this.updateStatus.set(updateStatus);
|
||||||
|
if (current.currentVersion.length !== 0 && current.currentVersion !== updateStatus.currentVersion) {
|
||||||
|
// !! Server has been updated!!!
|
||||||
|
this.reloadPage();
|
||||||
|
}
|
||||||
|
if (updateStatus.getActiveRelease().updateAvailable) {
|
||||||
|
this.promptForUpdate.set(true);
|
||||||
|
}
|
||||||
|
this.updatePromptForUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onLv2PluginsChanging(): void {
|
||||||
|
this.expectDisconnect(ReconnectReason.ReloadingPlugins);
|
||||||
// this.webSocket?.reconnect(); // let the server do it for us.
|
// this.webSocket?.reconnect(); // let the server do it for us.
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -639,19 +755,21 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
setState(state: State) {
|
setState(state: State) {
|
||||||
if (this.state.get() !== state) {
|
if (this.state.get() !== state) {
|
||||||
this.state.set(state);
|
this.state.set(state);
|
||||||
|
this.updatePromptForUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePlugins(plugin: PluginClass) {
|
validatePluginClasses(plugin: PluginClass) {
|
||||||
if (plugin.plugin_type === PluginType.None) {
|
if (plugin.plugin_type === PluginType.None) {
|
||||||
console.log("Error: No plugin type for uri '" + plugin.uri + "'");
|
console.log("Error: No plugin type for uri '" + plugin.uri + "'");
|
||||||
}
|
}
|
||||||
for (let i = 0; i < plugin.children.length; ++i) {
|
for (let i = 0; i < plugin.children.length; ++i) {
|
||||||
this.validatePlugins(plugin.children[i]);
|
this.validatePluginClasses(plugin.children[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private reconnectReason: ReconnectReason = ReconnectReason.Disconnected;
|
private reconnectReason: ReconnectReason = ReconnectReason.Disconnected;
|
||||||
|
|
||||||
isReloading(): boolean {
|
isReloading(): boolean {
|
||||||
@@ -681,13 +799,17 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.androidHost?.setDisconnected(false);
|
this.androidHost?.setDisconnected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectReason = ReconnectReason.Disconnected;
|
this.expectDisconnect(ReconnectReason.Disconnected); // the next expected disconnect will be an actual disconnect.
|
||||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
||||||
|
|
||||||
// reload state, but not configuration.
|
// reload state, but not configuration.
|
||||||
this.getWebSocket().request<number>("hello")
|
this.getWebSocket().request<number>("hello")
|
||||||
.then(clientId => {
|
.then(clientId => {
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
|
return this.getUpdateStatus(); // detects whether server has been upgraded.
|
||||||
|
})
|
||||||
|
.then((updateStatus) => {
|
||||||
|
|
||||||
return this.getWebSocket().request<any>("plugins");
|
return this.getWebSocket().request<any>("plugins");
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -774,7 +896,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maxFileUploadSize: number = 512*1024*1024;
|
maxFileUploadSize: number = 512 * 1024 * 1024;
|
||||||
maxPresetUploadSize: number = 1024 * 1024;
|
maxPresetUploadSize: number = 1024 * 1024;
|
||||||
debug: boolean = false;
|
debug: boolean = false;
|
||||||
|
|
||||||
@@ -795,7 +917,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.androidHost = new FakeAndroidHost();
|
this.androidHost = new FakeAndroidHost();
|
||||||
}
|
}
|
||||||
this.debug = !!data.debug;
|
this.debug = !!data.debug;
|
||||||
let { socket_server_port, socket_server_address,max_upload_size } = data;
|
let { socket_server_port, socket_server_address, max_upload_size } = data;
|
||||||
if ((!socket_server_address) || socket_server_address === "*") {
|
if ((!socket_server_address) || socket_server_address === "*") {
|
||||||
socket_server_address = window.location.hostname;
|
socket_server_address = window.location.hostname;
|
||||||
}
|
}
|
||||||
@@ -825,6 +947,9 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
return this.getUpdateStatus();
|
||||||
|
})
|
||||||
|
.then((updateStatus) => {
|
||||||
const isoRequest = new Request('iso_codes.json');
|
const isoRequest = new Request('iso_codes.json');
|
||||||
return fetch(isoRequest);
|
return fetch(isoRequest);
|
||||||
})
|
})
|
||||||
@@ -868,7 +993,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|
||||||
this.plugin_classes.set(new PluginClass().deserialize(data));
|
this.plugin_classes.set(new PluginClass().deserialize(data));
|
||||||
this.validatePlugins(this.plugin_classes.get());
|
this.validatePluginClasses(this.plugin_classes.get());
|
||||||
|
|
||||||
return this.getWebSocket().request<PresetIndex>("getPresets");
|
return this.getWebSocket().request<PresetIndex>("getPresets");
|
||||||
})
|
})
|
||||||
@@ -1131,7 +1256,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLv2StateChanged(instanceId: number, state: [boolean,any]): void {
|
private onLv2StateChanged(instanceId: number, state: [boolean, any]): void {
|
||||||
let item = this.pedalboard.get().getItem(instanceId);
|
let item = this.pedalboard.get().getItem(instanceId);
|
||||||
item.lv2State = state;
|
item.lv2State = state;
|
||||||
for (let item of this.stateChangedListeners) {
|
for (let item of this.stateChangedListeners) {
|
||||||
@@ -1182,33 +1307,31 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.webSocket?.send("setPatchProperty", body);
|
this.webSocket?.send("setPatchProperty", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputVolume(volume_db: number) : void {
|
setInputVolume(volume_db: number): void {
|
||||||
this._setInputVolume(volume_db,true);
|
this._setInputVolume(volume_db, true);
|
||||||
}
|
}
|
||||||
previewInputVolume(volume_db: number) : void {
|
previewInputVolume(volume_db: number): void {
|
||||||
nullCast(this.webSocket).send("previewInputVolume",volume_db);
|
nullCast(this.webSocket).send("previewInputVolume", volume_db);
|
||||||
|
|
||||||
}
|
}
|
||||||
previewOutputVolume(volume_db: number) : void {
|
previewOutputVolume(volume_db: number): void {
|
||||||
nullCast(this.webSocket).send("previewOutputVolume",volume_db);
|
nullCast(this.webSocket).send("previewOutputVolume", volume_db);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setInputVolume(volume_db: number, notifyServer: boolean) : void {
|
private _setInputVolume(volume_db: number, notifyServer: boolean): void {
|
||||||
let changed: boolean = false;
|
let changed: boolean = false;
|
||||||
|
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (pedalboard.input_volume_db !== volume_db)
|
if (pedalboard.input_volume_db !== volume_db) {
|
||||||
{
|
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
newPedalboard.input_volume_db = volume_db;
|
newPedalboard.input_volume_db = volume_db;
|
||||||
this.pedalboard.set(newPedalboard);
|
this.pedalboard.set(newPedalboard);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed)
|
if (changed) {
|
||||||
{
|
|
||||||
if (notifyServer) {
|
if (notifyServer) {
|
||||||
nullCast(this.webSocket).send("setInputVolume",volume_db);
|
nullCast(this.webSocket).send("setInputVolume", volume_db);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
||||||
@@ -1220,26 +1343,23 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutputVolume(volume_db: number, notifyServer: boolean) : void
|
setOutputVolume(volume_db: number, notifyServer: boolean): void {
|
||||||
{
|
this._setOutputVolume(volume_db, true);
|
||||||
this._setOutputVolume(volume_db,true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setOutputVolume(volume_db: number, notifyServer: boolean) : void {
|
private _setOutputVolume(volume_db: number, notifyServer: boolean): void {
|
||||||
let changed: boolean = false;
|
let changed: boolean = false;
|
||||||
|
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (pedalboard.output_volume_db !== volume_db)
|
if (pedalboard.output_volume_db !== volume_db) {
|
||||||
{
|
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
newPedalboard.output_volume_db = volume_db;
|
newPedalboard.output_volume_db = volume_db;
|
||||||
this.pedalboard.set(newPedalboard);
|
this.pedalboard.set(newPedalboard);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed)
|
if (changed) {
|
||||||
{
|
|
||||||
if (notifyServer) {
|
if (notifyServer) {
|
||||||
nullCast(this.webSocket).send("setOutputVolume",volume_db);
|
nullCast(this.webSocket).send("setOutputVolume", volume_db);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
|
||||||
let item = this._controlValueChangeItems[i];
|
let item = this._controlValueChangeItems[i];
|
||||||
@@ -1256,15 +1376,13 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
|
|
||||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
|
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
|
||||||
{
|
this._setInputVolume(value, notifyServer);
|
||||||
this._setInputVolume(value,notifyServer);
|
|
||||||
return;
|
return;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
} else if (instanceId === Pedalboard.END_CONTROL) {
|
||||||
{
|
this._setOutputVolume(value, notifyServer);
|
||||||
this._setOutputVolume(value,notifyServer);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let item = newPedalboard.getItem(instanceId);
|
let item = newPedalboard.getItem(instanceId);
|
||||||
let changed = item.setControlValue(key, value);
|
let changed = item.setControlValue(key, value);
|
||||||
|
|
||||||
@@ -1365,7 +1483,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
while (true) {
|
while (true) {
|
||||||
let v = it.next();
|
let v = it.next();
|
||||||
if (v.done) break;
|
if (v.done) break;
|
||||||
let item = v.value;
|
let item = v.value;
|
||||||
if (item.instanceId === itemId) {
|
if (item.instanceId === itemId) {
|
||||||
item.deserialize(new PedalboardItem()); // skeezy way to re-initialize.
|
item.deserialize(new PedalboardItem()); // skeezy way to re-initialize.
|
||||||
item.instanceId = ++newPedalboard.nextInstanceId;
|
item.instanceId = ++newPedalboard.nextInstanceId;
|
||||||
@@ -1395,16 +1513,14 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
// mouse is down. Don't update EVERYBODY, but we must change
|
// mouse is down. Don't update EVERYBODY, but we must change
|
||||||
// the control on the running audio plugin.
|
// the control on the running audio plugin.
|
||||||
// TODO: respect "expensive" port attribute.
|
// TODO: respect "expensive" port attribute.
|
||||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
|
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
|
||||||
{
|
|
||||||
this.previewInputVolume(value);
|
this.previewInputVolume(value);
|
||||||
return;
|
return;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
} else if (instanceId === Pedalboard.END_CONTROL) {
|
||||||
{
|
|
||||||
this.previewOutputVolume(value);
|
this.previewOutputVolume(value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setServerControl("previewControl", instanceId, key, value);
|
this._setServerControl("previewControl", instanceId, key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1530,13 +1646,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
addPedalboardItem(instanceId: number, append: boolean): number {
|
addPedalboardItem(instanceId: number, append: boolean): number {
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (instanceId === Pedalboard.START_CONTROL && append)
|
if (instanceId === Pedalboard.START_CONTROL && append) {
|
||||||
{
|
|
||||||
instanceId = pedalboard.items[0].instanceId;
|
instanceId = pedalboard.items[0].instanceId;
|
||||||
append = false;
|
append = false;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL && !append)
|
} else if (instanceId === Pedalboard.END_CONTROL && !append) {
|
||||||
{
|
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
||||||
instanceId = pedalboard.items[pedalboard.items.length-1].instanceId;
|
|
||||||
append = true;
|
append = true;
|
||||||
}
|
}
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
@@ -1555,14 +1669,12 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
addPedalboardSplitItem(instanceId: number, append: boolean): number {
|
addPedalboardSplitItem(instanceId: number, append: boolean): number {
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
|
|
||||||
if (instanceId === Pedalboard.START_CONTROL && append)
|
if (instanceId === Pedalboard.START_CONTROL && append) {
|
||||||
{
|
|
||||||
instanceId = pedalboard.items[0].instanceId;
|
instanceId = pedalboard.items[0].instanceId;
|
||||||
append = false;
|
append = false;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL && !append)
|
} else if (instanceId === Pedalboard.END_CONTROL && !append) {
|
||||||
{
|
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
||||||
instanceId = pedalboard.items[pedalboard.items.length-1].instanceId;
|
|
||||||
append = true;
|
append = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1615,7 +1727,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> {
|
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> {
|
||||||
// default behaviour is to save after the currently selected preset.
|
// default behaviour is to save after the currently selected preset.
|
||||||
if (saveAfterInstanceId === -1) {
|
if (saveAfterInstanceId === -1) {
|
||||||
@@ -1636,21 +1747,94 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUpdateStatus(): Promise<UpdateStatus> {
|
||||||
|
return new Promise<UpdateStatus>(
|
||||||
|
(accept, reject) => {
|
||||||
|
nullCast(this.webSocket)
|
||||||
|
.request<UpdateStatus>('getUpdateStatus')
|
||||||
|
.then((result) => {
|
||||||
|
let updateStatus = new UpdateStatus().deserialize(result);
|
||||||
|
this.onUpdateStatusChanged(updateStatus);
|
||||||
|
accept(result);
|
||||||
|
}).catch(
|
||||||
|
(e) => {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
launchExternalUrl(url: string) {
|
||||||
|
if (this.isAndroidHosted()) {
|
||||||
|
try {
|
||||||
|
this.androidHost?.launchExternalUrl(url);
|
||||||
|
} catch (e) {
|
||||||
|
// if they haven't updated their client yet, just don't do it.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
window.open(url, "_blank")?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLater(delayMs: number) {
|
||||||
|
let futureDate = new Date(Date.now() + delayMs);
|
||||||
|
localStorage.setItem('nextUpdateTime', futureDate.toISOString());
|
||||||
|
|
||||||
|
this.updatePromptForUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNow(): Promise<void> {
|
||||||
|
return new Promise<void>(
|
||||||
|
(accept,reject) => {
|
||||||
|
let updateStatus = this.updateStatus.get();
|
||||||
|
if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable)
|
||||||
|
{
|
||||||
|
this.setState(State.DownloadingUpdate);
|
||||||
|
let url = updateStatus.getActiveRelease().updateUrl;
|
||||||
|
nullCast(this.webSocket)
|
||||||
|
.request<void>('updateNow', url)
|
||||||
|
.then(()=> {
|
||||||
|
this.setState(State.InstallingUpdate);
|
||||||
|
accept();
|
||||||
|
})
|
||||||
|
.catch(
|
||||||
|
(e: any)=>{
|
||||||
|
this.setState(State.Ready); // TODO: hopefully we haven't had an intermediate disconnect.
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error("Invalid update request."));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdateTime(): Date | null {
|
||||||
|
let item = localStorage.getItem('nextUpdateTime');
|
||||||
|
if (item) {
|
||||||
|
return new Date(item);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// deprecated.
|
// deprecated.
|
||||||
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
||||||
return nullCast(this.webSocket)
|
return nullCast(this.webSocket)
|
||||||
.request<string[]>('requestFileList', piPedalFileProperty);
|
.request<string[]>('requestFileList', piPedalFileProperty);
|
||||||
}
|
}
|
||||||
requestFileList2(relativeDirectoryPath: string,piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
|
requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> {
|
||||||
return nullCast(this.webSocket)
|
return nullCast(this.webSocket)
|
||||||
.request<FileEntry[]>('requestFileList2',
|
.request<FileEntry[]>('requestFileList2',
|
||||||
{relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty}
|
{ relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteUserFile(fileName: string) : Promise<boolean>
|
deleteUserFile(fileName: string): Promise<boolean> {
|
||||||
{
|
return nullCast(this.webSocket).request<boolean>('deleteUserFile', fileName);
|
||||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile',fileName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1718,7 +1902,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
|
|
||||||
setJackSettings(jackSettings: JackChannelSelection): void {
|
setJackSettings(jackSettings: JackChannelSelection): void {
|
||||||
this.reconnectReason = ReconnectReason.LoadingSettings;
|
this.expectDisconnect(ReconnectReason.LoadingSettings);
|
||||||
this.webSocket?.send("setJackSettings", jackSettings);
|
this.webSocket?.send("setJackSettings", jackSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1823,7 +2007,15 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private isClosed = false;
|
||||||
|
close() {
|
||||||
|
if (!this.isClosed)
|
||||||
|
{
|
||||||
|
this.isClosed = true;
|
||||||
|
this.webSocket?.close();
|
||||||
|
this.webSocket = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
||||||
let result = new Promise<boolean>((resolve, reject) => {
|
let result = new Promise<boolean>((resolve, reject) => {
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
@@ -2107,7 +2299,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
|
||||||
let result = new Promise<string>((resolve, reject) => {
|
let result = new Promise<string>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
if (file.size > this.maxFileUploadSize) {
|
if (file.size > this.maxFileUploadSize) {
|
||||||
@@ -2117,17 +2309,15 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
let parsedUrl = new URL(url);
|
let parsedUrl = new URL(url);
|
||||||
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
|
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
|
||||||
let query = parsedUrl.search;
|
let query = parsedUrl.search;
|
||||||
if (query.length === 0)
|
if (query.length === 0) {
|
||||||
{
|
|
||||||
query += '?';
|
query += '?';
|
||||||
} else {
|
} else {
|
||||||
query += '&';
|
query += '&';
|
||||||
}
|
}
|
||||||
if (!fileNameOnly)
|
if (!fileNameOnly) {
|
||||||
{
|
|
||||||
reject("Invalid filename.");
|
reject("Invalid filename.");
|
||||||
}
|
}
|
||||||
query += "filename=" + encodeURIComponent(fileNameOnly??"");
|
query += "filename=" + encodeURIComponent(fileNameOnly ?? "");
|
||||||
parsedUrl.search = query;
|
parsedUrl.search = query;
|
||||||
url = parsedUrl.toString();
|
url = parsedUrl.toString();
|
||||||
fetch(
|
fetch(
|
||||||
@@ -2146,7 +2336,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
reject("Upload failed. " + response.statusText);
|
reject("Upload failed. " + response.statusText);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
@@ -2254,8 +2444,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
if (!ws)
|
if (!ws) {
|
||||||
{
|
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2271,8 +2460,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
|
||||||
{
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
@@ -2295,8 +2483,8 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty) : Promise<FilePropertyDirectoryTree> {
|
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty): Promise<FilePropertyDirectoryTree> {
|
||||||
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
if (!ws) {
|
if (!ws) {
|
||||||
@@ -2306,15 +2494,14 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
ws.request<FilePropertyDirectoryTree>(
|
ws.request<FilePropertyDirectoryTree>(
|
||||||
"getFilePropertyDirectoryTree",
|
"getFilePropertyDirectoryTree",
|
||||||
uiFileProperty
|
uiFileProperty
|
||||||
).then((result)=> {
|
).then((result) => {
|
||||||
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
resolve(new FilePropertyDirectoryTree().deserialize(result));
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
reject(e);
|
reject(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
|
||||||
{
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
@@ -2441,11 +2628,11 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
//resolve();
|
//resolve();
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
});
|
});
|
||||||
this.reconnectReason = ReconnectReason.LoadingSettings;
|
this.expectDisconnect(ReconnectReason.LoadingSettings);
|
||||||
this.webSocket?.reconnect(); // close immediately, and wait for recoonnect.
|
// this.webSocket?.reconnect(); // avoid races by letting the server do it for us.
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2524,8 +2711,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
++ix;
|
++ix;
|
||||||
if (ix >= uiPlugin.controls.length) return;
|
if (ix >= uiPlugin.controls.length) return;
|
||||||
while (!uiPlugin.controls[ix].is_input)
|
while (!uiPlugin.controls[ix].is_input) {
|
||||||
{
|
|
||||||
++ix;
|
++ix;
|
||||||
if (ix >= uiPlugin.controls.length) return;
|
if (ix >= uiPlugin.controls.length) return;
|
||||||
}
|
}
|
||||||
@@ -2558,8 +2744,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
--ix;
|
--ix;
|
||||||
if (ix < 0) return;
|
if (ix < 0) return;
|
||||||
while (!uiPlugin.controls[ix].is_input)
|
while (!uiPlugin.controls[ix].is_input) {
|
||||||
{
|
|
||||||
--ix;
|
--ix;
|
||||||
if (ix < 0) return;
|
if (ix < 0) return;
|
||||||
}
|
}
|
||||||
@@ -2590,6 +2775,20 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setUpdatePolicy(updatePolicy: UpdatePolicyT) : void {
|
||||||
|
let iPolicy = updatePolicy as number;
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.webSocket.send("setUpdatePolicy", iPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
forceUpdateCheck() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.webSocket.send("forceUpdateCheck");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
preloadImages(imageList: string): void {
|
preloadImages(imageList: string): void {
|
||||||
let imageNames = imageList.split(';');
|
let imageNames = imageList.split(';');
|
||||||
for (let i = 0; i < imageNames.length; ++i) {
|
for (let i = 0; i < imageNames.length; ++i) {
|
||||||
@@ -2619,8 +2818,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the ID of the new preset.
|
// returns the ID of the new preset.
|
||||||
newPresetItem(createAfter: number): Promise<number>
|
newPresetItem(createAfter: number): Promise<number> {
|
||||||
{
|
|
||||||
return nullCast(this.webSocket).request<number>("newPreset");
|
return nullCast(this.webSocket).request<number>("newPreset");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2630,14 +2828,13 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
setTheme(value: ColorTheme) {
|
setTheme(value: ColorTheme) {
|
||||||
|
|
||||||
if (this.getTheme() !== value)
|
if (this.getTheme() !== value) {
|
||||||
{
|
|
||||||
setColorScheme(value);
|
setColorScheme(value);
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
this.reloadPage();
|
this.reloadPage();
|
||||||
},
|
},
|
||||||
200);
|
200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2647,7 +2844,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
//window.location.reload();
|
//window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance: PiPedalModel | undefined = undefined;
|
let instance: PiPedalModel | undefined = undefined;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export interface PiPedalSocketListener {
|
|||||||
onError: (message: string, exception?: Error) => void;
|
onError: (message: string, exception?: Error) => void;
|
||||||
onConnectionLost: () => void;
|
onConnectionLost: () => void;
|
||||||
onReconnect: () => void;
|
onReconnect: () => void;
|
||||||
onReconnecting: (retry: number, maxRetries: number) => void;
|
onReconnecting: (retry: number, maxRetries: number) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PiPedalSocket {
|
class PiPedalSocket {
|
||||||
@@ -183,6 +183,7 @@ class PiPedalSocket {
|
|||||||
|
|
||||||
handleClose(_event: any): any {
|
handleClose(_event: any): any {
|
||||||
if (this.retrying) {
|
if (this.retrying) {
|
||||||
|
this.close();
|
||||||
// treat this as a fatal error.
|
// treat this as a fatal error.
|
||||||
if (this.listener) {
|
if (this.listener) {
|
||||||
this.listener.onError("Server connection lost.");
|
this.listener.onError("Server connection lost.");
|
||||||
@@ -230,7 +231,9 @@ class PiPedalSocket {
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listener.onReconnecting(this.retryCount,MAX_RETRIES);
|
if (!this.listener.onReconnecting(this.retryCount,MAX_RETRIES)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
++this.retryCount;
|
++this.retryCount;
|
||||||
|
|
||||||
this.connectInternal_()
|
this.connectInternal_()
|
||||||
@@ -262,6 +265,7 @@ class PiPedalSocket {
|
|||||||
this.socket.onmessage = null;
|
this.socket.onmessage = null;
|
||||||
this.socket.onopen = null;
|
this.socket.onopen = null;
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
|
this.socket = undefined;
|
||||||
}
|
}
|
||||||
} catch (ignored)
|
} catch (ignored)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -393,6 +393,12 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
showThemeSelectDialog: true
|
showThemeSelectDialog: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCheckForUpdates()
|
||||||
|
{
|
||||||
|
this.model.showUpdateDialog();
|
||||||
|
}
|
||||||
|
|
||||||
handleMidiMessageSettings() {
|
handleMidiMessageSettings() {
|
||||||
this.setState({
|
this.setState({
|
||||||
showSystemMidiBindingsDialog: true
|
showSystemMidiBindingsDialog: true
|
||||||
@@ -786,6 +792,18 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
</div>
|
</div>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|
||||||
|
<ButtonBase
|
||||||
|
className={classes.setting}
|
||||||
|
onClick={() => { this.handleCheckForUpdates(); }} >
|
||||||
|
<SelectHoverBackground selected={false} showHover={true} />
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
|
||||||
|
Check for updates...</Typography>
|
||||||
|
</div>
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ButtonBase className={classes.setting} disabled={disableShutdown}
|
<ButtonBase className={classes.setting} disabled={disableShutdown}
|
||||||
onClick={() => this.handleRestart()} >
|
onClick={() => this.handleRestart()} >
|
||||||
<SelectHoverBackground selected={false} showHover={true} />
|
<SelectHoverBackground selected={false} showHover={true} />
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Robin E. R. Davies
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
* so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import DialogEx from './DialogEx';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import { UpdateStatus, UpdateRelease, UpdatePolicyT,intToUpdatePolicyT } from './Updater';
|
||||||
|
import { PiPedalModelFactory, PiPedalModel } from './PiPedalModel';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
|
|
||||||
|
//const UPDATE_CHECK_DELAY = 86400000; // one day in ms.
|
||||||
|
const UPDATE_CHECK_DELAY = 30 * 1000; // testing
|
||||||
|
const CLOSE_DELAY = 100; // ms.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface UpdateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdateDialogState {
|
||||||
|
updateStatus: UpdateStatus;
|
||||||
|
compactLandscape: boolean;
|
||||||
|
alertDialogOpen: boolean;
|
||||||
|
alertDialogMessage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class UpdateDialog extends React.Component<UpdateDialogProps, UpdateDialogState> {
|
||||||
|
private model: PiPedalModel;
|
||||||
|
|
||||||
|
constructor(props: UpdateDialogProps) {
|
||||||
|
super(props);
|
||||||
|
this.model = PiPedalModelFactory.getInstance();
|
||||||
|
let updateStatus = this.model.updateStatus.get();
|
||||||
|
this.state = {
|
||||||
|
updateStatus: updateStatus,
|
||||||
|
compactLandscape: false,
|
||||||
|
alertDialogOpen: false,
|
||||||
|
alertDialogMessage: ""
|
||||||
|
};
|
||||||
|
this.onUpdateStatusChanged = this.onUpdateStatusChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateStatusChanged(newValue: UpdateStatus)
|
||||||
|
{
|
||||||
|
if (!newValue.equals(this.state.updateStatus))
|
||||||
|
{
|
||||||
|
this.setState({updateStatus: newValue});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mounted: boolean = false;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.mounted = true;
|
||||||
|
this.model.updateStatus.addOnChangedHandler(this.onUpdateStatusChanged);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.model.updateStatus.removeOnChangedHandler(this.onUpdateStatusChanged);
|
||||||
|
this.mounted = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private handleOk() {
|
||||||
|
|
||||||
|
}
|
||||||
|
private handleUpdateNow() {
|
||||||
|
this.model.updateNow()
|
||||||
|
.then(() => { }) // all handling is done by model, so we can run ui-less.
|
||||||
|
.catch((e: any) => {
|
||||||
|
this.showAlert(e.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private updatingLater: boolean = false;
|
||||||
|
private handleUpdateLater() {
|
||||||
|
this.updatingLater = true;
|
||||||
|
this.model.updateLater(UPDATE_CHECK_DELAY);
|
||||||
|
this.model.showUpdateDialog(false); // allow close if launched from the settings window.
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClose() {
|
||||||
|
// ideally, we'd like to not close at all, but it screws up the nav backstack if we dont.
|
||||||
|
// so, close, but do so very briefly
|
||||||
|
if (!this.updatingLater) {
|
||||||
|
this.model.updateLater(CLOSE_DELAY); // close and re-open immediately.
|
||||||
|
}
|
||||||
|
this.updatingLater = false;
|
||||||
|
this.model.showUpdateDialog(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseAlert() {
|
||||||
|
this.setState({ alertDialogOpen: false, alertDialogMessage: "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert(message: string) {
|
||||||
|
this.setState({ alertDialogOpen: true, alertDialogMessage: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewReleaseNotes() {
|
||||||
|
this.model.launchExternalUrl("https://rerdavies.github.io/pipedal/ReleaseNotes.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
upToDate(): boolean {
|
||||||
|
let updateStatus = this.state.updateStatus;
|
||||||
|
let updateRelease: UpdateRelease = updateStatus.getActiveRelease();
|
||||||
|
return updateStatus.isValid && !updateRelease.updateAvailable
|
||||||
|
}
|
||||||
|
canUpgrade(): boolean {
|
||||||
|
let updateStatus = this.state.updateStatus;
|
||||||
|
let updateRelease = updateStatus.getActiveRelease();
|
||||||
|
|
||||||
|
return updateStatus.isValid && updateStatus.isOnline && updateRelease.updateAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPolicySelected(newValue: string | number): void {
|
||||||
|
if (newValue.toString() === "") return;
|
||||||
|
let updatePolicy = intToUpdatePolicyT(newValue as number);
|
||||||
|
this.model.setUpdatePolicy(updatePolicy);
|
||||||
|
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let updateStatus = this.state.updateStatus;
|
||||||
|
let updateRelease = updateStatus.getActiveRelease();
|
||||||
|
let upToDate = this.upToDate();
|
||||||
|
let canUpgrade = this.canUpgrade();
|
||||||
|
let compact = this.state.compactLandscape;
|
||||||
|
return (
|
||||||
|
<DialogEx tag="update" open={this.props.open} onClose={() => { this.handleClose(); }}
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
(!compact) &&
|
||||||
|
(
|
||||||
|
<DialogTitle>
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }} >
|
||||||
|
<Typography style={{ flexGrow: 1, flexShrink: 1, marginRight: 20 }} noWrap>PiPedal Updates</Typography>
|
||||||
|
<Select style={{opacity: 0.6}} variant="standard" value={updateStatus.updatePolicy as number} onChange={(ev) => { this.onPolicySelected(ev.target.value); }}>
|
||||||
|
<MenuItem value={UpdatePolicyT.ReleaseOnly as number}>Release only</MenuItem>
|
||||||
|
<MenuItem value={UpdatePolicyT.ReleaseOrBeta as number}>Release or Beta</MenuItem>
|
||||||
|
<MenuItem value={UpdatePolicyT.Development as number}>Development</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</DialogTitle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(!compact) &&
|
||||||
|
(
|
||||||
|
<Divider />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
{
|
||||||
|
(upToDate) && (
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
|
||||||
|
PiPedal is up to date.
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{(canUpgrade) && (
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
|
||||||
|
A new version of the PiPedal server is available. Would you like to update now?
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap" }}>
|
||||||
|
<div>
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" >
|
||||||
|
Current version:
|
||||||
|
</Typography>
|
||||||
|
{(canUpgrade) && (
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
|
||||||
|
Update version:
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div style={{ flexShrink: 1, flexGrow: 1, marginLeft: 12 }}>
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" >
|
||||||
|
{updateStatus.currentVersionDisplayName}
|
||||||
|
</Typography>
|
||||||
|
{(canUpgrade) && (
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
|
||||||
|
{updateRelease.upgradeVersionDisplayName}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
(updateStatus.errorMessage.length !== 0) &&
|
||||||
|
(
|
||||||
|
<Typography variant="body2" style={{ marginTop: 12 }}>
|
||||||
|
{
|
||||||
|
updateStatus.errorMessage
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Button style={{ marginLeft: 16, marginTop: 12 }} onClick={() => { this.onViewReleaseNotes(); }}>View release notes</Button>
|
||||||
|
</DialogContent>
|
||||||
|
<Divider />
|
||||||
|
<DialogActions>
|
||||||
|
{
|
||||||
|
(canUpgrade) ?
|
||||||
|
(
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}>
|
||||||
|
<Button variant="dialogSecondary" onClick={() => { this.handleUpdateLater(); }}>Update later</Button>
|
||||||
|
<div style={{ flexGrow: 1, flexShrink: 1 }} />
|
||||||
|
<Button variant="dialogPrimary" onClick={() => { this.handleUpdateNow(); }}>Update now</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
) : (
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}
|
||||||
|
onClick={()=>{ this.model.showUpdateDialog(false); }}
|
||||||
|
>
|
||||||
|
<div style={{ flexGrow: 1, flexShrink: 1 }} />
|
||||||
|
<Button variant="dialogPrimary">OK</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</DialogActions>
|
||||||
|
|
||||||
|
<DialogEx
|
||||||
|
tag="UpdateAlert"
|
||||||
|
open={this.state.alertDialogOpen}
|
||||||
|
onClose={() => { this.handleCloseAlert(); }}
|
||||||
|
aria-describedby="Alert Dialog"
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="body2" color="secondaryText">
|
||||||
|
{
|
||||||
|
this.state.alertDialogMessage
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="dialogPrimary" onClick={() => { this.handleCloseAlert(); }} autoFocus>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogEx>
|
||||||
|
|
||||||
|
</DialogEx >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (c) 2024 Robin Davies
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
// subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
export enum UpdatePolicyT {
|
||||||
|
// must match declaration in Updater.hpp
|
||||||
|
ReleaseOnly = 0,
|
||||||
|
ReleaseOrBeta = 1,
|
||||||
|
Development = 2,
|
||||||
|
|
||||||
|
};
|
||||||
|
export function intToUpdatePolicyT(intValue: number): UpdatePolicyT {
|
||||||
|
return (Object.values(UpdatePolicyT).find(enumValue => enumValue === intValue)) as UpdatePolicyT;
|
||||||
|
}
|
||||||
|
export class UpdateRelease {
|
||||||
|
deserialize(input: any): UpdateRelease {
|
||||||
|
this.updateAvailable = input.updateAvailable;
|
||||||
|
this.upgradeVersion = input.upgradeVersion;
|
||||||
|
this.upgradeVersionDisplayName = input.upgradeVersionDisplayName;
|
||||||
|
this.assetName = input.assetName;
|
||||||
|
this.updateUrl = input.updateUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateAvailable: boolean = false;
|
||||||
|
upgradeVersion: string = ""; //just the version.
|
||||||
|
upgradeVersionDisplayName: string = ""; // display name for the version.
|
||||||
|
assetName: string = ""; // filename only
|
||||||
|
updateUrl: string = ""; // url from which to download the .deb file.
|
||||||
|
|
||||||
|
equals(other: UpdateRelease): boolean {
|
||||||
|
return (this.updateAvailable === other.updateAvailable) &&
|
||||||
|
(this.upgradeVersion === other.upgradeVersion) &&
|
||||||
|
(this.upgradeVersionDisplayName === other.upgradeVersionDisplayName) &&
|
||||||
|
(this.assetName === other.assetName) &&
|
||||||
|
(this.updateUrl === other.updateUrl)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateStatus {
|
||||||
|
lastUpdateTime: number = 0;
|
||||||
|
isValid: boolean = false;
|
||||||
|
errorMessage: string = "";
|
||||||
|
updatePolicy: UpdatePolicyT = UpdatePolicyT.ReleaseOrBeta;
|
||||||
|
isOnline: boolean = false;
|
||||||
|
currentVersion: string = "";
|
||||||
|
currentVersionDisplayName: string = "";
|
||||||
|
|
||||||
|
releaseOnlyRelease: UpdateRelease = new UpdateRelease();
|
||||||
|
releaseOrBetaRelease: UpdateRelease = new UpdateRelease();
|
||||||
|
devRelease: UpdateRelease = new UpdateRelease();
|
||||||
|
|
||||||
|
deserialize(input: any): UpdateStatus {
|
||||||
|
this.lastUpdateTime = input.lastUpdateTime;
|
||||||
|
this.isValid = input.isValid;
|
||||||
|
this.errorMessage = input.errorMessage;
|
||||||
|
this.isOnline = input.isOnline;
|
||||||
|
this.updatePolicy = intToUpdatePolicyT(input.updatePolicy);
|
||||||
|
this.currentVersion = input.currentVersion;
|
||||||
|
this.currentVersionDisplayName = input.currentVersionDisplayName;
|
||||||
|
this.releaseOnlyRelease = new UpdateRelease().deserialize(input.releaseOnlyRelease);
|
||||||
|
this.releaseOrBetaRelease = new UpdateRelease().deserialize(input.releaseOrBetaRelease);
|
||||||
|
this.devRelease = new UpdateRelease().deserialize(input.devRelease);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static deserialize_array(input: any): UpdateStatus[] {
|
||||||
|
let result: UpdateStatus[] = [];
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
result[i] = new UpdateStatus().deserialize(input[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
getActiveRelease(): UpdateRelease {
|
||||||
|
switch (this.updatePolicy) {
|
||||||
|
case UpdatePolicyT.ReleaseOnly:
|
||||||
|
return this.releaseOnlyRelease;
|
||||||
|
case UpdatePolicyT.ReleaseOrBeta:
|
||||||
|
return this.releaseOrBetaRelease;
|
||||||
|
case UpdatePolicyT.Development:
|
||||||
|
return this.devRelease;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid argument.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equals(other: UpdateStatus): boolean {
|
||||||
|
return (this.isValid === other.isValid)
|
||||||
|
&& (this.lastUpdateTime === other.lastUpdateTime)
|
||||||
|
&& (this.errorMessage === other.errorMessage)
|
||||||
|
&& (this.isOnline === other.isOnline)
|
||||||
|
&& (this.currentVersion === other.currentVersion)
|
||||||
|
&& (this.currentVersionDisplayName === other.currentVersionDisplayName)
|
||||||
|
&& (this.releaseOnlyRelease.equals(other.releaseOnlyRelease))
|
||||||
|
&& (this.releaseOrBetaRelease.equals(other.releaseOrBetaRelease))
|
||||||
|
&& (this.devRelease.equals(other.devRelease))
|
||||||
|
;
|
||||||
|
// other properties are contractually dealt with.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+9
-2
@@ -89,12 +89,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
|
|||||||
|
|
||||||
set (USE_SANITIZE False)
|
set (USE_SANITIZE False)
|
||||||
|
|
||||||
|
|
||||||
if (!ENABLE_VST3)
|
if (!ENABLE_VST3)
|
||||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi")
|
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi -DARCHITECTURE=${CPACK_SYSTEM_NAME}")
|
||||||
else()
|
else()
|
||||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-psabi")
|
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-psabi")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
EXECUTE_PROCESS( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE )
|
||||||
|
message(STATUS DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||||
|
|
||||||
|
add_compile_definitions(DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||||
|
|
||||||
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
||||||
message(STATUS "Debug build")
|
message(STATUS "Debug build")
|
||||||
@@ -132,6 +137,7 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PIPEDAL_SOURCES
|
set (PIPEDAL_SOURCES
|
||||||
|
Updater.cpp Updater.hpp
|
||||||
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
|
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
|
||||||
WebServerConfig.cpp WebServerConfig.hpp
|
WebServerConfig.cpp WebServerConfig.hpp
|
||||||
Locale.hpp Locale.cpp
|
Locale.hpp Locale.cpp
|
||||||
@@ -278,7 +284,8 @@ add_executable(pipedaltest testMain.cpp
|
|||||||
|
|
||||||
InvertingMutexTest.cpp
|
InvertingMutexTest.cpp
|
||||||
jsonTest.cpp
|
jsonTest.cpp
|
||||||
|
UpdaterTest.cpp
|
||||||
|
|
||||||
utilTest.cpp
|
utilTest.cpp
|
||||||
|
|
||||||
AlsaDriverTest.cpp
|
AlsaDriverTest.cpp
|
||||||
|
|||||||
+52
-1
@@ -73,6 +73,11 @@ PiPedalModel::PiPedalModel()
|
|||||||
#else
|
#else
|
||||||
this->jackServerSettings = this->storage.GetJackServerSettings();
|
this->jackServerSettings = this->storage.GetJackServerSettings();
|
||||||
#endif
|
#endif
|
||||||
|
updater.SetUpdateListener(
|
||||||
|
[this](const UpdateStatus&updateStatus)
|
||||||
|
{
|
||||||
|
this->OnUpdateStatusChanged(updateStatus);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void PiPedalModel::Close()
|
void PiPedalModel::Close()
|
||||||
@@ -386,9 +391,9 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
|
|||||||
|
|
||||||
item->stateUpdateCount(item->stateUpdateCount() + 1);
|
item->stateUpdateCount(item->stateUpdateCount() + 1);
|
||||||
|
|
||||||
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
|
||||||
|
|
||||||
Lv2PluginState newState = item->lv2State();
|
Lv2PluginState newState = item->lv2State();
|
||||||
|
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
||||||
for (size_t i = 0; i < subscribers.size(); ++i)
|
for (size_t i = 0; i < subscribers.size(); ++i)
|
||||||
{
|
{
|
||||||
t[i] = this->subscribers[i];
|
t[i] = this->subscribers[i];
|
||||||
@@ -2172,3 +2177,49 @@ void PiPedalModel::SetRestartListener(std::function<void(void)> &&listener)
|
|||||||
{
|
{
|
||||||
this->restartListener = std::move(listener);
|
this->restartListener = std::move(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus&updateStatus)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
|
||||||
|
if (this->currentUpdateStatus != updateStatus)
|
||||||
|
{
|
||||||
|
this->currentUpdateStatus = updateStatus;
|
||||||
|
FireUpdateStatusChanged(this->currentUpdateStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus&updateStatus)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
|
||||||
|
std::vector<IPiPedalModelSubscriber *> t;
|
||||||
|
t.reserve(subscribers.size());
|
||||||
|
|
||||||
|
for (auto subscriber: subscribers)
|
||||||
|
{
|
||||||
|
t.push_back(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto subscriber: t)
|
||||||
|
{
|
||||||
|
subscriber->OnUpdateStatusChanged(updateStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateStatus PiPedalModel::GetUpdateStatus() {
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
return currentUpdateStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PiPedalModel::UpdateNow(const std::string&updateUrl)
|
||||||
|
{
|
||||||
|
sleep(5);
|
||||||
|
throw std::runtime_error("Not implemented yet.");
|
||||||
|
|
||||||
|
}
|
||||||
|
void PiPedalModel::ForceUpdateCheck() {
|
||||||
|
updater.ForceUpdateCheck();
|
||||||
|
}
|
||||||
|
void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy)
|
||||||
|
{
|
||||||
|
updater.SetUpdatePolicy(updatePolicy);
|
||||||
|
}
|
||||||
|
|||||||
+11
-2
@@ -30,6 +30,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include "Banks.hpp"
|
#include "Banks.hpp"
|
||||||
|
#include "Updater.hpp"
|
||||||
#include "PiPedalConfiguration.hpp"
|
#include "PiPedalConfiguration.hpp"
|
||||||
#include "JackServerSettings.hpp"
|
#include "JackServerSettings.hpp"
|
||||||
#include "WifiConfigSettings.hpp"
|
#include "WifiConfigSettings.hpp"
|
||||||
@@ -56,7 +57,7 @@ namespace pipedal
|
|||||||
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
|
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
|
||||||
virtual void OnInputVolumeChanged(float value) = 0;
|
virtual void OnInputVolumeChanged(float value) = 0;
|
||||||
virtual void OnOutputVolumeChanged(float value) = 0;
|
virtual void OnOutputVolumeChanged(float value) = 0;
|
||||||
|
virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) = 0;
|
||||||
virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0;
|
virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0;
|
||||||
virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0;
|
virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0;
|
||||||
virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0;
|
virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0;
|
||||||
@@ -86,6 +87,10 @@ namespace pipedal
|
|||||||
class PiPedalModel : private IAudioHostCallbacks
|
class PiPedalModel : private IAudioHostCallbacks
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
UpdateStatus currentUpdateStatus;
|
||||||
|
Updater updater;
|
||||||
|
void OnUpdateStatusChanged(const UpdateStatus&updateStatus);
|
||||||
std::function<void(void)> restartListener;
|
std::function<void(void)> restartListener;
|
||||||
|
|
||||||
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
|
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
|
||||||
@@ -199,6 +204,9 @@ namespace pipedal
|
|||||||
PiPedalModel();
|
PiPedalModel();
|
||||||
virtual ~PiPedalModel();
|
virtual ~PiPedalModel();
|
||||||
|
|
||||||
|
UpdateStatus GetUpdateStatus();
|
||||||
|
void UpdateNow(const std::string&updateUrl);
|
||||||
|
void FireUpdateStatusChanged(const UpdateStatus&updateStatus);
|
||||||
uint16_t GetWebPort() const { return webPort; }
|
uint16_t GetWebPort() const { return webPort; }
|
||||||
std::filesystem::path GetPluginUploadDirectory() const;
|
std::filesystem::path GetPluginUploadDirectory() const;
|
||||||
void Close();
|
void Close();
|
||||||
@@ -334,7 +342,8 @@ namespace pipedal
|
|||||||
|
|
||||||
std::map<std::string, bool> GetFavorites() const;
|
std::map<std::string, bool> GetFavorites() const;
|
||||||
void SetFavorites(const std::map<std::string, bool> &favorites);
|
void SetFavorites(const std::map<std::string, bool> &favorites);
|
||||||
|
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
|
||||||
|
void ForceUpdateCheck();
|
||||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||||
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
||||||
|
|
||||||
|
|||||||
+30
-1
@@ -20,6 +20,7 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
|
||||||
#include "PiPedalSocket.hpp"
|
#include "PiPedalSocket.hpp"
|
||||||
|
#include "Updater.hpp"
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include "viewstream.hpp"
|
#include "viewstream.hpp"
|
||||||
#include "PiPedalVersion.hpp"
|
#include "PiPedalVersion.hpp"
|
||||||
@@ -1001,7 +1002,19 @@ public:
|
|||||||
pReader->read(&handle);
|
pReader->read(&handle);
|
||||||
this->model.CancelMonitorPatchProperty(this->clientId, handle);
|
this->model.CancelMonitorPatchProperty(this->clientId, handle);
|
||||||
}
|
}
|
||||||
|
else if (message == "getUpdateStatus")
|
||||||
|
{
|
||||||
|
UpdateStatus updateStatus = model.GetUpdateStatus();
|
||||||
|
this->Reply(replyTo,"getUpdateStatus",updateStatus);
|
||||||
|
}
|
||||||
|
else if (message == "updateNow")
|
||||||
|
{
|
||||||
|
std::string updateUrl;
|
||||||
|
pReader->read(&updateUrl);
|
||||||
|
model.UpdateNow(updateUrl);
|
||||||
|
bool result = true;
|
||||||
|
this->Reply(replyTo,"updateNow",result);
|
||||||
|
}
|
||||||
else if (message == "getJackStatus")
|
else if (message == "getJackStatus")
|
||||||
{
|
{
|
||||||
JackHostStatus status = model.GetJackStatus();
|
JackHostStatus status = model.GetJackStatus();
|
||||||
@@ -1442,6 +1455,18 @@ public:
|
|||||||
pReader->read(&favorites);
|
pReader->read(&favorites);
|
||||||
this->model.SetFavorites(favorites);
|
this->model.SetFavorites(favorites);
|
||||||
}
|
}
|
||||||
|
else if (message == "setUpdatePolicy")
|
||||||
|
{
|
||||||
|
int iPolicy;
|
||||||
|
pReader->read(&iPolicy);
|
||||||
|
|
||||||
|
this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy);
|
||||||
|
}
|
||||||
|
else if (message == "forceUpdateCheck")
|
||||||
|
{
|
||||||
|
this->model.ForceUpdateCheck();
|
||||||
|
|
||||||
|
}
|
||||||
else if (message == "setSystemMidiBindings")
|
else if (message == "setSystemMidiBindings")
|
||||||
{
|
{
|
||||||
std::vector<MidiBinding> bindings;
|
std::vector<MidiBinding> bindings;
|
||||||
@@ -1581,6 +1606,10 @@ protected:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) {
|
||||||
|
Send("onUpdateStatusChanged",updateStatus);
|
||||||
|
}
|
||||||
|
|
||||||
virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state)
|
virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state)
|
||||||
{
|
{
|
||||||
Lv2StateChangedBody message { (uint64_t)instanceId, state};
|
Lv2StateChangedBody message { (uint64_t)instanceId, state};
|
||||||
|
|||||||
+609
@@ -0,0 +1,609 @@
|
|||||||
|
// Copyright (c) 2024 Robin Davies
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
// subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
#include "Updater.hpp"
|
||||||
|
#include "json.hpp"
|
||||||
|
#include "config.hpp"
|
||||||
|
#include <sys/eventfd.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <chrono>
|
||||||
|
#include <poll.h>
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include "SysExec.hpp"
|
||||||
|
#include "json_variant.hpp"
|
||||||
|
#include "ss.hpp"
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
|
||||||
|
#define TEST_UPDATE
|
||||||
|
|
||||||
|
#ifndef DEBUG
|
||||||
|
#undef TEST_UPDATE // do NOT leat this leak into a production build!
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static constexpr uint64_t CLOSE_EVENT = 0;
|
||||||
|
static constexpr uint64_t CHECK_NOW_EVENT = 1;
|
||||||
|
static constexpr uint64_t UNCACHED_CHECK_NOW_EVENT = 2;
|
||||||
|
|
||||||
|
|
||||||
|
static std::filesystem::path WORKING_DIRECTORY = "/var/pipedal/updates";
|
||||||
|
static std::filesystem::path UPDATE_STATUS_CACHE_FILE = WORKING_DIRECTORY / "updateStatus.json";
|
||||||
|
|
||||||
|
static std::string GITHUB_RELEASES_URL = "https://api.github.com/repos/rerdavies/pipedal/releases";
|
||||||
|
|
||||||
|
Updater::clock::duration Updater::updateRate = std::chrono::duration_cast<Updater::clock::duration>(std::chrono::days(1));
|
||||||
|
static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast<std::chrono::system_clock::duration>(std::chrono::minutes(30));
|
||||||
|
|
||||||
|
std::mutex cacheMutex;
|
||||||
|
static UpdateStatus GetCachedUpdateStatus()
|
||||||
|
{
|
||||||
|
std::lock_guard lock{cacheMutex};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists(UPDATE_STATUS_CACHE_FILE))
|
||||||
|
{
|
||||||
|
std::ifstream f{UPDATE_STATUS_CACHE_FILE};
|
||||||
|
if (!f.is_open())
|
||||||
|
{
|
||||||
|
json_reader reader(f);
|
||||||
|
UpdateStatus status;
|
||||||
|
reader.read(&status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Unable to read cached UpdateStatus. " << e.what()));
|
||||||
|
}
|
||||||
|
return UpdateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetCachedUpdateStatus(UpdateStatus &updateStatus)
|
||||||
|
{
|
||||||
|
std::lock_guard lock{cacheMutex};
|
||||||
|
updateStatus.LastUpdateTime(std::chrono::system_clock::now());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::ofstream f{UPDATE_STATUS_CACHE_FILE};
|
||||||
|
json_writer writer{f};
|
||||||
|
writer.write(updateStatus);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Unable to write cached UpdateStatus. " << e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Updater::Updater()
|
||||||
|
{
|
||||||
|
cachedUpdateStatus = GetCachedUpdateStatus();
|
||||||
|
updatePolicy = cachedUpdateStatus.UpdatePolicy();
|
||||||
|
currentResult = cachedUpdateStatus;
|
||||||
|
|
||||||
|
int fds[2];
|
||||||
|
int rc = pipe(fds);
|
||||||
|
if (rc != 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Updater: cant create event pipe.");
|
||||||
|
}
|
||||||
|
this->event_reader = fds[0];
|
||||||
|
this->event_writer = fds[1];
|
||||||
|
|
||||||
|
this->thread = std::make_unique<std::thread>([this]()
|
||||||
|
{ ThreadProc(); });
|
||||||
|
CheckNow();
|
||||||
|
}
|
||||||
|
Updater::~Updater()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
void Updater::Stop()
|
||||||
|
{
|
||||||
|
if (stopped)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopped = true;
|
||||||
|
|
||||||
|
if (event_writer != -1)
|
||||||
|
{
|
||||||
|
uint64_t value = CLOSE_EVENT;
|
||||||
|
write(this->event_writer, &value, sizeof(uint64_t));
|
||||||
|
}
|
||||||
|
if (thread)
|
||||||
|
{
|
||||||
|
thread->join();
|
||||||
|
thread = nullptr;
|
||||||
|
}
|
||||||
|
if (event_reader != -1)
|
||||||
|
{
|
||||||
|
close(event_reader);
|
||||||
|
}
|
||||||
|
if (event_writer != -1)
|
||||||
|
{
|
||||||
|
close(event_writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::CheckNow()
|
||||||
|
{
|
||||||
|
uint64_t value = CHECK_NOW_EVENT;
|
||||||
|
write(this->event_writer, &value, sizeof(uint64_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::SetUpdateListener(UpdateListener &&listener)
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
this->listener = listener;
|
||||||
|
if (hasInfo)
|
||||||
|
{
|
||||||
|
listener(currentResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::ThreadProc()
|
||||||
|
{
|
||||||
|
struct pollfd pfd;
|
||||||
|
pfd.fd = this->event_reader;
|
||||||
|
pfd.events = POLLIN;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int ret = poll(&pfd, 1, std::chrono::duration_cast<std::chrono::milliseconds>(updateRate).count()); // 1000 ms timeout
|
||||||
|
|
||||||
|
if (ret == -1)
|
||||||
|
{
|
||||||
|
Lv2Log::error("Updater: Poll error.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (ret == 0)
|
||||||
|
{
|
||||||
|
CheckForUpdate(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Event occurred
|
||||||
|
uint64_t value;
|
||||||
|
ssize_t s = read(event_reader, &value, sizeof(uint64_t));
|
||||||
|
if (s == sizeof(uint64_t))
|
||||||
|
{
|
||||||
|
if (value == CHECK_NOW_EVENT)
|
||||||
|
{
|
||||||
|
CheckForUpdate(true);
|
||||||
|
} else if (value == UNCACHED_CHECK_NOW_EVENT)
|
||||||
|
{
|
||||||
|
CheckForUpdate(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GithubAsset
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GithubAsset(json_variant &v);
|
||||||
|
std::string name;
|
||||||
|
std::string browser_download_url;
|
||||||
|
std::string updated_at;
|
||||||
|
};
|
||||||
|
class GithubRelease
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GithubRelease(json_variant &v);
|
||||||
|
|
||||||
|
const GithubAsset *GetDownloadForCurrentArchitecture() const;
|
||||||
|
bool draft = true;
|
||||||
|
bool prerelease = true;
|
||||||
|
std::string name;
|
||||||
|
std::string url;
|
||||||
|
std::string version;
|
||||||
|
std::string body;
|
||||||
|
std::vector<GithubAsset> assets;
|
||||||
|
std::string published_at;
|
||||||
|
};
|
||||||
|
|
||||||
|
GithubAsset::GithubAsset(json_variant &v)
|
||||||
|
{
|
||||||
|
auto o = v.as_object();
|
||||||
|
this->name = o->at("name").as_string();
|
||||||
|
this->browser_download_url = o->at("browser_download_url").as_string();
|
||||||
|
this->updated_at = o->at("updated_at").as_string();
|
||||||
|
}
|
||||||
|
GithubRelease::GithubRelease(json_variant &v)
|
||||||
|
{
|
||||||
|
auto o = v.as_object();
|
||||||
|
this->name = o->at("name").as_string();
|
||||||
|
this->draft = o->at("draft").as_bool();
|
||||||
|
this->prerelease = o->at("prerelease").as_bool();
|
||||||
|
this->body = o->at("body").as_string();
|
||||||
|
|
||||||
|
auto assets = o->at("assets").as_array();
|
||||||
|
for (size_t i = 0; i < assets->size(); ++i)
|
||||||
|
{
|
||||||
|
auto &el = assets->at(i);
|
||||||
|
this->assets.push_back(GithubAsset(el));
|
||||||
|
}
|
||||||
|
this->published_at = o->at("published_at").as_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> split(const std::string &s, char delimiter)
|
||||||
|
{
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
std::string token;
|
||||||
|
std::istringstream tokenStream(s);
|
||||||
|
while (std::getline(tokenStream, token, delimiter))
|
||||||
|
{
|
||||||
|
tokens.push_back(token);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
static std::string justTheVersion(const std::string &assetName)
|
||||||
|
{
|
||||||
|
// eg. pipedal_1.2.41_arm64.deb
|
||||||
|
auto t = split(assetName, '_');
|
||||||
|
if (t.size() != 3)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Unable to parse version.");
|
||||||
|
}
|
||||||
|
return t[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareVersions(const std::string &l, const std::string &r)
|
||||||
|
{
|
||||||
|
std::stringstream sl(l);
|
||||||
|
std::stringstream sr(r);
|
||||||
|
|
||||||
|
int majorL = -1, majorR = 1, minorL = -1,
|
||||||
|
minorR = -1, buildL = -1, buildR = -1;
|
||||||
|
sl >> majorL;
|
||||||
|
sr >> majorR;
|
||||||
|
if (majorL != majorR)
|
||||||
|
{
|
||||||
|
return (majorL < majorR) ? -1 : 1;
|
||||||
|
}
|
||||||
|
char discard;
|
||||||
|
sl >> discard >> minorL;
|
||||||
|
sr >> discard >> minorR;
|
||||||
|
if (minorL != minorR)
|
||||||
|
{
|
||||||
|
return minorL < minorR ? -1 : 1;
|
||||||
|
}
|
||||||
|
sl >> discard >> buildL;
|
||||||
|
sr >> discard >> buildR;
|
||||||
|
|
||||||
|
if (buildL != buildR)
|
||||||
|
{
|
||||||
|
return buildL < buildR ? -1 : 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static std::string normalizeReleaseName(const std::string &releaseName)
|
||||||
|
{
|
||||||
|
// e.g. "PiPedal 1.2.34 Release" -> "PiPedal v1.2.34-Release"
|
||||||
|
if (releaseName.empty())
|
||||||
|
return "";
|
||||||
|
|
||||||
|
std::string result = releaseName;
|
||||||
|
|
||||||
|
auto nPos = result.find(' ');
|
||||||
|
if (nPos == std::string::npos)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
++nPos;
|
||||||
|
if (nPos >= result.length())
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
char c = releaseName[nPos];
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
{
|
||||||
|
result.insert(result.begin() + nPos, 'v');
|
||||||
|
}
|
||||||
|
nPos = result.find(' ', nPos);
|
||||||
|
if (nPos != std::string::npos)
|
||||||
|
{
|
||||||
|
result.at(nPos) = '-';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsCacheValid(const UpdateStatus &updateStatus)
|
||||||
|
{
|
||||||
|
if (!updateStatus.IsValid() || !updateStatus.IsOnline())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto validStart = updateStatus.LastUpdateTime();
|
||||||
|
auto validEnd = validStart + CACHE_DURATION;
|
||||||
|
return now >= validStart && now < validEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
UpdateRelease Updater::getUpdateRelease(
|
||||||
|
const std::vector<GithubRelease> &githubReleases,
|
||||||
|
const std::string ¤tVersion,
|
||||||
|
const UpdateReleasePredicate &predicate)
|
||||||
|
{
|
||||||
|
for (const auto &githubRelease : githubReleases)
|
||||||
|
{
|
||||||
|
auto *asset = githubRelease.GetDownloadForCurrentArchitecture();
|
||||||
|
if (!asset)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!predicate(githubRelease))
|
||||||
|
continue;
|
||||||
|
UpdateRelease updateRelease;
|
||||||
|
updateRelease.upgradeVersion_ = justTheVersion(asset->name);
|
||||||
|
updateRelease.updateAvailable_ = compareVersions(currentVersion, updateRelease.upgradeVersion_) < 0;
|
||||||
|
updateRelease.upgradeVersionDisplayName_ = normalizeReleaseName(githubRelease.name);
|
||||||
|
updateRelease.assetName_ = asset->name;
|
||||||
|
updateRelease.updateUrl_ = asset->browser_download_url;
|
||||||
|
return updateRelease;
|
||||||
|
}
|
||||||
|
return UpdateRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::CheckForUpdate(bool useCache)
|
||||||
|
{
|
||||||
|
UpdateStatus updateResult;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard lock {mutex};
|
||||||
|
if (useCache && IsCacheValid(cachedUpdateStatus))
|
||||||
|
{
|
||||||
|
this->currentResult = cachedUpdateStatus;
|
||||||
|
this->currentResult.UpdatePolicy(this->updatePolicy);
|
||||||
|
if (listener)
|
||||||
|
{
|
||||||
|
listener(this->currentResult);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateResult = this->currentResult;
|
||||||
|
}
|
||||||
|
std::string args = SS("-s " << GITHUB_RELEASES_URL);
|
||||||
|
|
||||||
|
|
||||||
|
updateResult.errorMessage_ = "";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto result = sysExecForOutput("curl", args);
|
||||||
|
if (result.exitCode != EXIT_SUCCESS)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Server has no internet access.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::stringstream ss(result.output);
|
||||||
|
json_reader reader(ss);
|
||||||
|
json_variant vResult(reader);
|
||||||
|
|
||||||
|
if (vResult.is_object())
|
||||||
|
{
|
||||||
|
// an HTML error.
|
||||||
|
updateResult.isOnline_ = false;
|
||||||
|
auto o = vResult.as_object();
|
||||||
|
std::string message = o->at("message").as_string();
|
||||||
|
auto status_code = o->at("status_code").as_int64();
|
||||||
|
throw std::runtime_error(SS("Service error. ()" << status_code << ": " << message << ")"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
json_variant::array_ptr vArray = vResult.as_array();
|
||||||
|
|
||||||
|
std::vector<GithubRelease> releases;
|
||||||
|
for (size_t i = 0; i < vArray->size(); ++i)
|
||||||
|
{
|
||||||
|
auto &el = vArray->at(0);
|
||||||
|
GithubRelease release{el};
|
||||||
|
if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr)
|
||||||
|
{
|
||||||
|
releases.push_back(std::move(release));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(
|
||||||
|
releases.begin(),
|
||||||
|
releases.end(),
|
||||||
|
[](const GithubRelease &left, const GithubRelease &right)
|
||||||
|
{
|
||||||
|
return left.published_at > right.published_at; // latest date first.
|
||||||
|
});
|
||||||
|
updateResult.releaseOnlyRelease_ = getUpdateRelease(
|
||||||
|
releases,
|
||||||
|
updateResult.currentVersion_,
|
||||||
|
[](const GithubRelease &githubRelease)
|
||||||
|
{
|
||||||
|
return !githubRelease.prerelease &&
|
||||||
|
githubRelease.name.find("Release") != std::string::npos;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateResult.releaseOrBetaRelease_ = getUpdateRelease(
|
||||||
|
releases,
|
||||||
|
updateResult.currentVersion_,
|
||||||
|
[](const GithubRelease &githubRelease)
|
||||||
|
{
|
||||||
|
return !githubRelease.prerelease &&
|
||||||
|
(githubRelease.name.find("Release") != std::string::npos ||
|
||||||
|
githubRelease.name.find("Beta") != std::string::npos);
|
||||||
|
});
|
||||||
|
updateResult.devRelease_ = getUpdateRelease(
|
||||||
|
releases,
|
||||||
|
updateResult.currentVersion_,
|
||||||
|
[](const GithubRelease &githubRelease)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
#ifdef TEST_UPDATE
|
||||||
|
updateResult.releaseOrBetaRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.41-Beta";
|
||||||
|
updateResult.devRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.39-Experimental";
|
||||||
|
updateResult.devRelease_.upgradeVersion_ = "1.2.39";
|
||||||
|
updateResult.devRelease_.updateAvailable_ = false;
|
||||||
|
#endif
|
||||||
|
updateResult.isValid_ = true;
|
||||||
|
updateResult.isOnline_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Failed to fetch update info. " << e.what()));
|
||||||
|
updateResult.errorMessage_ = e.what();
|
||||||
|
updateResult.isValid_ = false;
|
||||||
|
updateResult.isOnline_ = false;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
updateResult.UpdatePolicy(this->updatePolicy);
|
||||||
|
this->currentResult = updateResult;
|
||||||
|
SetCachedUpdateStatus(this->currentResult);
|
||||||
|
if (listener)
|
||||||
|
{
|
||||||
|
listener(this->currentResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool UpdateRelease::operator==(const UpdateRelease &other) const
|
||||||
|
{
|
||||||
|
return (updateAvailable_ == other.updateAvailable_) &&
|
||||||
|
(upgradeVersion_ == other.upgradeVersion_) &&
|
||||||
|
(upgradeVersionDisplayName_ == other.upgradeVersionDisplayName_) &&
|
||||||
|
(assetName_ == other.assetName_) &&
|
||||||
|
(updateUrl_ == other.updateUrl_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateStatus::operator==(const UpdateStatus &other) const
|
||||||
|
{
|
||||||
|
return (lastUpdateTime_ == other.lastUpdateTime_) &&
|
||||||
|
(isValid_ == other.isValid_) &&
|
||||||
|
(errorMessage_ == other.errorMessage_) &&
|
||||||
|
(isOnline_ == other.isOnline_) &&
|
||||||
|
(currentVersion_ == other.currentVersion_) &&
|
||||||
|
(currentVersionDisplayName_ == other.currentVersionDisplayName_) &&
|
||||||
|
(updatePolicy_ == other.updatePolicy_) &&
|
||||||
|
(releaseOnlyRelease_ == other.releaseOnlyRelease_) &&
|
||||||
|
(releaseOrBetaRelease_ == other.releaseOrBetaRelease_) &&
|
||||||
|
(devRelease_ == other.devRelease_);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePolicyT Updater::GetUpdatePolicy()
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
return updatePolicy;
|
||||||
|
}
|
||||||
|
void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy)
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
if (updatePolicy == this->updatePolicy)
|
||||||
|
return;
|
||||||
|
this->updatePolicy = updatePolicy;
|
||||||
|
if (this->currentResult.UpdatePolicy() != updatePolicy)
|
||||||
|
{
|
||||||
|
this->currentResult.UpdatePolicy(updatePolicy);
|
||||||
|
SetCachedUpdateStatus(this->currentResult);
|
||||||
|
if (listener)
|
||||||
|
{
|
||||||
|
listener(currentResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Updater::ForceUpdateCheck()
|
||||||
|
{
|
||||||
|
uint64_t value = UNCACHED_CHECK_NOW_EVENT;
|
||||||
|
write(this->event_writer, &value, sizeof(uint64_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateStatus::UpdateStatus()
|
||||||
|
{
|
||||||
|
currentVersion_ = PROJECT_VER;
|
||||||
|
currentVersionDisplayName_ = PROJECT_DISPLAY_VERSION;
|
||||||
|
|
||||||
|
#ifdef TEST_UPDATE
|
||||||
|
// uncomment this line to test upgrading.
|
||||||
|
currentVersion_ = "1.2.39";
|
||||||
|
currentVersionDisplayName_ = "PiPedal 1.2.39-Debug";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point UpdateStatus::LastUpdateTime() const
|
||||||
|
{
|
||||||
|
std::chrono::system_clock::duration duration{this->lastUpdateTime_};
|
||||||
|
std::chrono::system_clock::time_point tp{duration};
|
||||||
|
return tp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateStatus::LastUpdateTime(const std::chrono::system_clock::time_point &timePoint)
|
||||||
|
{
|
||||||
|
this->lastUpdateTime_ = timePoint.time_since_epoch().count();
|
||||||
|
}
|
||||||
|
|
||||||
|
const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const
|
||||||
|
{
|
||||||
|
// deb package names end in {DEBIAN_ARCHITECTURE}.deb
|
||||||
|
// pipedal build gets this value from `dpkg --print-architecture`
|
||||||
|
#ifndef DEBIAN_ARCHITECTURE // deb package names end in {DEBIAN_ARCHITECTURE}.deb
|
||||||
|
#error DEBIAN_ARCHITECTURE not defined
|
||||||
|
#endif
|
||||||
|
std::string downloadEnding = SS("_" << (DEBIAN_ARCHITECTURE) << ".deb");
|
||||||
|
|
||||||
|
for (auto &asset : assets)
|
||||||
|
{
|
||||||
|
if (asset.name.ends_with(downloadEnding))
|
||||||
|
{
|
||||||
|
return &asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRelease::UpdateRelease()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(UpdateRelease)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, updateAvailable)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersion)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersionDisplayName)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, assetName)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, updateUrl)
|
||||||
|
JSON_MAP_END();
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(UpdateStatus)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, lastUpdateTime)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, isValid)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, errorMessage)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, isOnline)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, currentVersion)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, currentVersionDisplayName)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, updatePolicy)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, releaseOnlyRelease)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, releaseOrBetaRelease)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, devRelease)
|
||||||
|
JSON_MAP_END();
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) 2024 Robin Davies
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
// the Software without restriction, including without limitation the rights to
|
||||||
|
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
// subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include "json.hpp"
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
class GithubRelease;
|
||||||
|
|
||||||
|
namespace pipedal
|
||||||
|
{
|
||||||
|
|
||||||
|
class Updater;
|
||||||
|
|
||||||
|
enum class UpdatePolicyT
|
||||||
|
{
|
||||||
|
// ordinal values must not change, as files depend on them.
|
||||||
|
// must match declaration in Updater.tsx
|
||||||
|
|
||||||
|
ReleaseOnly = 0,
|
||||||
|
ReleaseOrBeta = 1,
|
||||||
|
Development = 2,
|
||||||
|
};
|
||||||
|
class UpdateRelease
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
friend class Updater;
|
||||||
|
bool updateAvailable_ = false;
|
||||||
|
std::string upgradeVersion_; // just the version.
|
||||||
|
std::string upgradeVersionDisplayName_; // display name for the version.
|
||||||
|
std::string assetName_; // filename only
|
||||||
|
std::string updateUrl_; // url from which to download the .deb file.
|
||||||
|
public:
|
||||||
|
UpdateRelease();
|
||||||
|
|
||||||
|
bool UpdateAvailable() const { return updateAvailable_; }
|
||||||
|
const std::string &UpgradeVersion() const { return upgradeVersion_; }
|
||||||
|
const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; }
|
||||||
|
const std::string &AssetName() const { return assetName_; }
|
||||||
|
const std::string &UpdateUrl() const { return updateUrl_; }
|
||||||
|
bool operator==(const UpdateRelease &other) const;
|
||||||
|
DECLARE_JSON_MAP(UpdateRelease);
|
||||||
|
};
|
||||||
|
|
||||||
|
class UpdateStatus
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
friend class Updater;
|
||||||
|
std::chrono::system_clock::time_point::rep lastUpdateTime_ = 0;
|
||||||
|
|
||||||
|
bool isValid_ = false;
|
||||||
|
std::string errorMessage_;
|
||||||
|
bool isOnline_ = false;
|
||||||
|
std::string currentVersion_;
|
||||||
|
std::string currentVersionDisplayName_;
|
||||||
|
|
||||||
|
int32_t updatePolicy_ = (int32_t)(UpdatePolicyT::ReleaseOrBeta);
|
||||||
|
UpdateRelease releaseOnlyRelease_;
|
||||||
|
UpdateRelease releaseOrBetaRelease_;
|
||||||
|
UpdateRelease devRelease_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
UpdateStatus();
|
||||||
|
std::chrono::system_clock::time_point LastUpdateTime() const;
|
||||||
|
void LastUpdateTime(const std::chrono::system_clock::time_point &timePoint);
|
||||||
|
|
||||||
|
bool IsValid() const { return isValid_; }
|
||||||
|
const std::string &ErrorMessage() const { return errorMessage_; }
|
||||||
|
bool IsOnline() const { return isOnline_; }
|
||||||
|
const std::string &CurrentVersion() const { return currentVersion_; }
|
||||||
|
const std::string &CurrentDisplayVersion() const { return currentVersionDisplayName_; }
|
||||||
|
UpdatePolicyT UpdatePolicy() const { return (UpdatePolicyT)updatePolicy_; }
|
||||||
|
void UpdatePolicy(UpdatePolicyT updatePreference) { this->updatePolicy_ = (int32_t)updatePreference; }
|
||||||
|
|
||||||
|
const UpdateRelease &ReleaseOnlyRelease() const { return releaseOnlyRelease_; }
|
||||||
|
const UpdateRelease &ReleaseOrBetaRelease() const { return releaseOrBetaRelease_; }
|
||||||
|
const UpdateRelease &DevRelease() const { return devRelease_; }
|
||||||
|
bool operator==(const UpdateStatus &other) const;
|
||||||
|
|
||||||
|
DECLARE_JSON_MAP(UpdateStatus);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Updater
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Updater();
|
||||||
|
~Updater();
|
||||||
|
using UpdateListener = std::function<void(const UpdateStatus &upateResult)>;
|
||||||
|
|
||||||
|
void SetUpdateListener(UpdateListener &&listener);
|
||||||
|
void CheckNow();
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
UpdatePolicyT GetUpdatePolicy();
|
||||||
|
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
|
||||||
|
void ForceUpdateCheck();
|
||||||
|
private:
|
||||||
|
UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta;
|
||||||
|
using UpdateReleasePredicate = std::function<bool(const GithubRelease &githubRelease)>;
|
||||||
|
|
||||||
|
UpdateRelease getUpdateRelease(
|
||||||
|
const std::vector<GithubRelease> &githubReleases,
|
||||||
|
const std::string ¤tVersion,
|
||||||
|
const UpdateReleasePredicate &predicate);
|
||||||
|
|
||||||
|
UpdateStatus cachedUpdateStatus;
|
||||||
|
bool stopped = false;
|
||||||
|
using clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
int event_reader = -1;
|
||||||
|
int event_writer = -1;
|
||||||
|
void ThreadProc();
|
||||||
|
void CheckForUpdate(bool useCache);
|
||||||
|
UpdateListener listener;
|
||||||
|
|
||||||
|
std::unique_ptr<std::thread> thread;
|
||||||
|
std::mutex mutex;
|
||||||
|
|
||||||
|
bool hasInfo = false;
|
||||||
|
UpdateStatus currentResult;
|
||||||
|
static clock::duration updateRate;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "pch.h"
|
||||||
|
#include "catch.hpp"
|
||||||
|
#include "Updater.hpp"
|
||||||
|
#include "json.hpp"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
|
||||||
|
TEST_CASE( "updater test", "[updater]" ) {
|
||||||
|
int nCalls = 0;
|
||||||
|
cout << "------ upater test ----" << endl;
|
||||||
|
{ Updater updater;
|
||||||
|
|
||||||
|
|
||||||
|
updater.SetUpdateListener(
|
||||||
|
[&nCalls](const UpdateStatus&updateStatus) mutable
|
||||||
|
{
|
||||||
|
|
||||||
|
cout << "updateStatus:" << endl;
|
||||||
|
json_writer writer(cout,false);
|
||||||
|
writer.write(updateStatus);
|
||||||
|
cout << endl;
|
||||||
|
++nCalls;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
REQUIRE(nCalls == 1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+30
-1
@@ -158,11 +158,40 @@ TEST_CASE("json smart ptrs", "[json_smart_ptrs][Build][Dev]")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename U>
|
||||||
|
U & VariantAs(json_variant& v)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Missing specialization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline json_null &VariantAs<json_null>(json_variant& v) { return v.as_null(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline bool &VariantAs<bool>(json_variant& v) { return v.as_bool(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline double &VariantAs<double>(json_variant& v) { return v.as_number(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::string &VariantAs<std::string>(json_variant& v) { return v.as_string(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::shared_ptr<json_object> &VariantAs<std::shared_ptr<json_object>>(json_variant& v) { return v.as_object(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::shared_ptr<json_array> &VariantAs<std::shared_ptr<json_array>>(json_variant& v) { return v.as_array(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline json_variant &VariantAs<json_variant>(json_variant& v) { return v; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void TestVariantRoundTrip(const T &value)
|
void TestVariantRoundTrip(const T &value)
|
||||||
{
|
{
|
||||||
json_variant variant(value);
|
json_variant variant(value);
|
||||||
T out = variant.as<T>();
|
T &out = VariantAs<T>(variant);
|
||||||
REQUIRE(out == value);
|
REQUIRE(out == value);
|
||||||
|
|
||||||
std::string output;
|
std::string output;
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
#include <Uri.hpp>
|
#include <Uri.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
using namespace piddle;
|
using namespace pipedal;
|
||||||
|
|
||||||
TEST_CASE( "uri test", "[uri]" ) {
|
TEST_CASE( "uri test", "[uri]" ) {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user