diff --git a/app/src/Events.js b/app/src/Events.js index 0aa66f35..e25ec03d 100644 --- a/app/src/Events.js +++ b/app/src/Events.js @@ -2,6 +2,7 @@ import React, {useEffect} from 'react'; import {useRecurringTimeout} from "./hooks/customHooks"; import {getEvents} from "./api"; import {toast} from "react-semantic-toasts-2"; +import {useLocalStorage} from "./components/Common"; const apiEventName = 'apiEvent'; diff --git a/app/src/components/Channels.js b/app/src/components/Channels.js index cf0a4790..3cb97e10 100644 --- a/app/src/components/Channels.js +++ b/app/src/components/Channels.js @@ -88,7 +88,6 @@ function ChannelStatistics({statistics}) { function ChannelPage({create, header}) { const [disabled, setDisabled] = useState(false); - const [errors, setErrors] = useState({}); const [error, setError] = useState(false); const [success, setSuccess] = useState(false); const [messageHeader, setMessageHeader] = useState(); @@ -97,24 +96,22 @@ function ChannelPage({create, header}) { const navigate = useNavigate(); const {channelId} = useParams(); - const {channel, changeValue, fetchChannel} = useChannel(channelId); + const {channel, ready: channelReady, changeValue, fetchChannel} = useChannel(channelId); const {SingleTag} = React.useContext(TagsContext); const [tagEditModalOpen, setTagEditModalOpen] = useState(false); - const [newTagName, setNewTagName] = useState(channel ? channel.tag_name : null); + const [newTagName, setNewTagName] = useState(null); const [moveToTagDirectory, setMoveToTagDirectory] = useState(true); const [newTagDirectory, setNewTagDirectory] = useState(''); useTitle(_.isEmpty(channel) ? null : `${channel.name} Channel`); React.useEffect(() => { - console.debug(channel); - if (!_.isEmpty(channel)) { - setNewTagName(channel.tag_name); - } - }, [JSON.stringify(channel)]); + setNewTagName(channel.tag_name); + }, [channel.tag_name]); - if (_.isEmpty(channel)) { + if (!create && !channelReady) { + // Waiting for editing Channel to be fetched. return ; } @@ -147,7 +144,6 @@ function ChannelPage({create, header}) { name: channel.name, directory: channel.directory, url: channel.url, - tag_name: channel.tag_name, }; setDisabled(true); @@ -155,6 +151,8 @@ function ChannelPage({create, header}) { let response = null; try { if (create !== undefined) { + // Can create a Channel with a Tag. + body.tag_name = channel.tag_name; response = await createChannel(body); } else { response = await updateChannel(channelId, body); @@ -172,7 +170,7 @@ function ChannelPage({create, header}) { setDisabled(false); } - if (response && response.status >= 200 && response.status < 300) { + if (response && response.ok) { let location = response.headers.get('Location'); let channelResponse = await fetch(location); let data = await channelResponse.json(); @@ -198,13 +196,7 @@ function ChannelPage({create, header}) { // Some error occurred. let error = await response.json(); let cause = error.cause; - if (error.code === 'UNKNOWN_DIRECTORY' || (cause && cause.code === 'UNKNOWN_DIRECTORY')) { - setErrorMessage( - 'Invalid directory', - 'This directory does not exist.', - 'directory', - ); - } else if (cause && cause.code === 'CHANNEL_DIRECTORY_CONFLICT') { + if (cause && cause.code === 'CHANNEL_DIRECTORY_CONFLICT') { setErrorMessage( 'Invalid directory', 'This directory is already used by another channel', @@ -249,13 +241,6 @@ function ChannelPage({create, header}) { setDownloadModalOpen(false); } - const handleTagModalOpen = (e) => { - if (e) { - e.preventDefault(); - } - setTagEditModalOpen(true); - } - const handleTagEditChannel = async () => { try { await tagChannel(channelId, newTagName, moveToTagDirectory ? newTagDirectory : null); @@ -348,7 +333,6 @@ function ChannelPage({create, header}) { error={error} success={success} autoComplete="off" - onSubmit={handleSubmit} > @@ -358,7 +342,6 @@ function ChannelPage({create, header}) { type="text" placeholder="Short Channel Name" disabled={disabled} - error={errors.name} value={channel.name} onChange={(e, {value}) => changeValue('name', value)} /> @@ -382,7 +365,6 @@ function ChannelPage({create, header}) { name="url" type="url" placeholder='https://example.com/channel/videos' - error={errors.url} value={channel.url} onChange={handleInputChange} /> @@ -410,6 +392,7 @@ function ChannelPage({create, header}) { size='small' confirmContent='Are you sure you want to delete this channel? No video files will be deleted.' confirmButton='Delete' + confirmHeader='Delete Channel?' onClick={handleDelete} obeyWROLMode={true} style={{marginTop: '1em'}} @@ -423,7 +406,7 @@ function ChannelPage({create, header}) { >Refresh {tagEditModal} diff --git a/app/src/components/Common.js b/app/src/components/Common.js index c97994ad..0b44ef48 100644 --- a/app/src/components/Common.js +++ b/app/src/components/Common.js @@ -1451,6 +1451,7 @@ export function useAPIButton( disabled, confirmContent, confirmButton, + confirmHeader, themed = true, obeyWROLMode = false, icon = null, @@ -1565,6 +1566,7 @@ export function useAPIButton( onCancel={() => setConfirmOpen(false)} onConfirm={localOnConfirm} confirmButton={confirmButton} + header={confirmHeader} /> } @@ -1580,6 +1582,7 @@ export function APIButton({ disabled, confirmContent, confirmButton, + confirmHeader, themed, obeyWROLMode, icon, @@ -1593,6 +1596,7 @@ export function APIButton({ disabled, confirmContent, confirmButton, + confirmHeader, themed, obeyWROLMode, icon, @@ -1810,4 +1814,38 @@ const validUrlRegex = /^(http|https):\/\/[^ "]+$/; export function validURL(url) { return !(url && !validUrlRegex.test(url)); -} \ No newline at end of file +} + +export function useLocalStorage(key, initialValue, decode = JSON.parse, encode = JSON.stringify) { + // Use localstorage to store some JSON encode-able. + + // Initialize state with the value from localStorage or initial value + const [value, setValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + // Parse the stored item (integer, bool, etc.). Use the initial value if empty. + return item ? decode(item) : initialValue; + } catch (error) { + console.error('Error reading localStorage:', error); + return initialValue; + } + }); + + // Save to localStorage when the value changes + useEffect(() => { + try { + window.localStorage.setItem(key, encode(value)); + } catch (error) { + console.error('Error setting localStorage:', error); + } + }, [key, value, encode]); + + // Return the stored value and a function to update it + return [value, setValue]; +} + +export function useLocalStorageInt(key, initialValue) { + // Use localStorage to store an integer. + const [storedValue, setStoredValue] = useLocalStorage(key, initialValue, parseInt, (num) => num.toString()); + return [storedValue, setStoredValue]; +} diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js index b8396c44..c1cf6fb3 100644 --- a/app/src/components/Nav.js +++ b/app/src/components/Nav.js @@ -2,7 +2,7 @@ import React from "react"; import {Link, NavLink} from "react-router-dom"; import {Dropdown, Icon as SIcon, Menu} from "semantic-ui-react"; import {Media, SettingsContext, StatusContext, ThemeContext} from "../contexts/contexts"; -import {CPUTemperatureIcon, DarkModeToggle, HotspotStatusIcon, NAME, SystemLoadIcon} from "./Common"; +import {CPUTemperatureIcon, DarkModeToggle, HotspotStatusIcon, NAME, SystemLoadIcon, useLocalStorage} from "./Common"; import {ShareButton} from "./Share"; import {useWROLMode} from "../hooks/customHooks"; import {SearchIconButton} from "./Search"; @@ -59,15 +59,28 @@ function NavIconWrapper(props) { return
{props.children}
} +function useNavColorSetting() { + // Use localstorage to avoid flickering navbar color on startup. + const {settings} = React.useContext(SettingsContext); + const [navColor, setNavColor] = useLocalStorage('nav_color', 'violet'); + + React.useEffect(() => { + const newColor = settings.nav_color || 'violet'; + setNavColor(newColor); + localStorage.setItem('nav_color', newColor); + }, [settings.nav_color]); + + return navColor +} + export function NavBar() { const wrolModeEnabled = useWROLMode(); const {status} = React.useContext(StatusContext); - const {settings} = React.useContext(SettingsContext); + const navColor = useNavColorSetting(); const wrolpiIcon = WROLPi Home Icon; const name = {NAME || wrolpiIcon}; const topNavText = wrolModeEnabled ? <>{name}  : name; const {i} = React.useContext(ThemeContext); - const navColor = settings && settings.nav_color ? settings.nav_color : 'violet'; const homeLink = {topNavText} diff --git a/app/src/components/calculators/ElectricalCalculators.js b/app/src/components/calculators/ElectricalCalculators.js index c09cc3be..c6477734 100644 --- a/app/src/components/calculators/ElectricalCalculators.js +++ b/app/src/components/calculators/ElectricalCalculators.js @@ -1,4 +1,4 @@ -import {HelpPopup, roundDigits, Toggle} from "../Common"; +import {HelpPopup, roundDigits, Toggle, useLocalStorage, useLocalStorageInt} from "../Common"; import React, {useState} from "react"; import {Form, Header, Segment, Table} from "../Theme"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; @@ -264,23 +264,15 @@ export const calcPowerLossPercent = (isSAE, volts, amps, resistancePerKiloLength return (voltageDrop / volts) * 100; // Return as a percentage }; -const PowerLossCalculator = ({defaultVolts = 120}) => { - const [volts, setVolts] = useState(defaultVolts); - const [wireType, setWireType] = useState('solid'); - const [isSAE, setIsSAE] = useState(true); - const [length, setLength] = useState(100); +const PowerLossCalculator = ({volts, setVolts}) => { + const [wireType, setWireType] = useLocalStorage('calculators.power_loss.wire_type', 'solid'); + const [isSAE, setIsSAE] = useLocalStorage('calculators.power_loss.is_sae', true); + const [length, setLength] = useLocalStorageInt('calculators.power_loss.length', 100); const [ampsRange] = useState([1, 5, 10, 20, 40, 100]); const {inverted} = React.useContext(ThemeContext); const warningBackgroundColor = inverted === 'inverted' ? '#332020' : '#ffbebe'; - React.useEffect(() => { - // Copy voltage from Ohm's Law calculator. - if (defaultVolts > 0) { - setVolts(defaultVolts); - } - }, [defaultVolts]); - const resistances = isSAE ? resistancesPerKFeet[wireType] : resistancesPerKm[wireType]; @@ -385,9 +377,9 @@ const PowerLossCalculator = ({defaultVolts = 120}) => { }; export const ElectricalCalculators = () => { - const [volts, setVolts] = React.useState(); + const [volts, setVolts] = useLocalStorageInt('calculators.electrical.volts', 120); return - + } diff --git a/app/src/components/calculators/TemperatureCalculator.js b/app/src/components/calculators/TemperatureCalculator.js index 53435653..979031bb 100644 --- a/app/src/components/calculators/TemperatureCalculator.js +++ b/app/src/components/calculators/TemperatureCalculator.js @@ -13,7 +13,7 @@ export function TemperatureCalculator() { const handleInputChange = (e, {name, value}) => { if (value === '') { // User has cleared the input; - setState({volt: '', ohm: '', amp: ''}); + setState({celsius: '', fahrenheit: '', kelvin: ''}); return } diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js index 17ed4553..729369f2 100644 --- a/app/src/hooks/customHooks.js +++ b/app/src/hooks/customHooks.js @@ -428,7 +428,7 @@ export const useChannel = (channel_id) => { match_regex: '', tag_name: null, }; - const [fetched, setFetched] = useState(false); + const [ready, setReady] = useState(false); const [channel, setChannel] = useState(emptyChannel); const [original, setOriginal] = useState({}); @@ -442,8 +442,9 @@ export const useChannel = (channel_id) => { c['match_regex'] = c['match_regex'] || ''; setChannel(c); setOriginal(c); - setFetched(true); + setReady(true); } catch (e) { + setReady(false); console.error(e); toast({ type: 'error', @@ -459,7 +460,7 @@ export const useChannel = (channel_id) => { useEffect(() => { localGetChannel(); - }, [channel_id]) + }, [channel_id]); const changeValue = (name, value) => { const newChannel = {...channel, [name]: value}; @@ -467,7 +468,7 @@ export const useChannel = (channel_id) => { setChannel(newChannel); } - return {channel, changeValue, original, fetched, fetchChannel: localGetChannel}; + return {channel, changeValue, original, ready, fetchChannel: localGetChannel}; } export const useChannels = () => {