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

4
.gitignore vendored
View file

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

87
app.go
View file

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

View file

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

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

View file

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

View file

@ -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 (
<div id='Dashboard'>
<div className='header'>
<div className='header-left'></div>
<div className='highlights'>
{/* <Highlight description={'Session'} type={'stopwatch'} value={createdOn} /> */}
<Highlight description={'Viewers'} type={'count'} value={watchingNow} />
<Highlight description={'Followers'} type={'count'} value={channelFollowers} />
<Highlight description={'Subscribers'} type={'count'} value={subscriberCount} />
</div>
<div className='header-right'></div>
<>
<div className='modal' style={{ zIndex: modalZ ? 10 : -10 }}>
<span>show this instead</span>
<button onClick={closeModal}>close</button>
</div>
<div className='main'>
<div className='main-left'>
<StreamActivity title={'Stream Activity'} events={activityEvents()} />
<div id='Dashboard'>
<div className='header'>
<div className='header-left'></div>
<div className='highlights'>
{/* <Highlight description={'Session'} type={'stopwatch'} value={createdOn} /> */}
<Highlight description={'Viewers'} type={'count'} value={watchingNow} />
<Highlight
description={'Followers'}
type={'count'}
value={channelFollowers}
/>
<Highlight
description={'Subscribers'}
type={'count'}
value={subscriberCount}
/>
</div>
<div className='header-right'></div>
</div>
<div className='main-right'>
<StreamChat title={'Stream Chat'} />
<div className='main'>
<div className='main-left'>
<StreamActivity title={'Stream Activity'} events={activityEvents()} />
</div>
<div className='main-right'>
<StreamChat title={'Stream Chat'} />
</div>
<div></div>
</div>
<div></div>
<StreamInfo
active={active}
channel={channelName !== '' ? channelName : username}
title={streamTitle}
categories={streamCategories}
likes={streamLikes}
live={streamLive}
dislikes={streamDislikes}
home={home}
play={startQuery}
pause={stopQuery}
settings={openModal}
/>
</div>
<StreamInfo
active={active}
channel={channelName}
title={streamTitle}
categories={streamCategories}
likes={streamLikes}
live={streamLive}
dislikes={streamDislikes}
play={startQuery}
pause={stopQuery}
/>
</div>
</>
);
}

View file

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

View file

@ -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 (
<div id='SignIn'>
<div className='signin-title'>
<div className='signin-header'>
<span className='signin-title-text'>Rum Goggles</span>
<span className='signin-title-subtext'>Rumble Stream Dashboard</span>
</div>
<div className='signin-center'>
<ChannelList channels={config.channels} openStreamDashboard={openStreamDashboard} />
</div>
<div className='signin-input-box'>
<label className='signin-label'>Stream Key:</label>
<label className='signin-label'>Add Channel</label>
<div className='signin-input-button'>
<input
id='StreamKey'
@ -43,7 +72,7 @@ function SignIn() {
</button>
</div>
</div>
<div className='signin-title'></div>
<div className='signin-footer'></div>
</div>
);
}

View file

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

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
}