diff --git a/ui/package.json b/ui/package.json
index 2b52fcd..5a87da3 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,6 +16,7 @@
"react-redux": "^8.0.1",
"react-scripts": "5.0.1",
"react-use-websocket": "^4.3.1",
+ "sass": "^1.58.3",
"typescript": "^4.6.0",
"web-vitals": "^2.1.0"
},
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index ee07272..fcd8b00 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import logo from './logo.svg';
import { RadioToggler } from './features/radio/Radio';
import './App.css';
@@ -7,7 +6,6 @@ function App() {
return (
-
diff --git a/ui/src/features/radio/Radio.module.css b/ui/src/features/radio/Radio.module.css
deleted file mode 100644
index 025bb72..0000000
--- a/ui/src/features/radio/Radio.module.css
+++ /dev/null
@@ -1,79 +0,0 @@
-.row {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.row > button {
- margin-left: 4px;
- margin-right: 8px;
-}
-
-.row:not(:last-child) {
- margin-bottom: 16px;
-}
-
-.value {
- font-size: 78px;
- padding-left: 16px;
- padding-right: 16px;
- margin-top: 2px;
- font-family: 'Courier New', Courier, monospace;
-}
-
-.button {
- appearance: none;
- background: none;
- font-size: 32px;
- padding-left: 12px;
- padding-right: 12px;
- outline: none;
- border: 2px solid transparent;
- color: rgb(112, 76, 182);
- padding-bottom: 4px;
- cursor: pointer;
- background-color: rgba(112, 76, 182, 0.1);
- border-radius: 2px;
- transition: all 0.15s;
-}
-
-.textbox {
- font-size: 32px;
- padding: 2px;
- width: 64px;
- text-align: center;
- margin-right: 4px;
-}
-
-.button:hover,
-.button:focus {
- border: 2px solid rgba(112, 76, 182, 0.4);
-}
-
-.button:active {
- background-color: rgba(112, 76, 182, 0.2);
-}
-
-.asyncButton {
- composes: button;
- position: relative;
-}
-
-.asyncButton:after {
- content: '';
- background-color: rgba(112, 76, 182, 0.15);
- display: block;
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- opacity: 0;
- transition: width 1s linear, opacity 0.5s ease 1s;
-}
-
-.asyncButton:active:after {
- width: 0%;
- opacity: 1;
- transition: 0s;
-}
diff --git a/ui/src/features/radio/Radio.module.scss b/ui/src/features/radio/Radio.module.scss
new file mode 100644
index 0000000..fb01bcc
--- /dev/null
+++ b/ui/src/features/radio/Radio.module.scss
@@ -0,0 +1,116 @@
+@-webkit-keyframes rotor{
+ from{ -webkit-transform: rotate(0deg); }
+ to{ -webkit-transform: rotate(360deg); }
+}
+@-moz-keyframes rotor{
+ from{ -moz-transform: rotate(0deg); }
+ to{ -moz-transform: rotate(360deg); }
+}
+@-o-keyframes rotor{
+ from{ -o-transform: rotate(0deg); }
+ to{ -o-transform: rotate(360deg); }
+}
+@keyframes rotor{
+ from{ transform: rotate(0deg); }
+ to{ transform: rotate(360deg); }
+}
+
+$circle-size: 350px;
+$inner-size: 310px;
+$border-radius: 160px;
+$margin: 20px;
+$shadow-size: 20px;
+$rotor-color-active: #6eeb34;
+$rotor-color-stopped: #ff0000;
+$rotor-color-loading: #fcc203;
+$disconnected-color: #8a8987;
+$hover-color: #fff7b0;
+
+@mixin wedge($color) {
+ background-image: linear-gradient(45deg,
+ white 0%,
+ white 30%,
+ $color 30%,
+ $color 70%,
+ white 70%,
+ white 100%);
+}
+
+@mixin rotor-animation {
+ -webkit-animation: rotor 1.5s linear 0s infinite normal;
+ -mox-animation: rotor 1.5s linear 0s infinite normal;
+ -o-animation: rotor 1.5s linear 0s infinite normal;
+ animation: rotor 1.5s linear 0s infinite normal;
+}
+
+@mixin circle-shape {
+ width: $circle-size;
+ height: $circle-size;
+ border-radius: $border-radius;
+ /*** outer circle position: under */
+ z-index: 1;
+ position: absolute;
+}
+
+.buttonWrap {
+ width: $circle-size;
+ height: $circle-size;
+ margin: $margin 0;
+ display: inline-block;
+ position: relative;
+
+ /* Inner circle */
+ .clicker {
+ width: $inner-size;
+ height: $inner-size; /* 20px smaller b/c of margin below */
+ margin: $margin;
+ background-color: #fff;
+ border-radius: $border-radius;
+
+ /* Overlays this circle on the .circle */
+ z-index: 2;
+ position: absolute;
+
+ /* centers the text: adjust to desired size */
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ flex-direction: column;
+ text-align: center;
+ font-size: 60px;
+ user-select: none;
+
+ /* shadow */
+ -webkit-box-shadow: 0px 0px $shadow-size 0px rgba(0, 0, 0, 0.5);
+ -moz-box-shadow: 0px 0px $shadow-size 0px rgba(0, 0, 0, 0.5);
+ -o-box-shadow: 0px 0px $shadow-size 0px rgba(0, 0, 0, 0.5);
+ box-shadow: 0px 0px $shadow-size 0px rgba(0, 0, 0, 0.5);
+
+ &:hover {
+ background-color: $hover-color;
+ }
+ }
+
+ /** outer circle **/
+ .circleDisconnected {
+ @include circle-shape;
+ background-color: $disconnected-color;
+ }
+
+ .circleStarted {
+ @include circle-shape;
+ @include wedge($rotor-color-active);
+ @include rotor-animation;
+ }
+
+ .circleLoading {
+ @include circle-shape;
+ @include wedge($rotor-color-loading);
+ @include rotor-animation;
+ }
+
+ .circleStopped {
+ @include circle-shape;
+ background-color: $rotor-color-stopped;
+ }
+}
diff --git a/ui/src/features/radio/Radio.tsx b/ui/src/features/radio/Radio.tsx
index 982b894..b8e0a4d 100644
--- a/ui/src/features/radio/Radio.tsx
+++ b/ui/src/features/radio/Radio.tsx
@@ -4,12 +4,7 @@ import useWebSocket from 'react-use-websocket';
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import { RadioState, ConnectionState } from '../../app/types';
import {
- setRadioStarted,
- setRadioStopped,
toggleRadioState,
- setConnected,
- setDisconnected,
- toggleConnection,
selectLoading,
selectRadio,
selectConnection,
@@ -19,6 +14,7 @@ import {
getOnWSClose,
getOnWSMessage,
} from './radioWS';
+import styles from './Radio.module.scss';
const getWSUrl = async () => {
const wsUrl = process.env.WS_ADDR ? process.env.WS_ADDR : "ws://localhost/liveness";
@@ -27,7 +23,7 @@ const getWSUrl = async () => {
};
export function RadioToggler() {
- const loadingState = useAppSelector(selectLoading);
+ const loading = useAppSelector(selectLoading);
const radioState = useAppSelector(selectRadio);
const connectionState = useAppSelector(selectConnection);
@@ -42,24 +38,39 @@ export function RadioToggler() {
const onWSMessage = getOnWSMessage(dispatch);
useEffect(() => {
if (lastJsonMessage != null) {
- onWSMessage(lastJsonMessage);
+ if (!loading) {
+ onWSMessage(lastJsonMessage);
+ }
}
});
- if (loadingState) {
- return Loading...
;
+ let buttonStatus = "";
+ let circleStyle = styles.circleDisconnected;
+ if (loading) {
+ buttonStatus = "Loading";
+ circleStyle = styles.circleLoading;
+ } else if (connectionState) {
+ switch (radioState) {
+ case RadioState.STARTED:
+ buttonStatus = "ON AIR";
+ circleStyle = styles.circleStarted;
+ break;
+ case RadioState.STOPPED:
+ buttonStatus = "OFFLINE";
+ circleStyle = styles.circleStopped;
+ break;
+ }
}
return (
-
-
-
+
);
}
diff --git a/ui/yarn.lock b/ui/yarn.lock
index c756b1e..101cca7 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -3053,7 +3053,7 @@ check-types@^11.1.1:
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.2.tgz#7afc0b6a860d686885062f2dba888ba5710335b4"
integrity sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==
-chokidar@^3.4.2, chokidar@^3.5.3:
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -4997,6 +4997,11 @@ immer@^9.0.16, immer@^9.0.7:
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b"
integrity sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==
+immutable@^4.0.0:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a"
+ integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==
+
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -7899,6 +7904,15 @@ sass-loader@^12.3.0:
klona "^2.0.4"
neo-async "^2.6.2"
+sass@^1.58.3:
+ version "1.58.3"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.3.tgz#2348cc052061ba4f00243a208b09c40e031f270d"
+ integrity sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==
+ dependencies:
+ chokidar ">=3.0.0 <4.0.0"
+ immutable "^4.0.0"
+ source-map-js ">=0.6.2 <2.0.0"
+
sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -8105,7 +8119,7 @@ source-list-map@^2.0.0, source-list-map@^2.0.1:
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
-source-map-js@^1.0.1, source-map-js@^1.0.2:
+"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==