aboutsummaryrefslogtreecommitdiff
path: root/users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings
diff options
context:
space:
mode:
Diffstat (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/settings')
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/aerospace-display-manager.jsx83
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/color-picker.jsx109
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/icon-picker.jsx130
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-component.jsx159
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-inner.jsx123
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-item.jsx143
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings-widgets.jsx180
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/settings.jsx94
-rwxr-xr-xusers/ryan/modules/simple-bar/simple-bar-source/lib/components/settings/user-widgets-creator.jsx297
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 @@
1import * as Uebersicht from "uebersicht";
2import * as Utils from "../../utils";
3import * as Icons from "../icons/icons.jsx";
4
5const { 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 */
14export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Settings from "../../settings";
3
4const { 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 */
14export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Icons from "../icons/icons.jsx";
3import { SuspenseIcon } from "../icons/icon.jsx";
4
5const { 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 */
16export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Utils from "../../utils";
3import SettingsInner from "./settings-inner.jsx";
4import SettingsWidgets from "./settings-widgets.jsx";
5import * as Settings from "../../settings";
6
7const { React } = Uebersicht;
8
9const TABS_STORAGE_KEY = "simple-bar-last-current-settings-tab";
10const 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 */
24export 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 */
155function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Icons from "../icons/icons.jsx";
3import SettingsItem from "./settings-item.jsx";
4import * as Utils from "../../utils";
5import * as Settings from "../../settings";
6
7const { 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 */
19export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Utils from "../../utils";
3
4const { 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 */
19export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Icons from "../icons/icons.jsx";
3import * as Utils from "../../utils";
4import * as Settings from "../../settings";
5import SettingsInner from "./settings-inner.jsx";
6
7const { React } = Uebersicht;
8
9const 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 */
43export 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 */
151function 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Utils from "../../utils";
3import * as Settings from "../../settings";
4import { useSimpleBarContext } from "../simple-bar-context.jsx";
5
6export { settingsStyles as styles } from "../../styles/components/settings/settings";
7
8const { React } = Uebersicht;
9
10// Settings component is lazy loaded. That way,
11// it isn't loaded everytime simple-bar is refreshed
12const 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 */
21export 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 @@
1import * as Uebersicht from "uebersicht";
2import * as Icons from "../icons/icons.jsx";
3import * as Settings from "../../settings";
4import * as Utils from "../../utils";
5import ColorPicker from "./color-picker.jsx";
6import IconPicker from "./icon-picker.jsx";
7
8const { 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 */
17export 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 */
88function 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}