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 0000000..785e701 Binary files /dev/null and b/frontend/src/assets/icons/plus-circle-fill.png differ 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 +}