From 0e97fe4ea7cba8b6d0bc63b66877020761f56692 Mon Sep 17 00:00:00 2001 From: tyler Date: Tue, 19 Dec 2023 16:26:11 -0500 Subject: [PATCH] Added configuration file and functionality to save channels --- .gitignore | 4 +- app.go | 87 ++++++++++++- frontend/src/assets/icons/index.jsx | 2 + .../src/assets/icons/plus-circle-fill.png | Bin 0 -> 4887 bytes frontend/src/components/ChannelList.css | 66 ++++++++++ frontend/src/components/ChannelList.jsx | 34 +++++ frontend/src/components/StreamInfo.jsx | 10 +- frontend/src/screens/Dashboard.css | 10 ++ frontend/src/screens/Dashboard.jsx | 118 +++++++++++++----- frontend/src/screens/SignIn.css | 31 ++++- frontend/src/screens/SignIn.jsx | 37 +++++- internal/api/api.go | 4 +- internal/config/config.go | 46 +++++++ 13 files changed, 400 insertions(+), 49 deletions(-) create mode 100644 frontend/src/assets/icons/plus-circle-fill.png create mode 100644 frontend/src/components/ChannelList.css create mode 100644 frontend/src/components/ChannelList.jsx create mode 100644 internal/config/config.go diff --git a/.gitignore b/.gitignore index 9f51806..e076fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ node_modules frontend/dist frontend/wailsjs -.prettierignore \ No newline at end of file +.prettierignore + +config.json \ No newline at end of file diff --git a/app.go b/app.go index cba1dda..31bb295 100644 --- a/app.go +++ b/app.go @@ -2,14 +2,26 @@ package main import ( "context" + "errors" "fmt" + "log" + "os" + "sync" "github.com/tylertravisty/go-utils/random" + "github.com/tylertravisty/rum-goggles/internal/config" + rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go" +) + +const ( + configFilepath = "./config.json" ) // App struct type App struct { - ctx context.Context + ctx context.Context + cfg *config.App + cfgMu sync.Mutex } // NewApp creates a new App application struct @@ -21,6 +33,79 @@ func NewApp() *App { // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx + err := a.loadConfig() + if err != nil { + // TODO: handle error better on startup + log.Fatal("error loading config: ", err) + } +} + +func (a *App) loadConfig() error { + cfg, err := config.Load(configFilepath) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("error loading config: %v", err) + } + + return a.newConfig() + } + + a.cfg = cfg + return nil +} + +func (a *App) newConfig() error { + cfg := &config.App{Channels: []config.Channel{}} + err := cfg.Save(configFilepath) + if err != nil { + return fmt.Errorf("error saving new config: %v", err) + } + + a.cfg = cfg + return nil +} + +func (a *App) Config() *config.App { + return a.cfg +} + +func (a *App) SaveConfig() error { + err := a.cfg.Save(configFilepath) + if err != nil { + // TODO: log error; return user error + return fmt.Errorf("Error saving config") + } + + return nil +} + +func (a *App) AddChannel(url string) (*config.App, error) { + client := rumblelivestreamlib.Client{StreamKey: url} + resp, err := client.Request() + if err != nil { + // TODO: log error + fmt.Println("error requesting api:", err) + return nil, fmt.Errorf("error querying API") + } + + name := resp.Username + if resp.ChannelName != "" { + name = resp.ChannelName + } + + channel := config.Channel{ApiUrl: url, Name: name} + + a.cfgMu.Lock() + defer a.cfgMu.Unlock() + a.cfg.Channels = append(a.cfg.Channels, channel) + err = a.cfg.Save(configFilepath) + if err != nil { + // TODO: log error + fmt.Println("error saving config:", err) + return nil, fmt.Errorf("error saving new channel") + } + + return a.cfg, nil } // Greet returns a greeting for the given name diff --git a/frontend/src/assets/icons/index.jsx b/frontend/src/assets/icons/index.jsx index e506d73..6e2b0b5 100644 --- a/frontend/src/assets/icons/index.jsx +++ b/frontend/src/assets/icons/index.jsx @@ -5,6 +5,7 @@ import heart from './heart-fill.png'; import house from './house.png'; import pause from './pause-fill.png'; import play from './play-fill.png'; +import plus_circle from './plus-circle-fill.png'; import star from './star-fill.png'; import thumbs_down from './hand-thumbs-down.png'; import thumbs_up from './hand-thumbs-up.png'; @@ -16,6 +17,7 @@ export const Heart = heart; export const House = house; export const Pause = pause; export const Play = play; +export const PlusCircle = plus_circle; export const Star = star; export const ThumbsDown = thumbs_down; export const ThumbsUp = thumbs_up; diff --git a/frontend/src/assets/icons/plus-circle-fill.png b/frontend/src/assets/icons/plus-circle-fill.png new file mode 100644 index 0000000000000000000000000000000000000000..785e701afcbcd2e54497d3a424f1d56d4b888ea7 GIT binary patch literal 4887 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 JPDHLkV1jPW>!|<$ literal 0 HcmV?d00001 diff --git a/frontend/src/components/ChannelList.css b/frontend/src/components/ChannelList.css new file mode 100644 index 0000000..7cbd1a9 --- /dev/null +++ b/frontend/src/components/ChannelList.css @@ -0,0 +1,66 @@ +.channel-list { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + width: 100%; +} + +.channel-list-title { + color: #85c742; + font-family: sans-serif; + font-size: 24px; + font-weight: bold; + padding: 5px; +} + +.channels { + background-color: white; + border: 1px solid #D6E0EA; + border-radius: 5px; + height: 100%; + overflow: auto; + width: 100%; +} + +.channel { + align-items: center; + /* border-top: 1px solid #D6E0EA; */ + display: flex; +} + +.channel-add { + background-color: #f3f5f8; + border: none; + padding: 10px; +} + +.channel-add:hover { + cursor: pointer; +} + +.channel-add-icon { + height: 36px; + width: 36px; +} + +.channel-button { + background-color: white; + border: none; + border-radius: 5px; + color: #061726; + font-family: sans-serif; + font-size: 24px; + font-weight: bold; + overflow: hidden; + padding: 10px 10px; + text-align: left; + white-space: nowrap; + width: 100%; +} + +.channel-button:hover { + background-color: #85c742; + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/ChannelList.jsx b/frontend/src/components/ChannelList.jsx new file mode 100644 index 0000000..ea11bca --- /dev/null +++ b/frontend/src/components/ChannelList.jsx @@ -0,0 +1,34 @@ +import { PlusCircle } from '../assets/icons'; +import './ChannelList.css'; + +function ChannelList(props) { + const sortChannelsAlpha = () => { + let sorted = [...props.channels].sort((a, b) => + a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 + ); + return sorted; + }; + + return ( +
+ Channels +
+ {sortChannelsAlpha().map((channel, index) => ( +
+ +
+ ))} +
+ {/* */} +
+ ); +} + +export default ChannelList; diff --git a/frontend/src/components/StreamInfo.jsx b/frontend/src/components/StreamInfo.jsx index ba0b68b..a25fe9d 100644 --- a/frontend/src/components/StreamInfo.jsx +++ b/frontend/src/components/StreamInfo.jsx @@ -26,10 +26,10 @@ function StreamInfo(props) {
- {props.live ? props.categories.primary.title : 'none'} + {props.live ? props.categories.primary.title : 'primary'} - {props.live ? props.categories.secondary.title : 'none'} + {props.live ? props.categories.secondary.title : 'secondary'}
@@ -54,17 +54,17 @@ function StreamInfo(props) {
- -
diff --git a/frontend/src/screens/Dashboard.css b/frontend/src/screens/Dashboard.css index 0f135af..41750f6 100644 --- a/frontend/src/screens/Dashboard.css +++ b/frontend/src/screens/Dashboard.css @@ -46,6 +46,16 @@ height: 100%; } +.modal { + background-color: white; + color: red; + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; +} + .highlights { align-items: center; display: flex; diff --git a/frontend/src/screens/Dashboard.jsx b/frontend/src/screens/Dashboard.jsx index 04180ef..98c3ff0 100644 --- a/frontend/src/screens/Dashboard.jsx +++ b/frontend/src/screens/Dashboard.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import { Start, Stop } from '../../wailsjs/go/api/Api'; import './Dashboard.css'; @@ -10,12 +10,15 @@ import StreamEvent from '../components/StreamEvent'; import StreamActivity from '../components/StreamActivity'; import StreamChat from '../components/StreamChat'; import StreamInfo from '../components/StreamInfo'; +import { NavSignIn } from './Navigation'; function Dashboard() { const location = useLocation(); + const navigate = useNavigate(); const [refresh, setRefresh] = useState(false); const [active, setActive] = useState(false); const [streamKey, setStreamKey] = useState(location.state.streamKey); + const [username, setUsername] = useState(''); const [channelName, setChannelName] = useState(''); const [followers, setFollowers] = useState({}); const [totalFollowers, setTotalFollowers] = useState(0); @@ -34,6 +37,7 @@ function Dashboard() { const [streamTitle, setStreamTitle] = useState(''); const [watchingNow, setWatchingNow] = useState(0); const [createdOn, setCreatedOn] = useState(''); + const [modalZ, setModalZ] = useState(false); useEffect(() => { console.log('use effect start'); @@ -44,6 +48,7 @@ function Dashboard() { console.log('query response received'); setRefresh(!refresh); setActive(true); + setUsername(response.username); setChannelName(response.channel_name); setFollowers(response.followers); setChannelFollowers(response.followers.num_followers); @@ -64,19 +69,40 @@ function Dashboard() { setStreamLive(false); } }); + + EventsOn('QueryResponseError', (error) => { + console.log('Query response error:', error); + setActive(false); + }); }, []); + const home = () => { + Stop() + .then(() => setActive(false)) + .then(() => { + navigate(NavSignIn); + }) + .catch((err) => { + console.log('Stop error:', err); + }); + }; + const startQuery = () => { console.log('start'); - Start(streamKey); - setActive(true); + Start(streamKey) + .then(() => { + setActive(true); + }) + .catch((err) => { + console.log('Start error:', err); + }); }; const stopQuery = () => { console.log('stop'); - Stop(); - // EventsEmit('StopQuery'); - setActive(false); + Stop().then(() => { + setActive(false); + }); }; const activityDate = (activity) => { @@ -95,39 +121,63 @@ function Dashboard() { return sorted; }; + const openModal = () => { + setModalZ(true); + }; + + const closeModal = () => { + setModalZ(false); + }; + return ( -
-
-
-
- {/* */} - - - -
-
+ <> +
+ show this instead +
-
-
- +
+
+
+
+ {/* */} + + + +
+
-
- +
+
+ +
+
+ +
+
-
+
- -
+ ); } diff --git a/frontend/src/screens/SignIn.css b/frontend/src/screens/SignIn.css index 961bfa8..3e08e32 100644 --- a/frontend/src/screens/SignIn.css +++ b/frontend/src/screens/SignIn.css @@ -12,6 +12,7 @@ flex-direction: column; justify-content: center; align-items: center; + padding: 10px 0px; width: 50%; } @@ -53,6 +54,15 @@ background-color: #77b23b; } +.signin-center { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + height: 50%; + width: 50%; +} + .signin-show { display: flex; align-items: center; @@ -75,15 +85,30 @@ } .signin-label { + color: #061726; + display: flex; font-family: sans-serif; font-weight: bold; - color: #061726; - justify-content: flex-start; + justify-content: center; + padding: 5px; text-transform: uppercase; width: 100%; } -.signin-title { +.signin-header { + align-items: center; + color: #061726; + display: flex; + flex-direction: column; + font-family: sans-serif; + font-weight: bold; + height: 10%; + justify-content: center; + margin: 20px; + text-align: center; +} + +.signin-footer { align-items: center; color: #061726; display: flex; diff --git a/frontend/src/screens/SignIn.jsx b/frontend/src/screens/SignIn.jsx index 6888fc6..a8a8072 100644 --- a/frontend/src/screens/SignIn.jsx +++ b/frontend/src/screens/SignIn.jsx @@ -1,28 +1,57 @@ import { useEffect, useState } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; import { NavDashboard } from './Navigation'; +import { AddChannel, Config } from '../../wailsjs/go/main/App'; import { Eye, EyeSlash } from '../assets/icons'; import './SignIn.css'; +import ChannelList from '../components/ChannelList'; function SignIn() { const navigate = useNavigate(); + const [config, setConfig] = useState({ channels: [] }); const [streamKey, setStreamKey] = useState(''); const updateStreamKey = (event) => setStreamKey(event.target.value); const [showStreamKey, setShowStreamKey] = useState(false); const updateShowStreamKey = () => setShowStreamKey(!showStreamKey); + useEffect(() => { + Config() + .then((response) => { + console.log(response); + setConfig(response); + }) + .catch((err) => { + console.log('error getting config', err); + }); + }, []); + const saveStreamKey = () => { - navigate(NavDashboard, { state: { streamKey: streamKey } }); + AddChannel(streamKey) + .then((response) => { + console.log(response); + setConfig(response); + setStreamKey(''); + }) + .catch((err) => { + console.log('error adding channel', err); + }); + }; + + const openStreamDashboard = (key) => { + navigate(NavDashboard, { state: { streamKey: key } }); }; return (
-
+
Rum Goggles Rumble Stream Dashboard
+
+ +
- +
-
+
); } diff --git a/internal/api/api.go b/internal/api/api.go index c6966fb..99a78fa 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -91,7 +91,9 @@ func (a *Api) query(url string) { if err != nil { // TODO: log error fmt.Println("client.Request err:", err) - // a.Stop() + a.Stop() + runtime.EventsEmit(a.ctx, "QueryResponseError", "Failed to query API") + return } // resp := &rumblelivestreamlib.LivestreamResponse{} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..7e8e351 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,46 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" +) + +type Channel struct { + ApiUrl string `json:"api_url"` + Name string `json:"name"` +} + +type App struct { + Channels []Channel `json:"channels"` +} + +func Load(filepath string) (*App, error) { + f, err := os.Open(filepath) + if err != nil { + return nil, fmt.Errorf("config: error opening file: %w", err) + } + + var app App + decoder := json.NewDecoder(f) + err = decoder.Decode(&app) + if err != nil { + return nil, fmt.Errorf("config: error decoding file into json: %v", err) + } + + return &app, nil +} + +func (app *App) Save(filepath string) error { + b, err := json.MarshalIndent(app, "", "\t") + if err != nil { + return fmt.Errorf("config: error encoding config into json: %v", err) + } + + err = os.WriteFile(filepath, b, 0666) + if err != nil { + return fmt.Errorf("config: error writing config file: %v", err) + } + + return nil +}