Added configuration file and functionality to save channels
This commit is contained in:
parent
7b83b321fe
commit
0e97fe4ea7
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,3 +4,5 @@ frontend/dist
|
|||
frontend/wailsjs
|
||||
|
||||
.prettierignore
|
||||
|
||||
config.json
|
85
app.go
85
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
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
|
|
BIN
frontend/src/assets/icons/plus-circle-fill.png
Normal file
BIN
frontend/src/assets/icons/plus-circle-fill.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
66
frontend/src/components/ChannelList.css
Normal file
66
frontend/src/components/ChannelList.css
Normal 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;
|
||||
}
|
34
frontend/src/components/ChannelList.jsx
Normal file
34
frontend/src/components/ChannelList.jsx
Normal 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;
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
Start(streamKey)
|
||||
.then(() => {
|
||||
setActive(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Start error:', err);
|
||||
});
|
||||
};
|
||||
|
||||
const stopQuery = () => {
|
||||
console.log('stop');
|
||||
Stop();
|
||||
// EventsEmit('StopQuery');
|
||||
Stop().then(() => {
|
||||
setActive(false);
|
||||
});
|
||||
};
|
||||
|
||||
const activityDate = (activity) => {
|
||||
|
@ -95,15 +121,36 @@ function Dashboard() {
|
|||
return sorted;
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setModalZ(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalZ(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='modal' style={{ zIndex: modalZ ? 10 : -10 }}>
|
||||
<span>show this instead</span>
|
||||
<button onClick={closeModal}>close</button>
|
||||
</div>
|
||||
<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} />
|
||||
<Highlight
|
||||
description={'Followers'}
|
||||
type={'count'}
|
||||
value={channelFollowers}
|
||||
/>
|
||||
<Highlight
|
||||
description={'Subscribers'}
|
||||
type={'count'}
|
||||
value={subscriberCount}
|
||||
/>
|
||||
</div>
|
||||
<div className='header-right'></div>
|
||||
</div>
|
||||
|
@ -118,16 +165,19 @@ function Dashboard() {
|
|||
</div>
|
||||
<StreamInfo
|
||||
active={active}
|
||||
channel={channelName}
|
||||
channel={channelName !== '' ? channelName : username}
|
||||
title={streamTitle}
|
||||
categories={streamCategories}
|
||||
likes={streamLikes}
|
||||
live={streamLive}
|
||||
dislikes={streamDislikes}
|
||||
home={home}
|
||||
play={startQuery}
|
||||
pause={stopQuery}
|
||||
settings={openModal}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
46
internal/config/config.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue