diff options
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings')
9 files changed, 1318 insertions, 0 deletions
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/aerospace-display-manager.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/aerospace-display-manager.jsx new file mode 100755 index 0000000..8c4fa98 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/aerospace-display-manager.jsx | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Utils from "../../utils"; | ||
| 3 | import * as Icons from "../icons/icons.jsx"; | ||
| 4 | |||
| 5 | const { React } = Uebersicht; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * UserWidgetsCreator component allows users to create and manage custom widgets. | ||
| 9 | * @param {Object} props - The component props. | ||
| 10 | * @param {Object} props.defaultValue - The default value for the widgets. | ||
| 11 | * @param {Function} props.onChange - The function to call when the widgets change. | ||
| 12 | * @returns {JSX.Element} The UserWidgetsCreator component. | ||
| 13 | */ | ||
| 14 | export default function AerospaceDisplayManager({ defaultValue, onChange }) { | ||
| 15 | const [indexes, setIndexes] = React.useState(defaultValue || {}); | ||
| 16 | const [displays, setDisplays] = React.useState(Object.keys(defaultValue)); | ||
| 17 | |||
| 18 | const addDisplay = () => { | ||
| 19 | setDisplays((current) => { | ||
| 20 | const highest = Math.max(...current, 0); | ||
| 21 | const newIndex = highest + 1; | ||
| 22 | setIndexes((current) => { | ||
| 23 | return { ...current, [newIndex]: Number(current[newIndex]) || 1 }; | ||
| 24 | }); | ||
| 25 | return [...current, newIndex]; | ||
| 26 | }); | ||
| 27 | }; | ||
| 28 | |||
| 29 | const removeDisplay = (index) => { | ||
| 30 | setDisplays((current) => { | ||
| 31 | const newDisplays = current.filter((display) => display !== index); | ||
| 32 | setIndexes((current) => { | ||
| 33 | const newIndexes = { ...current }; | ||
| 34 | delete newIndexes[index]; | ||
| 35 | return newIndexes; | ||
| 36 | }); | ||
| 37 | return newDisplays; | ||
| 38 | }); | ||
| 39 | }; | ||
| 40 | |||
| 41 | React.useEffect(() => { | ||
| 42 | const diffs = Utils.compareObjects(defaultValue, indexes); | ||
| 43 | const hasDiffs = Object.keys(diffs).length > 0; | ||
| 44 | if (hasDiffs) onChange({ target: { value: indexes } }); | ||
| 45 | }, [defaultValue, onChange, indexes]); | ||
| 46 | |||
| 47 | return ( | ||
| 48 | <div className="aerospace-display-manager"> | ||
| 49 | <div className="aerospace-display-manager__label"> | ||
| 50 | <em>AeroSpace</em> Custom display indexes | ||
| 51 | </div> | ||
| 52 | <div className="aerospace-display-manager__displays"> | ||
| 53 | {displays.map((display) => { | ||
| 54 | const index = indexes[display] || 1; | ||
| 55 | |||
| 56 | const updateIndex = (e) => { | ||
| 57 | const value = e.target.value; | ||
| 58 | setIndexes((current) => { | ||
| 59 | return { ...current, [display]: Number(value) }; | ||
| 60 | }); | ||
| 61 | }; | ||
| 62 | |||
| 63 | return ( | ||
| 64 | <div key={display} className="aerospace-display-manager__display"> | ||
| 65 | <span>{display}</span>: | ||
| 66 | <input type="number" value={index} onChange={updateIndex} /> | ||
| 67 | <button | ||
| 68 | className="aerospace-display-manager__remove-display" | ||
| 69 | onClick={() => removeDisplay(display)} | ||
| 70 | > | ||
| 71 | <Icons.Close /> | ||
| 72 | </button> | ||
| 73 | </div> | ||
| 74 | ); | ||
| 75 | })} | ||
| 76 | </div> | ||
| 77 | <button className="aerospace-display-manager__add" onClick={addDisplay}> | ||
| 78 | <Icons.Add /> | ||
| 79 | Add a display | ||
| 80 | </button> | ||
| 81 | </div> | ||
| 82 | ); | ||
| 83 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/color-picker.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/color-picker.jsx new file mode 100755 index 0000000..dbff879 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/color-picker.jsx | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Settings from "../../settings"; | ||
| 3 | |||
| 4 | const { React } = Uebersicht; | ||
| 5 | |||
| 6 | /** | ||
| 7 | * ColorPicker component allows users to select a color from predefined options or input a custom color. | ||
| 8 | * @param {Object} props - The component props. | ||
| 9 | * @param {Function} props.callback - The callback function to handle color selection. | ||
| 10 | * @param {number} props.index - The index of the color picker. | ||
| 11 | * @param {string} props.selectedColor - The initially selected color. | ||
| 12 | * @returns {JSX.Element} The ColorPicker component. | ||
| 13 | */ | ||
| 14 | export default function ColorPicker({ callback, index, selectedColor }) { | ||
| 15 | // Determine if the selected color is a custom color | ||
| 16 | const isSelectedCustom = !Settings.userWidgetColors.includes(selectedColor); | ||
| 17 | // State to manage the open/close status of the color picker | ||
| 18 | const [open, setOpen] = React.useState(false); | ||
| 19 | // State to manage the currently selected color | ||
| 20 | const [selected, setSelected] = React.useState(selectedColor); | ||
| 21 | // State to manage the custom color input | ||
| 22 | const [customColor, setCustomColor] = React.useState( | ||
| 23 | isSelectedCustom ? selectedColor : undefined, | ||
| 24 | ); | ||
| 25 | |||
| 26 | // Toggle the color picker open/close status | ||
| 27 | const onClick = () => setOpen(!open); | ||
| 28 | |||
| 29 | /** | ||
| 30 | * Handle custom color input change. | ||
| 31 | * @param {Object} e - The event object. | ||
| 32 | */ | ||
| 33 | const onCustomColorChange = (e) => { | ||
| 34 | const value = e.target.value; | ||
| 35 | setCustomColor(value); | ||
| 36 | callback?.(index, "backgroundColor", value); | ||
| 37 | }; | ||
| 38 | |||
| 39 | /** | ||
| 40 | * Handle custom color submission. | ||
| 41 | */ | ||
| 42 | const onCustomColorSubmit = () => { | ||
| 43 | setSelected(customColor); | ||
| 44 | setOpen(false); | ||
| 45 | }; | ||
| 46 | |||
| 47 | return ( | ||
| 48 | <div className="color-picker"> | ||
| 49 | <button | ||
| 50 | className="color-picker__button" | ||
| 51 | style={{ | ||
| 52 | backgroundColor: isSelectedCustom ? customColor : `var(${selected})`, | ||
| 53 | }} | ||
| 54 | onClick={onClick} | ||
| 55 | /> | ||
| 56 | {open && ( | ||
| 57 | <div className="color-picker__colors" onClick={() => setOpen(false)}> | ||
| 58 | {Settings.userWidgetColors.map((color) => { | ||
| 59 | /** | ||
| 60 | * Handle predefined color selection. | ||
| 61 | * @param {Object} e - The event object. | ||
| 62 | */ | ||
| 63 | const onClick = (e) => { | ||
| 64 | e.stopPropagation(); | ||
| 65 | setCustomColor(undefined); | ||
| 66 | setSelected(color); | ||
| 67 | callback?.(index, "backgroundColor", color); | ||
| 68 | setOpen(false); | ||
| 69 | }; | ||
| 70 | return ( | ||
| 71 | <button | ||
| 72 | key={color} | ||
| 73 | onClick={onClick} | ||
| 74 | style={{ backgroundColor: `var(${color})` }} | ||
| 75 | /> | ||
| 76 | ); | ||
| 77 | })} | ||
| 78 | <div | ||
| 79 | className="color-picker__custom-color" | ||
| 80 | onClick={(e) => e.stopPropagation()} | ||
| 81 | > | ||
| 82 | <div | ||
| 83 | className="color-picker__custom-color-preview" | ||
| 84 | style={{ backgroundColor: customColor }} | ||
| 85 | /> | ||
| 86 | <input | ||
| 87 | type="text" | ||
| 88 | className="color-picker__custom-color-input" | ||
| 89 | placeholder="Or any valid css color value (hex, rgb-a, hsl)" | ||
| 90 | onChange={onCustomColorChange} | ||
| 91 | defaultValue={customColor} | ||
| 92 | autoCapitalize="false" | ||
| 93 | autoComplete="false" | ||
| 94 | autoCorrect="false" | ||
| 95 | spellCheck={false} | ||
| 96 | /> | ||
| 97 | <button | ||
| 98 | className="color-picker__custom-color-submit" | ||
| 99 | disabled={!customColor} | ||
| 100 | onClick={onCustomColorSubmit} | ||
| 101 | > | ||
| 102 | Ok | ||
| 103 | </button> | ||
| 104 | </div> | ||
| 105 | </div> | ||
| 106 | )} | ||
| 107 | </div> | ||
| 108 | ); | ||
| 109 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/icon-picker.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/icon-picker.jsx new file mode 100755 index 0000000..ca3bdab --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/icon-picker.jsx | |||
| @@ -0,0 +1,130 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Icons from "../icons/icons.jsx"; | ||
| 3 | import { SuspenseIcon } from "../icons/icon.jsx"; | ||
| 4 | |||
| 5 | const { React } = Uebersicht; | ||
| 6 | |||
| 7 | /** | ||
| 8 | * IconPicker component allows users to select an icon from a list of available icons. | ||
| 9 | * | ||
| 10 | * @param {Object} props - The component props. | ||
| 11 | * @param {Function} props.callback - The callback function to be called when an icon is selected. | ||
| 12 | * @param {number} props.index - The index of the icon picker. | ||
| 13 | * @param {string} props.selectedIcon - The initially selected icon. | ||
| 14 | * @returns {JSX.Element} The IconPicker component. | ||
| 15 | */ | ||
| 16 | export default function IconPicker({ callback, index, selectedIcon }) { | ||
| 17 | // State to manage the open/close state of the icon picker dropdown | ||
| 18 | const [open, setOpen] = React.useState(false); | ||
| 19 | // State to manage the currently selected icon | ||
| 20 | const [selected, setSelected] = React.useState(selectedIcon); | ||
| 21 | // State to manage the search term for filtering icons | ||
| 22 | const [searchTerm, setSearchTerm] = React.useState(""); | ||
| 23 | |||
| 24 | // Get the selected icon component | ||
| 25 | const Icon = Icons[selected]; | ||
| 26 | // Get the list of all available icon keys | ||
| 27 | const keys = Object.keys(Icons); | ||
| 28 | |||
| 29 | // Filter icons based on search term | ||
| 30 | const filteredKeys = React.useMemo(() => { | ||
| 31 | if (!searchTerm.trim()) { | ||
| 32 | return keys; | ||
| 33 | } | ||
| 34 | return keys.filter((key) => | ||
| 35 | key.toLowerCase().includes(searchTerm.toLowerCase()), | ||
| 36 | ); | ||
| 37 | }, [keys, searchTerm]); | ||
| 38 | |||
| 39 | /** | ||
| 40 | * Toggles the open state of the icon picker dropdown. | ||
| 41 | */ | ||
| 42 | const onClick = () => setOpen(!open); | ||
| 43 | |||
| 44 | /** | ||
| 45 | * Handles search input changes. | ||
| 46 | * | ||
| 47 | * @param {React.ChangeEvent<HTMLInputElement>} e - The input change event. | ||
| 48 | */ | ||
| 49 | const onSearchChange = (e) => { | ||
| 50 | setSearchTerm(e.target.value); | ||
| 51 | }; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Clears the search term. | ||
| 55 | */ | ||
| 56 | const clearSearch = () => { | ||
| 57 | setSearchTerm(""); | ||
| 58 | }; | ||
| 59 | |||
| 60 | return ( | ||
| 61 | <div className="icon-picker"> | ||
| 62 | <button className="icon-picker__button" onClick={onClick}> | ||
| 63 | <SuspenseIcon> | ||
| 64 | <Icon /> | ||
| 65 | </SuspenseIcon> | ||
| 66 | </button> | ||
| 67 | {open && ( | ||
| 68 | <div className="icon-picker__dropdown"> | ||
| 69 | <div | ||
| 70 | className="icon-picker__search" | ||
| 71 | onClick={(e) => e.stopPropagation()} | ||
| 72 | > | ||
| 73 | <button | ||
| 74 | className="icon-picker__back" | ||
| 75 | onClick={() => setOpen(false)} | ||
| 76 | type="button" | ||
| 77 | > | ||
| 78 | <Icons.ChevronLeft /> | ||
| 79 | Back | ||
| 80 | </button> | ||
| 81 | <input | ||
| 82 | type="text" | ||
| 83 | placeholder="Search icons..." | ||
| 84 | value={searchTerm} | ||
| 85 | onChange={onSearchChange} | ||
| 86 | className="icon-picker__search-input" | ||
| 87 | autoFocus | ||
| 88 | /> | ||
| 89 | {searchTerm && ( | ||
| 90 | <button | ||
| 91 | className="icon-picker__search-clear" | ||
| 92 | onClick={clearSearch} | ||
| 93 | type="button" | ||
| 94 | > | ||
| 95 | ✕ | ||
| 96 | </button> | ||
| 97 | )} | ||
| 98 | </div> | ||
| 99 | <div className="icon-picker__icons" onClick={() => setOpen(false)}> | ||
| 100 | {filteredKeys.length > 0 ? ( | ||
| 101 | filteredKeys.map((key) => { | ||
| 102 | /** | ||
| 103 | * Handles the icon selection. | ||
| 104 | * | ||
| 105 | * @param {React.MouseEvent} e - The click event. | ||
| 106 | */ | ||
| 107 | const onClick = (e) => { | ||
| 108 | e.stopPropagation(); | ||
| 109 | setSelected(key); | ||
| 110 | callback?.(index, "icon", key); | ||
| 111 | setOpen(false); | ||
| 112 | }; | ||
| 113 | const Icon = Icons[key]; | ||
| 114 | return ( | ||
| 115 | <button key={key} onClick={onClick} title={key}> | ||
| 116 | <Icon /> | ||
| 117 | </button> | ||
| 118 | ); | ||
| 119 | }) | ||
| 120 | ) : ( | ||
| 121 | <div className="icon-picker__no-results"> | ||
| 122 | No icons found for "{searchTerm}" | ||
| 123 | </div> | ||
| 124 | )} | ||
| 125 | </div> | ||
| 126 | </div> | ||
| 127 | )} | ||
| 128 | </div> | ||
| 129 | ); | ||
| 130 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-component.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-component.jsx new file mode 100755 index 0000000..8e48b66 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-component.jsx | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Utils from "../../utils"; | ||
| 3 | import SettingsInner from "./settings-inner.jsx"; | ||
| 4 | import SettingsWidgets from "./settings-widgets.jsx"; | ||
| 5 | import * as Settings from "../../settings"; | ||
| 6 | |||
| 7 | const { React } = Uebersicht; | ||
| 8 | |||
| 9 | const TABS_STORAGE_KEY = "simple-bar-last-current-settings-tab"; | ||
| 10 | const TABS = [ | ||
| 11 | "global", | ||
| 12 | "themes", | ||
| 13 | "process", | ||
| 14 | "spacesDisplay", | ||
| 15 | "widgets", | ||
| 16 | "customStyles", | ||
| 17 | ]; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Main settings component. | ||
| 21 | * @param {Object} props - Component properties. | ||
| 22 | * @param {Function} props.closeSettings - Function to close the settings. | ||
| 23 | */ | ||
| 24 | export default function Component({ closeSettings }) { | ||
| 25 | // State to keep track of the current tab | ||
| 26 | const [currentTab, setCurrentTab] = React.useState(getLastCurrentTab()); | ||
| 27 | // State to keep track of pending changes | ||
| 28 | const [pendingChanges, setPendingChanges] = React.useState(0); | ||
| 29 | // Get current settings | ||
| 30 | const settings = Settings.get(); | ||
| 31 | // State to keep track of new settings | ||
| 32 | const [newSettings, setNewSettings] = React.useState(settings); | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Update the current tab and store it in session storage. | ||
| 36 | * @param {number} tab - The index of the tab to switch to. | ||
| 37 | */ | ||
| 38 | const updateTab = (tab) => { | ||
| 39 | setCurrentTab(tab); | ||
| 40 | window.sessionStorage.setItem(TABS_STORAGE_KEY, tab); | ||
| 41 | }; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Refresh the simple-bar with new settings. | ||
| 45 | * @param {Event} e - The event object. | ||
| 46 | */ | ||
| 47 | const refreshSimpleBar = async (e) => { | ||
| 48 | Utils.clickEffect(e); | ||
| 49 | setPendingChanges(0); | ||
| 50 | await Settings.set(newSettings); | ||
| 51 | Utils.hardRefresh(); | ||
| 52 | }; | ||
| 53 | |||
| 54 | // Effect to calculate the number of pending changes | ||
| 55 | React.useEffect(() => { | ||
| 56 | const diffs = Utils.compareObjects(settings, newSettings); | ||
| 57 | const deepDiffs = Object.keys(diffs).reduce( | ||
| 58 | (acc, key) => [...acc, ...Object.keys(diffs[key])], | ||
| 59 | [], | ||
| 60 | ); | ||
| 61 | setPendingChanges(deepDiffs.length); | ||
| 62 | }, [newSettings, settings]); | ||
| 63 | |||
| 64 | return ( | ||
| 65 | <div className="settings"> | ||
| 66 | <div className="settings__overlay" onClick={closeSettings} /> | ||
| 67 | <div className="settings__outer"> | ||
| 68 | <div className="settings__header"> | ||
| 69 | <button | ||
| 70 | className="settings__header-dot settings__header-dot--close" | ||
| 71 | onClick={closeSettings} | ||
| 72 | /> | ||
| 73 | <span className="settings__header-dot settings__header-dot--disabled" /> | ||
| 74 | <span className="settings__header-dot settings__header-dot--disabled" /> | ||
| 75 | Settings | ||
| 76 | </div> | ||
| 77 | <div className="settings__tabs"> | ||
| 78 | {Object.keys(Settings.defaultSettings).map((key) => { | ||
| 79 | const setting = Settings.data[key]; | ||
| 80 | const hideTab = !TABS.includes(key); | ||
| 81 | |||
| 82 | if (!setting || hideTab) return null; | ||
| 83 | |||
| 84 | const { label } = setting; | ||
| 85 | const i = TABS.indexOf(key); | ||
| 86 | const classes = Utils.classNames("settings__tab", { | ||
| 87 | "settings__tab--current": i === currentTab, | ||
| 88 | }); | ||
| 89 | return ( | ||
| 90 | <button key={i} className={classes} onClick={() => updateTab(i)}> | ||
| 91 | {label} | ||
| 92 | </button> | ||
| 93 | ); | ||
| 94 | })} | ||
| 95 | </div> | ||
| 96 | <div className="settings__inner"> | ||
| 97 | {Object.keys(Settings.defaultSettings).map((key) => { | ||
| 98 | const setting = Settings.data[key]; | ||
| 99 | const hideContent = !TABS.includes(key); | ||
| 100 | |||
| 101 | if (!setting || hideContent) return null; | ||
| 102 | |||
| 103 | const isWidgetsTab = key === "widgets"; | ||
| 104 | |||
| 105 | const { label } = setting; | ||
| 106 | return ( | ||
| 107 | <div | ||
| 108 | key={key} | ||
| 109 | className="settings__category" | ||
| 110 | style={{ transform: `translateX(-${100 * currentTab}%)` }} | ||
| 111 | > | ||
| 112 | {isWidgetsTab ? ( | ||
| 113 | <SettingsWidgets | ||
| 114 | newSettings={newSettings} | ||
| 115 | setNewSettings={setNewSettings} | ||
| 116 | /> | ||
| 117 | ) : ( | ||
| 118 | <React.Fragment> | ||
| 119 | <div className="settings__inner-title">{label}</div> | ||
| 120 | <SettingsInner | ||
| 121 | settingKey={key} | ||
| 122 | setting={setting} | ||
| 123 | newSettings={newSettings} | ||
| 124 | setNewSettings={setNewSettings} | ||
| 125 | /> | ||
| 126 | </React.Fragment> | ||
| 127 | )} | ||
| 128 | </div> | ||
| 129 | ); | ||
| 130 | })} | ||
| 131 | </div> | ||
| 132 | <div className="settings__bottom"> | ||
| 133 | {pendingChanges !== 0 && ( | ||
| 134 | <div className="settings__pending-changes"> | ||
| 135 | <b>{pendingChanges}</b> pending change{pendingChanges > 1 && "s"} | ||
| 136 | </div> | ||
| 137 | )} | ||
| 138 | <button | ||
| 139 | className="settings__refresh-button" | ||
| 140 | onClick={refreshSimpleBar} | ||
| 141 | disabled={!pendingChanges} | ||
| 142 | > | ||
| 143 | Confirm changes and refresh simple-bar | ||
| 144 | </button> | ||
| 145 | </div> | ||
| 146 | </div> | ||
| 147 | </div> | ||
| 148 | ); | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * Get the last current tab from session storage. | ||
| 153 | * @returns {number} The index of the last current tab. | ||
| 154 | */ | ||
| 155 | function getLastCurrentTab() { | ||
| 156 | const storedLastCurrentTab = window.sessionStorage.getItem(TABS_STORAGE_KEY); | ||
| 157 | if (storedLastCurrentTab) return parseInt(storedLastCurrentTab, 10); | ||
| 158 | return 0; | ||
| 159 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-inner.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-inner.jsx new file mode 100755 index 0000000..e318b25 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-inner.jsx | |||
| @@ -0,0 +1,123 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Icons from "../icons/icons.jsx"; | ||
| 3 | import SettingsItem from "./settings-item.jsx"; | ||
| 4 | import * as Utils from "../../utils"; | ||
| 5 | import * as Settings from "../../settings"; | ||
| 6 | |||
| 7 | const { React } = Uebersicht; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * SettingsInner component to render each setting category. | ||
| 11 | * @param {Object} props - Component properties. | ||
| 12 | * @param {string} props.settingKey - Key of current setting. | ||
| 13 | * @param {Object} props.setting - Contains infos & documentation data. | ||
| 14 | * @param {string[]} props.setting.infos - Current settings information. | ||
| 15 | * @param {string} props.setting.documentation - Current setting documentation link. | ||
| 16 | * @param {Object} props.newSettings - Object containing current modified settings. | ||
| 17 | * @param {Function} props.setNewSettings - Function allowing to save newly modified settings. | ||
| 18 | */ | ||
| 19 | export default function SettingsInner({ | ||
| 20 | settingKey, | ||
| 21 | setting, | ||
| 22 | newSettings, | ||
| 23 | setNewSettings, | ||
| 24 | }) { | ||
| 25 | const { infos, documentation } = setting; | ||
| 26 | return ( | ||
| 27 | <> | ||
| 28 | {Object.keys(Settings.defaultSettings[settingKey]).map((subKey) => { | ||
| 29 | const subSetting = Settings.data[subKey]; | ||
| 30 | if (!subSetting) return null; | ||
| 31 | const { | ||
| 32 | Component, | ||
| 33 | fullWidth, | ||
| 34 | label, | ||
| 35 | options, | ||
| 36 | placeholder, | ||
| 37 | title, | ||
| 38 | type, | ||
| 39 | minHeight, | ||
| 40 | } = subSetting; | ||
| 41 | |||
| 42 | const code = settingKey + subKey; | ||
| 43 | const defaultValue = newSettings[settingKey][subKey]; | ||
| 44 | |||
| 45 | const classes = Utils.classNames("settings__item", { | ||
| 46 | "settings__item--radio": type === "radio", | ||
| 47 | "settings__item--text": | ||
| 48 | type === "text" || type === "number" || type === "color", | ||
| 49 | "settings__item--textarea": type === "textarea", | ||
| 50 | "settings__item--color": type === "color", | ||
| 51 | "settings__item--full-width": fullWidth, | ||
| 52 | }); | ||
| 53 | |||
| 54 | const onChange = (e) => { | ||
| 55 | let value = e.target.value; | ||
| 56 | if (type === "checkbox") { | ||
| 57 | value = e.target.checked; | ||
| 58 | } | ||
| 59 | if (type === "number") { | ||
| 60 | value = parseFloat(value); | ||
| 61 | if (isNaN(value)) { | ||
| 62 | value = 0; | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | const updatedSettings = { | ||
| 67 | ...newSettings, | ||
| 68 | [settingKey]: { ...newSettings[settingKey], [subKey]: value }, | ||
| 69 | }; | ||
| 70 | setNewSettings(updatedSettings); | ||
| 71 | }; | ||
| 72 | |||
| 73 | return ( | ||
| 74 | <React.Fragment key={code}> | ||
| 75 | {title && <div className="settings__item-title">{title}</div>} | ||
| 76 | <div | ||
| 77 | className={classes} | ||
| 78 | onChange={type === "radio" ? onChange : undefined} | ||
| 79 | > | ||
| 80 | <SettingsItem | ||
| 81 | code={code} | ||
| 82 | Component={Component} | ||
| 83 | defaultValue={defaultValue} | ||
| 84 | label={label} | ||
| 85 | onChange={onChange} | ||
| 86 | options={options} | ||
| 87 | placeholder={placeholder} | ||
| 88 | type={type} | ||
| 89 | minHeight={minHeight} | ||
| 90 | /> | ||
| 91 | </div> | ||
| 92 | </React.Fragment> | ||
| 93 | ); | ||
| 94 | })} | ||
| 95 | {documentation && ( | ||
| 96 | <div className="settings__documentation"> | ||
| 97 | <Icons.OpenBook className="settings__documentation-icon" /> | ||
| 98 | <span className="settings__documentation-title"> | ||
| 99 | You{"'"}ll find all the information about these settings{" "} | ||
| 100 | <a | ||
| 101 | href={`https://www.jeantinland.com/toolbox/simple-bar/documentation${documentation}`} | ||
| 102 | > | ||
| 103 | here on the documentation | ||
| 104 | </a>{" "} | ||
| 105 | hosted on jeantinland.com. | ||
| 106 | </span> | ||
| 107 | </div> | ||
| 108 | )} | ||
| 109 | {infos && infos.length && ( | ||
| 110 | <div className="settings__infos"> | ||
| 111 | <div className="settings__infos-title">Tips</div> | ||
| 112 | {infos.map((info, i) => ( | ||
| 113 | <div | ||
| 114 | key={i} | ||
| 115 | className="settings__info" | ||
| 116 | dangerouslySetInnerHTML={{ __html: info }} | ||
| 117 | /> | ||
| 118 | ))} | ||
| 119 | </div> | ||
| 120 | )} | ||
| 121 | </> | ||
| 122 | ); | ||
| 123 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-item.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-item.jsx new file mode 100755 index 0000000..91c6f54 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-item.jsx | |||
| @@ -0,0 +1,143 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Utils from "../../utils"; | ||
| 3 | |||
| 4 | const { React } = Uebersicht; | ||
| 5 | |||
| 6 | /** | ||
| 7 | * Item component to render different types of settings inputs. | ||
| 8 | * @param {Object} props - Component properties. | ||
| 9 | * @param {string} props.code - Unique code for the setting. | ||
| 10 | * @param {React.Component} props.Component - Custom component for the setting. | ||
| 11 | * @param {any} props.defaultValue - Default value of the setting. | ||
| 12 | * @param {string} props.label - Label for the setting. | ||
| 13 | * @param {string} props.type - Type of the setting input. | ||
| 14 | * @param {Array} [props.options] - Options for select or radio inputs. | ||
| 15 | * @param {string} [props.placeholder] - Placeholder for text inputs. | ||
| 16 | * @param {number} [props.minHeight] - Minimum height for textarea inputs. | ||
| 17 | * @param {Function} props.onChange - Change handler for the setting input. | ||
| 18 | */ | ||
| 19 | export default function SettingsItem({ | ||
| 20 | code, | ||
| 21 | Component, | ||
| 22 | defaultValue, | ||
| 23 | label, | ||
| 24 | type, | ||
| 25 | options, | ||
| 26 | placeholder, | ||
| 27 | minHeight, | ||
| 28 | onChange, | ||
| 29 | }) { | ||
| 30 | const onClick = (e) => Utils.clickEffect(e); | ||
| 31 | if (type === "component") { | ||
| 32 | return <Component defaultValue={defaultValue} onChange={onChange} />; | ||
| 33 | } | ||
| 34 | if (type === "select") { | ||
| 35 | return ( | ||
| 36 | <React.Fragment> | ||
| 37 | <label htmlFor={code} dangerouslySetInnerHTML={{ __html: label }} /> | ||
| 38 | <select | ||
| 39 | id={code} | ||
| 40 | className="settings__select" | ||
| 41 | onChange={onChange} | ||
| 42 | defaultValue={defaultValue} | ||
| 43 | > | ||
| 44 | {options.map((option) => ( | ||
| 45 | <option key={option.code} value={option.code}> | ||
| 46 | {option.name} | ||
| 47 | </option> | ||
| 48 | ))} | ||
| 49 | </select> | ||
| 50 | </React.Fragment> | ||
| 51 | ); | ||
| 52 | } | ||
| 53 | if (type === "radio") { | ||
| 54 | return options.map((option) => ( | ||
| 55 | <div className="settings__item-option" key={option} onClick={onClick}> | ||
| 56 | <input | ||
| 57 | name={code} | ||
| 58 | id={option} | ||
| 59 | value={option} | ||
| 60 | type="radio" | ||
| 61 | defaultChecked={option === defaultValue} | ||
| 62 | /> | ||
| 63 | <label | ||
| 64 | htmlFor={option} | ||
| 65 | dangerouslySetInnerHTML={{ __html: `${option} ${label}` }} | ||
| 66 | /> | ||
| 67 | </div> | ||
| 68 | )); | ||
| 69 | } | ||
| 70 | if (type === "text" || type === "color") { | ||
| 71 | return ( | ||
| 72 | <React.Fragment> | ||
| 73 | <label htmlFor={code} dangerouslySetInnerHTML={{ __html: label }} /> | ||
| 74 | {type === "color" && ( | ||
| 75 | <div | ||
| 76 | className="settings__item-color-pill" | ||
| 77 | style={{ backgroundColor: defaultValue || "transparent" }} | ||
| 78 | /> | ||
| 79 | )} | ||
| 80 | <input | ||
| 81 | id={code} | ||
| 82 | type="text" | ||
| 83 | defaultValue={defaultValue} | ||
| 84 | placeholder={placeholder} | ||
| 85 | onChange={onChange} | ||
| 86 | autoComplete="off" | ||
| 87 | autoCorrect="off" | ||
| 88 | autoCapitalize="off" | ||
| 89 | spellCheck={false} | ||
| 90 | /> | ||
| 91 | </React.Fragment> | ||
| 92 | ); | ||
| 93 | } | ||
| 94 | if (type === "number") { | ||
| 95 | return ( | ||
| 96 | <React.Fragment> | ||
| 97 | <label htmlFor={code} dangerouslySetInnerHTML={{ __html: label }} /> | ||
| 98 | <input | ||
| 99 | id={code} | ||
| 100 | type="number" | ||
| 101 | value={defaultValue} | ||
| 102 | placeholder={placeholder} | ||
| 103 | onChange={onChange} | ||
| 104 | autoComplete="off" | ||
| 105 | /> | ||
| 106 | </React.Fragment> | ||
| 107 | ); | ||
| 108 | } | ||
| 109 | if (type === "textarea") { | ||
| 110 | return ( | ||
| 111 | <React.Fragment> | ||
| 112 | <label htmlFor={code} dangerouslySetInnerHTML={{ __html: label }} /> | ||
| 113 | <textarea | ||
| 114 | id={code} | ||
| 115 | defaultValue={defaultValue} | ||
| 116 | placeholder={placeholder} | ||
| 117 | onChange={onChange} | ||
| 118 | autoComplete="off" | ||
| 119 | autoCorrect="off" | ||
| 120 | autoCapitalize="off" | ||
| 121 | spellCheck={false} | ||
| 122 | style={{ minHeight }} | ||
| 123 | /> | ||
| 124 | </React.Fragment> | ||
| 125 | ); | ||
| 126 | } | ||
| 127 | return ( | ||
| 128 | <React.Fragment> | ||
| 129 | <input | ||
| 130 | id={code} | ||
| 131 | type="checkbox" | ||
| 132 | defaultChecked={defaultValue} | ||
| 133 | onChange={onChange} | ||
| 134 | onClick={onClick} | ||
| 135 | /> | ||
| 136 | <label | ||
| 137 | htmlFor={code} | ||
| 138 | onClick={onClick} | ||
| 139 | dangerouslySetInnerHTML={{ __html: label }} | ||
| 140 | /> | ||
| 141 | </React.Fragment> | ||
| 142 | ); | ||
| 143 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-widgets.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-widgets.jsx new file mode 100755 index 0000000..736e4bb --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-widgets.jsx | |||
| @@ -0,0 +1,180 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Icons from "../icons/icons.jsx"; | ||
| 3 | import * as Utils from "../../utils"; | ||
| 4 | import * as Settings from "../../settings"; | ||
| 5 | import SettingsInner from "./settings-inner.jsx"; | ||
| 6 | |||
| 7 | const { React } = Uebersicht; | ||
| 8 | |||
| 9 | const settingsKeys = { | ||
| 10 | batteryWidget: "batteryWidgetOptions", | ||
| 11 | browserTrackWidget: "browserTrackWidgetOptions", | ||
| 12 | cpuWidget: "cpuWidgetOptions", | ||
| 13 | cryptoWidget: "cryptoWidgetOptions", | ||
| 14 | dateWidget: "dateWidgetOptions", | ||
| 15 | gpuWidget: "gpuWidgetOptions", | ||
| 16 | memoryWidget: "memoryWidgetOptions", | ||
| 17 | keyboardWidget: "keyboardWidgetOptions", | ||
| 18 | micWidget: "micWidgetOptions", | ||
| 19 | mpdWidget: "mpdWidgetOptions", | ||
| 20 | musicWidget: "musicWidgetOptions", | ||
| 21 | netstatsWidget: "netstatsWidgetOptions", | ||
| 22 | nextMeetingWidget: "nextMeetingWidgetOptions", | ||
| 23 | notificationsWidget: "notificationsWidgetOptions", | ||
| 24 | soundWidget: "soundWidgetOptions", | ||
| 25 | spotifyWidget: "spotifyWidgetOptions", | ||
| 26 | stockWidget: "stockWidgetOptions", | ||
| 27 | timeWidget: "timeWidgetOptions", | ||
| 28 | vpnWidget: "vpnWidgetOptions", | ||
| 29 | githubWidget: "githubWidgetOptions", | ||
| 30 | weatherWidget: "weatherWidgetOptions", | ||
| 31 | wifiWidget: "networkWidgetOptions", | ||
| 32 | youtubeMusicWidget: "youtubeMusicWidgetOptions", | ||
| 33 | zoomWidget: "zoomWidgetOptions", | ||
| 34 | userWidgets: "userWidgets", | ||
| 35 | }; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * SettingsWidgets component to render widgets setting category. | ||
| 39 | * @param {Object} props - Component properties. | ||
| 40 | * @param {Object} props.newSettings - Object containing current modified settings. | ||
| 41 | * @param {Function} props.setNewSettings - Function allowing to save newly modified settings. | ||
| 42 | */ | ||
| 43 | export default function SettingsWidgets({ newSettings, setNewSettings }) { | ||
| 44 | const [currentWidget, setCurrentWidget] = React.useState(null); | ||
| 45 | const currentSetting = Settings.data[currentWidget]; | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Removes the current widget setting, | ||
| 49 | */ | ||
| 50 | const removeCurrentWidget = () => { | ||
| 51 | setCurrentWidget(null); | ||
| 52 | }; | ||
| 53 | |||
| 54 | /** | ||
| 55 | * Prevent checkbox click from propagating to the parent element, | ||
| 56 | * allowing to avoid triggering the widget selection. | ||
| 57 | */ | ||
| 58 | const handleCheckboxClick = (e) => { | ||
| 59 | e.stopPropagation(); | ||
| 60 | Utils.clickEffect(e); | ||
| 61 | }; | ||
| 62 | |||
| 63 | /** | ||
| 64 | * Updates the current widget based on the clicked element. | ||
| 65 | * @param {string} widget - The key of the widget to update. | ||
| 66 | */ | ||
| 67 | const updateCurrentWidget = (widget) => (e) => { | ||
| 68 | if (widget === "processWidget") return; | ||
| 69 | |||
| 70 | Utils.clickEffect(e); | ||
| 71 | const widgetKey = settingsKeys[widget]; | ||
| 72 | setCurrentWidget(currentWidget === widgetKey ? null : widgetKey); | ||
| 73 | }; | ||
| 74 | |||
| 75 | return ( | ||
| 76 | <div className="settings__widgets"> | ||
| 77 | <Breadcrumb | ||
| 78 | currentSetting={currentSetting} | ||
| 79 | removeCurrentWidget={removeCurrentWidget} | ||
| 80 | /> | ||
| 81 | {!currentWidget ? ( | ||
| 82 | <div className="settings__widgets-list"> | ||
| 83 | <div | ||
| 84 | className="settings__widgets-item" | ||
| 85 | onClick={updateCurrentWidget("userWidgets")} | ||
| 86 | > | ||
| 87 | Custom widgets | ||
| 88 | <Icons.ChevronRight className="settings__widgets-item-icon" /> | ||
| 89 | </div> | ||
| 90 | {Object.keys(Settings.defaultSettings.widgets).map((subKey) => { | ||
| 91 | const subSetting = Settings.data[subKey]; | ||
| 92 | if (!subSetting) return null; | ||
| 93 | const { label } = subSetting; | ||
| 94 | const defaultValue = newSettings.widgets[subKey]; | ||
| 95 | const isProcess = subKey === "processWidget"; | ||
| 96 | |||
| 97 | const classes = Utils.classNames("settings__widgets-item", { | ||
| 98 | "settings__widgets-item--process": isProcess, | ||
| 99 | }); | ||
| 100 | |||
| 101 | const onChange = (e) => { | ||
| 102 | const value = e.target.checked; | ||
| 103 | |||
| 104 | const updatedSettings = { | ||
| 105 | ...newSettings, | ||
| 106 | widgets: { ...newSettings.widgets, [subKey]: value }, | ||
| 107 | }; | ||
| 108 | setNewSettings(updatedSettings); | ||
| 109 | }; | ||
| 110 | |||
| 111 | return ( | ||
| 112 | <div | ||
| 113 | key={subKey} | ||
| 114 | className={classes} | ||
| 115 | onClick={updateCurrentWidget(subKey)} | ||
| 116 | > | ||
| 117 | <input | ||
| 118 | type="checkbox" | ||
| 119 | defaultChecked={defaultValue} | ||
| 120 | onChange={onChange} | ||
| 121 | onClick={handleCheckboxClick} | ||
| 122 | /> | ||
| 123 | {label} | ||
| 124 | {!isProcess && ( | ||
| 125 | <Icons.ChevronRight className="settings__widgets-item-icon" /> | ||
| 126 | )} | ||
| 127 | </div> | ||
| 128 | ); | ||
| 129 | })} | ||
| 130 | </div> | ||
| 131 | ) : ( | ||
| 132 | <div className="settings__widget-settings"> | ||
| 133 | <SettingsInner | ||
| 134 | settingKey={currentWidget} | ||
| 135 | setting={currentSetting} | ||
| 136 | newSettings={newSettings} | ||
| 137 | setNewSettings={setNewSettings} | ||
| 138 | /> | ||
| 139 | </div> | ||
| 140 | )} | ||
| 141 | </div> | ||
| 142 | ); | ||
| 143 | } | ||
| 144 | |||
| 145 | /** | ||
| 146 | * SettingsWidgets component to render widgets setting category. | ||
| 147 | * @param {Object} props - Component properties. | ||
| 148 | * @param {Object} props.currentSetting - Object containing the current setting. | ||
| 149 | * @param {Function} props.removeCurrentWidget - Function removing the current setting allowing to go back to widget list. | ||
| 150 | */ | ||
| 151 | function Breadcrumb({ currentSetting, removeCurrentWidget }) { | ||
| 152 | if (!currentSetting) { | ||
| 153 | return ( | ||
| 154 | <div className="settings__widgets-breadcrumb"> | ||
| 155 | <div className="settings__widgets-breadcrumb-title">Widgets</div> | ||
| 156 | </div> | ||
| 157 | ); | ||
| 158 | } | ||
| 159 | const { label } = currentSetting; | ||
| 160 | return ( | ||
| 161 | <div className="settings__widgets-breadcrumb"> | ||
| 162 | <button | ||
| 163 | className="settings__widgets-breadcrumb-title" | ||
| 164 | onClick={removeCurrentWidget} | ||
| 165 | > | ||
| 166 | Widgets | ||
| 167 | </button> | ||
| 168 | <span className="settings__widgets-breadcrumb-current"> | ||
| 169 | {">"} {label} | ||
| 170 | </span> | ||
| 171 | <button | ||
| 172 | className="settings__widgets-breadcrumb-back" | ||
| 173 | onClick={removeCurrentWidget} | ||
| 174 | > | ||
| 175 | <Icons.ChevronLeft className="settings__widgets-breadcrumb-back-icon" /> | ||
| 176 | Back | ||
| 177 | </button> | ||
| 178 | </div> | ||
| 179 | ); | ||
| 180 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings.jsx new file mode 100755 index 0000000..c63f853 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings.jsx | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Utils from "../../utils"; | ||
| 3 | import * as Settings from "../../settings"; | ||
| 4 | import { useSimpleBarContext } from "../simple-bar-context.jsx"; | ||
| 5 | |||
| 6 | export { settingsStyles as styles } from "../../styles/components/settings/settings"; | ||
| 7 | |||
| 8 | const { React } = Uebersicht; | ||
| 9 | |||
| 10 | // Settings component is lazy loaded. That way, | ||
| 11 | // it isn't loaded everytime simple-bar is refreshed | ||
| 12 | const Component = React.lazy(() => import("./settings-component.jsx")); | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Wrapper component that handles keyboard shortcuts for various actions | ||
| 16 | * and manages the visibility of the settings component. | ||
| 17 | * | ||
| 18 | * @component | ||
| 19 | * @returns {React.Fragment} A fragment containing the settings component if visible. | ||
| 20 | */ | ||
| 21 | export function Wrapper() { | ||
| 22 | const { pushMissive } = useSimpleBarContext(); | ||
| 23 | const [visible, setVisible] = React.useState(false); | ||
| 24 | |||
| 25 | const closeSettings = () => { | ||
| 26 | setVisible(false); | ||
| 27 | Utils.blurBar(); | ||
| 28 | }; | ||
| 29 | |||
| 30 | // Actions handled by this function are | ||
| 31 | // > Hard refresh simple-bar with cmd/ctrl + r | ||
| 32 | // > Open settings with cmd/ctrl + , | ||
| 33 | // > Toggle dark theme with cmd/ctrl + t | ||
| 34 | const handleKeydown = React.useCallback( | ||
| 35 | (e) => { | ||
| 36 | const { ctrlKey, key, metaKey } = e; | ||
| 37 | if ((ctrlKey || metaKey) && key === "r") { | ||
| 38 | e.preventDefault(); | ||
| 39 | Utils.hardRefresh(); | ||
| 40 | } | ||
| 41 | if ((ctrlKey || metaKey) && key === ",") { | ||
| 42 | e.preventDefault(); | ||
| 43 | setVisible(true); | ||
| 44 | } | ||
| 45 | if ((ctrlKey || metaKey) && key === "t") { | ||
| 46 | e.preventDefault(); | ||
| 47 | // We retrieve the latest version of settings | ||
| 48 | const settings = Settings.get(); | ||
| 49 | const AUTO = "auto"; | ||
| 50 | // If current value is auto, it is stored as is | ||
| 51 | // otherwise, it is toggled | ||
| 52 | let newValue = AUTO; | ||
| 53 | if (settings.global.theme !== AUTO) { | ||
| 54 | newValue = settings.global.theme; | ||
| 55 | } | ||
| 56 | // OS is instructed to toggle dark theme | ||
| 57 | Uebersicht.run( | ||
| 58 | `osascript -e 'tell app "System Events" to tell appearance preferences to set dark mode to not dark mode'`, | ||
| 59 | ); | ||
| 60 | // A notification is pushed either in Macos notification center or via internal missives system | ||
| 61 | Utils.notification("Toggling dark theme...", pushMissive); | ||
| 62 | if (newValue !== AUTO) { | ||
| 63 | // Only theme value is updated | ||
| 64 | const updatedSettings = { | ||
| 65 | ...settings, | ||
| 66 | global: { ...settings.global, theme: newValue }, | ||
| 67 | }; | ||
| 68 | // Settings are updated and simple-bar is hard refreshed | ||
| 69 | Settings.set(updatedSettings); | ||
| 70 | Utils.hardRefresh(); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | }, | ||
| 74 | [pushMissive], | ||
| 75 | ); | ||
| 76 | |||
| 77 | React.useEffect(() => { | ||
| 78 | // We bind keydown event on Übersicht document when <Settings /> component is mounted | ||
| 79 | // Event listener is removed on unmount | ||
| 80 | document.addEventListener("keydown", handleKeydown); | ||
| 81 | return () => document.removeEventListener("keydown", handleKeydown); | ||
| 82 | }, [handleKeydown]); | ||
| 83 | |||
| 84 | // Only a fragment is returned if <Settings /> component is not visible | ||
| 85 | return ( | ||
| 86 | <React.Fragment> | ||
| 87 | {visible && ( | ||
| 88 | <React.Suspense fallback={<React.Fragment />}> | ||
| 89 | <Component visible={visible} closeSettings={closeSettings} /> | ||
| 90 | </React.Suspense> | ||
| 91 | )} | ||
| 92 | </React.Fragment> | ||
| 93 | ); | ||
| 94 | } | ||
diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/user-widgets-creator.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/user-widgets-creator.jsx new file mode 100755 index 0000000..a026047 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/user-widgets-creator.jsx | |||
| @@ -0,0 +1,297 @@ | |||
| 1 | import * as Uebersicht from "uebersicht"; | ||
| 2 | import * as Icons from "../icons/icons.jsx"; | ||
| 3 | import * as Settings from "../../settings"; | ||
| 4 | import * as Utils from "../../utils"; | ||
| 5 | import ColorPicker from "./color-picker.jsx"; | ||
| 6 | import IconPicker from "./icon-picker.jsx"; | ||
| 7 | |||
| 8 | const { React } = Uebersicht; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * UserWidgetsCreator component allows users to create and manage custom widgets. | ||
| 12 | * @param {Object} props - The component props. | ||
| 13 | * @param {Object} props.defaultValue - The default value for the widgets. | ||
| 14 | * @param {Function} props.onChange - The function to call when the widgets change. | ||
| 15 | * @returns {JSX.Element} The UserWidgetsCreator component. | ||
| 16 | */ | ||
| 17 | export default function UserWidgetsCreator({ defaultValue, onChange }) { | ||
| 18 | const [widgets, setWidgets] = React.useState(defaultValue || {}); | ||
| 19 | const keys = Object.keys(widgets); | ||
| 20 | |||
| 21 | // Determine the highest widget ID and calculate the new ID for a new widget | ||
| 22 | const highestId = keys.reduce((acc, key) => { | ||
| 23 | const keyAsNumber = parseInt(key, 10); | ||
| 24 | return keyAsNumber > acc ? keyAsNumber : acc; | ||
| 25 | }, 1); | ||
| 26 | const newId = highestId + 1; | ||
| 27 | |||
| 28 | // Function to add a new widget | ||
| 29 | const onClick = () => | ||
| 30 | setWidgets((widgets) => ({ | ||
| 31 | ...widgets, | ||
| 32 | [newId]: { ...Settings.userWidgetDefault }, | ||
| 33 | })); | ||
| 34 | |||
| 35 | // Function to handle changes to a widget | ||
| 36 | const onWidgetChange = (index, field, value) => { | ||
| 37 | const newWidgets = { ...widgets }; | ||
| 38 | const newKeys = Object.keys(newWidgets); | ||
| 39 | const updatedWidgets = newKeys.reduce((acc, key, i) => { | ||
| 40 | const widget = newWidgets[key]; | ||
| 41 | return { | ||
| 42 | ...acc, | ||
| 43 | [i + 1]: key === index ? { ...widget, [field]: value } : widget, | ||
| 44 | }; | ||
| 45 | }, {}); | ||
| 46 | setWidgets(updatedWidgets); | ||
| 47 | }; | ||
| 48 | |||
| 49 | // Effect to detect changes in widgets and call onChange prop | ||
| 50 | React.useEffect(() => { | ||
| 51 | const diffs = Utils.compareObjects(defaultValue, widgets); | ||
| 52 | const hasDiffs = Object.keys(diffs).length > 0; | ||
| 53 | if (hasDiffs) onChange({ target: { value: widgets } }); | ||
| 54 | }, [defaultValue, onChange, widgets]); | ||
| 55 | |||
| 56 | return ( | ||
| 57 | <div className="user-widgets-creator"> | ||
| 58 | {keys.map((key, i) => ( | ||
| 59 | <UserWidgetCreator | ||
| 60 | key={`${key}-${widgets[key].backgroundColor}`} | ||
| 61 | index={key} | ||
| 62 | onWidgetChange={onWidgetChange} | ||
| 63 | setWidgets={setWidgets} | ||
| 64 | widget={widgets[key]} | ||
| 65 | isFirst={i === 0} | ||
| 66 | isLast={i === keys.length - 1} | ||
| 67 | /> | ||
| 68 | ))} | ||
| 69 | <button className="user-widgets-creator__add" onClick={onClick}> | ||
| 70 | <Icons.Add /> | ||
| 71 | Add a custom widget | ||
| 72 | </button> | ||
| 73 | </div> | ||
| 74 | ); | ||
| 75 | } | ||
| 76 | |||
| 77 | /** | ||
| 78 | * UserWidgetCreator component allows users to configure individual widgets. | ||
| 79 | * @param {Object} props - The component props. | ||
| 80 | * @param {string} props.index - The index of the widget. | ||
| 81 | * @param {boolean} props.isFirst - Whether the widget is the first in the list. | ||
| 82 | * @param {boolean} props.isLast - Whether the widget is the last in the list. | ||
| 83 | * @param {Function} props.onWidgetChange - The function to call when the widget changes. | ||
| 84 | * @param {Function} props.setWidgets - The function to set the widgets state. | ||
| 85 | * @param {Object} props.widget - The widget data. | ||
| 86 | * @returns {JSX.Element} The UserWidgetCreator component. | ||
| 87 | */ | ||
| 88 | function UserWidgetCreator({ | ||
| 89 | index, | ||
| 90 | isFirst, | ||
| 91 | isLast, | ||
| 92 | onWidgetChange, | ||
| 93 | setWidgets, | ||
| 94 | widget, | ||
| 95 | }) { | ||
| 96 | const { | ||
| 97 | title, | ||
| 98 | icon, | ||
| 99 | backgroundColor, | ||
| 100 | output, | ||
| 101 | onClickAction, | ||
| 102 | onRightClickAction, | ||
| 103 | onMiddleClickAction, | ||
| 104 | refreshFrequency, | ||
| 105 | active = true, | ||
| 106 | noIcon = false, | ||
| 107 | hideWhenNoOutput = true, | ||
| 108 | showOnDisplay = "", | ||
| 109 | } = widget; | ||
| 110 | |||
| 111 | const indexAsNumber = parseInt(index, 10); | ||
| 112 | |||
| 113 | // Function to remove a widget | ||
| 114 | const onRemoveClick = () => { | ||
| 115 | setWidgets((widgets) => { | ||
| 116 | const keys = Object.keys(widgets); | ||
| 117 | return keys.reduce( | ||
| 118 | (acc, key) => (key === index ? acc : { ...acc, [key]: widgets[key] }), | ||
| 119 | {}, | ||
| 120 | ); | ||
| 121 | }); | ||
| 122 | }; | ||
| 123 | |||
| 124 | // Function to handle changes to widget fields | ||
| 125 | const onChange = (field, chexbox = false) => { | ||
| 126 | return (e) => { | ||
| 127 | const value = (chexbox ? e?.target?.checked : e?.target?.value) ?? ""; | ||
| 128 | onWidgetChange(index, field, value); | ||
| 129 | }; | ||
| 130 | }; | ||
| 131 | |||
| 132 | // Function to move the widget up in the list | ||
| 133 | const onBeforeClick = () => { | ||
| 134 | setWidgets((widgets) => { | ||
| 135 | const swapedWidget = widgets[indexAsNumber - 1]; | ||
| 136 | return { | ||
| 137 | ...widgets, | ||
| 138 | [indexAsNumber - 1]: widget, | ||
| 139 | [indexAsNumber]: swapedWidget, | ||
| 140 | }; | ||
| 141 | }); | ||
| 142 | }; | ||
| 143 | |||
| 144 | // Function to move the widget down in the list | ||
| 145 | const onAfterClick = () => { | ||
| 146 | setWidgets((widgets) => { | ||
| 147 | const swapedWidget = widgets[indexAsNumber + 1]; | ||
| 148 | return { | ||
| 149 | ...widgets, | ||
| 150 | [indexAsNumber + 1]: widget, | ||
| 151 | [indexAsNumber]: swapedWidget, | ||
| 152 | }; | ||
| 153 | }); | ||
| 154 | }; | ||
| 155 | |||
| 156 | return ( | ||
| 157 | <div key={indexAsNumber} className="user-widget-creator"> | ||
| 158 | <div className="user-widget-creator__sort-buttons"> | ||
| 159 | {!isFirst && ( | ||
| 160 | <button | ||
| 161 | className="user-widget-creator__sort-button user-widget-creator__sort-button--before" | ||
| 162 | onClick={onBeforeClick} | ||
| 163 | > | ||
| 164 | <Icons.ChevronTop /> | ||
| 165 | </button> | ||
| 166 | )} | ||
| 167 | {!isLast && ( | ||
| 168 | <button | ||
| 169 | className="user-widget-creator__sort-button user-widget-creator__sort-button--after" | ||
| 170 | onClick={onAfterClick} | ||
| 171 | > | ||
| 172 | <Icons.ChevronBottom /> | ||
| 173 | </button> | ||
| 174 | )} | ||
| 175 | </div> | ||
| 176 | <button className="user-widget-creator__remove" onClick={onRemoveClick}> | ||
| 177 | <Icons.Remove /> | ||
| 178 | </button> | ||
| 179 | <div className="user-widget-creator__index"> | ||
| 180 | n°<b>{index}</b> | ||
| 181 | </div> | ||
| 182 | <IconPicker callback={onWidgetChange} index={index} selectedIcon={icon} /> | ||
| 183 | <div className="user-widget-creator__right"> | ||
| 184 | <div className="user-widget-creator__right-top"> | ||
| 185 | <ColorPicker | ||
| 186 | callback={onWidgetChange} | ||
| 187 | index={index} | ||
| 188 | selectedColor={backgroundColor} | ||
| 189 | /> | ||
| 190 | <input | ||
| 191 | className="user-widget-creator__title" | ||
| 192 | onChange={onChange("title")} | ||
| 193 | type="text" | ||
| 194 | defaultValue={title} | ||
| 195 | /> | ||
| 196 | <label htmlFor={`refresh-frequency-${index}`}> | ||
| 197 | Refresh frequency (ms): | ||
| 198 | </label> | ||
| 199 | <input | ||
| 200 | className="user-widget-creator__refresh-frequency" | ||
| 201 | onChange={onChange("refreshFrequency")} | ||
| 202 | id={`refresh-frequency-${index}`} | ||
| 203 | type="text" | ||
| 204 | defaultValue={refreshFrequency} | ||
| 205 | /> | ||
| 206 | </div> | ||
| 207 | <div className="user-widget-creator__input-group"> | ||
| 208 | <label htmlFor={`show-on-display-${index}`}>Show on display: </label> | ||
| 209 | <input | ||
| 210 | className="user-widget-creator__show-on-display" | ||
| 211 | onChange={onChange("showOnDisplay")} | ||
| 212 | id={`show-on-display-${index}`} | ||
| 213 | type="text" | ||
| 214 | defaultValue={showOnDisplay} | ||
| 215 | placeholder="example: 1,2 (leave blank to show on all displays)" | ||
| 216 | spellCheck={false} | ||
| 217 | /> | ||
| 218 | </div> | ||
| 219 | <div className="user-widget-creator__input-group"> | ||
| 220 | <label htmlFor={`output-${index}`}>Command/script path: </label> | ||
| 221 | <input | ||
| 222 | className="user-widget-creator__output" | ||
| 223 | onChange={onChange("output")} | ||
| 224 | id={`output-${index}`} | ||
| 225 | type="text" | ||
| 226 | defaultValue={output} | ||
| 227 | spellCheck={false} | ||
| 228 | /> | ||
| 229 | </div> | ||
| 230 | <div className="user-widget-creator__input-group"> | ||
| 231 | <label htmlFor={`on-click-action-${index}`}> | ||
| 232 | On click command/script path:{" "} | ||
| 233 | </label> | ||
| 234 | <input | ||
| 235 | className="user-widget-creator__on-click-action" | ||
| 236 | onChange={onChange("onClickAction")} | ||
| 237 | id={`on-click-action-${index}`} | ||
| 238 | type="text" | ||
| 239 | defaultValue={onClickAction} | ||
| 240 | spellCheck={false} | ||
| 241 | /> | ||
| 242 | </div> | ||
| 243 | <div className="user-widget-creator__input-group"> | ||
| 244 | <label htmlFor={`on-right-click-action-${index}`}> | ||
| 245 | On right click command/script path:{" "} | ||
| 246 | </label> | ||
| 247 | <input | ||
| 248 | className="user-widget-creator__on-right-click-action" | ||
| 249 | onChange={onChange("onRightClickAction")} | ||
| 250 | id={`on-right-click-action-${index}`} | ||
| 251 | type="text" | ||
| 252 | defaultValue={onRightClickAction} | ||
| 253 | spellCheck={false} | ||
| 254 | /> | ||
| 255 | </div> | ||
| 256 | <div className="user-widget-creator__input-group"> | ||
| 257 | <label htmlFor={`on-middle-click-action-${index}`}> | ||
| 258 | On middle click command/script path:{" "} | ||
| 259 | </label> | ||
| 260 | <input | ||
| 261 | className="user-widget-creator__on-middle-click-action" | ||
| 262 | onChange={onChange("onMiddleClickAction")} | ||
| 263 | id={`on-middle-click-action-${index}`} | ||
| 264 | type="text" | ||
| 265 | defaultValue={onMiddleClickAction} | ||
| 266 | spellCheck={false} | ||
| 267 | /> | ||
| 268 | </div> | ||
| 269 | <div className="user-widget-creator__input-group"> | ||
| 270 | <input | ||
| 271 | id={`active-${index}`} | ||
| 272 | type="checkbox" | ||
| 273 | defaultChecked={active} | ||
| 274 | onChange={onChange("active", true)} | ||
| 275 | /> | ||
| 276 | <label htmlFor={`active-${index}`}>Active</label> | ||
| 277 | <input | ||
| 278 | id={`no-icon-${index}`} | ||
| 279 | type="checkbox" | ||
| 280 | defaultChecked={noIcon} | ||
| 281 | onChange={onChange("noIcon", true)} | ||
| 282 | /> | ||
| 283 | <label htmlFor={`no-icon-${index}`}>No icon</label> | ||
| 284 | <input | ||
| 285 | id={`hide-when-no-output-${index}`} | ||
| 286 | type="checkbox" | ||
| 287 | defaultChecked={hideWhenNoOutput} | ||
| 288 | onChange={onChange("hideWhenNoOutput", true)} | ||
| 289 | /> | ||
| 290 | <label htmlFor={`hide-when-no-output-${index}`}> | ||
| 291 | Hide when no script output | ||
| 292 | </label> | ||
| 293 | </div> | ||
| 294 | </div> | ||
| 295 | </div> | ||
| 296 | ); | ||
| 297 | } | ||
