diff --git a/CMakeLists.txt b/CMakeLists.txt index d053763..86c47f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,9 +48,10 @@ install (FILES ${PROJECT_SOURCE_DIR}/build/src/notices.txt DESTINATION /etc/pipedal/react/var/) install (FILES ${PROJECT_SOURCE_DIR}/src/template.service - ${PROJECT_SOURCE_DIR}/src/templateShutdown.service + ${PROJECT_SOURCE_DIR}/src/templateAdmin.service ${PROJECT_SOURCE_DIR}/src/templateJack.service - DESTINATION /etc/pipedal ) + ${PROJECT_SOURCE_DIR}/src/defaultFavorites.json + DESTINATION /etc/pipedal/config ) install (DIRECTORY ${LV2_SOURCE_DIRECTORY} DESTINATION /usr/lib/lv2 diff --git a/react/public/var/config.json b/react/public/var/config.json index 2d62624..0e07f34 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 80, + "socket_server_port": 8080, "socket_server_address": "*", "debug": true, "max_upload_size": 1048576, diff --git a/react/src/LoadPluginDialog.tsx b/react/src/LoadPluginDialog.tsx index 0234d89..d4730f1 100644 --- a/react/src/LoadPluginDialog.tsx +++ b/react/src/LoadPluginDialog.tsx @@ -19,7 +19,7 @@ import React, { ReactNode, SyntheticEvent, CSSProperties } from 'react'; -import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import { PiPedalModel, PiPedalModelFactory, FavoritesList } from './PiPedalModel'; import { UiPlugin, PluginType } from './Lv2Plugin'; import ButtonBase from '@mui/material/ButtonBase'; import Button from '@mui/material/Button'; @@ -42,10 +42,10 @@ import SearchControl from './SearchControl'; import SearchFilter from './SearchFilter'; import { FixedSizeGrid } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; - -import Slide, {SlideProps} from '@mui/material/Slide'; +import StarBorderIcon from '@mui/icons-material/StarBorder'; +import Slide, { SlideProps } from '@mui/material/Slide'; import { createStyles, Theme } from '@mui/material/styles'; -import { WithStyles, withStyles} from '@mui/styles'; +import { WithStyles, withStyles } from '@mui/styles'; export type CloseEventHandler = () => void; @@ -119,6 +119,9 @@ const pluginGridStyles = (theme: Theme) => createStyles({ paddingLeft: 16, whiteSpace: "nowrap" }, + favoriteDecoration: { + flex: "0 0 auto" + }, table: { borderCollapse: "collapse", }, @@ -167,6 +170,7 @@ type PluginGridState = { grid_cell_width: number, grid_cell_columns: number, minimumItemWidth: number, + favoritesList: FavoritesList } @@ -203,7 +207,8 @@ export const LoadPluginDialog = client_height: window.innerHeight, grid_cell_width: this.getCellWidth(window.innerWidth), grid_cell_columns: this.getCellColumns(window.innerWidth), - minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220 + minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220, + favoritesList: this.model.favorites.get() }; this.updateWindowSize = this.updateWindowSize.bind(this); @@ -211,6 +216,7 @@ export const LoadPluginDialog = this.handleOk = this.handleOk.bind(this); this.handleSearchStringReady = this.handleSearchStringReady.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); + this.handleFavoritesChanged = this.handleFavoritesChanged.bind(this); } nominal_column_width: number = 250; @@ -264,12 +270,23 @@ export const LoadPluginDialog = grid_cell_columns: this.getCellColumns(window.innerWidth) }); } + + handleFavoritesChanged() { + this.setState( + { + favoritesList: this.model.favorites.get() + }); + } componentDidMount() { super.componentDidMount(); this.updateWindowSize(); window.addEventListener('resize', this.updateWindowSize); + this.model.favorites.addOnChangedHandler(this.handleFavoritesChanged); + this.setState({ favoritesList: this.model.favorites.get() }); + } componentWillUnmount() { + this.model.favorites.removeOnChangedHandler(this.handleFavoritesChanged); super.componentWillUnmount(); window.removeEventListener('resize', this.updateWindowSize); } @@ -352,18 +369,17 @@ export const LoadPluginDialog = let isDoubleClick = e.detail === 2; // we have to synthesize double clicks because // DOM rewrites interfere with natural double click. - if (isDoubleClick) - { + if (isDoubleClick) { this.props.onOk(uri); } } handleMouseEnter(e: SyntheticEvent, uri: string): void { - // this.setHoverUri(uri); + // this.setHoverUri(uri); } handleMouseLeave(e: SyntheticEvent, uri: string): void { - // this.setHoverUri(""); + // this.setHoverUri(""); } onInfoClicked(): void { @@ -421,6 +437,9 @@ export const LoadPluginDialog = let score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name); if (score !== 0) { + if (this.state.favoritesList[plugin.uri]) { + score += 32768; + } results.push({ score: score, plugin: plugin }); } } @@ -469,6 +488,7 @@ export const LoadPluginDialog = let value = this.gridItems[item]; let classes = this.props.classes; + let isFavorite: boolean = this.state.favoritesList[value.uri] ?? false; return (
{ this.onDoubleClick(e, value.uri) }} @@ -491,12 +511,19 @@ export const LoadPluginDialog =
+
+ +
); } + setFavorite(pluginUri: string | undefined, isFavorite: boolean): void { + if (!pluginUri) return; + this.model.setFavorite(pluginUri, isFavorite); + } gridItems: UiPlugin[] = []; gridColumnCount: number = 1; @@ -516,6 +543,7 @@ export const LoadPluginDialog = this.gridItems = this.getFilteredPlugins(); let gridColumnCount = this.state.grid_cell_columns; this.gridColumnCount = gridColumnCount; + let isFavorite = this.state.favoritesList[this.state.selected_uri ?? ""]; return (
-
+
{ this.cancel(); }} style={{ flex: "0 0 auto" }} > @@ -567,7 +595,7 @@ export const LoadPluginDialog = key={this.state.filterType} onChange={(e) => { this.onFilterChange(e); }} style={{ flex: "0 0 160px" }} - > + > {this.createFilterOptions()}
@@ -625,6 +653,16 @@ export const LoadPluginDialog = {this.info_string(selectedPlugin)} +
+ { this.setFavorite(this.state.selected_uri, !isFavorite); }} + > + + +
@@ -640,6 +678,16 @@ export const LoadPluginDialog = {this.info_string(selectedPlugin)} +
+ { this.setFavorite(this.state.selected_uri, !isFavorite); }} + > + + +
diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 7462950..1e8648e 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -299,6 +299,9 @@ interface ControlChangedBody { value: number; }; +export interface FavoritesList { + [url: string]: boolean; +} export interface PiPedalModel { clientId: number; @@ -319,6 +322,7 @@ export interface PiPedalModel { jackServerSettings: ObservableProperty; wifiConfigSettings: ObservableProperty; governorSettings: ObservableProperty; + favorites: ObservableProperty; presets: ObservableProperty; @@ -422,6 +426,8 @@ export interface PiPedalModel { zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void; clearZoomedControl(): void; + + setFavorite(pluginUrl: string, isFavorite: boolean): void; }; class PiPedalModelImpl implements PiPedalModel { @@ -453,6 +459,8 @@ class PiPedalModelImpl implements PiPedalModel { wifiConfigSettings: ObservableProperty = new ObservableProperty(new WifiConfigSettings()); governorSettings: ObservableProperty = new ObservableProperty(new GovernorSettings()); + favorites: ObservableProperty = new ObservableProperty({}); + presets: ObservableProperty = new ObservableProperty ( @@ -496,6 +504,25 @@ class PiPedalModelImpl implements PiPedalModel { if (this.visibilityState.get() === VisibilityState.Hidden) return; this.onError(errorMessage); } + + compareFavorites(left: FavoritesList, right: FavoritesList): boolean + { + for (let key of Object.keys(left)) + { + if (!right[key]) { + return false; + } + } + + for (let key of Object.keys(right)) + { + if (!left[key]) { + return false; + } + } + return true; + + } onSocketMessage(header: PiPedalMessageHeader, body?: any) { if (this.visibilityState.get() === VisibilityState.Hidden) return; @@ -512,7 +539,13 @@ class PiPedalModelImpl implements PiPedalModel { if (header.replyTo) { this.webSocket?.reply(header.replyTo, "onMonitorPortOutput", true); } - + } else if (message === "onFavoritesChanged") + { + let favorites = body as FavoritesList; + if (!this.compareFavorites(favorites,this.favorites.get())) + { + this.favorites.set(favorites); + } } else if (message === "onChannelSelectionChanged") { let channelSelectionBody = body as ChannelSelectionChangedBody; let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection); @@ -705,6 +738,10 @@ class PiPedalModelImpl implements PiPedalModel { .then((data) => { this.banks.set(new BankIndex().deserialize(data)); + return this.getWebSocket().request("getFavorites"); + }) + .then((data)=> { + this.favorites.set(data); this.setState(State.Ready); }) @@ -839,6 +876,17 @@ class PiPedalModelImpl implements PiPedalModel { }) .then((data) => { this.banks.set(new BankIndex().deserialize(data)); + if (this.webSocket) + { + // MUST not allow reconnect until at least one complete load has finished. + this.webSocket.canReconnect = true; + } + + return this.getWebSocket().request("getFavorites"); + }) + .then((data)=> { + this.favorites.set(data); + if (this.webSocket) { // MUST not allow reconnect until at least one complete load has finished. @@ -1891,6 +1939,29 @@ class PiPedalModelImpl implements PiPedalModel { this.zoomedUiControl.set(undefined); } + setFavorite(pluginUrl: string, isFavorite: boolean): void + { + let favorites = this.favorites.get(); + let newFavorites: FavoritesList = {}; + Object.assign(newFavorites,favorites); + if (isFavorite) + { + newFavorites[pluginUrl] = true; + } else { + if (newFavorites[pluginUrl]) + { + delete newFavorites[pluginUrl]; + } + } + this.favorites.set(newFavorites); + if (this.webSocket) + { + this.webSocket.send("setFavorites",newFavorites); + } + // stub: update server. + } + + preloadImages(imageList: string): void { let imageNames = imageList.split(';'); for (let i = 0; i < imageNames.length; ++i) { diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 55001ed..24a187e 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -378,7 +378,7 @@ void InstallJackService() // deploy the systemd service file std::map map; // nothing to customize. - WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/templateJack.service"), GetServiceFileName(JACK_SERVICE)); + WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/config/templateJack.service"), GetServiceFileName(JACK_SERVICE)); MaybeStartJackService(); } @@ -554,7 +554,7 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo map["COMMAND"] = s.str(); } - WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/template.service"), GetServiceFileName(NATIVE_SERVICE)); + WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/config/template.service"), GetServiceFileName(NATIVE_SERVICE)); map["DESCRIPTION"] = "PiPedal Shutdown Service"; { @@ -566,7 +566,7 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo map["COMMAND"] = s.str(); } - WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/templateShutdown.service"), GetServiceFileName(ADMIN_SERVICE)); + WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/config/templateAdmin.service"), GetServiceFileName(ADMIN_SERVICE)); sysExec(SYSTEMCTL_BIN " daemon-reload"); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index c7751e1..cca6764 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -111,7 +111,8 @@ PiPedalModel::~PiPedalModel() void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration) { - storage.SetDataRoot(configuration.GetLocalStoragePath().c_str()); + storage.SetConfigRoot(configuration.GetDocRoot()); + storage.SetDataRoot(configuration.GetLocalStoragePath()); storage.Initialize(); // Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory. @@ -1316,3 +1317,28 @@ const std::filesystem::path& PiPedalModel::GetWebRoot() const { return webRoot; } + +std::map PiPedalModel::GetFavorites() const +{ + std::lock_guard guard(const_cast(mutex)); + + return storage.GetFavorites(); +} +void PiPedalModel::SetFavorites(const std::map &favorites) +{ + std::lock_guard guard(mutex); + storage.SetFavorites(favorites); + + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + std::vector t; + t.reserve(this->subscribers.size()); + + for (size_t i = 0; i < subscribers.size(); ++i) + { + t.push_back(this->subscribers[i]); + } + for (size_t i = 0; i < t.size(); ++i) + { + t[i]->OnFavoritesChanged(favorites); + } +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 79aa3ba..da71c2d 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -59,6 +59,7 @@ public: virtual void OnNotifyAtomOutput(int64_t clientModel,uint64_t instanceId,const std::string&atomType, const std::string&atomJson) = 0; virtual void OnWifiConfigSettingsChanged(const WifiConfigSettings&wifiConfigSettings) = 0; virtual void OnGovernorSettingsChanged(const std::string &governor) = 0; + virtual void OnFavoritesChanged(const std::map &favorites) = 0; virtual void Close() = 0; }; @@ -235,6 +236,9 @@ public: std::vector GetAlsaDevices(); const std::filesystem::path& GetWebRoot() const; + + std::map GetFavorites() const; + void SetFavorites(const std::map &favorites); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 2624d5a..40a8553 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1098,7 +1098,7 @@ public: [this, subscriptionHandle_](const bool &result) { this->portMonitorAck(subscriptionHandle_); // please can we have some more. - // (....complexity not worth the effort. :-( ) + }, [](const std::exception &e) { @@ -1169,6 +1169,16 @@ public: { this->Reply(replyTo, "imageList", imageList); + } else if (message == "getFavorites") + { + std::map favorites = this->model.GetFavorites(); + this->Reply(replyTo,"getFavorites",favorites); + + } else if (message == "setFavorites") + { + std::map favorites; + pReader->read(&favorites); + this->model.SetFavorites(favorites); } else { @@ -1243,6 +1253,10 @@ protected: } public: + virtual void OnFavoritesChanged(const std::map &favorites) + { + Send("onFavoritesChanged",favorites); + } virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) { ChannelSelectionChangedBody body; diff --git a/src/Storage.cpp b/src/Storage.cpp index 203a33f..fb6afe4 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -36,6 +36,7 @@ const char *BANKS_FILENAME = "index.banks"; Storage::Storage() { + SetConfigRoot("~/var/Config"); SetDataRoot("~/var/PiPedal"); } @@ -127,7 +128,7 @@ std::string Storage::SafeEncodeName(const std::string &name) return s.str(); } -std::filesystem::path ResolveHomePath(std::filesystem::path path) +std::filesystem::path ResolveHomePath(const std::filesystem::path& path) { if (path.begin() == path.end()) return path; @@ -156,7 +157,12 @@ std::filesystem::path ResolveHomePath(std::filesystem::path path) } return result; } -void Storage::SetDataRoot(const char *path) + +void Storage::SetConfigRoot(const std::filesystem::path& path) +{ + this->configRoot = ResolveHomePath(path); +} +void Storage::SetDataRoot(const std::filesystem::path& path) { this->dataRoot = ResolveHomePath(path); } @@ -1186,6 +1192,37 @@ uint64_t Storage::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId } +std::map Storage::GetFavorites() const +{ + std::map result; + + std::filesystem::path fileName = this->dataRoot / "favorites.json"; + if (!std::filesystem::exists(fileName)) + { + fileName = this->configRoot / "defaultFavorites.json"; + } + std::ifstream f; + f.open(fileName); + if (f.is_open()) + { + json_reader reader(f); + reader.read(&result); + } + return result; + + +} +void Storage::SetFavorites(const std::map&favorites) { + std::filesystem::path fileName = this->dataRoot / "favorites.json"; + std::ofstream f; + f.open(fileName); + if (f.is_open()) + { + json_writer writer(f); + writer.write(favorites); + } +} + JSON_MAP_BEGIN(CurrentPreset) JSON_MAP_REFERENCE(CurrentPreset,modified) diff --git a/src/Storage.hpp b/src/Storage.hpp index e638a87..c77728c 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -26,6 +26,7 @@ #include "Banks.hpp" #include "JackConfiguration.hpp" #include "WifiConfigSettings.hpp" +#include namespace pipedal { @@ -44,6 +45,7 @@ public: class Storage { private: std::filesystem::path dataRoot; + std::filesystem::path configRoot; BankIndex bankIndex; BankFile currentBank; PluginPresetIndex pluginPresetIndex; @@ -78,7 +80,8 @@ public: void Initialize(); void CreateBank(const std::string & name); - void SetDataRoot(const char*path); + void SetDataRoot(const std::filesystem::path& path); + void SetConfigRoot(const std::filesystem::path& path); std::vector GetPedalBoards(); @@ -140,6 +143,9 @@ public: void UpdatePluginPresets(const PluginUiPresets &pluginPresets); uint64_t CopyPluginPreset(const std::string&pluginUri,uint64_t presetId); + std::map GetFavorites() const; + void SetFavorites(const std::map&favorites); + }; diff --git a/src/defaultFavorites.json b/src/defaultFavorites.json new file mode 100644 index 0000000..c70b0ef --- /dev/null +++ b/src/defaultFavorites.json @@ -0,0 +1,12 @@ +{ + "http://two-play.com/plugins/toob-cab-sim": true, + "http://two-play.com/plugins/toob-chorus": true, + "http://two-play.com/plugins/toob-delay": true, + "http://two-play.com/plugins/toob-freeverb": true, + "http://two-play.com/plugins/toob-input_stage": true, + "http://two-play.com/plugins/toob-ml": true, + "http://two-play.com/plugins/toob-power-stage-2": true, + "http://two-play.com/plugins/toob-spectrum": true, + "http://two-play.com/plugins/toob-tone-stack": true, + "http://two-play.com/plugins/toob-tuner": true +} \ No newline at end of file diff --git a/src/templateShutdown.service b/src/templateAdmin.service similarity index 100% rename from src/templateShutdown.service rename to src/templateAdmin.service