Added configuration file and functionality to save channels

This commit is contained in:
tyler 2023-12-19 16:26:11 -05:00
parent 7b83b321fe
commit 0e97fe4ea7
13 changed files with 400 additions and 49 deletions

2
.gitignore vendored
View file

@ -4,3 +4,5 @@ frontend/dist
frontend/wailsjs frontend/wailsjs
.prettierignore .prettierignore
config.json

85
app.go
View file

@ -2,14 +2,26 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log"
"os"
"sync"
"github.com/tylertravisty/go-utils/random" "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 // App struct
type App struct { type App struct {
ctx context.Context ctx context.Context
cfg *config.App
cfgMu sync.Mutex
} }
// NewApp creates a new App application struct // NewApp creates a new App application struct
@ -21,6 +33,79 @@ func NewApp() *App {
// so we can call the runtime methods // so we can call the runtime methods
func (a *App) startup(ctx context.Context) { func (a *App) startup(ctx context.Context) {
a.ctx = ctx 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 // Greet returns a greeting for the given name

View file

@ -5,6 +5,7 @@ import heart from './heart-fill.png';
import house from './house.png'; import house from './house.png';
import pause from './pause-fill.png'; import pause from './pause-fill.png';
import play from './play-fill.png'; import play from './play-fill.png';
import plus_circle from './plus-circle-fill.png';
import star from './star-fill.png'; import star from './star-fill.png';
import thumbs_down from './hand-thumbs-down.png'; import thumbs_down from './hand-thumbs-down.png';
import thumbs_up from './hand-thumbs-up.png'; import thumbs_up from './hand-thumbs-up.png';
@ -16,6 +17,7 @@ export const Heart = heart;
export const House = house; export const House = house;
export const Pause = pause; export const Pause = pause;
export const Play = play; export const Play = play;
export const PlusCircle = plus_circle;
export const Star = star; export const Star = star;
export const ThumbsDown = thumbs_down; export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up; export const ThumbsUp = thumbs_up;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -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;
}

View file

@ -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 (
<div className='channel-list'>
<span className='channel-list-title'>Channels</span>
<div className='channels'>
{sortChannelsAlpha().map((channel, index) => (
<div className='channel' style={index === 0 ? { borderTop: 'none' } : {}}>
<button
className='channel-button'
onClick={() => props.openStreamDashboard(channel.api_url)}
>
{channel.name}
</button>
</div>
))}
</div>
{/* <button className='channel-add'>
<img className='channel-add-icon' src={PlusCircle} />
</button> */}
</div>
);
}
export default ChannelList;

View file

@ -26,10 +26,10 @@ function StreamInfo(props) {
<div className='stream-info-subtitle'> <div className='stream-info-subtitle'>
<div className='stream-info-categories'> <div className='stream-info-categories'>
<span className='stream-info-category'> <span className='stream-info-category'>
{props.live ? props.categories.primary.title : 'none'} {props.live ? props.categories.primary.title : 'primary'}
</span> </span>
<span className='stream-info-category'> <span className='stream-info-category'>
{props.live ? props.categories.secondary.title : 'none'} {props.live ? props.categories.secondary.title : 'secondary'}
</span> </span>
</div> </div>
<div className='stream-info-likes'> <div className='stream-info-likes'>
@ -54,17 +54,17 @@ function StreamInfo(props) {
<div className='stream-info-footer'> <div className='stream-info-footer'>
<div></div> <div></div>
<div className='stream-info-controls'> <div className='stream-info-controls'>
<button className='stream-info-control-button'> <button className='stream-info-control-button' onClick={props.home}>
<img className='stream-info-control' src={House} /> <img className='stream-info-control' src={House} />
</button> </button>
<button className='stream-info-control-button'> <button className='stream-info-control-button'>
<img <img
onClick={props.active ? props.pause : props.play}
className='stream-info-control' className='stream-info-control'
onClick={props.active ? props.pause : props.play}
src={props.active ? Pause : Play} src={props.active ? Pause : Play}
/> />
</button> </button>
<button className='stream-info-control-button'> <button className='stream-info-control-button' onClick={props.settings}>
<img className='stream-info-control' src={Gear} /> <img className='stream-info-control' src={Gear} />
</button> </button>
</div> </div>

View file

@ -46,6 +46,16 @@
height: 100%; height: 100%;
} }
.modal {
background-color: white;
color: red;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
.highlights { .highlights {
align-items: center; align-items: center;
display: flex; display: flex;

View file

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; 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 { Start, Stop } from '../../wailsjs/go/api/Api';
import './Dashboard.css'; import './Dashboard.css';
@ -10,12 +10,15 @@ import StreamEvent from '../components/StreamEvent';
import StreamActivity from '../components/StreamActivity'; import StreamActivity from '../components/StreamActivity';
import StreamChat from '../components/StreamChat'; import StreamChat from '../components/StreamChat';
import StreamInfo from '../components/StreamInfo'; import StreamInfo from '../components/StreamInfo';
import { NavSignIn } from './Navigation';
function Dashboard() { function Dashboard() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [streamKey, setStreamKey] = useState(location.state.streamKey); const [streamKey, setStreamKey] = useState(location.state.streamKey);
const [username, setUsername] = useState('');
const [channelName, setChannelName] = useState(''); const [channelName, setChannelName] = useState('');
const [followers, setFollowers] = useState({}); const [followers, setFollowers] = useState({});
const [totalFollowers, setTotalFollowers] = useState(0); const [totalFollowers, setTotalFollowers] = useState(0);
@ -34,6 +37,7 @@ function Dashboard() {
const [streamTitle, setStreamTitle] = useState(''); const [streamTitle, setStreamTitle] = useState('');
const [watchingNow, setWatchingNow] = useState(0); const [watchingNow, setWatchingNow] = useState(0);
const [createdOn, setCreatedOn] = useState(''); const [createdOn, setCreatedOn] = useState('');
const [modalZ, setModalZ] = useState(false);
useEffect(() => { useEffect(() => {
console.log('use effect start'); console.log('use effect start');
@ -44,6 +48,7 @@ function Dashboard() {
console.log('query response received'); console.log('query response received');
setRefresh(!refresh); setRefresh(!refresh);
setActive(true); setActive(true);
setUsername(response.username);
setChannelName(response.channel_name); setChannelName(response.channel_name);
setFollowers(response.followers); setFollowers(response.followers);
setChannelFollowers(response.followers.num_followers); setChannelFollowers(response.followers.num_followers);
@ -64,19 +69,40 @@ function Dashboard() {
setStreamLive(false); 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 = () => { const startQuery = () => {
console.log('start'); console.log('start');
Start(streamKey); Start(streamKey)
.then(() => {
setActive(true); setActive(true);
})
.catch((err) => {
console.log('Start error:', err);
});
}; };
const stopQuery = () => { const stopQuery = () => {
console.log('stop'); console.log('stop');
Stop(); Stop().then(() => {
// EventsEmit('StopQuery');
setActive(false); setActive(false);
});
}; };
const activityDate = (activity) => { const activityDate = (activity) => {
@ -95,15 +121,36 @@ function Dashboard() {
return sorted; return sorted;
}; };
const openModal = () => {
setModalZ(true);
};
const closeModal = () => {
setModalZ(false);
};
return ( return (
<>
<div className='modal' style={{ zIndex: modalZ ? 10 : -10 }}>
<span>show this instead</span>
<button onClick={closeModal}>close</button>
</div>
<div id='Dashboard'> <div id='Dashboard'>
<div className='header'> <div className='header'>
<div className='header-left'></div> <div className='header-left'></div>
<div className='highlights'> <div className='highlights'>
{/* <Highlight description={'Session'} type={'stopwatch'} value={createdOn} /> */} {/* <Highlight description={'Session'} type={'stopwatch'} value={createdOn} /> */}
<Highlight description={'Viewers'} type={'count'} value={watchingNow} /> <Highlight description={'Viewers'} type={'count'} value={watchingNow} />
<Highlight description={'Followers'} type={'count'} value={channelFollowers} /> <Highlight
<Highlight description={'Subscribers'} type={'count'} value={subscriberCount} /> description={'Followers'}
type={'count'}
value={channelFollowers}
/>
<Highlight
description={'Subscribers'}
type={'count'}
value={subscriberCount}
/>
</div> </div>
<div className='header-right'></div> <div className='header-right'></div>
</div> </div>
@ -118,16 +165,19 @@ function Dashboard() {
</div> </div>
<StreamInfo <StreamInfo
active={active} active={active}
channel={channelName} channel={channelName !== '' ? channelName : username}
title={streamTitle} title={streamTitle}
categories={streamCategories} categories={streamCategories}
likes={streamLikes} likes={streamLikes}
live={streamLive} live={streamLive}
dislikes={streamDislikes} dislikes={streamDislikes}
home={home}
play={startQuery} play={startQuery}
pause={stopQuery} pause={stopQuery}
settings={openModal}
/> />
</div> </div>
</>
); );
} }

View file

@ -12,6 +12,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 10px 0px;
width: 50%; width: 50%;
} }
@ -53,6 +54,15 @@
background-color: #77b23b; background-color: #77b23b;
} }
.signin-center {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
height: 50%;
width: 50%;
}
.signin-show { .signin-show {
display: flex; display: flex;
align-items: center; align-items: center;
@ -75,15 +85,30 @@
} }
.signin-label { .signin-label {
color: #061726;
display: flex;
font-family: sans-serif; font-family: sans-serif;
font-weight: bold; font-weight: bold;
color: #061726; justify-content: center;
justify-content: flex-start; padding: 5px;
text-transform: uppercase; text-transform: uppercase;
width: 100%; 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; align-items: center;
color: #061726; color: #061726;
display: flex; display: flex;

View file

@ -1,28 +1,57 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { NavDashboard } from './Navigation'; import { NavDashboard } from './Navigation';
import { AddChannel, Config } from '../../wailsjs/go/main/App';
import { Eye, EyeSlash } from '../assets/icons'; import { Eye, EyeSlash } from '../assets/icons';
import './SignIn.css'; import './SignIn.css';
import ChannelList from '../components/ChannelList';
function SignIn() { function SignIn() {
const navigate = useNavigate(); const navigate = useNavigate();
const [config, setConfig] = useState({ channels: [] });
const [streamKey, setStreamKey] = useState(''); const [streamKey, setStreamKey] = useState('');
const updateStreamKey = (event) => setStreamKey(event.target.value); const updateStreamKey = (event) => setStreamKey(event.target.value);
const [showStreamKey, setShowStreamKey] = useState(false); const [showStreamKey, setShowStreamKey] = useState(false);
const updateShowStreamKey = () => setShowStreamKey(!showStreamKey); const updateShowStreamKey = () => setShowStreamKey(!showStreamKey);
useEffect(() => {
Config()
.then((response) => {
console.log(response);
setConfig(response);
})
.catch((err) => {
console.log('error getting config', err);
});
}, []);
const saveStreamKey = () => { 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 ( return (
<div id='SignIn'> <div id='SignIn'>
<div className='signin-title'> <div className='signin-header'>
<span className='signin-title-text'>Rum Goggles</span> <span className='signin-title-text'>Rum Goggles</span>
<span className='signin-title-subtext'>Rumble Stream Dashboard</span> <span className='signin-title-subtext'>Rumble Stream Dashboard</span>
</div> </div>
<div className='signin-center'>
<ChannelList channels={config.channels} openStreamDashboard={openStreamDashboard} />
</div>
<div className='signin-input-box'> <div className='signin-input-box'>
<label className='signin-label'>Stream Key:</label> <label className='signin-label'>Add Channel</label>
<div className='signin-input-button'> <div className='signin-input-button'>
<input <input
id='StreamKey' id='StreamKey'
@ -43,7 +72,7 @@ function SignIn() {
</button> </button>
</div> </div>
</div> </div>
<div className='signin-title'></div> <div className='signin-footer'></div>
</div> </div>
); );
} }

View file

@ -91,7 +91,9 @@ func (a *Api) query(url string) {
if err != nil { if err != nil {
// TODO: log error // TODO: log error
fmt.Println("client.Request err:", err) fmt.Println("client.Request err:", err)
// a.Stop() a.Stop()
runtime.EventsEmit(a.ctx, "QueryResponseError", "Failed to query API")
return
} }
// resp := &rumblelivestreamlib.LivestreamResponse{} // resp := &rumblelivestreamlib.LivestreamResponse{}

46
internal/config/config.go Normal file
View file

@ -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
}