diff --git a/CHANGELOG.md b/CHANGELOG.md index bf886494c..81f12d24f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Update Airplay icon to modern variant * Add gradient to background to break up the solid color * Reformat Admin Settings portion of Admin Panel (previously known as the Updater) + * Add ability to create streams, presets from the home screen ## 0.4.5 * Web App diff --git a/web/src/pages/Settings/Presets/CreatePresetModal/CreatePresetModal.jsx b/web/src/components/CreatePresetModal/CreatePresetModal.jsx similarity index 100% rename from web/src/pages/Settings/Presets/CreatePresetModal/CreatePresetModal.jsx rename to web/src/components/CreatePresetModal/CreatePresetModal.jsx diff --git a/web/src/pages/Settings/Presets/CreatePresetModal/CreatePresetModal.scss b/web/src/components/CreatePresetModal/CreatePresetModal.scss similarity index 100% rename from web/src/pages/Settings/Presets/CreatePresetModal/CreatePresetModal.scss rename to web/src/components/CreatePresetModal/CreatePresetModal.scss diff --git a/web/src/components/CreateStreamModal/CreateStreamModal.jsx b/web/src/components/CreateStreamModal/CreateStreamModal.jsx new file mode 100644 index 000000000..dceb9b229 --- /dev/null +++ b/web/src/components/CreateStreamModal/CreateStreamModal.jsx @@ -0,0 +1,49 @@ +import React from "react"; +import PropTypes from "prop-types"; +import StreamTemplates from "./StreamTemplates.json"; +import TypeSelectModal from "./TypeSelectModal/TypeSelectModal"; +import StreamModal from "./StreamModal/StreamModal"; + +export default function CreateStreamModal({showSelect, onClose}) { + // A wrapper for two modals so that their workflows can easily be placed wherever needed + const [showCreate, setShowCreate] = React.useState(false); + const [selectedType, setSelectedType] = React.useState(""); + + const initEmptyStream = (type) => { + const streamTemplate = StreamTemplates.filter((t) => t.type === type)[0]; + let stream = { type: type, disabled: false }; + streamTemplate.fields.forEach((field) => { + stream[field.name] = field.default; + }); + return stream; + }; + + return( + <> + {showSelect && ( + { + onClose(); + }} + onSelect={(type) => { + setSelectedType(type); + onClose(); + setShowCreate(true); + }} + /> + )} + {showCreate && ( + { + setShowCreate(false); + }} + /> + )} + + ) +}; +CreateStreamModal.propTypes = { + showSelect: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, +}; diff --git a/web/src/pages/Settings/Streams/StreamModal/StreamModal.jsx b/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx similarity index 98% rename from web/src/pages/Settings/Streams/StreamModal/StreamModal.jsx rename to web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx index 940ac1885..f56eb2ff0 100644 --- a/web/src/pages/Settings/Streams/StreamModal/StreamModal.jsx +++ b/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx @@ -198,7 +198,7 @@ const InternetRadioSearch = ({ onChange }) => { InternetRadioSearch.propTypes = { onChange: PropTypes.func.isRequired, }; -const StreamModal = ({ stream, onClose, apply, del }) => { +const StreamModal = ({ stream, onClose, del }) => { const [streamFields, setStreamFields] = React.useState( JSON.parse(JSON.stringify(stream)) ); // set streamFields to copy of stream @@ -208,6 +208,14 @@ const StreamModal = ({ stream, onClose, apply, del }) => { const [hasError, setHasError] = React.useState(false); // Need a discrete hasError bool to trigger error const [renderAlertAnimation, setAlertAnimation] = React.useState(0); // Need a discrete hasError bool to trigger error + const apply = (stream) => { + return fetch("/api/stream", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(stream), + }); + }; + const streamTemplate = StreamTemplates.filter( (t) => t.type === stream.type )[0]; @@ -383,7 +391,6 @@ const StreamModal = ({ stream, onClose, apply, del }) => { StreamModal.propTypes = { stream: PropTypes.any.isRequired, onClose: PropTypes.func.isRequired, - apply: PropTypes.func.isRequired, del: PropTypes.func, }; diff --git a/web/src/pages/Settings/Streams/StreamModal/StreamModal.scss b/web/src/components/CreateStreamModal/StreamModal/StreamModal.scss similarity index 100% rename from web/src/pages/Settings/Streams/StreamModal/StreamModal.scss rename to web/src/components/CreateStreamModal/StreamModal/StreamModal.scss diff --git a/web/src/pages/Settings/Streams/StreamTemplates.json b/web/src/components/CreateStreamModal/StreamTemplates.json similarity index 100% rename from web/src/pages/Settings/Streams/StreamTemplates.json rename to web/src/components/CreateStreamModal/StreamTemplates.json diff --git a/web/src/pages/Settings/Streams/TypeSelectModal/TypeSelectModal.jsx b/web/src/components/CreateStreamModal/TypeSelectModal/TypeSelectModal.jsx similarity index 97% rename from web/src/pages/Settings/Streams/TypeSelectModal/TypeSelectModal.jsx rename to web/src/components/CreateStreamModal/TypeSelectModal/TypeSelectModal.jsx index 0489ebb2d..4d2edca7a 100644 --- a/web/src/pages/Settings/Streams/TypeSelectModal/TypeSelectModal.jsx +++ b/web/src/components/CreateStreamModal/TypeSelectModal/TypeSelectModal.jsx @@ -25,7 +25,7 @@ const TypeSelectModal = ({ onClose, onSelect }) => { return ( <> - +
Select A Stream Type
diff --git a/web/src/pages/Settings/Streams/TypeSelectModal/TypeSelectModal.scss b/web/src/components/CreateStreamModal/TypeSelectModal/TypeSelectModal.scss similarity index 100% rename from web/src/pages/Settings/Streams/TypeSelectModal/TypeSelectModal.scss rename to web/src/components/CreateStreamModal/TypeSelectModal/TypeSelectModal.scss diff --git a/web/src/components/PresetsModal/PresetsModal.jsx b/web/src/components/PresetsModal/PresetsModal.jsx index 0bb6aecfa..ecfd183b1 100644 --- a/web/src/components/PresetsModal/PresetsModal.jsx +++ b/web/src/components/PresetsModal/PresetsModal.jsx @@ -6,8 +6,10 @@ import { useStatusStore } from "@/App"; import { useState } from "react"; import List from "@/components/List/List"; import ListItem from "@/components/List/ListItem/ListItem"; +import CreatePresetModal from "@/components/CreatePresetModal/CreatePresetModal"; import PropTypes from "prop-types"; +import RectangularAddButton from "../RectangularAddButton/RectangularAddButton"; const timeSince = (timeStamp) => { var now = new Date(), @@ -71,6 +73,7 @@ const PresetsModal = ({ onClose }) => { presets.map((preset) => {if(preset){return false;}}) // Changed this line so that preset wouldn't go unused as per eslint ); const setSystemState = useStatusStore((s) => s.setSystemState); + const [createModalOpen, setCreateModalOpen] = React.useState(false); // resize presetStates (without overriding) if length changes if (presetStates.length > presets.length) { @@ -118,6 +121,8 @@ const PresetsModal = ({ onClose }) => {
Select Preset
{presetItems} + {setCreateModalOpen(true); onClose();}} /> + { createModalOpen && {setCreateModalOpen(false);}} /> }
diff --git a/web/src/components/RectangularAddButton/RectangularAddButton.jsx b/web/src/components/RectangularAddButton/RectangularAddButton.jsx new file mode 100644 index 000000000..9f4b0912d --- /dev/null +++ b/web/src/components/RectangularAddButton/RectangularAddButton.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import "./RectangularAddButton.scss" + +import PropTypes from "prop-types"; + +export default function RectangularAddButton({onClick}){ + return( +
+ + +
+ ) +} +RectangularAddButton.propTypes = { + onClick: PropTypes.func.isRequired, +}; diff --git a/web/src/components/RectangularAddButton/RectangularAddButton.scss b/web/src/components/RectangularAddButton/RectangularAddButton.scss new file mode 100644 index 000000000..24e440ae7 --- /dev/null +++ b/web/src/components/RectangularAddButton/RectangularAddButton.scss @@ -0,0 +1,19 @@ +@use "src/general"; + +.rectangular-add-button { + // @include general.low-shadow; + width: 100%; + height: 4rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 3rem; + color: general.$controls-color; + border-color: general.$secondary; + border-style: solid; + border-radius: 1rem; + background-color: general.$bg; + + @include general.button-hover; +} diff --git a/web/src/components/StreamsModal/StreamsModal.jsx b/web/src/components/StreamsModal/StreamsModal.jsx index a65de8745..e0a3e29e7 100644 --- a/web/src/components/StreamsModal/StreamsModal.jsx +++ b/web/src/components/StreamsModal/StreamsModal.jsx @@ -10,6 +10,8 @@ import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; import Divider from "@mui/material/Divider"; +import RectangularAddButton from "@/components/RectangularAddButton/RectangularAddButton"; +import CreateStreamModal from "@/components/CreateStreamModal/CreateStreamModal"; import PropTypes from "prop-types"; @@ -44,6 +46,7 @@ const StreamsModal = ({ const status = useStatusStore((state) => state.status); const playingStreamIds = status.sources.filter( (s) => s.input !== 'None').map( (s) => parseInt(s.input.replace('stream=', ''))); const playingStreams = streams.filter( (s) => playingStreamIds.includes(s.id) ); + const [createModalOpen, setCreateModalOpen] = React.useState(false); let filteredStreams = streams.filter( (s) => !playingStreamIds.includes(s.id) && !s.disabled); // If there are any running bluetooth or FM streams, mark other instances as disabled - we do not (yet) @@ -133,6 +136,9 @@ const StreamsModal = ({ return ( {streamsList} + {setCreateModalOpen(true);}} /> + {setCreateModalOpen(false);}} /> + ); }; diff --git a/web/src/pages/Home/Home.jsx b/web/src/pages/Home/Home.jsx index 215a53bf4..26643acff 100644 --- a/web/src/pages/Home/Home.jsx +++ b/web/src/pages/Home/Home.jsx @@ -1,5 +1,6 @@ import React from "react"; import PlayerCardFb from "@/components/PlayerCard/PlayerCardFb"; +import RectangularAddButton from "@/components/RectangularAddButton/RectangularAddButton"; import "./Home.scss"; import { useStatusStore } from "@/App.jsx"; import ZonesModal from "@/components/ZonesModal/ZonesModal"; @@ -35,14 +36,7 @@ const PresetAndAdd = ({ if (cards.length < sources.length) { return (
-
{ - initSource(nextAvailableSource); - }} - > - + -
+ {initSource(nextAvailableSource);}} />
{ - const streamTemplate = StreamTemplates.filter((t) => t.type === type)[0]; - let stream = { type: type, disabled: false }; - streamTemplate.fields.forEach((field) => { - stream[field.name] = field.default; - }); - return stream; -}; +import StreamModal from "@/components/CreateStreamModal/StreamModal/StreamModal"; const applyStreamChanges = (stream) => { return fetch(`/api/streams/${stream.id}`, { @@ -31,14 +21,6 @@ const applyStreamChanges = (stream) => { }); }; -const makeNewStream = (stream) => { - return fetch("/api/stream", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(stream), - }); -}; - const deleteStream = (stream) => { fetch(`/api/streams/${stream.id}`, { method: "DELETE" }); }; @@ -76,9 +58,7 @@ StreamListItem.propTypes = { const Streams = ({ onClose }) => { const streams = useStatusStore((state) => state.status.streams); - const [showModal, setShowModal] = React.useState(false); const [showSelect, setShowSelect] = React.useState(false); - const [selectedType, setSelectedType] = React.useState(""); return (
@@ -95,28 +75,7 @@ const Streams = ({ onClose }) => {
- - {showSelect && ( - { - setShowSelect(false); - }} - onSelect={(type) => { - setSelectedType(type); - setShowSelect(false); - setShowModal(true); - }} - /> - )} - {showModal && ( - { - setShowModal(false); - }} - /> - )} + {setShowSelect(false);}} />
); };