Compare commits

...

24 commits
v0 ... main

Author SHA1 Message Date
tyler 71fb9a28a3 Removed github directory 2024-06-14 13:57:28 -04:00
tyler daceefe548 Migrated from github 2024-06-14 13:49:03 -04:00
tyler 3f29337fd6 Really fixed WSOD when deleting account/channel 2024-06-01 14:42:09 -04:00
tyler 983c031325 Fixed WSOD bug when deleting account/channel and details becomes null before delete modal is removed 2024-06-01 14:18:32 -04:00
tyler 0bf60429ec Fixed bug deleting pages 2024-06-01 13:31:06 -04:00
tyler 04424b4abb Fixed bug when deleting pages; fixed UI bug when updating chat bot 2024-06-01 12:58:38 -04:00
tyler f9a5ae7c01 Updated rumble-livestream-lib-go 2024-05-31 11:39:25 -04:00
tyler 142ced1d3a Deactivate page on delete 2024-05-29 10:52:53 -04:00
tyler bb5098f472 Added note 2024-05-29 10:38:59 -04:00
tyler aea79d55bf Added check in event menu to make sure user chooses an account or channel on follow event 2024-05-28 17:45:05 -04:00
tyler e1fcceb721 Fixed bug in event rant options where min rant would change to string 2024-05-28 17:21:49 -04:00
tyler 562b90ebf7 Implemented chatbot event triggers on follow, raid, rant, and subscribe 2024-05-28 16:44:31 -04:00
tyler 1e87346086 Merge branch 'main' into v1 2024-05-22 13:00:57 -04:00
tyler 47437dbfbb Commit notes 2024-05-22 12:54:36 -04:00
tyler 14ef632e6c Feature parity with v0 2024-05-22 12:51:46 -04:00
tyler 2906a372d5 Added chatbot functionality 2024-05-02 15:30:25 -04:00
tyler 79031a1978 Migrated api package into events package 2024-04-10 15:19:00 -04:00
tyler 419e67ff21 Updated icons and licenses 2024-04-05 11:05:17 -04:00
tyler e7ca3cfbf0 Include notes 2024-04-04 10:47:56 -04:00
tyler b97012ed21 Implemented dashboard and login/logout 2024-04-04 10:46:14 -04:00
tyler e68567c010 Implemented functionality to add accounts and channels 2024-03-20 12:36:45 -04:00
tyler 08d6bc3782 Created account service; implemented sign in 2024-02-24 16:00:04 -05:00
tyler 954af040d1 Removed default README 2024-02-23 12:12:15 -05:00
tyler 7d3c6e34ec Started v1 2024-02-23 12:10:39 -05:00
1538 changed files with 289046 additions and 4683 deletions

View file

@ -1,34 +0,0 @@
name: Wails build Windows
on:
workflow_dispatch:
env:
# Necessary for most environments as build failure can occur due to OOM issues
NODE_OPTIONS: "--max-old-space-size=4096"
jobs:
build:
strategy:
# Failure in one platform build won't impact the others
fail-fast: false
matrix:
build:
- name: 'rum-goggles'
platform: 'windows/amd64'
os: 'windows-latest'
runs-on: ${{ matrix.build.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: Build wails
uses: tylertravisty/wails-build-action@v0.1.0
id: build
with:
build-name: ${{ matrix.build.name }}
build-platform: ${{ matrix.build.platform }}
go-version: '1.20'

9
.gitignore vendored
View file

@ -1,8 +1 @@
build/bin/ .prettierignore
node_modules
frontend/dist
frontend/wailsjs
.prettierignore
config.json

View file

@ -1,5 +1,93 @@
# Roadmap
Rum Goggles App:
v1-beta:
- Chat polls
- Stream statistics
v1.N:
- Download subscriber data, track re-subs, trigger rules on re-subs
- Stream moderator bot
- OBS integration
Rum Goggles Service:
- Chat polls
- Monitor all live stream chats
- Channel points
- Spam/Troll tracking
# Bugs
Chat bot rule menu back button does not work in macOS
If connection to chat stream breaks, gracefully handle error
- try to reconnect
- let the chat rules know, etc.
- test with VPN
# Doing # Doing
Add bypass to commands for:
- Host, admin, mod, etc.
Monitor how many handlers are listening to a producer.
- If producer.Stop is called, subtract from count.
- If count == 0, stop producer
Change API producer to monitor changes and only send new events, one at a time, to app, instead of the entire response; create datatype for single API event
- update chatbot and page handlers to use single API events
- page details should add to activity list one at a time
- store page list in Go, send entire list to frontend on updates
- list can be updated by any producer
Add timeouts to event triggers to prevent rate limit?
Don't stop rule if chat error is 429 Too Many Requests
Check if sender is logged in before running rule. If not, return rule error.
Add max rant amount for commands
Button to export log file -> user selects folder
Style scroll bars on Windows
- WebView2 issue
Indicator in chatbot that producer is running
- this can be in many different places as needed
Custom stream moderator rules
- block on keywords
- block on regex/pattern
- blocks can be: timed, stream, forever
Next steps:
- enable/disable rules from starting, including start all/stop all buttons
- delete page needs to handle new architecture
- app.producers.Active(*name) instead of app.producers.ApiP.Active(*name)
- app.producers.Stop(*name)
- activatePage: verify defer page.activeMu.Unlock does not conflict with display function
- in chatbot list, show indicator if any rules in chatbot are running
For Dashboard page,
- Api or chat producer could error, need to be able to start/restart both handlers on error
- Show user error if api or chat stop/error
On API errors
- include backoff multiple, if exceeded then stop API
Add option to delete API key for accounts?
Add better styles/icon to account details menu
Start screen:
- check for new updates and tell user
Trigger on event from API vs. trigger on event from chat
- Chat bot trigger on follow requires API key
- Give user warning when setting up trigger on follow that it will only work with accounts/channels for which user has saved an API key
Reset session information in config on logout Reset session information in config on logout
Show error when choosing file "chooseFile" Show error when choosing file "chooseFile"
@ -14,7 +102,7 @@ Update
- github.com/rhysd/go-github-selfupdate - github.com/rhysd/go-github-selfupdate
- github.com/inconshreveable/go-update - github.com/inconshreveable/go-update
Create loading indicator before API is called When API key is added, loading indicator freezes all user interactions. Need to give user a graceful way to stop/close add channel if it breaks.
If api query returns error: If api query returns error:
- stop interval - stop interval
@ -33,3 +121,6 @@ User settings:
- API query timer (default: 2s) - API query timer (default: 2s)
# To Do # To Do
Currently relies on Rumble to manage account username case-sensitivity.
- Change database table to use UNIQUE COLLATE NOCASE on account username

563
app.go
View file

@ -1,563 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"sync"
"time"
"github.com/tylertravisty/rum-goggles/internal/api"
"github.com/tylertravisty/rum-goggles/internal/chatbot"
"github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type chat struct {
username string
password string
url string
}
// App struct
type App struct {
ctx context.Context
cfg *config.App
cfgMu sync.Mutex
api *api.Api
apiMu sync.Mutex
cb *chatbot.ChatBot
cbMu sync.Mutex
logError *log.Logger
logInfo *log.Logger
}
// NewApp creates a new App application struct
func NewApp() *App {
app := &App{}
err := app.initLog()
if err != nil {
log.Fatal("error initializing log:", err)
}
app.api = api.NewApi(app.logError, app.logInfo)
return app
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.api.Startup(ctx)
err := a.loadConfig()
if err != nil {
a.logError.Fatal("error loading config: ", err)
}
}
func (a *App) initLog() error {
f, err := config.LogFile()
if err != nil {
return fmt.Errorf("error opening log file: %v", err)
}
a.logInfo = log.New(f, "[info]", log.LstdFlags|log.Lshortfile)
a.logError = log.New(f, "[error]", log.LstdFlags|log.Lshortfile)
return nil
}
func (a *App) loadConfig() error {
cfg, err := config.Load()
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: map[string]config.Channel{}}
err := cfg.Save()
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) AddChannel(url string) (*config.App, error) {
client := rumblelivestreamlib.Client{ApiKey: url}
resp, err := client.Request()
if err != nil {
a.logError.Println("error executing api request:", err)
return nil, fmt.Errorf("Error querying API. Verify key and try again.")
}
name := resp.Username
if resp.ChannelName != "" {
name = resp.ChannelName
}
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
_, err = a.cfg.NewChannel(url, name)
if err != nil {
a.logError.Println("error creating new channel:", err)
return nil, fmt.Errorf("Error creating new channel. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving channel information. Try again.")
}
return a.cfg, nil
}
func (a *App) ChatBotMessages(cid string) (map[string]config.ChatMessage, error) {
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return nil, fmt.Errorf("Cannot find channel. Try reloading.")
}
return channel.ChatBot.Messages, nil
}
func (a *App) AddChatMessage(cid string, cm config.ChatMessage) (map[string]config.ChatMessage, error) {
var err error
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
_, err = a.cfg.NewChatMessage(cid, cm)
if err != nil {
a.logError.Println("error creating new chat:", err)
return nil, fmt.Errorf("Error creating new chat message. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving chat message information. Try again.")
}
a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
return a.cfg.Channels[cid].ChatBot.Messages, nil
}
func (a *App) DeleteChatMessage(cid string, cm config.ChatMessage) (map[string]config.ChatMessage, error) {
a.cbMu.Lock()
if a.cb != nil {
err := a.cb.StopMessage(cm.ID)
if err != nil {
a.logError.Println("error stopping chat bot message:", err)
return nil, fmt.Errorf("Error stopping message. Try Again.")
}
}
a.cbMu.Unlock()
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
err := a.cfg.DeleteChatMessage(cid, cm)
if err != nil {
a.logError.Println("error deleting chat message:", err)
return nil, fmt.Errorf("Error deleting chat message. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving chat message information. Try again.")
}
a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
return a.cfg.Channels[cid].ChatBot.Messages, nil
}
func (a *App) UpdateChatMessage(cid string, cm config.ChatMessage) (map[string]config.ChatMessage, error) {
var err error
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
_, err = a.cfg.UpdateChatMessage(cid, cm)
if err != nil {
a.logError.Println("error updating chat message:", err)
return nil, fmt.Errorf("Error updating chat message. Try again.")
}
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return nil, fmt.Errorf("Error saving chat message information. Try again.")
}
a.updateChatBotConfig(a.cfg.Channels[cid].ChatBot)
return a.cfg.Channels[cid].ChatBot.Messages, nil
}
type NewChatBotResponse struct {
LoggedIn bool `json:"logged_in"`
StreamUrl string `json:"stream_url"`
Username string `json:"username"`
}
func (a *App) GetChatBot(cid string) (NewChatBotResponse, error) {
if a.cb == nil {
return NewChatBotResponse{}, fmt.Errorf("Chat bot not initalized.")
}
loggedIn, err := a.cb.LoggedIn()
if err != nil {
a.logError.Println("error checking if chat bot is logged in:", err)
return NewChatBotResponse{}, fmt.Errorf("Error checking if chat bot is logged in. Try again.")
}
return NewChatBotResponse{loggedIn, a.cb.Cfg.Session.Client.LiveStreamUrl, a.cb.Cfg.Session.Username}, nil
}
func (a *App) NewChatBot(cid string) (NewChatBotResponse, error) {
a.cbMu.Lock()
defer a.cbMu.Unlock()
if a.cb != nil {
err := a.resetChatBot()
if err != nil {
a.logError.Println("error resetting chat bot:", err)
return NewChatBotResponse{}, fmt.Errorf("Error creating chat bot. Try Again.")
}
}
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return NewChatBotResponse{}, fmt.Errorf("Channel does not exist.")
}
if channel.ChatBot.Session.Client.LiveStreamUrl == "" {
return NewChatBotResponse{}, nil
}
var err error
a.cb, err = chatbot.NewChatBot(a.ctx, channel.ChatBot, a.logError)
if err != nil {
a.logError.Println("error creating new chat bot:", err)
return NewChatBotResponse{}, fmt.Errorf("Error creating new chat bot. Try again.")
}
loggedIn, err := a.cb.LoggedIn()
if err != nil {
a.logError.Println("error checking if chat bot is logged in:", err)
return NewChatBotResponse{}, fmt.Errorf("Error checking if chat bot is logged in. Try again.")
}
if loggedIn {
err = a.cb.StartChatStream()
if err != nil {
a.logError.Println("error starting chat stream:", err)
return NewChatBotResponse{}, fmt.Errorf("Error connecting to chat. Try again.")
}
}
return NewChatBotResponse{loggedIn, channel.ChatBot.Session.Client.LiveStreamUrl, channel.ChatBot.Session.Username}, nil
}
func (a *App) LoginChatBot(cid string, username string, password string, streamUrl string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
if a.cb != nil {
err := a.resetChatBot()
if err != nil {
a.logError.Println("error resetting chat bot:", err)
return fmt.Errorf("Error creating chat bot. Try Again.")
}
}
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return fmt.Errorf("Channel does not exist.")
}
channel.ChatBot.Session.Client.LiveStreamUrl = streamUrl
var err error
a.cb, err = chatbot.NewChatBot(a.ctx, channel.ChatBot, a.logError)
if err != nil {
a.logError.Println("error creating new chat bot:", err)
return fmt.Errorf("Error creating new chat bot. Try again.")
}
cookies, err := a.cb.Login(username, password)
if err != nil {
a.logError.Println("error logging into chat bot:", err)
return fmt.Errorf("Error logging in. Try again.")
}
channel.ChatBot.Session = config.ChatBotSession{
Client: rumblelivestreamlib.NewClientOptions{
Cookies: cookies,
LiveStreamUrl: streamUrl,
},
Username: username,
}
a.cfg.Channels[cid] = channel
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return fmt.Errorf("Error saving session information. Try again.")
}
a.cb.Cfg.Session = channel.ChatBot.Session
err = a.cb.StartChatStream()
if err != nil {
a.logError.Println("error starting chat stream:", err)
return fmt.Errorf("Error connecting to chat. Try again.")
}
return nil
}
func (a *App) StopAllChatBot(cid string) error {
if a.cb == nil {
return fmt.Errorf("Chat bot not initialized.")
}
err := a.cb.StopAllMessages()
if err != nil {
a.logError.Println("error stopping all chat bot messages:", err)
return fmt.Errorf("Error stopping messages.")
}
return nil
}
func (a *App) StartAllChatBot(cid string) error {
if a.cb == nil {
return fmt.Errorf("Chat bot not initialized.")
}
err := a.cb.StartAllMessages()
if err != nil {
a.logError.Println("error starting all chat bot messages:", err)
return fmt.Errorf("Error starting messages.")
}
return nil
}
func (a *App) UpdateChatBotUrl(cid string, streamUrl string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
if a.cb == nil {
return fmt.Errorf("Chat bot not initialized.")
}
err := a.resetChatBot()
if err != nil {
a.logError.Println("error resetting chat bot:", err)
return fmt.Errorf("Error creating chat bot. Try Again.")
}
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return fmt.Errorf("Channel does not exist.")
}
channel.ChatBot.Session.Client.LiveStreamUrl = streamUrl
a.cb, err = chatbot.NewChatBot(a.ctx, channel.ChatBot, a.logError)
if err != nil {
a.logError.Println("error creating new chat bot:", err)
return fmt.Errorf("Error creating new chat bot. Try again.")
}
a.cfg.Channels[cid] = channel
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return fmt.Errorf("Error saving session information. Try again.")
}
a.cb.Cfg.Session.Client.LiveStreamUrl = streamUrl
err = a.cb.StartChatStream()
if err != nil {
a.logError.Println("error starting chat stream:", err)
return fmt.Errorf("Error connecting to chat. Try again.")
}
return nil
}
func (a *App) ResetChatBot(cid string, logout bool) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
a.cfgMu.Lock()
defer a.cfgMu.Unlock()
if a.cb == nil {
return nil
}
err := a.cb.StopAllMessages()
if err != nil {
return fmt.Errorf("error stopping all chat bot messages: %v", err)
}
if logout {
err := a.cb.Logout()
if err != nil {
return fmt.Errorf("error logging out of chat bot: %v", err)
}
//TODO: reset session in config
channel, exists := a.cfg.Channels[cid]
if !exists {
a.logError.Println("channel does not exist:", cid)
return fmt.Errorf("Channel does not exist.")
}
channel.ChatBot.Session = config.ChatBotSession{}
a.cfg.Channels[cid] = channel
err = a.cfg.Save()
if err != nil {
a.logError.Println("error saving config:", err)
return fmt.Errorf("Error saving session information. Try again.")
}
}
err = a.resetChatBot()
if err != nil {
a.logError.Println("error resetting chat bot:", err)
return fmt.Errorf("Error resetting chat bot. Try Again.")
}
return nil
}
func (a *App) resetChatBot() error {
if a.cb == nil {
// return fmt.Errorf("chat bot is nil")
return nil
}
err := a.cb.StopAllMessages()
if err != nil {
return fmt.Errorf("error stopping all chat bot messages: %v", err)
}
err = a.cb.StopChatStream()
if err != nil {
return fmt.Errorf("error stopping chat stream: %v", err)
}
a.cb = nil
return nil
}
func (a *App) StartChatBotMessage(mid string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
if a.cb == nil {
return fmt.Errorf("Chat bot not initialized.")
}
err := a.cb.StartMessage(mid)
if err != nil {
a.logError.Println("error starting chat bot message:", err)
return fmt.Errorf("Error starting message. Try Again.")
}
return nil
}
func (a *App) StopChatBotMessage(mid string) error {
a.cbMu.Lock()
defer a.cbMu.Unlock()
// If chat bot not initialized, then stop does nothing
if a.cb == nil {
return nil
}
err := a.cb.StopMessage(mid)
if err != nil {
a.logError.Println("error stopping chat bot message:", err)
return fmt.Errorf("Error stopping message. Try Again.")
}
return nil
}
func (a *App) StartApi(cid string) error {
channel, found := a.cfg.Channels[cid]
if !found {
a.logError.Println("could not find channel CID:", cid)
return fmt.Errorf("channel CID not found")
}
err := a.api.Start(channel.ApiUrl, channel.Interval*time.Second)
if err != nil {
a.logError.Println("error starting api:", err)
return fmt.Errorf("error starting API")
}
return nil
}
func (a *App) StopApi() {
a.api.Stop()
}
func (a *App) updateChatBotConfig(cfg config.ChatBot) {
a.cbMu.Lock()
defer a.cbMu.Unlock()
if a.cb != nil {
a.cb.Cfg = cfg
}
}
func (a *App) OpenFileDialog() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
a.logError.Println("error getting home directory:", err)
return "", fmt.Errorf("Error opening file explorer. Try again.")
}
filepath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{DefaultDirectory: home})
if err != nil {
a.logError.Println("error opening file dialog:", err)
return "", fmt.Errorf("Error opening file explorer. Try again.")
}
return filepath, err
}
func (a *App) FilepathBase(path string) string {
return filepath.Base(path)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1,31 +0,0 @@
import eye from './eye.png';
import eye_slash from './eye-slash.png';
import gear from './gear.png';
import gear_fill from './gear-fill.png';
import heart from './heart-fill.png';
import house from './house.png';
import pause from './pause-fill.png';
import play from './play-fill.png';
import play_green from './play-fill-green.png';
import plus_circle from './plus-circle-fill.png';
import star from './star-fill.png';
import stop from './stop-fill.png';
import thumbs_down from './hand-thumbs-down.png';
import thumbs_up from './hand-thumbs-up.png';
import x_lg from './x-lg.png';
export const Eye = eye;
export const EyeSlash = eye_slash;
export const Gear = gear;
export const GearFill = gear_fill;
export const Heart = heart;
export const House = house;
export const Pause = pause;
export const Play = play;
export const PlayGreen = play_green;
export const PlusCircle = plus_circle;
export const Star = star;
export const Stop = stop;
export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up;
export const XLg = x_lg;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,66 +0,0 @@
.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

@ -1,39 +0,0 @@
import { PlusCircle } from '../assets/icons';
import './ChannelList.css';
function ChannelList(props) {
const sortChannelsAlpha = () => {
let keys = Object.keys(props.channels);
// let sorted = [...props.channels].sort((a, b) =>
// a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
// );
let sorted = [...keys].sort((a, b) =>
props.channels[a].name.toLowerCase() > props.channels[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(props.channels[channel].id)}
>
{props.channels[channel].name}
</button>
</div>
))}
</div>
{/* <button className='channel-add'>
<img className='channel-add-icon' src={PlusCircle} />
</button> */}
</div>
);
}
export default ChannelList;

View file

@ -1,64 +0,0 @@
.chat-bot-error {
border: 1px solid red;
box-sizing: border-box;
color: red;
font-family: monospace;
font-size: 16px;
padding: 5px;
text-align: center;
width: 100%;
}
.chat-bot-modal {
align-items: left;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
}
.chat-bot-setting {
align-items: start;
display: flex;
flex-direction: column;
padding-top: 10px;
width: 100%;
}
.chat-bot-setting-label {
color: white;
font-family: sans-serif;
font-size: 20px;
padding-bottom: 5px;
width: 100%;
}
.chat-bot-setting-input {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.chat-bot-description {
align-items: center;
display: flex;
flex-direction: row;
justify-content: start;
padding-top: 10px;
width: 100%;
}
.chat-bot-description-label {
color: white;
font-family: sans-serif;
font-size: 20px;
padding-bottom: 5px;
padding-right: 5px;
}

View file

@ -1,174 +0,0 @@
import { useEffect, useState } from 'react';
import { Modal, SmallModal } from './Modal';
import { LoginChatBot, UpdateChatBotUrl } from '../../wailsjs/go/main/App';
import './ChatBot.css';
export function ChatBotModal(props) {
const [error, setError] = useState('');
const [loggedIn, setLoggedIn] = useState(props.loggedIn);
const [password, setPassword] = useState('');
const [saving, setSaving] = useState(false);
const updatePassword = (event) => setPassword(event.target.value);
const [url, setUrl] = useState(props.streamUrl);
const updateUrl = (event) => setUrl(event.target.value);
const [username, setUsername] = useState(props.username);
const updateUsername = (event) => setUsername(event.target.value);
useEffect(() => {
if (saving) {
// let user = username;
// let p = password;
// let u = url;
// props.onSubmit(user, p, u);
// NewChatBot(props.cid, username, password, url)
if (loggedIn) {
UpdateChatBotUrl(props.cid, url)
.then(() => {
reset();
props.onUpdate(url);
})
.catch((error) => {
setSaving(false);
setError(error);
console.log('Error updating chat bot:', error);
});
} else {
LoginChatBot(props.cid, username, password, url)
.then(() => {
reset();
props.onLogin();
})
.catch((error) => {
setSaving(false);
setError(error);
console.log('Error creating new chat bot:', error);
});
}
}
}, [saving]);
const reset = () => {
setError('');
setLoggedIn(false);
setPassword('');
setSaving(false);
setUrl('');
setUsername('');
};
const close = () => {
reset();
props.onClose();
};
const logout = () => {
reset();
props.onLogout();
};
const submit = () => {
if (username === '') {
setError('Add username');
return;
}
if (password === '' && !loggedIn) {
setError('Add password');
return;
}
if (url === '') {
setError('Add stream URL');
return;
}
setSaving(true);
// let user = username;
// let p = password;
// let u = url;
// reset();
// props.onSubmit(user, p, u);
};
return (
<>
<Modal
onClose={close}
show={props.show}
style={{ minWidth: '300px', maxWidth: '400px' }}
cancelButton={loggedIn ? '' : 'Cancel'}
onCancel={close}
deleteButton={loggedIn ? 'Logout' : ''}
onDelete={logout}
submitButton={saving ? 'Saving' : 'Save'}
onSubmit={
saving
? () => {
console.log('Saving');
}
: submit
}
title={'Chat Bot'}
>
<div className='chat-bot-modal'>
{loggedIn ? (
<div className='chat-bot-description'>
<span className='chat-bot-description-label'>Logged in:</span>
<span
className='chat-bot-description-label'
style={{ fontWeight: 'bold' }}
>
{username}
</span>
</div>
) : (
<div className='chat-bot-setting'>
<span className='chat-bot-setting-label'>Username</span>
<input
className='chat-bot-setting-input'
onChange={updateUsername}
placeholder='Username'
type='text'
value={username}
/>
</div>
)}
{!loggedIn && (
<div className='chat-bot-setting'>
<span className='chat-bot-setting-label'>Password</span>
<input
className='chat-bot-setting-input'
onChange={updatePassword}
placeholder='Password'
type='password'
value={password}
/>
</div>
)}
<div className='chat-bot-setting'>
<span className='chat-bot-setting-label'>Stream URL</span>
<input
className='chat-bot-setting-input'
onChange={updateUrl}
placeholder='https://'
type='text'
value={url}
/>
</div>
</div>
</Modal>
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '100px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
</>
);
}
export function StreamChatMessageItem() {}

View file

@ -1,44 +0,0 @@
.chat-message {
align-items: start;
background-color: rgba(6,23,38,1);
padding: 10px;
display: flex;
flex-direction: row;
}
.chat-message-user-image {
border-radius: 50%;
height: 22px;
margin-right: 8px;
width: 22px;
}
.chat-message-user-initial {
align-items: center;
background-color: #37c;
border: 1px solid #eee;
border-radius: 50%;
color: #eee;
display: flex;
font-family: sans-serif;
font-size: 12px;
font-weight: bold;
height: 22px;
justify-content: center;
margin-right: 8px;
width: 22px;
}
.chat-message-username {
color: white;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-right: 3px;
}
.chat-message-text {
color: white;
font-family: sans-serif;
font-size: 14px;
}

View file

@ -1,28 +0,0 @@
import './ChatMessage.css';
function ChatMessage(props) {
const upperInitial = () => {
return props.message.username[0].toUpperCase();
};
return (
<div className='chat-message'>
{props.message.image === '' || props.message.image === undefined ? (
<span className='chat-message-user-initial'>{upperInitial()}</span>
) : (
<img className='chat-message-user-image' src={props.message.image} />
)}
<div>
<span
className='chat-message-username'
style={props.message.color && { color: props.message.color }}
>
{props.message.username}
</span>
<span className='chat-message-text'>{props.message.text}</span>
</div>
</div>
);
}
export default ChatMessage;

View file

@ -1,24 +0,0 @@
.highlight {
align-items: start;
color: white;
display: flex;
background-color: #75a54b;
border-radius: 0.5rem;
flex-direction: column;
font-family: sans-serif;
font-weight: bold;
height: 40px;
justify-content: center;
min-width: 90px;
padding: 5px 10px;
width: 75px;
}
.highlight-value {
font-family: monospace;
font-size: 20px;
}
.highlight-description {
font-size: 12px;
}

View file

@ -1,72 +0,0 @@
import './Highlight.css';
function Highlight(props) {
const countString = () => {
switch (true) {
case props.value <= 0:
return '-';
case props.value < 1000:
return props.value;
case props.value < 1000000:
return (props.value / 1000).toFixed(3).slice(0, -2) + 'K';
case props.value < 1000000000:
return (props.value / 1000000).toFixed(6).slice(0, -5) + 'M';
default:
return 'Inf';
}
};
const stopwatchString = () => {
if (isNaN(Date.parse(props.value))) {
return '--:--';
}
let now = new Date();
let date = new Date(props.value);
let diff = now - date;
let msMinute = 1000 * 60;
let msHour = msMinute * 60;
let msDay = msHour * 24;
let days = Math.floor(diff / msDay);
let hours = Math.floor((diff - days * msDay) / msHour);
let minutes = Math.floor((diff - days * msDay - hours * msHour) / msMinute);
if (diff >= 100 * msDay) {
return days + 'd';
}
if (diff >= msDay) {
return days + 'd ' + hours + 'h';
}
if (hours < 10) {
hours = '0' + hours;
}
if (minutes < 10) {
minutes = '0' + minutes;
}
return hours + ':' + minutes;
};
const valueString = () => {
switch (props.type) {
case 'count':
return countString();
case 'stopwatch':
return stopwatchString();
default:
return props.value;
}
};
return (
<div className='highlight'>
<span className='highlight-value'>{valueString()}</span>
<span className='highlight-description'>{props.description}</span>
</div>
);
}
export default Highlight;

View file

@ -1,24 +0,0 @@
.stream-activity {
width: 100%;
height: 100%;
}
.stream-activity-header {
text-align: left;
background-color: rgba(6,23,38,1);
border-bottom: 1px solid #495a6a;
height: 19px;
padding: 10px 20px;
}
.stream-activity-title {
color: white;
font-family: sans-serif;
font-size: 12px;
font-weight: bold;
}
.stream-activity-list {
overflow-y: auto;
height: calc(100vh - 84px - 40px - 179px);
}

View file

@ -1,20 +0,0 @@
import StreamEvent from './StreamEvent';
import './StreamActivity.css';
function StreamActivity(props) {
return (
<div className='stream-activity'>
<div className='stream-activity-header'>
<span className='stream-activity-title'>{props.title}</span>
</div>
<div className='stream-activity-list'>
{props.events.map((event, index) => (
<StreamEvent event={event} />
))}
</div>
</div>
);
}
export default StreamActivity;

View file

@ -1,28 +0,0 @@
.stream-chat {
width: 100%;
height: 100%;
}
.stream-chat-header {
align-items: center;
background-color: rgba(6,23,38,1);
border-bottom: 1px solid #495a6a;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 19px;
padding: 10px 20px;
text-align: left;
}
.stream-chat-list {
overflow-y: auto;
height: calc(100vh - 84px - 40px - 179px);
}
.stream-chat-title {
color: white;
font-family: sans-serif;
font-size: 12px;
font-weight: bold;
}

View file

@ -1,38 +0,0 @@
import { useState } from 'react';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import ChatMessage from './ChatMessage';
import './StreamChat.css';
function StreamChat(props) {
const [messages, setMessages] = useState([
{
color: '#ec131f',
image: 'https://ak2.rmbl.ws/z0/V/m/v/E/VmvEe.asF.4-18osof-s35kf7.jpeg',
username: 'tylertravisty',
text: 'Hello, world this is si s a a sdf asd f',
},
{
username: 'tylertravisty',
text: 'Another chat message',
},
]);
EventsOn('ChatMessage', (msg) => {
setMessages(...messages, msg);
});
return (
<div className='stream-chat'>
<div className='stream-chat-header'>
<span className='stream-chat-title'>{props.title}</span>
</div>
<div className='stream-chat-list'>
{messages.map((message, index) => (
<ChatMessage message={message} />
))}
</div>
</div>
);
}
export default StreamChat;

View file

@ -1,104 +0,0 @@
.stream-chatbot {
width: 100%;
height: 100%;
}
.stream-chatbot-button {
align-items: center;
border: none;
display: flex;
justify-content: center;
padding: 0px;
}
.stream-chatbot-button:hover {
cursor: pointer;
}
.stream-chatbot-button-title {
background-color: rgba(6,23,38,1);
}
.stream-chatbot-button-chat {
align-items: center;
background-color: #000312;
display: flex;
justify-content: center;
width: 10%;
}
.stream-chatbot-icon {
height: 24px;
width: 24px;
}
.stream-chatbot-controls {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 55px;
}
.stream-chatbot-header {
align-items: center;
background-color: rgba(6,23,38,1);
border-bottom: 1px solid #495a6a;
display: flex;
flex-direction: row;
justify-content: space-between;
height: 19px;
padding: 10px 20px;
text-align: left;
}
.stream-chatbot-item {
border-bottom: 1px solid #82b1ff;
box-sizing: border-box;
color: white;
display: flex;
flex-direction: row;
font-family: sans-serif;
justify-content: space-between;
padding: 10px 20px;
width: 100%;
}
.stream-chatbot-item-sender {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
padding-left: 10px;
width: 20%;
}
.stream-chatbot-item-interval {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
padding-left: 10px;
width: 20%;
}
.stream-chatbot-item-message {
align-items: center;
display: flex;
justify-content: left;
overflow: hidden;
white-space: nowrap;
width: 50%;
}
.stream-chatbot-list {
overflow-y: auto;
height: calc(100vh - 84px - 40px - 179px);
}
.stream-chatbot-title {
color: white;
font-family: sans-serif;
font-size: 12px;
font-weight: bold;
}

View file

@ -1,237 +0,0 @@
import { useEffect, useState } from 'react';
import { FilepathBase, StartChatBotMessage, StopChatBotMessage } from '../../wailsjs/go/main/App';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { GearFill, Pause, Play, PlayGreen, PlusCircle, Stop } from '../assets/icons';
import './StreamChatBot.css';
import { SmallModal } from './Modal';
function StreamChatBot(props) {
const sortChatsAlpha = () => {
let keys = Object.keys(props.chats);
let sorted = [...keys].sort((a, b) =>
props.chats[a].text.toLowerCase() > props.chats[b].text.toLowerCase() ? 1 : -1
);
return sorted;
};
return (
<div className='stream-chatbot'>
<div className='stream-chatbot-header'>
<span className='stream-chatbot-title'>{props.title}</span>
<div className='stream-chatbot-controls'>
<button
className='stream-chatbot-button stream-chatbot-button-title'
onClick={props.onPlayAll}
>
<img className='stream-chatbot-icon' src={PlayGreen} />
</button>
<button
className='stream-chatbot-button stream-chatbot-button-title'
onClick={props.onStopAll}
>
<img className='stream-chatbot-icon' src={Stop} />
</button>
</div>
<div className='stream-chatbot-controls'>
<button
className='stream-chatbot-button stream-chatbot-button-title'
onClick={props.onAdd}
>
<img className='stream-chatbot-icon' src={PlusCircle} />
</button>
<button
className='stream-chatbot-button stream-chatbot-button-title'
onClick={props.onSettings}
>
<img className='stream-chatbot-icon' src={GearFill} />
</button>
</div>
</div>
<div className='stream-chatbot-list'>
{sortChatsAlpha().map((chat, index) => (
<StreamChatItem
activateMessage={props.activateMessage}
chat={props.chats[chat]}
isMessageActive={props.isMessageActive}
onItemClick={props.onEdit}
/>
))}
</div>
</div>
);
}
export default StreamChatBot;
function StreamChatItem(props) {
// const [active, setActive] = useState(props.isMessageActive(props.chat.id));
const [active, setActive] = useState(false);
const [error, setError] = useState('');
const [filename, setFilename] = useState(props.chat.text_file);
useEffect(() => {
if (props.chat.text_file !== '') {
FilepathBase(props.chat.text_file).then((name) => {
setFilename(name);
});
}
// setActive(props.isMessageActive(props.chat.id));
}, [props]);
const changeActive = (bool) => {
// console.log('ChangeActive:', bool);
// props.chat.active = bool;
// props.activateMessage(props.chat.id, bool);
setActive(bool);
};
useEffect(() => {
EventsOn('ChatBotCommandActive-' + props.chat.id, (mid) => {
console.log('ChatBotCommandActive', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(true);
}
});
EventsOn('ChatBotCommandError-' + props.chat.id, (mid) => {
console.log('ChatBotCommandError', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(false);
}
});
EventsOn('ChatBotMessageActive-' + props.chat.id, (mid) => {
console.log('ChatBotMessageActive', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(true);
}
});
EventsOn('ChatBotMessageError-' + props.chat.id, (mid) => {
console.log('ChatBotMessageError', props.chat.id, mid);
if (mid === props.chat.id) {
changeActive(false);
}
});
}, []);
const prependZero = (value) => {
if (value < 10) {
return '0' + value;
}
return '' + value;
};
const printInterval = (interval) => {
let hours = Math.floor(interval / 3600);
let minutes = Math.floor(interval / 60 - hours * 60);
let seconds = Math.floor(interval - hours * 3600 - minutes * 60);
// hours = prependZero(hours);
// minutes = prependZero(minutes);
// seconds = prependZero(seconds);
// return hours + ':' + minutes + ':' + seconds;
return hours + 'h ' + minutes + 'm ' + seconds + 's';
};
const intervalToTimer = (interval) => {
let hours = Math.floor(interval / 3600);
let minutes = Math.floor(interval / 60 - hours * 60);
let seconds = Math.floor(interval - hours * 3600 - minutes * 60);
if (minutes !== 0) {
seconds = prependZero(seconds);
}
if (hours !== 0) {
minutes = prependZero(minutes);
}
if (hours === 0) {
hours = '';
if (minutes === 0) {
minutes = '';
if (seconds === 0) {
seconds = '';
}
}
}
return hours + minutes + seconds;
};
const openChat = () => {
props.onItemClick({
id: props.chat.id,
as_channel: props.chat.as_channel,
command: props.chat.command,
interval: intervalToTimer(props.chat.interval),
on_command: props.chat.on_command,
on_command_follower: props.chat.on_command_follower,
on_command_rant_amount: props.chat.on_command_rant_amount,
on_command_subscriber: props.chat.on_command_subscriber,
text: props.chat.text,
text_file: props.chat.text_file,
});
};
const startMessage = () => {
StartChatBotMessage(props.chat.id)
.then(() => {
changeActive(true);
})
.catch((error) => {
setError(error);
});
};
const stopMessage = () => {
StopChatBotMessage(props.chat.id).then(() => {
changeActive(false);
});
};
return (
<>
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
<div className='stream-chatbot-item' onClick={() => openChat()}>
<span className='stream-chatbot-item-message'>
{props.chat.text_file !== '' ? filename : props.chat.text}
</span>
<span className='stream-chatbot-item-interval'>
{props.chat.on_command
? props.chat.command
: printInterval(props.chat.interval)}
</span>
<span className='stream-chatbot-item-sender'>
{props.chat.as_channel ? 'Channel' : 'User'}
</span>
<button
className='stream-chatbot-button stream-chatbot-button-chat'
onClick={(e) => {
e.stopPropagation();
console.log('message ID:', props.chat.id);
if (active) {
console.log('Stop message');
stopMessage();
} else {
console.log('Start message');
startMessage();
}
}}
>
<img className='stream-chatbot-icon' src={active ? Pause : Play} />
</button>
</div>
</>
);
}

View file

@ -1,358 +0,0 @@
/* .modal-chat {
align-items: center;
background-color: red;
color: black;
display: flex;
height: 50%;
justify-content: center;
opacity: 1;
width: 50%;
}
.modal-container {
align-items: center;
display: flex;
height: 100vh;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100vw;
} */
.chat-toggle {
align-items: center;
display: flex;
justify-content: space-between;
padding-top: 10px;
width: 100%;
}
.chat-toggle-label {
color: white;
font-family: sans-serif;
padding-right: 10px;
}
.chat-command {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
padding-top: 10px;
width: 100%;
}
.chat-command-input {
border: none;
border-radius: 34px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 5px 10px 5px 10px;
text-align: center;
width: 100%;
}
.chat-command-option {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
}
.chat-command-options {
align-items: center;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.chat-command-label {
color: white;
height: 29px;
}
.chat-command-rant-amount {
border: none;
/* border-radius: 34px; */
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
/* padding: 5px 10px 5px 10px; */
padding: 5px;
text-align: center;
}
.chat-command-rant-amount-label {
color: white;
font-family: sans-serif;
padding-right: 10px;
}
.chat-command-rant-amount-symbol {
color: white;
font-family: sans-serif;
font-size: 20px;
padding-right: 1px;
}
.chat-interval {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 10px;
width: 100%;
}
.chat-interval-input {
border: none;
border-radius: 34px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 5px 10px 5px 10px;
text-align: right;
}
.chat-interval-input-zero::placeholder {
text-align: center;
}
.chat-interval-input-value::placeholder {
color: black;
opacity: 1;
text-align: center;
}
.chat-interval-label {
color: white;
font-family: sans-serif;
padding-right: 10px;
}
.chat-options {
display: flex;
flex-direction: column;
width: 100%;
}
.stream-chat-message {
align-items: center;
color: white;
display: flex;
flex-direction: column;
font-family: sans-serif;
justify-content: start;
width: 100%;
}
.stream-chat-message-error {
border: 1px solid red;
box-sizing: border-box;
color: red;
font-family: monospace;
font-size: 16px;
padding: 5px;
text-align: center;
width: 100%;
}
.stream-chat-message-label {
padding: 5px 0px;
/* width: 50%; */
}
.stream-chat-message-modal {
align-items: left;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
}
.stream-chat-message-textarea {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.stream-chat-message-title {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.stream-chat-message-title-right {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
}
.chat-toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.chat-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.chat-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #495a6a;
-webkit-transition: .4s;
transition: .4s;
}
.chat-toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .chat-toggle-slider {
background-color: #85c742;
}
input:checked + .chat-toggle-slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.chat-toggle-slider.round {
border-radius: 34px;
}
.chat-toggle-slider.round:before {
border-radius: 50%;
}
.chat-toggle-check-container {
display: block;
position: relative;
padding-left: 16px;
margin-bottom: 15px;
cursor: pointer;
font-size: 15px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.chat-toggle-check-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.chat-toggle-check {
border-radius: 3px;
position: absolute;
top: 0;
left: 0;
height: 15px;
width: 15px;
background-color: #495a6a;
}
.chat-toggle-check-container:hover input ~ .chat-toggle-check {
background-color: #495a6a;
}
.chat-toggle-check-container input:checked ~ .chat-toggle-check {
background-color: #85c742;
}
.chat-toggle-check:after {
content: "";
position: absolute;
display: none;
}
.chat-toggle-check-container input:checked ~ .chat-toggle-check:after {
display: block;
}
.chat-toggle-check-container .chat-toggle-check:after {
left: 4px;
top: 1px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.chat-toggle-check-label {
color: white;
font-family: sans-serif;
padding-right: 5px;
}
.choose-file {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.choose-file-button-box {
min-width: 100px;
width: 100px;
}
.choose-file-button {
background-color: #85c742;
border: none;
border-radius: 5px;
color: #061726;
cursor: pointer;
font-size: 16px;
text-decoration: none;
/* width: 200px; */
width: 100%;
}
.choose-file-path {
overflow: scroll;
margin-left: 5px;
white-space: nowrap;
}

View file

@ -1,404 +0,0 @@
import { useEffect, useState } from 'react';
import { Modal, SmallModal } from './Modal';
import { OpenFileDialog } from '../../wailsjs/go/main/App';
import './StreamChatMessage.css';
export function StreamChatMessageModal(props) {
const [asChannel, setAsChannel] = useState(props.asChannel);
const [chatCommand, setChatCommand] = useState(props.chatCommand);
const [error, setError] = useState('');
const [onCommand, setOnCommand] = useState(props.onCommand);
const [onCommandFollower, setOnCommandFollower] = useState(props.onCommandFollower);
const [onCommandRantAmount, setOnCommandRantAmount] = useState(props.onCommandRantAmount);
const [onCommandSubscriber, setOnCommandSubscriber] = useState(props.onCommandSubscriber);
const [openDelete, setOpenDelete] = useState(false);
const [readFromFile, setReadFromFile] = useState(false);
const [text, setText] = useState(props.text);
const [textFile, setTextFile] = useState(props.textFile);
const updateText = (event) => setText(event.target.value);
const [timer, setTimer] = useState(props.interval);
useEffect(() => {
console.log('update chat');
setAsChannel(props.asChannel);
setOnCommand(props.onCommand);
setOnCommandFollower(props.onCommandFollower);
setOnCommandSubscriber(props.onCommandSubscriber);
setOnCommandRantAmount(props.onCommandRantAmount);
setError('');
setReadFromFile(props.textFile !== '');
setText(props.text);
setTextFile(props.textFile);
setTimer(props.interval);
}, []);
const reset = () => {
setAsChannel(false);
setChatCommand(false);
setError('');
setReadFromFile(false);
setText('');
setTextFile('');
setOnCommand(false);
setOnCommandFollower(false);
setOnCommandSubscriber(false);
setOnCommandRantAmount(0);
setTimer('');
};
const close = () => {
reset();
props.onClose();
};
const submit = () => {
if (!readFromFile && text === '') {
setError('Add message');
return;
}
if (readFromFile && textFile === '') {
setError('Select file containing messages');
return;
}
if (timer === '') {
setError('Set timer');
return;
}
if (onCommand && chatCommand === '') {
setError('Add command');
return;
}
let message = {
id: props.chatID,
as_channel: asChannel,
command: chatCommand,
interval: timerToInterval(),
on_command: onCommand,
on_command_follower: onCommandFollower,
on_command_rant_amount: onCommandRantAmount,
on_command_subscriber: onCommandSubscriber,
text: text,
text_file: textFile,
};
props.onSubmit(message);
};
const deleteMessage = () => {
if (props.chatID === '') {
close();
return;
}
setOpenDelete(true);
};
const confirmDelete = () => {
reset();
setOpenDelete(false);
props.onDelete(props.chatID);
};
const updateChatCommand = (e) => {
let command = e.target.value;
if (command.length === 1) {
if (command !== '!') {
command = '!' + command;
}
}
command = command.toLowerCase();
let postfix = command.replace('!', '');
if (postfix !== '' && !/^[a-z0-9]+$/gi.test(postfix)) {
return;
}
setChatCommand(command);
};
const updateTimerBackspace = (e) => {
if (timer.length === 0) {
return;
}
if (e.keyCode === 8) {
setTimer(timer.substring(0, timer.length - 1));
}
};
const updateTimer = (e) => {
let nums = '0123456789';
let digit = e.target.value;
if (!nums.includes(digit)) {
return;
}
if (timer.length === 6) {
return;
}
if (timer.length === 0 && digit === '0') {
return;
}
setTimer(timer + digit);
};
const timerToInterval = () => {
let prefix = '0'.repeat(6 - timer.length);
let t = prefix + timer;
let hours = parseInt(t.substring(0, 2));
let minutes = parseInt(t.substring(2, 4));
let seconds = parseInt(t.substring(4, 6));
return hours * 3600 + minutes * 60 + seconds;
};
const printTimer = () => {
if (timer === '') {
return '00:00:00';
}
let prefix = '0'.repeat(6 - timer.length);
let t = prefix + timer;
return t.substring(0, 2) + ':' + t.substring(2, 4) + ':' + t.substring(4, 6);
};
const checkChannelToggle = (e) => {
setAsChannel(e.target.checked);
};
const checkCommandToggle = (e) => {
setOnCommand(e.target.checked);
};
const checkCommandFollower = (e) => {
setOnCommandFollower(e.target.checked);
};
const checkCommandSubscriber = (e) => {
setOnCommandSubscriber(e.target.checked);
};
const updateRantAmount = (e) => {
let amount = parseInt(e.target.value);
if (isNaN(amount)) {
amount = 0;
}
setOnCommandRantAmount(amount);
};
const checkReadFromFile = (e) => {
setReadFromFile(e.target.checked);
if (!e.target.checked) {
setTextFile('');
}
};
const chooseFile = () => {
OpenFileDialog()
.then((filepath) => {
if (filepath !== '') {
setTextFile(filepath);
}
})
.catch((error) => setError(error));
};
return (
<>
<Modal
onClose={close}
show={props.show}
style={{ minHeight: '500px', minWidth: '300px', maxWidth: '400px' }}
cancelButton={props.chatID === '' ? 'Cancel' : ''}
onCancel={deleteMessage}
deleteButton={props.chatID === '' ? '' : 'Delete'}
onDelete={deleteMessage}
submitButton={'Save'}
onSubmit={submit}
title={'Chat Message'}
>
<div className='stream-chat-message-modal'>
<div className='stream-chat-message'>
{/* {error && <span className='stream-chat-message-error'>{error}</span>} */}
<div className='stream-chat-message-title'>
<span className='stream-chat-message-label'>Message</span>
<div className='stream-chat-message-title-right'>
<span className='chat-toggle-check-label'>Read from file</span>
<label className='chat-toggle-check-container'>
<input
checked={readFromFile}
onChange={checkReadFromFile}
type='checkbox'
/>
<span className='chat-toggle-check'></span>
</label>
</div>
</div>
{readFromFile ? (
<div className='choose-file'>
<div className='choose-file-button-box'>
<button className='choose-file-button' onClick={chooseFile}>
Choose file
</button>
</div>
<span className='choose-file-path'>{textFile}</span>
</div>
) : (
<textarea
className='stream-chat-message-textarea'
cols='25'
onChange={updateText}
rows='4'
value={text}
/>
)}
</div>
<div className='chat-options'>
<div className='chat-interval'>
<span className='chat-interval-label'>
{onCommand ? 'Command timeout' : 'Chat interval'}
</span>
<input
className={
timer === ''
? 'chat-interval-input chat-interval-input-zero'
: 'chat-interval-input chat-interval-input-value'
}
onKeyDown={updateTimerBackspace}
onInput={updateTimer}
placeholder={printTimer()}
size='8'
type='text'
value={''}
/>
</div>
<div className='chat-toggle'>
<span className='chat-toggle-label'>Chat as channel</span>
<label className='chat-toggle-switch'>
<input
onChange={checkChannelToggle}
type='checkbox'
checked={asChannel}
/>
<span className='chat-toggle-slider round'></span>
</label>
</div>
<div className='chat-toggle'>
<span className='chat-toggle-label'>Chat on command</span>
<label className='chat-toggle-switch'>
<input
onChange={checkCommandToggle}
type='checkbox'
checked={onCommand}
/>
<span className='chat-toggle-slider round'></span>
</label>
</div>
{onCommand ? (
<div>
<div className='chat-command'>
<input
className='chat-command-input'
onInput={updateChatCommand}
placeholder={'!command'}
size='8'
type='text'
value={chatCommand}
/>
</div>
<div className='chat-command-options'>
<div className='chat-toggle'>
<span className='chat-toggle-label'>Followers only</span>
<label className='chat-toggle-switch'>
<input
onChange={checkCommandFollower}
type='checkbox'
checked={onCommandFollower}
/>
<span className='chat-toggle-slider round'></span>
</label>
</div>
<div className='chat-toggle'>
<span className='chat-toggle-label'>Subscribers only</span>
<label className='chat-toggle-switch'>
<input
onChange={checkCommandSubscriber}
type='checkbox'
checked={onCommandSubscriber}
/>
<span className='chat-toggle-slider round'></span>
</label>
</div>
<div className='chat-interval'>
<span className='chat-command-rant-amount-label'>
Minimum rant amount
</span>
<div>
<span className='chat-command-rant-amount-symbol'>
$
</span>
<input
className='chat-command-rant-amount'
onChange={updateRantAmount}
placeholder='0'
size='4'
type='text'
value={
onCommandRantAmount === 0
? ''
: onCommandRantAmount
}
/>
</div>
</div>
</div>
</div>
) : (
<div className='chat-command'>
<span className='chat-command-label'>{'\u00A0'}</span>
</div>
)}
</div>
</div>
</Modal>
<SmallModal
onClose={() => setOpenDelete(false)}
show={openDelete}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
cancelButton={'Cancel'}
onCancel={() => setOpenDelete(false)}
deleteButton={'Delete'}
message={
'Are you sure you want to delete this message? You cannot undo this action.'
}
onDelete={confirmDelete}
title={'Delete Message'}
/>
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '100px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
</>
);
}
export function StreamChatMessageItem() {}

View file

@ -1,42 +0,0 @@
.stream-event {
border-bottom: 1px solid #82b1ff;
color: white;
display: flex;
flex-direction: row;
font-family: sans-serif;
justify-content: space-between;
padding: 10px 20px;
}
.stream-event-left {
align-items: center;
display: flex;
flex-direction: row;
}
.stream-event-icon {
width: 20px;
height: 20px;
padding-right: 10px;
}
.stream-event-left-text {
display: flex;
flex-direction: column;
}
.stream-event-username {
font-size: 14px;
font-weight: bold;
}
.stream-event-description {
color: #88a0b8;
font-size: 14px;
}
.stream-event-date {
align-items: center;
display: flex;
font-family: monospace;
}

View file

@ -1,111 +0,0 @@
import { Heart, Star } from '../assets/icons';
import './StreamEvent.css';
function StreamEvent(props) {
const dateDate = (date) => {
const options = { month: 'short' };
let month = new Intl.DateTimeFormat('en-US', options).format(date);
let day = date.getDate();
return month + ' ' + day;
};
const dateDay = (date) => {
let now = new Date();
let today = now.getDay();
switch (date.getDay()) {
case 0:
return 'Sunday';
case 1:
return 'Monday';
case 2:
return 'Tuesday';
case 3:
return 'Wednesday';
case 4:
return 'Thursday';
case 5:
return 'Friday';
case 6:
return 'Saturday';
}
};
const dateTime = (date) => {
let now = new Date();
let today = now.getDay();
let day = date.getDay();
if (today !== day) {
return dateDay(date);
}
let hours24 = date.getHours();
let hours = hours24 % 12 || 12;
let minutes = date.getMinutes();
if (minutes < 10) {
minutes = '0' + minutes;
}
let mer = 'pm';
if (hours24 < 12) {
mer = 'am';
}
return hours + ':' + minutes + ' ' + mer;
};
const dateString = (d) => {
if (isNaN(Date.parse(d))) {
return 'Who knows?';
}
let now = new Date();
let date = new Date(d);
// Fix Rumble's timezone problem
date.setHours(date.getHours() - 4);
let diff = now - date;
switch (true) {
case diff < 0:
return 'In the future!?';
case diff < 60000:
return 'Now';
case diff < 3600000:
let minutes = Math.floor(diff / 1000 / 60);
let postfix = ' minutes ago';
if (minutes == 1) {
postfix = ' minute ago';
}
return minutes + postfix;
case diff < 86400000:
return dateTime(date);
case diff < 604800000:
return dateDay(date);
default:
return dateDate(date);
}
};
return (
<div className='stream-event'>
<div className='stream-event-left'>
{props.event.followed_on && <img className='stream-event-icon' src={Heart}></img>}
{props.event.subscribed_on && <img className='stream-event-icon' src={Star}></img>}
<div className='stream-event-left-text'>
<span className='stream-event-username'>{props.event.username}</span>
<span className='stream-event-description'>
{props.event.followed_on && 'Followed you'}
{props.event.subscribed_on && 'Subscribed'}
</span>
</div>
</div>
<span className='stream-event-date'>
{props.event.followed_on && dateString(props.event.followed_on)}
{props.event.subscribed_on && dateString(props.event.subscribed_on)}
</span>
</div>
);
}
export default StreamEvent;

View file

@ -1,120 +0,0 @@
.stream-info {
display: flex;
flex-direction: column;
/* padding: 20px 0px; */
width: 100%;
}
.stream-info-title {
color: white;
font-family: sans-serif;
font-size: 20px;
font-weight: bold;
}
.stream-info-categories {
padding: 5px 0px;
margin-right: 50px;
}
.stream-info-category {
background-color: rgba(6,23,38,1);
border: 1px solid white;
border-radius: 30px;
color: white;
font-family: sans-serif;
margin-right: 5px;
padding: 1px 7px;
}
.stream-info-channel {
color: white;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
padding: 5px 20px;
}
.stream-info-controls {
align-items: center;
border: 1px solid white;
border-radius: 5px;
display: flex;
justify-content: center;
margin: 5px 0px 20px 0px;
}
.stream-info-control {
height: 32px;
padding: 5px;
width: 32px;
}
.stream-info-control-button {
background-color: #000312;
border-radius: 5px;
cursor: pointer;
border: none;
padding: 0px;
}
.stream-info-control-button:hover {
background-color: rgba(6,23,38,1);
}
.stream-info-footer {
align-items: center;
display: flex;
justify-content: center;
}
.stream-info-likes {
align-items: center;
display: flex;
flex-direction: row;
padding: 5px 0px;
}
.stream-info-likes-count {
color: white;
font-family: monospace;
font-size: 16px;
padding: 0px 5px;
}
.stream-info-likes-left {
align-items: center;
display: flex;
flex-direction: row;
background-color: rgba(6,23,38,1);
border: 1px solid white;
border-radius: 10rem 0rem 0rem 10rem;
margin-right: 1px;
}
.stream-info-likes-right {
align-items: center;
display: flex;
flex-direction: row;
background-color: rgba(6,23,38,1);
border: 1px solid white;
border-radius: 0rem 10rem 10rem 0rem;
}
.stream-info-likes-icon {
align-items: center;
display: flex;
flex-direction: row;
height: 16px;
padding: 5px;
width: 16px;
}
.stream-info-live {
padding: 10px 20px 5px 20px;
}
.stream-info-subtitle {
align-items: center;
display: flex;
flex-direction: row;
}

View file

@ -1,77 +0,0 @@
import { Gear, House, Pause, Play, ThumbsDown, ThumbsUp } from '../assets/icons';
import './StreamInfo.css';
function StreamInfo(props) {
const likesString = (likes) => {
switch (true) {
case likes <= 0:
return '0';
case likes < 1000:
return likes;
case likes < 1000000:
return (likes / 1000).toFixed(3).slice(0, -2) + 'K';
case likes < 1000000000:
return (likes / 1000000).toFixed(6).slice(0, -5) + 'M';
default:
return 'Inf';
}
};
return (
<div className='stream-info'>
<div className='stream-info-live'>
<div className='stream-info-title'>
<span>{props.live ? props.title : '-'}</span>
</div>
<div className='stream-info-subtitle'>
<div className='stream-info-categories'>
<span className='stream-info-category'>
{props.live ? props.categories.primary.title : 'primary'}
</span>
<span className='stream-info-category'>
{props.live ? props.categories.secondary.title : 'secondary'}
</span>
</div>
<div className='stream-info-likes'>
<div className='stream-info-likes-left'>
<img className='stream-info-likes-icon' src={ThumbsUp} />
<span className='stream-info-likes-count'>
{props.live ? likesString(props.likes) : '-'}
</span>
</div>
<div className='stream-info-likes-right'>
<img className='stream-info-likes-icon' src={ThumbsDown} />
<span className='stream-info-likes-count'>
{props.live ? likesString(props.dislikes) : '-'}
</span>
</div>
</div>
</div>
</div>
<div className='stream-info-channel'>
<span>Channel: {props.channel}</span>
</div>
<div className='stream-info-footer'>
<div></div>
<div className='stream-info-controls'>
<button className='stream-info-control-button' onClick={props.home}>
<img className='stream-info-control' src={House} />
</button>
<button className='stream-info-control-button'>
<img
className='stream-info-control'
onClick={props.active ? props.pause : props.play}
src={props.active ? Pause : Play}
/>
</button>
<button className='stream-info-control-button' onClick={props.settings}>
<img className='stream-info-control' src={Gear} />
</button>
</div>
<div></div>
</div>
</div>
);
}
export default StreamInfo;

View file

@ -1,72 +0,0 @@
#Dashboard {
align-items: center;
background-color: #000312;
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
align-items: center;
display: flex;
flex-direction: row;
height: 62px;
justify-content: center;
padding: 10px 0px;
width: 100%;
}
.header-left {
width: 20%;
}
.header-right {
width: 20%;
}
.main {
border-bottom: 1px solid #495a6a;
border-top: 1px solid #495a6a;
display: flex;
flex-direction: row;
height: calc(100vh - 83px - 179px);
justify-content: space-between;
width: 100%;
}
.main-left {
border-right: 1px solid #495a6a;
width: 33%;
height: 100%;
}
.main-middle {
border-right: 1px solid #495a6a;
width: 33%;
height: 100%;
}
.main-right {
width: 67%;
height: 100%;
}
.modal {
background-color: white;
color: red;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
.highlights {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-evenly;
height: 50px;
width: 60%;
}

View file

@ -1,449 +0,0 @@
import { useEffect, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import {
AddChatMessage,
ChatBotMessages,
DeleteChatMessage,
GetChatBot,
NewChatBot,
ResetChatBot,
StartAllChatBot,
StartApi,
StopAllChatBot,
StopApi,
StopChatBotMessage,
UpdateChatBotUrl,
UpdateChatMessage,
} from '../../wailsjs/go/main/App';
import './Dashboard.css';
import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime';
import { Heart, Star } from '../assets/icons';
import { ChatBotModal } from '../components/ChatBot';
import Highlight from '../components/Highlight';
import { SmallModal } from '../components/Modal';
import StreamEvent from '../components/StreamEvent';
import StreamActivity from '../components/StreamActivity';
import StreamChat from '../components/StreamChat';
import StreamChatBot from '../components/StreamChatBot';
import StreamInfo from '../components/StreamInfo';
import { NavSignIn } from './Navigation';
import { StreamChatMessageModal } from '../components/StreamChatMessage';
function Dashboard() {
const location = useLocation();
const navigate = useNavigate();
const [error, setError] = useState('');
const [refresh, setRefresh] = useState(false);
const [active, setActive] = useState(false);
const [openChatBot, setOpenChatBot] = useState(false);
const [chatBotMessages, setChatBotMessages] = useState({});
const [chatBotMessagesActive, setChatBotMessagesActive] = useState({});
const [chatBotSessionLoggedIn, setChatBotSessionLoggedIn] = useState(false);
const [chatBotSessionStreamUrl, setChatBotSessionStreamUrl] = useState('');
const [chatBotSessionUsername, setChatBotSessionUsername] = useState('');
const [chatAsChannel, setChatAsChannel] = useState(false);
const [chatCommand, setChatCommand] = useState('');
const [chatOnCommand, setChatOnCommand] = useState(false);
const [chatOnCommandFollower, setChatOnCommandFollower] = useState(false);
const [chatOnCommandRantAmount, setChatOnCommandRantAmount] = useState(0);
const [chatOnCommandSubscriber, setChatOnCommandSubscriber] = useState(false);
const [chatID, setChatID] = useState('');
const [chatInterval, setChatInterval] = useState('');
const [chatText, setChatText] = useState('');
const [chatTextFile, setChatTextFile] = useState('');
const [openChat, setOpenChat] = useState(false);
const [cid, setCID] = useState(location.state.cid);
const [username, setUsername] = useState('');
const [channelName, setChannelName] = useState('');
const [followers, setFollowers] = useState({});
const [totalFollowers, setTotalFollowers] = useState(0);
const [channelFollowers, setChannelFollowers] = useState(0);
const [recentFollowers, setRecentFollowers] = useState([]);
const [subscribers, setSubscribers] = useState({});
const [subscriberCount, setSubscriberCount] = useState(0);
const [recentSubscribers, setRecentSubscribers] = useState([]);
const [streamCategories, setStreamCategories] = useState({
primary: { title: '' },
secondary: { title: '' },
});
const [streamLikes, setStreamLikes] = useState(0);
const [streamLive, setStreamLive] = useState(false);
const [streamDislikes, setStreamDislikes] = useState(0);
const [streamTitle, setStreamTitle] = useState('');
const [watchingNow, setWatchingNow] = useState(0);
const [createdOn, setCreatedOn] = useState('');
useEffect(() => {
console.log('use effect start');
// TODO: catch error
StartApi(cid);
setActive(true);
ChatBotMessages(cid).then((messages) => {
console.log(messages);
setChatBotMessages(messages);
});
NewChatBot(cid).then((response) => {
setChatBotSessionLoggedIn(response.logged_in);
setChatBotSessionStreamUrl(response.stream_url);
setChatBotSessionUsername(response.username);
});
EventsOn('QueryResponse', (response) => {
// console.log('query response received');
setRefresh(!refresh);
setActive(true);
setUsername(response.username);
setChannelName(response.channel_name);
setFollowers(response.followers);
setChannelFollowers(response.followers.num_followers);
setTotalFollowers(response.followers.num_followers_total);
setRecentFollowers(response.followers.recent_followers);
setSubscribers(response.subscribers);
setSubscriberCount(response.subscribers.num_subscribers);
setRecentSubscribers(response.subscribers.recent_subscribers);
if (response.livestreams.length > 0) {
setStreamLive(true);
setStreamCategories(response.livestreams[0].categories);
setStreamLikes(response.livestreams[0].likes);
setStreamDislikes(response.livestreams[0].dislikes);
setStreamTitle(response.livestreams[0].title);
setCreatedOn(response.livestreams[0].created_on);
setWatchingNow(response.livestreams[0].watching_now);
} else {
setStreamLive(false);
}
});
EventsOn('QueryResponseError', (error) => {
setError(error);
// console.log('Query response error:', error);
setActive(false);
});
EventsOn('ChatBotChatStreamError', (error) => {
setError(error);
});
}, []);
const home = () => {
StopApi()
.then(() => setActive(false))
.then(() => {
ResetChatBot(cid, false);
})
.then(() => {
navigate(NavSignIn);
})
.catch((error) => {
setError(error);
console.log('Stop error:', error);
});
};
const startQuery = () => {
console.log('start');
StartApi(cid)
.then(() => {
setActive(true);
})
.catch((error) => {
setError(error);
console.log('Start error:', error);
});
};
const stopQuery = () => {
console.log('stop');
StopApi().then(() => {
setActive(false);
});
};
const activityDate = (activity) => {
if (activity.followed_on) {
return activity.followed_on;
}
if (activity.subscribed_on) {
return activity.subscribed_on;
}
};
const activityEvents = () => {
let sorted = [...recentFollowers, ...recentSubscribers].sort((a, b) =>
activityDate(a) < activityDate(b) ? 1 : -1
);
return sorted;
};
const newChat = () => {
setChatAsChannel(false);
setChatCommand('');
setChatID('');
setChatInterval('');
setChatText('');
setChatTextFile('');
setChatOnCommand(false);
setChatOnCommandFollower(false);
setChatOnCommandRantAmount(0);
setChatOnCommandSubscriber(false);
setOpenChat(true);
};
// const editChat = (id, asChannel, command, interval, onCommand, text, textFile) => {
const editChat = (message) => {
setChatAsChannel(message.as_channel);
setChatCommand(message.command);
setChatID(message.id);
setChatInterval(message.interval);
setChatOnCommand(message.on_command);
setChatOnCommandFollower(message.on_command_follower);
setChatOnCommandRantAmount(message.on_command_rant_amount);
setChatOnCommandSubscriber(message.on_command_subscriber);
setChatText(message.text);
setChatTextFile(message.text_file);
setOpenChat(true);
};
const deleteChat = (id) => {
setOpenChat(false);
if (id === '') {
return;
}
let message = { id: id };
StopChatBotMessage(id)
.then(() => {
// DeleteChatMessage(id, cid)
DeleteChatMessage(cid, message)
.then((messages) => {
setChatBotMessages(messages);
})
.catch((error) => {
setError(error);
// console.log('Error deleting message:', error);
});
})
.catch((error) => {
setError(error);
// console.log('Error stopping message:', error);
});
};
// const saveChat = (id, asChannel, command, interval, onCommand, text, textFile) => {
const saveChat = (message) => {
setOpenChat(false);
if (message.id === '') {
// AddChatMessage(cid, asChannel, command, interval, onCommand, text, textFile)
AddChatMessage(cid, message)
.then((messages) => {
setChatBotMessages(messages);
})
.catch((error) => {
setError(error);
console.log('Error saving chat:', error);
});
return;
}
// UpdateChatMessage(id, cid, asChannel, command, interval, onCommand, text, textFile)
UpdateChatMessage(cid, message)
.then((messages) => {
console.log(messages);
setChatBotMessages(messages);
})
.catch((error) => {
setError(error);
console.log('Error saving chat:', error);
});
};
// TODO: this never gets called - delete
const saveChatBot = (username, password, url) => {
NewChatBot(cid, username, password, url)
.then(() => {
setOpenChatBot(false);
})
.catch((error) => console.log('Error creating new chat bot:', error));
};
const updateChatBot = (url) => {
setChatBotSessionStreamUrl(url);
setOpenChatBot(false);
};
const loginChatBot = () => {
GetChatBot(cid)
.then((response) => {
setChatBotSessionLoggedIn(response.logged_in);
setChatBotSessionStreamUrl(response.stream_url);
setChatBotSessionUsername(response.username);
})
.catch((error) => {
setError(error);
console.log('Error getting chat bot:', error);
})
.finally(() => {
setOpenChatBot(false);
});
};
const logoutChatBot = () => {
ResetChatBot(cid, true)
.then(() => {
NewChatBot(cid).then((response) => {
console.log('NewChatBot response:', response);
setChatBotSessionLoggedIn(response.logged_in);
setChatBotSessionStreamUrl(response.stream_url);
setChatBotSessionUsername(response.username);
});
})
.catch((error) => {
setError(error);
console.log('Error resetting chat bot:', error);
})
.finally(() => {
setOpenChatBot(false);
});
};
const chatBotStartAll = () => {
StartAllChatBot(cid).catch((error) => {
setError(error);
console.log('Error starting all chat bot messages:', error);
});
};
const chatBotStopAll = () => {
StopAllChatBot(cid)
.then(() => {
setChatBotMessagesActive({});
})
.catch((error) => {
setError(error);
console.log('Error stopping all chat bot messages:', error);
});
};
const activateMessage = (id, active) => {
// console.log('Dashboard activateMessage:', id, active);
chatBotMessagesActive[id] = active;
};
const isMessageActive = (id) => {
// console.log('is Message active start', id, chatBotMessagesActive[id]);
if (chatBotMessagesActive[id] === undefined) {
chatBotMessagesActive[id] = false;
}
// console.log('is Message active after', id, chatBotMessagesActive[id]);
return chatBotMessagesActive[id];
};
return (
<>
{openChat && (
<StreamChatMessageModal
chatID={chatID}
asChannel={chatAsChannel}
chatCommand={chatCommand}
onCommand={chatOnCommand}
onCommandFollower={chatOnCommandFollower}
onCommandRantAmount={chatOnCommandRantAmount}
onCommandSubscriber={chatOnCommandSubscriber}
interval={chatInterval}
onClose={() => setOpenChat(false)}
onDelete={deleteChat}
onSubmit={saveChat}
show={openChat}
text={chatText}
textFile={chatTextFile}
/>
)}
{openChatBot && (
<ChatBotModal
cid={cid}
loggedIn={chatBotSessionLoggedIn}
onClose={() => setOpenChatBot(false)}
onLogin={loginChatBot}
onLogout={logoutChatBot}
onSubmit={saveChatBot}
onUpdate={updateChatBot}
show={openChatBot}
streamUrl={chatBotSessionStreamUrl}
username={chatBotSessionUsername}
/>
)}
<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'>
<div className='main-left'>
<StreamActivity title={'Stream Activity'} events={activityEvents()} />
</div>
{/* <div className='main-middle'>
<StreamChat title={'Stream Chat'} />
</div> */}
<div className='main-right'>
<StreamChatBot
activateMessage={activateMessage}
chats={chatBotMessages}
onAdd={newChat}
onEdit={editChat}
onPlayAll={chatBotStartAll}
onSettings={() => setOpenChatBot(true)}
onStopAll={chatBotStopAll}
title={'Chat Bot'}
isMessageActive={isMessageActive}
/>
</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>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
</>
);
}
export default Dashboard;

View file

@ -1,2 +0,0 @@
export const NavSignIn = '/';
export const NavDashboard = '/dashboard';

View file

@ -1,144 +0,0 @@
#SignIn {
align-items: center;
background-color: #f3f5f8;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100vh;
}
.add-channel-description {
font-family: sans-serif;
font-size: 12px;
padding-bottom: 5px;
}
.add-channel-error {
color: red;
font-family: sans-serif;
font-size: 12px;
padding-top: 5px;
}
.signin-input-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px 0px;
width: 50%;
}
.signin-input-button {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.signin-input {
border-bottom: 2px solid #D6E0EA;
border-left: 2px solid #D6E0EA;
border-right: none;
border-top: 2px solid #D6E0EA;
border-radius: 10rem 0rem 0rem 10rem;
background-color: white;
color: #061726;
outline: none;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 0;
padding-top: 0.5rem;
width: 70%;
}
.signin-button {
background-color: #85c742;
border: none;
border-radius: 0rem 10rem 10rem 0rem;
color: #061726;
cursor: pointer;
font-weight: bold;
text-decoration: none;
width: 20%;
}
.signin-button:hover {
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;
justify-content: center;
background-color: white;
border-bottom: 2px solid #D6E0EA;
border-left: none;
border-right: 2px solid #D6E0EA;
border-top: 2px solid #D6E0EA;
color: #061726;
cursor: pointer;
font-weight: bold;
text-decoration: none;
width: 10%;
}
.signin-show-icon {
height: 16px;
width: 16px;
}
.signin-label {
color: #061726;
display: flex;
font-family: sans-serif;
font-weight: bold;
justify-content: center;
padding: 5px;
text-transform: uppercase;
width: 100%;
}
.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;
flex-direction: column;
font-family: sans-serif;
font-weight: bold;
height: 10%;
justify-content: center;
margin: 20px;
text-align: center;
}
.signin-title-text {
font-size: 20px;
margin: 0;
}
.signin-title-subtext {
font-size: 12px;
margin: 0;
}

View file

@ -1,107 +0,0 @@
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';
import { SmallModal } from '../components/Modal';
function SignIn() {
const [error, setError] = useState('');
const navigate = useNavigate();
const [config, setConfig] = useState({ channels: {} });
const [addChannelError, setAddChannelError] = useState('');
const [streamKey, setStreamKey] = useState('');
const updateStreamKey = (event) => setStreamKey(event.target.value);
const [showStreamKey, setShowStreamKey] = useState(false);
const updateShowStreamKey = () => setShowStreamKey(!showStreamKey);
useEffect(() => {
Config()
.then((response) => {
setConfig(response);
})
.catch((error) => {
// TODO: display error to user
setError('Error loading config: ' + error);
console.log('error getting config', error);
});
}, []);
const saveStreamKey = () => {
AddChannel(streamKey)
.then((response) => {
console.log(response);
setConfig(response);
setStreamKey('');
})
.catch((error) => {
console.log('error adding channel', error);
setAddChannelError(error);
});
};
const openStreamDashboard = (cid) => {
navigate(NavDashboard, { state: { cid: cid } });
};
return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '300px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
<div id='SignIn'>
<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'>Add Channel</label>
<span className='add-channel-description'>
Copy your API key from your Rumble account
</span>
<div className='signin-input-button'>
<input
id='StreamKey'
className='signin-input'
onChange={updateStreamKey}
placeholder='Stream Key'
type={showStreamKey ? 'text' : 'password'}
value={streamKey}
/>
<button className='signin-show' onClick={updateShowStreamKey}>
<img
className='signin-show-icon'
src={showStreamKey ? EyeSlash : Eye}
></img>
</button>
<button className='signin-button' onClick={saveStreamKey}>
Save
</button>
</div>
<span className='add-channel-error'>
{addChannelError ? addChannelError : '\u00A0'}
</span>
</div>
<div className='signin-footer'></div>
</div>
</>
);
}
export default SignIn;

View file

@ -1,10 +0,0 @@
html {
}
body {
margin: 0;
}
#app {
/* height: 100vh; */
}

View file

@ -1,107 +0,0 @@
package api
import (
"context"
"fmt"
"log"
"sync"
"time"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type Api struct {
ctx context.Context
cancel context.CancelFunc
cancelMu sync.Mutex
logError *log.Logger
logInfo *log.Logger
querying bool
queryingMu sync.Mutex
}
func NewApi(logError *log.Logger, logInfo *log.Logger) *Api {
return &Api{logError: logError, logInfo: logInfo}
}
func (a *Api) Startup(ctx context.Context) {
a.ctx = ctx
}
func (a *Api) Start(url string, interval time.Duration) error {
a.logInfo.Println("Api.Start")
if url == "" {
return fmt.Errorf("empty stream key")
}
a.queryingMu.Lock()
start := !a.querying
a.querying = true
a.queryingMu.Unlock()
if start {
a.logInfo.Println("Start querying")
ctx, cancel := context.WithCancel(context.Background())
a.cancelMu.Lock()
a.cancel = cancel
a.cancelMu.Unlock()
go a.start(ctx, url, interval)
} else {
a.logInfo.Println("Querying already started")
}
return nil
}
func (a *Api) Stop() {
a.logInfo.Println("Stop querying")
a.cancelMu.Lock()
if a.cancel != nil {
a.cancel()
}
a.cancelMu.Unlock()
}
func (a *Api) start(ctx context.Context, url string, interval time.Duration) {
for {
a.query(url)
timer := time.NewTimer(interval)
select {
case <-ctx.Done():
a.queryingMu.Lock()
a.querying = false
a.queryingMu.Unlock()
timer.Stop()
return
case <-timer.C:
}
}
}
func (a *Api) query(url string) {
// a.logInfo.Println("QueryAPI")
client := rumblelivestreamlib.Client{ApiKey: url}
resp, err := client.Request()
if err != nil {
a.logError.Println("api: error executing client request:", err)
a.Stop()
runtime.EventsEmit(a.ctx, "QueryResponseError", "Failed to query API")
return
}
// resp := &rumblelivestreamlib.LivestreamResponse{}
// resp.Followers.RecentFollowers = append(resp.Followers.RecentFollowers, rumblelivestreamlib.Follower{"tyler-follow", "2023-12-12T21:53:34-04:00"})
// resp.Subscribers.RecentSubscribers = append(resp.Subscribers.RecentSubscribers, rumblelivestreamlib.Subscriber{"tyler-sub", "tyler-sub", 500, 5, "2023-12-14T21:53:34-04:00"})
// resp.Subscribers.RecentSubscribers = append(resp.Subscribers.RecentSubscribers, rumblelivestreamlib.Subscriber{"tyler-sub", "tyler-sub", 500, 5, "2023-12-13T21:53:34-04:00"})
// resp.Subscribers.RecentSubscribers = append(resp.Subscribers.RecentSubscribers, rumblelivestreamlib.Subscriber{"tyler-sub", "tyler-sub", 500, 5, "2023-11-13T21:53:34-04:00"})
// resp.Livestreams = []rumblelivestreamlib.Livestream{
// {
// CreatedOn: "2023-12-16T16:13:30+00:00",
// WatchingNow: 4},
// }
runtime.EventsEmit(a.ctx, "QueryResponse", &resp)
}
// TODO: if start errors, send event

View file

@ -1,506 +0,0 @@
package chatbot
import (
"bufio"
"bytes"
"context"
"crypto/rand"
"fmt"
"html/template"
"log"
"math/big"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/tylertravisty/rum-goggles/internal/config"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type ChatBot struct {
ctx context.Context
cancelChatStream context.CancelFunc
cancelChatStreamMu sync.Mutex
channelID int
client *rumblelivestreamlib.Client
commands map[string]chan rumblelivestreamlib.ChatView
commandsMu sync.Mutex
Cfg config.ChatBot
logError *log.Logger
messages map[string]*message
messagesMu sync.Mutex
}
type message struct {
cancel context.CancelFunc
cancelMu sync.Mutex
asChannel bool
command string
id string
interval time.Duration
onCommand bool
onCommandFollower bool
onCommandRantAmount int
OnCommandSubscriber bool
text string
textFromFile []string
}
func NewChatBot(ctx context.Context, cfg config.ChatBot, logError *log.Logger) (*ChatBot, error) {
// client, err := rumblelivestreamlib.NewClient("", validUrl(streamUrl))
client, err := rumblelivestreamlib.NewClient(cfg.Session.Client)
if err != nil {
return nil, fmt.Errorf("chatbot: error creating new client: %v", err)
}
return &ChatBot{ctx: ctx, client: client, Cfg: cfg, commands: map[string]chan rumblelivestreamlib.ChatView{}, logError: logError, messages: map[string]*message{}}, nil
}
func validUrl(url string) string {
valid := url
if !strings.HasPrefix(valid, "https://") {
valid = "https://" + valid
}
return valid
}
func (cb *ChatBot) StartMessage(id string) error {
msg, exists := cb.Cfg.Messages[id]
if !exists {
return fmt.Errorf("chatbot: message does not exist")
}
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
m, exists := cb.messages[id]
if exists {
m.stop()
delete(cb.messages, id)
}
textFromFile := []string{}
if msg.TextFile != "" {
file, err := os.Open(msg.TextFile)
if err != nil {
return fmt.Errorf("chatbot: error opening file with responses: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
textFromFile = append(textFromFile, line)
}
}
m = &message{
asChannel: msg.AsChannel,
command: msg.Command,
id: msg.ID,
interval: msg.Interval,
onCommand: msg.OnCommand,
onCommandFollower: msg.OnCommandFollower,
onCommandRantAmount: msg.OnCommandRantAmount,
OnCommandSubscriber: msg.OnCommandSubscriber,
text: msg.Text,
textFromFile: textFromFile,
}
ctx, cancel := context.WithCancel(context.Background())
m.cancelMu.Lock()
m.cancel = cancel
m.cancelMu.Unlock()
if msg.OnCommand {
go cb.startCommand(ctx, m)
} else {
go cb.startMessage(ctx, m)
}
cb.messages[id] = m
return nil
}
func (cb *ChatBot) startCommand(ctx context.Context, m *message) {
cb.commandsMu.Lock()
ch := make(chan rumblelivestreamlib.ChatView)
cb.commands[m.command] = ch
cb.commandsMu.Unlock()
var prev time.Time
for {
runtime.EventsEmit(cb.ctx, "ChatBotCommandActive-"+m.id, m.id)
// TODO: if error, emit error to user, stop loop?
select {
case <-ctx.Done():
runtime.EventsEmit(cb.ctx, "ChatBotMessageError-"+m.id, m.id)
return
case cv := <-ch:
if m.onCommandFollower && !cv.IsFollower {
break
}
subscriber := false
for _, badge := range cv.Badges {
if badge == "recurring_subscription" || badge == "locals_supporter" {
subscriber = true
}
}
if m.OnCommandSubscriber && !subscriber {
break
}
// if m.onCommandRantAmount > 0 && cv.Rant < m.onCommandRantAmount * 100 {
// break
// }
if cv.Rant < m.onCommandRantAmount*100 {
break
}
// TODO: parse !command
now := time.Now()
if now.Sub(prev) < m.interval*time.Second {
break
}
err := cb.chatCommand(m, cv)
if err != nil {
cb.logError.Println("error sending chat:", err)
cb.StopMessage(m.id)
runtime.EventsEmit(cb.ctx, "ChatBotCommandError-"+m.id, m.id)
return
} else {
prev = now
// runtime.EventsEmit(cb.ctx, "ChatBotCommandActive-"+m.id, m.id)
}
}
}
}
func (cb *ChatBot) startMessage(ctx context.Context, m *message) {
for {
// TODO: if error, emit error to user, stop loop?
err := cb.chat(m)
if err != nil {
cb.logError.Println("error sending chat:", err)
cb.StopMessage(m.id)
runtime.EventsEmit(cb.ctx, "ChatBotMessageError-"+m.id, m.id)
// TODO: stop this loop?
} else {
runtime.EventsEmit(cb.ctx, "ChatBotMessageActive-"+m.id, m.id)
}
timer := time.NewTimer(m.interval * time.Second)
select {
case <-ctx.Done():
timer.Stop()
runtime.EventsEmit(cb.ctx, "ChatBotMessageError-"+m.id, m.id)
return
case <-timer.C:
}
}
}
func (cb *ChatBot) chatCommand(m *message, cv rumblelivestreamlib.ChatView) error {
if cb.client == nil {
return fmt.Errorf("client is nil")
}
msgText := m.text
if len(m.textFromFile) > 0 {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(m.textFromFile))))
if err != nil {
return fmt.Errorf("error generating random number: %v", err)
}
msgText = m.textFromFile[n.Int64()]
}
tmpl, err := template.New("chat").Parse(msgText)
if err != nil {
return fmt.Errorf("error creating template: %v", err)
}
fields := struct {
ChannelName string
Username string
Rant int
}{
ChannelName: cv.ChannelName,
Username: cv.Username,
Rant: cv.Rant / 100,
}
var textB bytes.Buffer
err = tmpl.Execute(&textB, fields)
if err != nil {
return fmt.Errorf("error executing template: %v", err)
}
text := textB.String()
//err := cb.client.Chat(m.asChannel, text)
var channelID *int
if m.asChannel {
cid := cb.channelID
channelID = &cid
}
err = cb.client.Chat(text, channelID)
if err != nil {
return fmt.Errorf("error sending chat: %v", err)
}
return nil
}
func (cb *ChatBot) chat(m *message) error {
if cb.client == nil {
return fmt.Errorf("client is nil")
}
text := m.text
if len(m.textFromFile) > 0 {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(m.textFromFile))))
if err != nil {
return fmt.Errorf("error generating random number: %v", err)
}
text = m.textFromFile[n.Int64()]
}
//err := cb.client.Chat(m.asChannel, text)
var channelID *int
if m.asChannel {
cid := cb.channelID
channelID = &cid
}
err := cb.client.Chat(text, channelID)
if err != nil {
return fmt.Errorf("error sending chat: %v", err)
}
return nil
}
func (cb *ChatBot) StartAllMessages() error {
for _, msg := range cb.Cfg.Messages {
err := cb.StartMessage(msg.ID)
if err != nil {
return fmt.Errorf("error starting message: %v", err)
}
}
return nil
}
func (cb *ChatBot) StopAllMessages() error {
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
for id, m := range cb.messages {
m.stop()
delete(cb.messages, id)
if m.command != "" && m.onCommand {
cb.commandsMu.Lock()
ch, exists := cb.commands[m.command]
if exists {
close(ch)
delete(cb.commands, m.command)
}
cb.commandsMu.Unlock()
}
}
return nil
}
func (cb *ChatBot) StopMessage(id string) error {
cb.messagesMu.Lock()
defer cb.messagesMu.Unlock()
m, exists := cb.messages[id]
if exists {
m.stop()
delete(cb.messages, id)
if m.command != "" && m.onCommand {
cb.commandsMu.Lock()
defer cb.commandsMu.Unlock()
ch, exists := cb.commands[m.command]
if exists {
close(ch)
delete(cb.commands, m.command)
}
}
}
return nil
}
func (m *message) stop() {
m.cancelMu.Lock()
if m.cancel != nil {
m.cancel()
}
m.cancelMu.Unlock()
}
func (cb *ChatBot) LoggedIn() (bool, error) {
if cb.client == nil {
return false, fmt.Errorf("chatbot: client is nil")
}
loggedIn, err := cb.client.LoggedIn()
if err != nil {
return false, fmt.Errorf("chatbot: error checking if chat bot is logged in: %v", err)
}
return loggedIn, nil
}
func (cb *ChatBot) Login(username string, password string) ([]*http.Cookie, error) {
if cb.client == nil {
return nil, fmt.Errorf("chatbot: client is nil")
}
cookies, err := cb.client.Login(username, password)
if err != nil {
return nil, fmt.Errorf("chatbot: error logging in: %v", err)
}
return cookies, nil
}
func (cb *ChatBot) Logout() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
err := cb.client.Logout()
if err != nil {
return fmt.Errorf("chatbot: error logging out: %v", err)
}
return nil
}
func (cb *ChatBot) StartChatStream() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
ci, err := cb.client.ChatInfo(true)
if err != nil {
return fmt.Errorf("chatbot: error getting chat info: %v", err)
}
cb.channelID = ci.ChannelID
ctx, cancel := context.WithCancel(context.Background())
cb.cancelChatStreamMu.Lock()
cb.cancelChatStream = cancel
cb.cancelChatStreamMu.Unlock()
go cb.startChatStream(ctx)
// err = cb.client.StartChatStream(cb.handleChat, cb.handleError)
// if err != nil {
// return fmt.Errorf("chatbot: error starting chat stream: %v", err)
// }
return nil
}
func (cb *ChatBot) startChatStream(ctx context.Context) {
for {
err := cb.client.StartChatStream(cb.handleChat, cb.handleError)
if err != nil {
cb.logError.Println("error starting chat stream:", err)
runtime.EventsEmit(cb.ctx, "ChatBotChatStreamError", "Error starting chat stream.")
return
}
select {
case <-time.After(90 * time.Minute):
cb.client.StopChatStream()
break
case <-ctx.Done():
cb.client.StopChatStream()
return
}
}
}
func (cb *ChatBot) StopChatStream() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
// TODO: should a panic be caught here?
cb.cancelChatStreamMu.Lock()
if cb.cancelChatStream != nil {
cb.cancelChatStream()
} else {
cb.client.StopChatStream()
}
cb.cancelChatStreamMu.Unlock()
return nil
}
func (cb *ChatBot) RestartChatStream() error {
if cb.client == nil {
return fmt.Errorf("chatbot: client is nil")
}
cb.client.StopChatStream()
err := cb.client.StartChatStream(cb.handleChat, cb.handleError)
if err != nil {
return fmt.Errorf("chatbot: error starting chat stream: %v", err)
}
return nil
}
func (cb *ChatBot) handleChat(cv rumblelivestreamlib.ChatView) {
// runtime.EventsEmit(cb.ctx, "ChatMessageReceived", cv)
if cv.Type != "init" {
cb.handleCommand(cv)
}
}
func (cb *ChatBot) handleCommand(cv rumblelivestreamlib.ChatView) {
cb.commandsMu.Lock()
defer cb.commandsMu.Unlock()
words := strings.Split(cv.Text, " ")
first := words[0]
cmd, exists := cb.commands[first]
if !exists {
return
}
select {
case cmd <- cv:
return
default:
return
}
}
func (cb *ChatBot) handleError(err error) {
cb.logError.Println("error handling chat message:", err)
// runtime.EventsEmit(cb.ctx, "ChatError", err)
}

View file

@ -1,241 +0,0 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"time"
"github.com/tylertravisty/go-utils/random"
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
)
const (
CIDLen = 8
DefaultInterval = 10
configDir = ".rum-goggles"
configDirWin = "RumGoggles"
configFile = "config.json"
logFile = "logs.txt"
)
func LogFile() (*os.File, error) {
dir, err := buildConfigDir()
if err != nil {
return nil, fmt.Errorf("config: error getting config directory: %v", err)
}
err = os.MkdirAll(dir, 0750)
if err != nil {
return nil, fmt.Errorf("config: error making config directory: %v", err)
}
fp := filepath.Join(dir, logFile)
f, err := os.OpenFile(fp, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
return nil, fmt.Errorf("config: error opening log file: %v", err)
}
return f, nil
}
func buildConfigDir() (string, error) {
userDir, err := userDir()
if err != nil {
return "", fmt.Errorf("error getting user directory: %v", err)
}
var dir string
switch runtime.GOOS {
case "windows":
dir = filepath.Join(userDir, configDirWin)
default:
dir = filepath.Join(userDir, configDir)
}
return dir, nil
}
func userDir() (string, error) {
var dir string
var err error
switch runtime.GOOS {
case "windows":
dir, err = os.UserCacheDir()
default:
dir, err = os.UserHomeDir()
}
return dir, err
}
type ChatMessage struct {
ID string `json:"id"`
AsChannel bool `json:"as_channel"`
Command string `json:"command"`
Interval time.Duration `json:"interval"`
OnCommand bool `json:"on_command"`
OnCommandFollower bool `json:"on_command_follower"`
OnCommandRantAmount int `json:"on_command_rant_amount"`
OnCommandSubscriber bool `json:"on_command_subscriber"`
Text string `json:"text"`
TextFile string `json:"text_file"`
}
type ChatBotSession struct {
Client rumblelivestreamlib.NewClientOptions `json:"client"`
Username string `json:"username"`
}
type ChatBot struct {
Messages map[string]ChatMessage `json:"messages"`
Session ChatBotSession `json:"session"`
// Commands []ChatCommand
}
type Channel struct {
ID string `json:"id"`
ApiUrl string `json:"api_url"`
Name string `json:"name"`
Interval time.Duration `json:"interval"`
ChatBot ChatBot `json:"chat_bot"`
}
func (a *App) NewChannel(url string, name string) (string, error) {
for {
id, err := random.String(CIDLen)
if err != nil {
return "", fmt.Errorf("config: error generating ID: %v", err)
}
if _, exists := a.Channels[id]; !exists {
a.Channels[id] = Channel{id, url, name, DefaultInterval, ChatBot{Messages: map[string]ChatMessage{}}}
return id, nil
}
}
}
func (a *App) DeleteChatMessage(cid string, cm ChatMessage) error {
channel, exists := a.Channels[cid]
if !exists {
return fmt.Errorf("config: channel does not exist")
}
_, exists = channel.ChatBot.Messages[cm.ID]
if !exists {
return fmt.Errorf("config: message does not exist")
}
delete(channel.ChatBot.Messages, cm.ID)
return nil
}
func (a *App) NewChatMessage(cid string, cm ChatMessage) (string, error) {
if _, exists := a.Channels[cid]; !exists {
return "", fmt.Errorf("config: channel does not exist")
}
for {
id, err := random.String(CIDLen)
if err != nil {
return "", fmt.Errorf("config: error generating ID: %v", err)
}
_, exists := a.Channels[cid].ChatBot.Messages[id]
if !exists {
cm.ID = id
a.Channels[cid].ChatBot.Messages[id] = cm
return id, nil
}
}
}
func (a *App) UpdateChatMessage(cid string, cm ChatMessage) (string, error) {
channel, exists := a.Channels[cid]
if !exists {
return "", fmt.Errorf("config: channel does not exist")
}
_, exists = channel.ChatBot.Messages[cm.ID]
if !exists {
return "", fmt.Errorf("config: message does not exist")
}
channel.ChatBot.Messages[cm.ID] = cm
return cm.ID, nil
}
type App struct {
Channels map[string]Channel `json:"channels"`
}
func Load() (*App, error) {
dir, err := buildConfigDir()
if err != nil {
return nil, fmt.Errorf("config: error getting config directory: %v", err)
}
fp := filepath.Join(dir, configFile)
app, err := load(fp)
if err != nil {
return nil, fmt.Errorf("config: error loading config: %w", err)
}
return app, nil
}
func load(filepath string) (*App, error) {
f, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("error opening file: %w", err)
}
var app App
decoder := json.NewDecoder(f)
err = decoder.Decode(&app)
if err != nil {
return nil, fmt.Errorf("error decoding file into json: %v", err)
}
return &app, nil
}
func (a *App) Save() error {
dir, err := buildConfigDir()
if err != nil {
return fmt.Errorf("config: error getting config directory: %v", err)
}
err = os.MkdirAll(dir, 0750)
if err != nil {
return fmt.Errorf("config: error making config directory: %v", err)
}
fp := filepath.Join(dir, configFile)
err = a.save(fp)
if err != nil {
return fmt.Errorf("config: error saving config: %v", err)
}
return nil
}
func (app *App) save(filepath string) error {
b, err := json.MarshalIndent(app, "", "\t")
if err != nil {
return fmt.Errorf("error encoding config into json: %v", err)
}
err = os.WriteFile(filepath, b, 0666)
if err != nil {
return fmt.Errorf("error writing config file: %v", err)
}
return nil
}

View file

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

View file

@ -1,5 +0,0 @@
{
"devDependencies": {
"react-router-dom": "^6.20.1"
}
}

6
v1/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
build/bin
build/darwin
build/windows
node_modules
frontend/dist
frontend/wailsjs

1628
v1/app.go Normal file

File diff suppressed because it is too large Load diff

View file

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -54,9 +54,9 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
@ -64,11 +64,11 @@
"@babel/generator": "^7.23.6", "@babel/generator": "^7.23.6",
"@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-module-transforms": "^7.23.3", "@babel/helper-module-transforms": "^7.23.3",
"@babel/helpers": "^7.23.6", "@babel/helpers": "^7.23.9",
"@babel/parser": "^7.23.6", "@babel/parser": "^7.23.9",
"@babel/template": "^7.22.15", "@babel/template": "^7.23.9",
"@babel/traverse": "^7.23.6", "@babel/traverse": "^7.23.9",
"@babel/types": "^7.23.6", "@babel/types": "^7.23.9",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@ -252,14 +252,14 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz",
"integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/template": "^7.22.15", "@babel/template": "^7.23.9",
"@babel/traverse": "^7.23.6", "@babel/traverse": "^7.23.9",
"@babel/types": "^7.23.6" "@babel/types": "^7.23.9"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -280,9 +280,9 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
"dev": true, "dev": true,
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -371,23 +371,23 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.22.15", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.22.13", "@babel/code-frame": "^7.23.5",
"@babel/parser": "^7.22.15", "@babel/parser": "^7.23.9",
"@babel/types": "^7.22.15" "@babel/types": "^7.23.9"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz",
"integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.23.5", "@babel/code-frame": "^7.23.5",
@ -396,8 +396,8 @@
"@babel/helper-function-name": "^7.23.0", "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6", "@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.6", "@babel/parser": "^7.23.9",
"@babel/types": "^7.23.6", "@babel/types": "^7.23.9",
"debug": "^4.3.1", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
}, },
@ -406,9 +406,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.23.4", "@babel/helper-string-parser": "^7.23.4",
@ -466,9 +466,9 @@
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -490,9 +490,9 @@
"dev": true "dev": true
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.20", "version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -506,9 +506,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.45", "version": "18.2.58",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.58.tgz",
"integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==", "integrity": "sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@ -517,9 +517,9 @@
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "18.2.17", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz",
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
@ -565,9 +565,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.22.2", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -584,8 +584,8 @@
} }
], ],
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001565", "caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.601", "electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14", "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13" "update-browserslist-db": "^1.0.13"
}, },
@ -597,9 +597,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001570", "version": "1.0.30001589",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz",
"integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -675,9 +675,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.611", "version": "1.4.680",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.611.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.680.tgz",
"integrity": "sha512-ZtRpDxrjHapOwxtv+nuth5ByB8clyn8crVynmRNGO3wG3LOp8RTcyZDqwaI6Ng6y8FCK2hVZmJoqwCskKbNMaw==", "integrity": "sha512-4nToZ5jlPO14W82NkF32wyjhYqQByVaDmLy4J2/tYcAbJfgO2TKJC780Az1V13gzq4l73CJ0yuyalpXvxXXD9A==",
"dev": true "dev": true
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
@ -1038,9 +1038,9 @@
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -1106,9 +1106,9 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -1233,9 +1233,9 @@
"dev": true "dev": true
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.32", "version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1421,9 +1421,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "3.2.7", "version": "3.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",
"integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", "integrity": "sha512-EtQU16PLIJpAZol2cTLttNP1mX6L0SyI0pgQB1VOoWeQnMSvtiwovV3D6NcjN8CZQWWyESD2v5NGnpz5RvgOZA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
@ -1504,9 +1504,9 @@
"dev": true "dev": true
}, },
"@babel/core": { "@babel/core": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
"integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
@ -1514,11 +1514,11 @@
"@babel/generator": "^7.23.6", "@babel/generator": "^7.23.6",
"@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-module-transforms": "^7.23.3", "@babel/helper-module-transforms": "^7.23.3",
"@babel/helpers": "^7.23.6", "@babel/helpers": "^7.23.9",
"@babel/parser": "^7.23.6", "@babel/parser": "^7.23.9",
"@babel/template": "^7.22.15", "@babel/template": "^7.23.9",
"@babel/traverse": "^7.23.6", "@babel/traverse": "^7.23.9",
"@babel/types": "^7.23.6", "@babel/types": "^7.23.9",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@ -1650,14 +1650,14 @@
"dev": true "dev": true
}, },
"@babel/helpers": { "@babel/helpers": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz",
"integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==", "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/template": "^7.22.15", "@babel/template": "^7.23.9",
"@babel/traverse": "^7.23.6", "@babel/traverse": "^7.23.9",
"@babel/types": "^7.23.6" "@babel/types": "^7.23.9"
} }
}, },
"@babel/highlight": { "@babel/highlight": {
@ -1672,9 +1672,9 @@
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
"dev": true "dev": true
}, },
"@babel/plugin-syntax-jsx": { "@babel/plugin-syntax-jsx": {
@ -1727,20 +1727,20 @@
} }
}, },
"@babel/template": { "@babel/template": {
"version": "7.22.15", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.22.13", "@babel/code-frame": "^7.23.5",
"@babel/parser": "^7.22.15", "@babel/parser": "^7.23.9",
"@babel/types": "^7.22.15" "@babel/types": "^7.23.9"
} }
}, },
"@babel/traverse": { "@babel/traverse": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz",
"integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.23.5", "@babel/code-frame": "^7.23.5",
@ -1749,16 +1749,16 @@
"@babel/helper-function-name": "^7.23.0", "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6", "@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.23.6", "@babel/parser": "^7.23.9",
"@babel/types": "^7.23.6", "@babel/types": "^7.23.9",
"debug": "^4.3.1", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.23.6", "version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-string-parser": "^7.23.4", "@babel/helper-string-parser": "^7.23.4",
@ -1792,9 +1792,9 @@
} }
}, },
"@jridgewell/resolve-uri": { "@jridgewell/resolve-uri": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true "dev": true
}, },
"@jridgewell/set-array": { "@jridgewell/set-array": {
@ -1810,9 +1810,9 @@
"dev": true "dev": true
}, },
"@jridgewell/trace-mapping": { "@jridgewell/trace-mapping": {
"version": "0.3.20", "version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -1826,9 +1826,9 @@
"dev": true "dev": true
}, },
"@types/react": { "@types/react": {
"version": "18.2.45", "version": "18.2.58",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.58.tgz",
"integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==", "integrity": "sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
@ -1837,9 +1837,9 @@
} }
}, },
"@types/react-dom": { "@types/react-dom": {
"version": "18.2.17", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz",
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/react": "*" "@types/react": "*"
@ -1876,21 +1876,21 @@
} }
}, },
"browserslist": { "browserslist": {
"version": "4.22.2", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001565", "caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.601", "electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14", "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13" "update-browserslist-db": "^1.0.13"
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001570", "version": "1.0.30001589",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz",
"integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==",
"dev": true "dev": true
}, },
"chalk": { "chalk": {
@ -1941,9 +1941,9 @@
} }
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.4.611", "version": "1.4.680",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.611.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.680.tgz",
"integrity": "sha512-ZtRpDxrjHapOwxtv+nuth5ByB8clyn8crVynmRNGO3wG3LOp8RTcyZDqwaI6Ng6y8FCK2hVZmJoqwCskKbNMaw==", "integrity": "sha512-4nToZ5jlPO14W82NkF32wyjhYqQByVaDmLy4J2/tYcAbJfgO2TKJC780Az1V13gzq4l73CJ0yuyalpXvxXXD9A==",
"dev": true "dev": true
}, },
"esbuild": { "esbuild": {
@ -2117,9 +2117,9 @@
"optional": true "optional": true
}, },
"escalade": { "escalade": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"dev": true "dev": true
}, },
"escape-string-regexp": { "escape-string-regexp": {
@ -2160,9 +2160,9 @@
"dev": true "dev": true
}, },
"hasown": { "hasown": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dev": true, "dev": true,
"requires": { "requires": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -2251,9 +2251,9 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "8.4.32", "version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"dev": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
@ -2362,9 +2362,9 @@
} }
}, },
"vite": { "vite": {
"version": "3.2.7", "version": "3.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.7.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",
"integrity": "sha512-29pdXjk49xAP0QBr0xXqu2s5jiQIXNvE/xwd0vUizYT2Hzqe4BksNNoWllFVXJf4eLZ+UlVQmXfB4lWrc+t18g==", "integrity": "sha512-EtQU16PLIJpAZol2cTLttNP1mX6L0SyI0pgQB1VOoWeQnMSvtiwovV3D6NcjN8CZQWWyESD2v5NGnpz5RvgOZA==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",

View file

@ -1,3 +1,3 @@
#App { #app {
height: 100vh; height: 100vh;
} }

View file

@ -1,18 +1,18 @@
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { MemoryRouter as Router, Route, Routes, Link } from 'react-router-dom'; import { MemoryRouter as Router, Route, Routes, Link } from 'react-router-dom';
import './App.css'; import './App.css';
import { NavDashboard, NavSignIn, NavStartup } from './Navigation';
import { NavSignIn, NavDashboard } from './screens/Navigation';
import Dashboard from './screens/Dashboard'; import Dashboard from './screens/Dashboard';
import SignIn from './screens/SignIn'; import SignIn from './screens/SignIn';
import Startup from './screens/Startup';
function App() { function App() {
return ( return (
<Router> <Router>
<Routes> <Routes>
<Route path={NavSignIn} element={<SignIn />}></Route> <Route path={NavStartup} element={<Startup />} />
<Route path={NavDashboard} element={<Dashboard />}></Route> <Route path={NavSignIn} element={<SignIn />} />
<Route path={NavDashboard} element={<Dashboard />} />
</Routes> </Routes>
</Router> </Router>
); );

View file

@ -0,0 +1,3 @@
export const NavDashboard = '/dashboard';
export const NavSignIn = '/signin';
export const NavStartup = '/';

View file

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,51 @@
import chess_rook from './icons/Font-Awesome/chess-rook.png';
import chevron_down from './icons/twbs/chevron-down.png';
import chevron_left from './icons/twbs/chevron-left.png';
import chevron_right from './icons/twbs/chevron-right.png';
import circle_green_background from './icons/twbs/circle-green-background.png';
import circle_red_background from './icons/twbs/circle-red-background.png';
import eye from './icons/twbs/eye.png';
import eye_red from './icons/twbs/eye-red.png';
import eye_slash from './icons/twbs/eye-slash.png';
import gear_fill from './icons/twbs/gear-fill.png';
import gear_fill_white from './icons/twbs/gear-fill-white.png';
import heart from './icons/twbs/heart-fill.png';
import pause from './icons/twbs/pause-circle-green.png';
import pause_big from './icons/twbs/pause-fill.png';
import play from './icons/twbs/play-circle-green.png';
import play_big from './icons/twbs/play-fill.png';
import play_big_green from './icons/twbs/play-fill-green.png';
import plus_circle from './icons/twbs/plus-circle-fill.png';
import robot from './icons/Font-Awesome/robot.png';
import star from './icons/twbs/star-fill.png';
import stop_big_red from './icons/twbs/stop-fill-red.png';
import thumbs_down from './icons/twbs/hand-thumbs-down-fill.png';
import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png';
import x_lg from './icons/twbs/x-lg.png';
import logo from './logo/logo.png';
export const ChessRook = chess_rook;
export const ChevronLeft = chevron_left;
export const ChevronDown = chevron_down;
export const ChevronRight = chevron_right;
export const CircleGreenBackground = circle_green_background;
export const CircleRedBackground = circle_red_background;
export const Eye = eye;
export const EyeRed = eye_red;
export const EyeSlash = eye_slash;
export const Gear = gear_fill;
export const GearWhite = gear_fill_white;
export const Heart = heart;
export const Logo = logo;
export const Pause = pause;
export const PauseBig = pause_big;
export const Play = play;
export const PlayBig = play_big;
export const PlayBigGreen = play_big_green;
export const PlusCircle = plus_circle;
export const Robot = robot;
export const Star = star;
export const StopBigRed = stop_big_red;
export const ThumbsDown = thumbs_down;
export const ThumbsUp = thumbs_up;
export const XLg = x_lg;

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -0,0 +1,588 @@
.chatbot {
background-color: #344453;
display: flex;
flex-direction: column;
height: 100%;
min-width: 500px;
width: 100%;
}
.chatbot-header {
align-items: center;
border-bottom: 1px solid #061726;
box-sizing: border-box;
display: flex;
flex-direction: row;
min-height: 55px;
justify-content: space-between;
padding: 10px 20px;
width: 100%;
}
.chatbot-header-button {
align-items: center;
background-color: #344453;
border: none;
display: flex;
justify-content: center;
padding-left: 10px;
padding-right: 0px;
}
.chatbot-header-button:hover {
cursor: pointer;
}
.chatbot-header-button-icon {
height: 24px;
width: 24px;
}
.chatbot-header-left {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.chatbot-header-icon {
height: 28px;
width: 28px;
padding-right: 10px;
}
.chatbot-header-icon-back {
height: 28px;
width: 28px;
}
.chatbot-header-icon-back:hover {
/* background-color: #415568; */
cursor: pointer;
}
.chatbot-header-right {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.chatbot-header-title {
color: #eee;
font-family: sans-serif;
font-size: 20px;
font-weight: bold;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chatbot-list {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 0px;
}
.chatbot-list-item-button {
align-items: center;
background-color: #344453;
border: none;
border-radius: 3px;
display: flex;
justify-content: start;
padding: 15px 10px;
width: 100%;
}
.chatbot-list-item-button:hover {
background-color: #415568;
cursor: pointer;
}
.chatbot-list-item {
}
.chatbot-list-item-name {
color: #eee;
display: inline-block;
font-family: sans-serif;
font-size: 18px;
font-weight: bold;
max-width: 300px;
overflow: hidden;
padding: 0px 10px;
text-overflow: ellipsis;
white-space: nowrap;
/* width: 100%; */
}
.chatbot-modal-form {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
}
.chatbot-modal-input {
background-color: #061726;
border: none;
border-radius: 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.chatbot-modal-label {
color: white;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
width: 100%;
}
.chatbot-modal-label-warning {
color: #f23160;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
width: 100%;
}
.chatbot-modal-description {
color: #eee;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
text-align: center;
width: 100%;
}
.chatbot-modal-description-warning {
color: #f23160;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
margin-top: 10px;
text-align: center;
width: 100%;
}
.chatbot-modal-event-body {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
width: 100%;
}
.chatbot-modal-event-body-bottom {
align-items: center;
display: flex;
flex-direction: column;
height: 50%;
justify-content: space-between;
width: 100%;
}
.chatbot-modal-event-body-top {
align-items: center;
display: flex;
flex-direction: column;
height: 50%;
justify-content: space-evenly;
width: 100%;
}
.chatbot-modal-event-setting {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.chatbot-modal-event-options {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-evenly;
height: 158.5px;
width: 100%;
}
.chatbot-modal-event-options-follow {
align-items: center;
display: flex;
flex-direction: column;
justify-content: space-evenly;
height: 100%;
width: 100%;
}
.chatbot-modal-event-options-label {
align-items: center;
color: #eee;
display: flex;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
}
.chatbot-modal-event-options-label-warning {
align-items: center;
color: #f23160;
display: flex;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
}
.chatbot-modal-option-label {
align-items: center;
color: #eee;
display: flex;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
height: 32px;
}
.chatbot-modal-option-label-warning {
align-items: center;
color: #f23160;
display: flex;
font-family: sans-serif;
font-size: 16px;
font-style: italic;
font-weight: bold;
height: 32px;
}
.chatbot-modal-pages {
background-color: white;
/* border: 1px solid #D6E0EA; */
border-radius: 5px;
height: 100%;
overflow: auto;
width: 80%;
}
.chatbot-modal-page {
align-items: center;
display: flex;
}
.chatbot-modal-page-selected {
background-color: #85c742;
}
.chatbot-modal-page-button {
background-color: white;
border: none;
border-radius: 5px;
color: #061726;
font-family: sans-serif;
font-size: 18px;
font-weight: bold;
overflow: hidden;
padding: 10px 10px;
text-align: left;
white-space: nowrap;
width: 100%;
}
.chatbot-modal-page-button:hover {
background-color: #85c742;
cursor: pointer;
}
.chatbot-modal-review {
color: #eee;
font-family: sans-serif;
font-size: 16px;
height: 350px;
overflow-x: scroll;
overflow-y: scroll;
width: 100%;
}
.chatbot-modal-setting {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-top: 10px;
width: 100%;
}
.chatbot-modal-setting-description {
color: #eee;
font-family: sans-serif;
font-size: 16px;
}
.chatbot-modal-setting-description-warning {
color: #f23160;
font-family: sans-serif;
font-size: 16px;
font-style: italic;
}
.chatbot-modal-textarea {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.chatbot-modal-toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.chatbot-modal-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.chatbot-modal-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #495a6a;
-webkit-transition: .4s;
transition: .4s;
}
.chatbot-modal-toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
.chatbot-rules {
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding: 0px;
}
.chatbot-rule {
border-bottom: 1px solid #1f2e3c;
box-sizing: border-box;
color: white;
display: flex;
flex-direction: row;
font-family: sans-serif;
justify-content: space-between;
padding: 10px 20px;
}
.chatbot-rule-header {
font-weight: bold;
}
.chatbot-rule-output {
align-items: center;
display: flex;
justify-content: left;
overflow: hidden;
overflow-x: scroll;
white-space: nowrap;
width: 50%;
}
.chatbot-rule-buttons {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: center;
justify-content: space-evenly;
padding-left: 10px;
width: 75px;
}
.chatbot-rule-button {
align-items: center;
background-color: #344453;
border: none;
display: flex;
justify-content: center;
padding: 0px;
}
.chatbot-rule-button:hover {
cursor: pointer;
}
.chatbot-rule-button-icon {
height: 16px;
width: 16px;
}
.chatbot-rule-sender {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
overflow-x: scroll;
padding-left: 10px;
white-space: nowrap;
width: 25%;
}
.chatbot-rule-trigger {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: left;
overflow-x: scroll;
padding-left: 10px;
white-space: nowrap;
width: 25%;
}
.choose-file {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.choose-file-button-box {
min-width: 100px;
width: 100px;
}
.choose-file-button {
background-color: #85c742;
border: none;
border-radius: 5px;
color: #061726;
cursor: pointer;
font-size: 16px;
text-decoration: none;
/* width: 200px; */
width: 100%;
}
.choose-file-path {
color: #eee;
font-family: monospace;
font-size: 16px;
overflow: scroll;
margin-left: 5px;
white-space: nowrap;
}
input:checked + .chatbot-modal-toggle-slider {
background-color: #85c742;
}
input:checked + .chatbot-modal-toggle-slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.chatbot-modal-toggle-slider.round {
border-radius: 34px;
}
.chatbot-modal-toggle-slider.round:before {
border-radius: 50%;
}
.command-input {
border: none;
border-radius: 34px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 5px 10px 5px 10px;
text-align: center;
width: 100%;
}
.command-rant-amount {
border: none;
border-radius: 5px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 5px;
text-align: center;
}
.command-rant-amount-symbol {
color: #eee;
font-family: sans-serif;
font-size: 20px;
padding-right: 1px;
}
.dropdown-option-hide {
display: none;
}
.timer-input {
border: none;
border-radius: 34px;
box-sizing: border-box;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 5px 10px 5px 10px;
text-align: right;
}
.timer-input-zero::placeholder {
text-align: center;
}
.timer-input-value::placeholder {
color: black;
opacity: 1;
text-align: center;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
.dropdown {
width: 100%;
}
.dropdown-menu {
align-items: center;
background-color: white;
border: 1px solid #061726;
border-radius: 5px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px;
position: fixed;
z-index: 10;
}
.dropdown-menu-container {
width: 100%;
}
.dropdown-menu-background {
align-items: center;
display: flex;
height: 100vh;
justify-content: center;
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100vw;
z-index: 8;
}
.dropdown-menu-option {
background-color: white;
border: none;
border-radius: 5px;
box-sizing: border-box;
color: #061726;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
padding: 5px;
width: 100%;
}
.dropdown-menu-option-selected {
background-color: #77b23b;
}
.dropdown-menu-option:hover {
background-color: #77b23b;
cursor: pointer;
}
.dropdown-toggle {
align-items: center;
background-color: white;
border: 1px solid #061726;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 5px;
width: 100%;
}
.dropdown-toggle-text {
color: #061726;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
}
.dropdown-toggle-icon {
height: 20px;
width: 20px;
}

View file

@ -0,0 +1,97 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronDown } from '../assets';
import './DropDown.css';
export function DropDown(props) {
const [options, setOptions] = useState(props.options !== undefined ? props.options : []);
const [selected, setSelected] = useState(props.selected !== undefined ? props.selected : '');
const [toggled, setToggled] = useState(false);
const toggle = () => {
setToggled(!toggled);
};
useEffect(() => {
setSelected(props.selected !== undefined ? props.selected : '');
}, [props.selected]);
useEffect(() => {
setOptions(props.options !== undefined ? props.options : []);
}, [props.options]);
const select = (option) => {
props.select(option);
setSelected(option);
toggle();
};
return (
<div className='dropdown'>
<button className='dropdown-toggle' onClick={toggle}>
<div style={{ width: '20px' }}></div>
<span className='dropdown-toggle-text'>{selected}</span>
<img className='dropdown-toggle-icon' src={ChevronDown} />
</button>
{toggled && (
<DropDownMenu
options={options}
select={select}
selected={selected}
toggle={toggle}
/>
)}
</div>
);
}
function DropDownMenu(props) {
const menuRef = useRef();
const { width } = menuWidth(menuRef);
return (
<div className='dropdown-menu-container' ref={menuRef}>
{width !== undefined && (
<div className='dropdown-menu' style={{ width: width + 'px' }}>
{props.options.map((option, index) => (
<button
className={
props.selected === option
? 'dropdown-menu-option dropdown-menu-option-selected'
: 'dropdown-menu-option'
}
key={index}
onClick={() => props.select(option)}
>
{option}
</button>
))}
</div>
)}
<div className='dropdown-menu-background' onClick={props.toggle}></div>
</div>
);
}
export const menuWidth = (menuRef) => {
const [width, setWidth] = useState(0);
useEffect(() => {
const getWidth = () => ({ width: menuRef.current.offsetWidth });
const handleResize = () => {
setWidth(getWidth());
};
if (menuRef.current) {
setWidth(getWidth());
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [menuRef]);
return width;
};

View file

@ -1,7 +1,8 @@
.modal-background { .modal-background {
align-items: center; align-items: center;
background-color: transparent; /* background-color: transparent; */
background-color: rgba(0,0,0,0.8);
display: flex; display: flex;
height: 100vh; height: 100vh;
justify-content: center; justify-content: center;
@ -29,33 +30,51 @@
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
/* width: 20%; */ /* width: 20%; */
width: 70px; height: 40px;
min-width: 70px;
} }
.modal-button-cancel { .modal-button-cancel {
background-color: transparent; background-color: transparent;
border: 1px solid #495a6a; border: 1px solid #495a6a;
border-radius: 5px; border-radius: 5px;
color: #495a6a; /* color: #495a6a; */
color: white;
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
/* width: 20%; */ /* width: 20%; */
width: 70px; height: 40px;
min-width: 70px;
} }
.modal-button-delete { .modal-button-delete {
background-color: transparent; background-color: transparent;
border: 1px solid red; background-color: #f23160;
border: 1px solid #f23160;
border-radius: 5px; border-radius: 5px;
color: red; color: #eee;
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
/* width: 20%; */ height: 40px;
width: 70px; min-width: 70px;
}
.modal-button-delete-inactive {
background-color: transparent;
background-color: #d6e0ea80;
border: 1px solid #d6e0ea80;
border-radius: 5px;
color: #eee;
cursor: pointer;
font-size: 18px;
font-weight: bold;
text-decoration: none;
height: 40px;
min-width: 70px;
} }
.modal-close { .modal-close {
@ -79,8 +98,8 @@
.modal-container { .modal-container {
align-items: center; align-items: center;
background-color: rgba(6,23,38,1); background-color: #1f2e3c;
border: 1px solid #495a6a; /* border: 1px solid #495a6a; */
border-radius: 15px; border-radius: 15px;
color: black; color: black;
display: flex; display: flex;
@ -116,6 +135,20 @@
font-size: 24px; font-size: 24px;
} }
.small-modal-button-cancel {
background-color: transparent;
border: 1px solid #495a6a;
border-radius: 5px;
color: #495a6a;
cursor: pointer;
font-size: 18px;
font-weight: bold;
text-decoration: none;
/* width: 20%; */
height: 40px;
min-width: 70px;
}
.small-modal-button-delete { .small-modal-button-delete {
background-color: red; background-color: red;
border: none; border: none;
@ -126,14 +159,14 @@
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
/* width: 20%; */ /* width: 20%; */
width: 70px; min-width: 70px;
} }
.small-modal-container { .small-modal-container {
align-items: center; align-items: center;
/* background-color: rgba(6,23,38,1); */ /* background-color: rgba(6,23,38,1); */
background-color: white; background-color: white;
border: 1px solid #495a6a; /* border: 1px solid #495a6a; */
/* border: 1px solid black; */ /* border: 1px solid black; */
border-radius: 15px; border-radius: 15px;
color: black; color: black;
@ -167,10 +200,11 @@
.small-modal-message { .small-modal-message {
font-family: sans-serif; font-family: sans-serif;
font-size: 18px; font-size: 18px;
overflow-x: scroll;
} }
.small-modal-title { .small-modal-title {
color: black; color: black;
font-family: sans-serif; font-family: sans-serif;
font-size: 24px; font-size: 24px;
} }

View file

@ -1,12 +1,12 @@
import { XLg } from '../assets/icons'; import { XLg } from '../assets';
import './Modal.css'; import './Modal.css';
export function Modal(props) { export function Modal(props) {
return ( return (
<div <div
className='modal-background' className='modal-background'
onClick={props.onClose} onClick={props.backgroundClose && props.onClose}
style={{ zIndex: props.show ? 10 : -10 }} style={{ zIndex: props.show ? 8 : -8 }}
> >
<div <div
className='modal-container' className='modal-container'
@ -27,13 +27,25 @@ export function Modal(props) {
</button> </button>
)} )}
{props.deleteButton && ( {props.deleteButton && (
<button className='modal-button-delete' onClick={props.onDelete}> <button
className={
props.deleteActive
? 'modal-button-delete'
: 'modal-button-delete-inactive'
}
onClick={props.onDelete}
>
{props.deleteButton} {props.deleteButton}
</button> </button>
)} )}
{props.submitButton && ( {props.submitButton && (
<button className='modal-button' onClick={props.onSubmit}> <button className='modal-button' onClick={props.onSubmit}>
{props.submitButton} {/* {props.submitButton} */}
{props.submitLoading ? (
<div className='loader'></div>
) : (
props.submitButton
)}
</button> </button>
)} )}
</div> </div>
@ -65,7 +77,7 @@ export function SmallModal(props) {
</div> </div>
<div className='small-modal-footer'> <div className='small-modal-footer'>
{props.cancelButton && ( {props.cancelButton && (
<button className='modal-button-cancel' onClick={props.onCancel}> <button className='small-modal-button-cancel' onClick={props.onCancel}>
{props.cancelButton} {props.cancelButton}
</button> </button>
)} )}

View file

@ -0,0 +1,356 @@
.modal-delete-page {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-evenly;
width: 100%;
}
.modal-delete-page-header {
align-items: center;
display: flex;
flex-direction: column;
}
.modal-delete-page-input {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 10px;
width: 100%;
}
.modal-delete-page-input-text {
background-color: #061726;
border: none;
border-radius: 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.modal-delete-page-subtitle {
color: white;
font-family: sans-serif;
font-size: 14px;
margin-top: 10px;
text-align: center;
}
.modal-delete-page-title {
color: white;
font-family: sans-serif;
font-size: 24px;
font-weight: bold;
text-align: center;
}
.modal-delete-page-body {
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
}
.page-activity {
height: 100%;
overflow-y: auto;
width: 100%;
}
.page-activity-header {
align-items: center;
border-bottom: 1px solid #061726;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding: 10px 20px;
width: 100%;
}
.page-activity-stat {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 175px;
}
.page-activity-stat-text {
color: white;
font-family: monospace;
font-size: 16px;
}
.page-activity-stat-title {
color: white;
font-family: sans-serif;
font-size: 16px;
}
.page-details {
align-items: center;
background-color: #1f2e3c;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
/* padding: 0px 10px; */
width: 300px;
min-width: 300px;
}
.page-details-footer {
align-items: start;
border-top: 1px solid #061726;
box-sizing: border-box;
display: flex;
flex-direction: column;
/* height: 50px; */
justify-content: center;
padding: 10px 0px;
width: 100%;
}
.page-details-footer-title {
color: #eee;
font-family: sans-serif;
font-weight: bold;
padding: 0px 10px 10px 10px;
}
.page-details-footer-categories {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: start;
padding: 0px 10px;
width: 100%;
}
.page-details-footer-category {
align-items: center;
box-sizing: border-box;
color: #eee;
display: flex;
flex-direction: row;
font-family: sans-serif;
font-weight: bold;
justify-content: center;
padding-bottom: 5px;
text-align: center;
width: 100%;
}
.page-details-footer-stats {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: row;
justify-content: space-evenly;
padding-bottom: 10px;
width: 100%;
}
.page-details-footer-stat {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
width: 100px;
}
.page-details-footer-stat-icon {
height: 20px;
width: 20px;
}
.page-details-footer-stat-text {
color: white;
font-family: monospace;
font-size: 16px;
padding-left: 5px;
}
.page-details-footer-stat-text-red {
color: #f23160;
font-family: monospace;
font-size: 16px;
padding-left: 5px;
}
.page-details-header {
align-items: center;
border-bottom: 1px solid #061726;
box-sizing: border-box;
display: flex;
flex-direction: row;
min-height: 55px;
justify-content: space-between;
padding: 10px 20px;
width: 100%;
}
.page-details-header-left {
align-items: start;
display: flex;
flex-direction: column;
justify-content: center;
}
.page-details-header-right {
align-items: end;
display: flex;
flex-direction: row;
justify-content: end;
}
.page-details-header-button {
align-items: center;
background-color: #1f2e3c;
border: none;
display: flex;
justify-content: center;
padding-left: 10px;
padding-right: 0px;
}
.page-details-header-button:hover {
cursor: pointer;
}
.page-details-header-icon {
height: 24px;
width: 24px;
}
.page-details-header-title {
color: #eee;
font-family: sans-serif;
font-weight: bold;
}
.page-details-header-type {
color: #eee;
font-family: sans-serif;
font-size: 12px;
}
.page-details-settings {
background-color: #000312;
border-radius: 3px;
padding: 5px;
position: fixed;
transform: translate(0px, 60px);
transition: all 1s;
width: 270px;
z-index: 10;
}
.page-details-settings-background {
align-items: center;
display: flex;
height: 100vh;
justify-content: center;
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100vw;
z-index: 8;
}
.page-details-settings-button {
color: #eee;
font-family: sans-serif;
font-size: 16px;
background-color: #000312;
border: none;
border-radius: 3px;
box-sizing: border-box;
padding: 5px;
width: 100%;
}
.page-details-settings-button:hover {
background-color: #77b23b;
color: #000312;
cursor: pointer;
}
.page-details-settings-text {
color: #eee;
font-family: sans-serif;
font-size: 16px;
}
.page-event {
border-bottom: 1px solid #061726;
color: white;
display: flex;
flex-direction: row;
font-family: sans-serif;
justify-content: space-between;
overflow-x: wrap;
padding: 10px 20px;
}
.page-event-left {
align-items: center;
display: flex;
flex-direction: row;
}
.page-event-icon {
width: 20px;
height: 20px;
padding-right: 10px;
}
.page-event-left-text {
display: flex;
flex-direction: column;
}
.page-event-username {
font-size: 14px;
font-weight: bold;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.page-event-description {
color: #88a0b8;
font-size: 14px;
}
.page-event-date {
align-items: center;
display: flex;
font-family: monospace;
text-align: end;
}
.page-inactive {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
width: 100%;
}
.page-inactive-text {
/* border: 1px solid #eee;
border-radius: 30px; */
color: #eee;
padding: 20px;
}

View file

@ -0,0 +1,803 @@
import { useEffect, useState } from 'react';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import {
Eye,
EyeRed,
EyeSlash,
Gear,
Heart,
Play,
Pause,
Star,
ThumbsDown,
ThumbsUp,
ChessRook,
} from '../assets';
import './PageDetails.css';
import {
ActivateAccount,
ActivateChannel,
DeleteAccount,
DeleteChannel,
Login,
Logout,
UpdateAccountApi,
UpdateChannelApi,
} from '../../wailsjs/go/main/App';
import { Modal, SmallModal } from './Modal';
function countString(value) {
switch (true) {
case value <= 0 || value == undefined:
return '0';
case value < 1000:
return value;
case value < 1000000:
return (value / 1000).toFixed(3).slice(0, -2) + 'K';
case value < 1000000000:
return (value / 1000000).toFixed(6).slice(0, -5) + 'M';
default:
return 'Inf';
}
}
function PageDetails(props) {
const [activate, setActivate] = useState(false);
const [active, setActive] = useState(false);
const [activity, setActivity] = useState(null);
const [openApi, setOpenApi] = useState(false);
const [apiValid, setApiValid] = useState(true);
const [editingApi, setEditingApi] = useState(false);
const [editApi, setEditApi] = useState('');
const updateEditApi = (event) => {
setEditApi(event.target.value);
};
const [openDelete, setOpenDelete] = useState(false);
const [deleting, setDeleting] = useState(false);
const [deleteName, setDeleteName] = useState('');
const updateDeleteName = (event) => {
if (deleting) {
return;
}
setDeleteName(event.target.value);
};
const [details, setDetails] = useState(null);
const [error, setError] = useState('');
const [live, setLive] = useState(false);
const [liveTitle, setLiveTitle] = useState('');
const [openLogin, setOpenLogin] = useState(false);
const [loggingIn, setLoggingIn] = useState(false);
const [loginUsername, setLoginUsername] = useState('');
const updateLoginUsername = (event) => {
if (loggingIn) {
return;
}
setLoginUsername(event.target.value);
};
const [loginUsernameValid, setLoginUsernameValid] = useState(true);
const [loginPassword, setLoginPassword] = useState('');
const updateLoginPassword = (event) => {
if (loggingIn) {
return;
}
setLoginPassword(event.target.value);
};
const [loginPasswordValid, setLoginPasswordValid] = useState(true);
const [openLogout, setOpenLogout] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
const [settings, setSettings] = useState(false);
const triggerSettings = () => setSettings(!settings);
useEffect(() => {
EventsOn('PageDetails', (event) => {
setDetails(event);
// TODO: do I need to reset all editing/logging out/etc. values?
});
EventsOn('PageActivity', (event) => {
setActivity(event);
if (event !== null) {
setActive(true);
if (event.livestreams.length > 0) {
setLive(true);
} else {
setLive(false);
}
}
});
EventsOn('PageActive', (event) => {
if (event) {
setActive(true);
} else {
setActive(false);
setActivity(null);
setLive(false);
}
});
}, []);
useEffect(() => {
if (deleting) {
switch (true) {
case details.type === 'Channel':
DeleteChannel(details.id)
.then(() => resetDelete())
.catch((error) => {
setDeleting(false);
setError(error);
});
return;
case details.type === 'Account':
DeleteAccount(details.id)
.then(() => resetDelete())
.catch((error) => {
setDeleting(false);
setError(error);
});
return;
}
}
}, [deleting]);
useEffect(() => {
if (editingApi) {
switch (true) {
case details.type === 'Channel':
UpdateChannelApi(details.id, editApi)
.then(() => resetEditApi())
.catch((error) => {
setEditingApi(false);
setError(error);
});
return;
case details.type === 'Account':
UpdateAccountApi(details.id, editApi)
.then(() => resetEditApi())
.catch((error) => {
setEditingApi(false);
setError(error);
});
return;
}
}
}, [editingApi]);
useEffect(() => {
if (loggingIn && details.type === 'Account') {
Login(loginUsername, loginPassword)
.then(() => {
resetLogin();
})
.catch((error) => {
setLoggingIn(false);
setError(error);
});
} else if (loggingIn && details.type === 'Channel') {
resetLogin();
}
}, [loggingIn]);
useEffect(() => {
if (loggingOut && details.type === 'Account') {
Logout(details.id)
.catch((error) => {
setError(error);
})
.finally(() => resetLogout());
} else if (loggingOut && details.type === 'Channel') {
resetLogout();
}
}, [loggingOut]);
const activatePage = () => {
switch (true) {
case details.type === 'Channel':
ActivateChannel(details.id).catch((error) => {
setError(error);
});
return;
case details.type === 'Account':
ActivateAccount(details.id).catch((error) => {
setError(error);
});
return;
}
};
const deletePage = () => {
if (deleting || details.title !== deleteName) {
return;
}
setDeleting(true);
};
const resetDelete = () => {
setDeleteName('');
setDeleting(false);
setOpenDelete(false);
};
const submitEditApi = () => {
if (editingApi) {
return;
}
if (editApi === '') {
setApiValid(false);
return;
}
setEditingApi(true);
};
const closeEditApi = () => {
if (editingApi) {
return;
}
resetEditApi();
};
const resetEditApi = () => {
setOpenApi(false);
setApiValid(true);
setEditApi('');
setEditingApi(false);
};
const login = () => {
if (loginUsername === '') {
setLoginUsernameValid(false);
return;
}
if (loginPassword === '') {
setLoginPasswordValid(false);
return;
}
setLoggingIn(true);
};
const closeLogin = () => {
if (loggingIn) {
return;
}
setOpenLogin(false);
};
const resetLogin = () => {
setLoggingIn(false);
setOpenLogin(false);
};
const logout = () => {
setLoggingOut(true);
};
const closeLogout = () => {
if (loggingOut) {
return;
}
setOpenLogout(false);
};
const resetLogout = () => {
setLoggingOut(false);
setOpenLogout(false);
};
return (
<>
{openLogin && (
<Modal
backgroundClose={true}
cancelButton={'Cancel'}
onCancel={closeLogin}
onClose={closeLogin}
show={openLogin}
style={{
height: '480px',
minHeight: '480px',
width: '360px',
minWidth: '360px',
}}
submitButton={'Login'}
submitLoading={loggingIn}
onSubmit={login}
>
<ModalLogin
password={loginPassword}
passwordValid={loginPasswordValid}
updatePassword={updateLoginPassword}
username={loginUsername}
usernameValid={loginUsernameValid}
updateUsername={updateLoginUsername}
/>
</Modal>
)}
{openLogout && (
<SmallModal
cancelButton={'Cancel'}
onCancel={closeLogout}
onClose={closeLogout}
show={openLogout}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Logout'}
message={'Are you sure you want to log out of ' + details.title + '?'}
submitButton={loggingOut ? 'Logging out...' : 'Logout'}
onSubmit={logout}
/>
)}
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
{openDelete && details !== null && (
<Modal
backgroundClose={true}
cancelButton={'Cancel'}
onCancel={resetDelete}
onClose={resetDelete}
deleteButton={deleting ? 'Deleting' : 'Delete'}
onDelete={deletePage}
deleteActive={details.title === deleteName}
pageName={details.title}
show={openDelete}
style={{
height: '350px',
minHeight: '350px',
width: '350px',
minWidth: '350px',
}}
>
<div className='modal-delete-page'>
<div className='modal-delete-page-header'>
<span className='modal-delete-page-title'>Delete page</span>
<span className='modal-delete-page-subtitle'>
Are you sure you want to delete <b>{details.title}</b>? This cannot
be undone. You must type '{details.title}' into the box to delete.
</span>
</div>
<div className='modal-delete-page-body'>
<div className='modal-delete-page-input'>
<input
className='modal-delete-page-input-text'
onChange={updateDeleteName}
placeholder={details.title}
type={'text'}
value={deleteName}
></input>
</div>
</div>
</div>
</Modal>
)}
{openApi && (
<Modal
backgroundClose={true}
cancelButton={'Cancel'}
onCancel={closeEditApi}
onClose={closeEditApi}
show={openApi}
style={{
height: '480px',
minHeight: '480px',
width: '360px',
minWidth: '360px',
}}
submitButton={'Submit'}
submitLoading={editingApi}
onSubmit={submitEditApi}
>
<ModalEditApi
apiKey={editApi}
updateApiKey={updateEditApi}
apiValid={apiValid}
/>
</Modal>
)}
<div className='page-details'>
{details !== null && (
<>
<div className='page-details-header'>
<div className='page-details-header-left'>
<span className='page-details-header-title'>{details.title}</span>
<span className='page-details-header-type'>{details.type}</span>
</div>
<div className='page-details-header-right'>
{details.has_api && (
<button
className='page-details-header-button'
onClick={activatePage}
>
<img
className='page-details-header-icon'
src={active ? Pause : Play}
/>
</button>
)}
<button
className='page-details-header-button'
onClick={triggerSettings}
>
<img className='page-details-header-icon' src={Gear} />
</button>
</div>
</div>
{settings && (
<>
<div
className='page-details-settings-background'
onClick={triggerSettings}
></div>
<div className='page-details-settings'>
{details.type === 'Account' && (
<button
className='page-details-settings-button'
onClick={() => {
triggerSettings();
if (details.logged_in) {
setOpenLogout(true);
} else {
setOpenLogin(true);
}
}}
>
{details.logged_in ? 'Logout' : 'Login'}
</button>
)}
<button
className='page-details-settings-button'
onClick={() => {
triggerSettings();
setOpenApi(true);
}}
>
Edit API key
</button>
<button
className='page-details-settings-button'
onClick={() => {
triggerSettings();
setOpenDelete(true);
}}
>
Delete
</button>
</div>
</>
)}
{active && activity !== null && (
<>
<PageActivity activity={activity} />
{live && (
<DetailsFooter
categories={activity.livestreams[0].categories}
dislikes={activity.livestreams[0].dislikes}
likes={activity.livestreams[0].likes}
title={activity.livestreams[0].title}
viewers={activity.livestreams[0].watching_now}
/>
)}
</>
)}
{!active && (
<div className='page-inactive'>
<span className='page-inactive-text'>
{details.has_api
? 'Press play to start API'
: 'Open settings to add API key'}
</span>
</div>
)}
</>
)}
</div>
</>
);
}
export default PageDetails;
function PageActivity(props) {
const eventDate = (event) => {
if (event.followed_on) {
return event.followed_on;
}
if (event.subscribed_on) {
return event.subscribed_on;
}
};
const sortEvents = () => {
let sorted = [
...props.activity.followers.recent_followers,
...props.activity.subscribers.recent_subscribers,
].sort((a, b) => (eventDate(a) < eventDate(b) ? 1 : -1));
return sorted;
};
return (
<>
<div className='page-activity-header'>
<div className='page-activity-stat'>
<span className='page-activity-stat-title'>Followers:</span>
<span className='page-activity-stat-text'>
{countString(props.activity.followers.num_followers)}
</span>
</div>
<div className='page-activity-stat'>
<span className='page-activity-stat-title'>Subscribers:</span>
<span className='page-activity-stat-text'>
{countString(props.activity.subscribers.num_subscribers)}
</span>
</div>
</div>
<div className='page-activity'>
<div className='page-activity-list'>
{sortEvents().map((event, index) => (
<PageEvent event={event} key={index} />
))}
</div>
</div>
</>
);
}
function PageEvent(props) {
const dateDate = (date) => {
const options = { month: 'short' };
let month = new Intl.DateTimeFormat('en-US', options).format(date);
let day = date.getDate();
return month + ' ' + day;
};
const dateDay = (date) => {
let now = new Date();
let today = now.getDay();
switch (date.getDay()) {
case 0:
return 'Sunday';
case 1:
return 'Monday';
case 2:
return 'Tuesday';
case 3:
return 'Wednesday';
case 4:
return 'Thursday';
case 5:
return 'Friday';
case 6:
return 'Saturday';
}
};
const dateTime = (date) => {
let now = new Date();
let today = now.getDay();
let day = date.getDay();
if (today !== day) {
return dateDay(date);
}
let hours24 = date.getHours();
let hours = hours24 % 12 || 12;
let minutes = date.getMinutes();
if (minutes < 10) {
minutes = '0' + minutes;
}
let mer = 'pm';
if (hours24 < 12) {
mer = 'am';
}
return hours + ':' + minutes + ' ' + mer;
};
const dateString = (d) => {
if (isNaN(Date.parse(d))) {
return 'Who knows?';
}
let now = new Date();
let date = new Date(d);
// Fix Rumble's timezone problem
date.setHours(date.getHours() - 4);
let diff = now - date;
switch (true) {
case diff < 0:
return 'In the future!?';
case diff < 60000:
return 'Now';
case diff < 3600000:
let minutes = Math.floor(diff / 1000 / 60);
let postfix = ' mins ago';
if (minutes == 1) {
postfix = ' min ago';
}
return minutes + postfix;
case diff < 86400000:
return dateTime(date);
case diff < 604800000:
return dateDay(date);
default:
return dateDate(date);
}
};
return (
<div className='page-event'>
<div className='page-event-left'>
{props.event.followed_on && <img className='page-event-icon' src={Heart}></img>}
{props.event.subscribed_on && (
<img className='page-event-icon' src={ChessRook}></img>
)}
<div className='page-event-left-text'>
<span className='page-event-username'>{props.event.username}</span>
<span className='page-event-description'>
{props.event.followed_on && 'Followed you'}
{props.event.subscribed_on && 'Subscribed'}
</span>
</div>
</div>
<span className='page-event-date'>
{props.event.followed_on && dateString(props.event.followed_on)}
{props.event.subscribed_on && dateString(props.event.subscribed_on)}
</span>
</div>
);
}
function DetailsFooter(props) {
return (
<div className='page-details-footer'>
<span className='page-details-footer-title'>{props.title}</span>
<div className='page-details-footer-stats'>
<div className='page-details-footer-stat'>
<img className='page-details-footer-stat-icon' src={EyeRed} />
<span className='page-details-footer-stat-text-red'>
{countString(props.viewers)}
</span>
</div>
<div className='page-details-footer-stat'>
<img className='page-details-footer-stat-icon' src={ThumbsUp} />
<span className='page-details-footer-stat-text'>
{countString(props.likes)}
</span>
</div>
<div className='page-details-footer-stat'>
<img className='page-details-footer-stat-icon' src={ThumbsDown} />
<span className='page-details-footer-stat-text'>
{countString(props.dislikes)}
</span>
</div>
</div>
<div className='page-details-footer-categories'>
<span className='page-details-footer-category'>
{props.categories.primary.title}
</span>
<span className='page-details-footer-category'>
{props.categories.secondary.title}
</span>
</div>
</div>
);
}
function ModalEditApi(props) {
const [showKey, setShowKey] = useState(false);
const updateShowKey = () => setShowKey(!showKey);
return (
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Edit API Key</span>
<span className='modal-add-account-channel-subtitle'>Enter new API key below</span>
</div>
<div className='modal-add-account-channel-body'>
{props.apiValid === false ? (
<label className='modal-add-channel-label-warning'>
API KEY - Please enter a valid API key
</label>
) : (
<label className='modal-add-channel-label'>API KEY</label>
)}
<div className='modal-add-channel-key'>
<input
className='modal-add-channel-key-input'
onChange={props.updateApiKey}
placeholder={'Enter API key'}
type={showKey ? 'text' : 'password'}
value={props.apiKey}
></input>
<button className='modal-add-channel-key-show' onClick={updateShowKey}>
<img
className='modal-add-channel-key-show-icon'
src={showKey ? EyeSlash : Eye}
/>
</button>
</div>
<span className='modal-add-channel-description'>API KEYS SHOULD LOOK LIKE</span>
<span className='modal-add-channel-description-subtext'>
https://rumble.com/-livestream-api/get-data?key=really-long_string-of_random-characters
</span>
</div>
<div></div>
</div>
);
}
function ModalLogin(props) {
const [showPassword, setShowPassword] = useState(false);
const updateShowPassword = () => setShowPassword(!showPassword);
return (
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Login</span>
<span className='modal-add-account-channel-subtitle'>
Log into your Rumble account
</span>
</div>
<div className='modal-add-account-channel-body'>
{props.usernameValid === false ? (
<label className='modal-add-account-channel-label-warning'>
USERNAME - Please enter a valid username
</label>
) : (
<label className='modal-add-account-channel-label'>USERNAME</label>
)}
<div className='modal-add-account-channel-input'>
<input
className='modal-add-account-channel-input-text'
onChange={!props.loading && props.updateUsername}
placeholder={'Username'}
type={'text'}
value={props.username}
></input>
</div>
{props.passwordValid === false ? (
<label className='modal-add-account-channel-label-warning'>
PASSWORD - Please enter a valid password
</label>
) : (
<label className='modal-add-account-channel-label'>PASSWORD</label>
)}
<div className='modal-add-account-channel-input'>
<input
className='modal-add-account-channel-input-password'
onChange={!props.loading && props.updatePassword}
placeholder={'Password'}
type={showPassword ? 'text' : 'password'}
value={props.password}
></input>
<button
className='modal-add-account-channel-input-show'
onClick={updateShowPassword}
>
<img
className='modal-add-account-channel-input-show-icon'
src={showPassword ? EyeSlash : Eye}
/>
</button>
</div>
</div>
<div></div>
</div>
);
}

View file

@ -0,0 +1,362 @@
.page-sidebar {
align-items: center;
background-color: #061726;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 0px 10px;
width: 60px;
}
.page-sidebar-account-list {
border-top: 2px solid #273848;
padding-bottom: 10px;
}
.page-sidebar-body {
overflow-y: auto;
}
.page-sidebar-button {
align-items: center;
background-color: #061726;
border: none;
display: flex;
justify-content: center;
padding: 0px;
}
.page-sidebar-button:hover {
cursor: pointer;
}
.page-sidebar-button-icon {
height: 60px;
width: 60px;
}
.page-sidebar-footer {
padding-bottom: 10px;
}
.page-sidebar-icon {
height: 60px;
margin-top: 10px;
position: relative;
width: 60px;
}
.page-sidebar-icon:hover {
cursor: pointer;
}
.page-sidebar-icon-account {
bottom: 0px;
height: 24px;
left: 36px;
position: absolute;
width: 24px;
}
.page-sidebar-icon-hover {
background-color: #061726;
border-radius: 5px;
color: black;
padding: 10px;
position: fixed;
/* transform: translate(75px, -50px); */
z-index: 10;
}
.page-sidebar-icon-hover:before {
content:"";
position: absolute;
width: 0;
height: 0;
border-top: 3px solid transparent;
border-right: 3px solid #061726;
border-bottom: 3px solid transparent;
margin: 7px 0 0 -13px;
}
.page-sidebar-icon-hover-text {
color: white;
font-family: sans-serif;
font-weight: bold;
font-size: 16px;
}
.page-sidebar-icon-image {
/* border: 3px solid #85c742; */
/* border: 3px solid #ec0; */
/* border: 3px solid #f23160; */
border-radius: 50%;
height: 54px;
transition: border-radius 0.25s;
width: 54px;
}
.page-sidebar-icon-image:hover {
border-radius: 30%;
transition: border-radius 0.25s;
}
.page-sidebar-icon-initial {
align-items: center;
background-color: #3377cc;
/* border: 3px solid #3377cc; */
/* border: 3px solid #85c742; */
/* border: 3px solid #ec0; */
/* border: 3px solid #f23160; */
border-radius: 50%;
color: #eee;
display: flex;
font-family: sans-serif;
font-size: 34px;
font-weight: bold;
height: 54px;
justify-content: center;
transition: border-radius 0.25s;
width: 54px;
}
.page-sidebar-icon-initial:hover {
border-radius: 30%;
transition: border-radius 0.25s;
}
.modal-add-account-channel {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
width: 100%;
}
.modal-add-account-channel-header {
align-items: center;
display: flex;
flex-direction: column;
}
.modal-add-account-channel-subtitle {
color: white;
font-family: sans-serif;
font-size: 14px;
margin-top: 10px;
text-align: center;
}
.modal-add-account-channel-title {
color: white;
font-family: sans-serif;
font-size: 24px;
font-weight: bold;
text-align: center;
}
.modal-add-account-channel-body {
align-items: center;
display: flex;
flex-direction: column;
width: 100%;
}
.modal-add-account-channel-button {
align-items: center;
background-color: #1f2e3c;
border: 1px solid #d6e0ea;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 5px 0px;
padding: 20px;
width: 100%;
}
.modal-add-account-channel-button:hover {
background-color: rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.modal-add-account-channel-button-left {
color: white;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
}
.modal-add-account-channel-button-right-icon {
height: 20px;
width: 20px;
}
.modal-add-account-channel-input {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 10px;
width: 100%;
}
.modal-add-account-channel-input-password {
background-color: #061726;
border: none;
border-radius: 5px 0px 0px 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 90%;
}
.modal-add-account-channel-input-text {
background-color: #061726;
border: none;
border-radius: 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.modal-add-account-channel-input-show {
align-items: center;
background-color: #061726;
border: none;
border-radius: 0px 5px 5px 0px;
display: flex;
justify-content: center;
width: 10%;
}
.modal-add-account-channel-input-show:hover {
cursor: pointer;
}
.modal-add-account-channel-input-show-icon {
height: 16px;
width: 16px;
}
.modal-add-account-channel-label {
color: white;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
width: 100%;
}
.modal-add-account-channel-label-warning {
color: #f23160;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
width: 100%;
}
.modal-add-channel-description {
color: white;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
margin-top: 20px;
text-align: left;
width: 100%;
}
.modal-add-channel-description-subtext {
color: white;
font-family: sans-serif;
font-size: 16px;
margin-top: 10px;
text-align: left;
width: 100%;
}
.modal-add-channel-key {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.modal-add-channel-key-input {
background-color: #061726;
border: none;
border-radius: 5px 0px 0px 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 90%;
}
.modal-add-channel-key-show {
align-items: center;
background-color: #061726;
border: none;
border-radius: 0px 5px 5px 0px;
display: flex;
justify-content: center;
width: 10%;
}
.modal-add-channel-key-show:hover {
cursor: pointer;
}
.modal-add-channel-key-show-icon {
height: 16px;
width: 16px;
}
.modal-add-channel-label {
color: white;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
width: 100%;
}
.modal-add-channel-label-warning {
color: #f23160;
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
width: 100%;
}
/* HTML: <div class="loader"></div> */
.loader {
width: 60px;
aspect-ratio: 6;
--_g: no-repeat radial-gradient(circle closest-side,#061726 90%,#0000);
background:
var(--_g) 0% 50%,
var(--_g) 50% 50%,
var(--_g) 100% 50%;
background-size: calc(100%/3) 100%;
animation: l7 1s infinite linear;
}
@keyframes l7 {
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0% ,calc(100%/3) 100%}
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0% }
}

View file

@ -0,0 +1,654 @@
import { useEffect, useState } from 'react';
import { Modal, SmallModal } from './Modal';
import {
AccountList,
AddPage,
Login,
OpenAccount,
OpenChannel,
PageStatus,
} from '../../wailsjs/go/main/App';
import { EventsOff, EventsOn } from '../../wailsjs/runtime/runtime';
import {
ChevronRight,
CircleGreenBackground,
CircleRedBackground,
Eye,
EyeSlash,
PlusCircle,
} from '../assets';
import './PageSideBar.css';
function PageSideBar(props) {
const [accounts, setAccounts] = useState({});
const [error, setError] = useState('');
const [addOpen, setAddOpen] = useState(false);
// const [refresh, setRefresh] = useState(false);
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
EventsOn('PageSideBarAccounts', (event) => {
setAccounts(event);
});
}, []);
useEffect(() => {
AccountList()
.then((response) => {
setAccounts(response);
})
.catch((error) => {
setError(error);
});
}, []);
const sortAccounts = () => {
let keys = Object.keys(accounts);
let sorted = [...keys].sort((a, b) =>
accounts[a].account.username.toLowerCase() > accounts[b].account.username.toLowerCase()
? 1
: -1
);
return sorted;
};
const handleScroll = (event) => {
setScrollY(event.target.scrollTop);
};
const openAccount = (account) => {
OpenAccount(account.id).catch((error) => setError(error));
};
const openChannel = (channel) => {
OpenChannel(channel.id).catch((error) => setError(error));
};
return (
<>
{addOpen && (
<ModalAdd
onClose={() => setAddOpen(false)}
onRefresh={() => {
setRefresh(!refresh);
}}
show={addOpen}
/>
)}
<div className='page-sidebar'>
<div className='page-sidebar-body' onScroll={handleScroll}>
{sortAccounts().map((account, index) => (
<AccountChannels
account={accounts[account]}
key={index}
openAccount={openAccount}
openChannel={openChannel}
scrollY={scrollY}
top={index === 0}
/>
))}
</div>
<div className='page-sidebar-footer'>
<ButtonIcon
hoverText={'Add an account/channel'}
onClick={() => setAddOpen(true)}
scrollY={0}
/>
</div>
</div>
</>
);
}
export default PageSideBar;
function AccountChannels(props) {
const sortChannels = () => {
let sorted = [...props.account.channels].sort((a, b) =>
a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1
);
return sorted;
};
if (props.account.account !== undefined) {
return (
<div
className='page-sidebar-account-list'
style={props.top ? { borderTop: 'none' } : {}}
>
<button
className='page-sidebar-button'
key={0}
onClick={() => props.openAccount(props.account.account)}
>
<AccountIcon account={props.account.account} scrollY={props.scrollY} />
</button>
{sortChannels().map((channel, index) => (
<button
className='page-sidebar-button'
key={index + 1}
onClick={() => props.openChannel(channel)}
>
<ChannelIcon channel={channel} scrollY={props.scrollY} />
</button>
))}
</div>
);
}
}
function AccountIcon(props) {
const [apiActive, setApiActive] = useState(false);
const [hover, setHover] = useState(false);
const [isLive, setIsLive] = useState(false);
const [loggedIn, setLoggedIn] = useState(props.account.cookies !== null);
const [username, setUsername] = useState(props.account.username);
const iconBorder = () => {
if (!apiActive) {
return '3px solid #3377cc';
}
if (isLive) {
return '3px solid #85c742';
} else {
return '3px solid #f23160';
}
};
const pageName = (name) => {
if (name === undefined) return;
return '/user/' + name;
};
useEffect(() => {
if (username !== props.account.username) {
EventsOff(
'ApiActive-' + pageName(username),
'LoggedIn-' + pageName(username),
'PageLive-' + pageName(username)
);
setApiActive(false);
setIsLive(false);
}
EventsOn('ApiActive-' + pageName(props.account.username), (event) => {
setApiActive(event);
});
EventsOn('LoggedIn-' + pageName(props.account.username), (event) => {
setLoggedIn(event);
});
EventsOn('PageLive-' + pageName(props.account.username), (event) => {
setIsLive(event);
});
setUsername(props.account.username);
}, [props.account.username]);
useEffect(() => {
setLoggedIn(props.account.cookies !== null);
}, [props.account.cookies]);
useEffect(() => {
if (username !== '') {
PageStatus(pageName(username));
}
}, [username]);
return (
<div
className='page-sidebar-icon'
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{props.account.profile_image === null ? (
<span className='page-sidebar-icon-initial' style={{ border: iconBorder() }}>
{props.account.username[0].toUpperCase()}
</span>
) : (
<img
className='page-sidebar-icon-image'
src={props.account.profile_image}
style={{ border: iconBorder() }}
/>
)}
<img
className='page-sidebar-icon-account'
src={loggedIn ? CircleGreenBackground : CircleRedBackground}
/>
{hover && <HoverName name={pageName(username)} scrollY={props.scrollY} />}
</div>
);
}
function ButtonIcon(props) {
const [hover, setHover] = useState(false);
return (
<div
className='page-sidebar-icon'
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<button className='page-sidebar-button' onClick={props.onClick}>
<img className='page-sidebar-button-icon' src={PlusCircle} />
</button>
{hover && <HoverName name={props.hoverText} scrollY={props.scrollY} />}
</div>
);
}
function ChannelIcon(props) {
const [apiActive, setApiActive] = useState(false);
const [channelName, setChannelName] = useState(props.channel.name);
const [hover, setHover] = useState(false);
const [isLive, setIsLive] = useState(false);
const iconBorder = () => {
if (!apiActive) {
return '3px solid #3377cc';
}
if (isLive) {
return '3px solid #85c742';
} else {
return '3px solid #f23160';
}
};
const pageName = (name) => {
if (name === undefined) return;
return '/c/' + name.replace(/\s/g, '');
};
useEffect(() => {
if (channelName !== props.channel.name) {
EventsOff('PageLive-' + pageName(channelName), 'ApiActive-' + pageName(channelName));
setApiActive(false);
setIsLive(false);
}
EventsOn('PageLive-' + pageName(props.channel.name), (event) => {
setIsLive(event);
});
EventsOn('ApiActive-' + pageName(props.channel.name), (event) => {
setApiActive(event);
});
setChannelName(props.channel.name);
}, [props.channel.name]);
useEffect(() => {
if (channelName !== '') {
PageStatus(pageName(channelName));
}
}, [channelName]);
return (
<div
className='page-sidebar-icon'
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{props.channel.profile_image === null ? (
<span className='page-sidebar-icon-initial' style={{ border: iconBorder() }}>
{props.channel.name[0].toUpperCase()}
</span>
) : (
<img
className='page-sidebar-icon-image'
src={props.channel.profile_image}
style={{ border: iconBorder() }}
/>
)}
{hover && <HoverName name={pageName(channelName)} scrollY={props.scrollY} />}
</div>
);
}
function HoverName(props) {
return (
<div
className='page-sidebar-icon-hover'
style={{ transform: 'translate(75px, -' + (50 + props.scrollY) + 'px)' }}
>
<span className='page-sidebar-icon-hover-text'>{props.name}</span>
</div>
);
}
function ModalAdd(props) {
const [accountPassword, setAccountPassword] = useState('');
const [accountPasswordValid, setAccountPasswordValid] = useState(true);
const updateAccountPassword = (event) => {
if (loading()) {
return;
}
setAccountPassword(event.target.value);
};
const [accountUsername, setAccountUsername] = useState('');
const [accountUsernameValid, setAccountUsernameValid] = useState(true);
const updateAccountUsername = (event) => {
if (loading()) {
return;
}
setAccountUsername(event.target.value);
};
const [addAccountLoading, setAddAccountLoading] = useState(false);
const [addChannelLoading, setAddChannelLoading] = useState(false);
const [channelKey, setChannelKey] = useState('');
const [channelKeyValid, setChannelKeyValid] = useState(true);
const updateChannelKey = (event) => {
if (loading()) {
return;
}
setChannelKey(event.target.value);
};
const [error, setError] = useState('');
const [stage, setStage] = useState('start');
useEffect(() => {
if (addAccountLoading) {
Login(accountUsername, accountPassword)
.then(() => {
reset();
props.onClose();
//props.onRefresh();
})
.catch((error) => {
setAddAccountLoading(false);
setError(error);
});
}
}, [addAccountLoading]);
useEffect(() => {
if (addChannelLoading) {
AddPage(channelKey)
.then(() => {
reset();
props.onClose();
//props.onRefresh();
})
.catch((error) => {
setAddChannelLoading(false);
setError(error);
});
}
}, [addChannelLoading]);
const back = () => {
if (loading()) {
return;
}
reset();
};
const close = () => {
if (loading()) {
return;
}
reset();
props.onClose();
};
const reset = () => {
setStage('start');
resetAccount();
resetChannel();
};
const add = () => {
switch (stage) {
case 'account':
addAccount();
break;
case 'channel':
addChannel();
break;
default:
close();
}
};
const addAccount = () => {
if (loading()) {
return;
}
if (accountUsername === '') {
setAccountUsernameValid(false);
return;
}
if (accountPassword === '') {
setAccountPasswordValid(false);
return;
}
setAddAccountLoading(true);
};
const addChannel = () => {
if (loading()) {
return;
}
if (channelKey === '') {
setChannelKeyValid(false);
return;
}
setAddChannelLoading(true);
};
const loading = () => {
return addAccountLoading || addChannelLoading;
};
const resetAccount = () => {
setAccountPassword('');
setAccountPasswordValid(true);
setAccountUsername('');
setAccountUsernameValid(true);
setAddAccountLoading(false);
};
const resetChannel = () => {
setChannelKey('');
setChannelKeyValid(true);
setAddChannelLoading(false);
};
return (
<>
{error !== '' && (
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
)}
<Modal
cancelButton={stage !== 'start' ? 'Back' : ''}
onCancel={back}
onClose={close}
show={props.show}
style={{ height: '480px', minHeight: '480px', width: '360px', minWidth: '360px' }}
submitButton={stage !== 'start' ? 'Add' : ''}
submitLoading={loading()}
onSubmit={add}
>
{stage === 'start' && <ModalAddStart setStage={setStage} />}
{stage === 'account' && (
<ModalAddAccount
accountPassword={accountPassword}
accountPasswordValid={accountPasswordValid}
updateAccountPassword={updateAccountPassword}
accountUsername={accountUsername}
accountUsernameValid={accountUsernameValid}
updateAccountUsername={updateAccountUsername}
/>
)}
{stage === 'channel' && (
<ModalAddChannel
channelKey={channelKey}
channelKeyValid={channelKeyValid}
updateChannelKey={updateChannelKey}
/>
)}
</Modal>
</>
);
}
function ModalAddAccount(props) {
const [showKey, setShowKey] = useState(false);
const updateShowKey = () => setShowKey(!showKey);
const [showPassword, setShowPassword] = useState(false);
const updateShowPassword = () => setShowPassword(!showPassword);
return (
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Add Account</span>
<span className='modal-add-account-channel-subtitle'>
Log into your Rumble account
</span>
</div>
<div className='modal-add-account-channel-body'>
{props.accountUsernameValid === false ? (
<label className='modal-add-account-channel-label-warning'>
USERNAME - Please enter a valid username
</label>
) : (
<label className='modal-add-account-channel-label'>USERNAME</label>
)}
<div className='modal-add-account-channel-input'>
<input
className='modal-add-account-channel-input-text'
onChange={!props.loading && props.updateAccountUsername}
placeholder={'Username'}
type={'text'}
value={props.accountUsername}
></input>
</div>
{props.accountPasswordValid === false ? (
<label className='modal-add-account-channel-label-warning'>
PASSWORD - Please enter a valid password
</label>
) : (
<label className='modal-add-account-channel-label'>PASSWORD</label>
)}
<div className='modal-add-account-channel-input'>
<input
className='modal-add-account-channel-input-password'
onChange={!props.loading && props.updateAccountPassword}
placeholder={'Password'}
type={showPassword ? 'text' : 'password'}
value={props.accountPassword}
></input>
<button
className='modal-add-account-channel-input-show'
onClick={updateShowPassword}
>
<img
className='modal-add-account-channel-input-show-icon'
src={showPassword ? EyeSlash : Eye}
/>
</button>
</div>
</div>
<div></div>
</div>
);
}
function ModalAddChannel(props) {
const [showKey, setShowKey] = useState(false);
const updateShowKey = () => setShowKey(!showKey);
return (
<div className='modal-add-account-channel'>
<div className='modal-add-account-channel-header'>
<span className='modal-add-account-channel-title'>Add Channel</span>
<span className='modal-add-account-channel-subtitle'>
Copy an API key below to add a channel
</span>
</div>
<div className='modal-add-account-channel-body'>
{props.channelKeyValid === false ? (
<label className='modal-add-channel-label-warning'>
API KEY - Please enter a valid API key
</label>
) : (
<label className='modal-add-channel-label'>API KEY</label>
)}
<div className='modal-add-channel-key'>
<input
className='modal-add-channel-key-input'
onChange={!props.loading && props.updateChannelKey}
placeholder={'Enter API key'}
type={showKey ? 'text' : 'password'}
value={props.channelKey}
></input>
<button className='modal-add-channel-key-show' onClick={updateShowKey}>
<img
className='modal-add-channel-key-show-icon'
src={showKey ? EyeSlash : Eye}
/>
</button>
</div>
<span className='modal-add-channel-description'>API KEYS SHOULD LOOK LIKE</span>
<span className='modal-add-channel-description-subtext'>
https://rumble.com/-livestream-api/get-data?key=really-long_string-of_random-characters
</span>
</div>
<div></div>
</div>
);
}
function ModalAddStart(props) {
return (
<div className='modal-add-account-channel'>
<span className='modal-add-account-channel-title'>Add an Account or Channel</span>
<div className='modal-add-account-channel-body'>
<button
className='modal-add-account-channel-button'
onClick={() => props.setStage('account')}
>
<div className='modal-add-account-channel-button-left'>
<span>Add Account</span>
</div>
<img
className='modal-add-account-channel-button-right-icon'
src={ChevronRight}
/>
</button>
<button
className='modal-add-account-channel-button'
onClick={() => props.setStage('channel')}
>
<div className='modal-add-account-channel-button-left'>
<span>Add Channel</span>
</div>
<img
className='modal-add-account-channel-button-right-icon'
src={ChevronRight}
/>
</button>
</div>
<div></div>
</div>
);
}

View file

@ -0,0 +1,7 @@
.dashboard {
align-items: center;
display: flex;
flex-direction: row;
height: 100vh;
width: 100%;
}

View file

@ -0,0 +1,19 @@
import { useState } from 'react';
import { CircleGreenBackground, Heart } from '../assets';
import PageDetails from '../components/PageDetails';
import PageSideBar from '../components/PageSideBar';
import './Dashboard.css';
import ChatBot from '../components/ChatBot';
function Dashboard() {
return (
<div className='dashboard'>
<PageSideBar />
<PageDetails />
<ChatBot />
</div>
);
}
export default Dashboard;

View file

@ -0,0 +1,192 @@
.signin-body {
align-items: center;
background-color: #f3f5f8;
background-image: linear-gradient(to bottom, #85c742, #061726);
/* background-color: #85c742; */
display: flex;
flex-direction: column;
justify-content: space-evenly;
height: 100vh;
}
.signin-center {
align-items: center;
display: flex;
height: 60%;
justify-content: center;
width: 100%;
}
.signin-footer {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
height: 20%;
width: 100%;
}
.signin-footer-description {
color: #f3f5f8;
font-family: sans-serif;
}
.signin-header {
align-items: center;
display: flex;
flex-direction: row;
height: 20%;
justify-content: space-evenly;
width: 100%;
}
.signin-logo {
height: 100px;
width: 100px;
}
.signin-title {
align-items: center;
color: #061726;
display: flex;
flex-direction: column;
font-family: sans-serif;
font-weight: bold;
justify-content: center;
text-align: center;
}
.signin-title-text {
font-size: 20px;
margin: 0;
}
.signin-title-subtext {
font-size: 12px;
margin: 0;
}
.signin-window {
align-items: center;
background-color: #061726;
border-radius: 12px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
height: 425px;
max-width: 450px;
padding: 20px;
width: 80%;
}
.signin-window-header {
align-items: center;
display: flex;
flex-direction: column;
}
.signin-window-field {
align-items: center;
display: flex;
flex-direction: column;
margin: 10px 0px;
}
.signin-window-field-label {
color: #d6e0ea;
font-family: sans-serif;
font-size: 20px;
padding-bottom: 5px;
width: 100%;
}
.signin-window-field-input {
background-color: #1f2e3c;
border: none;
border-radius: 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 100%;
}
.signin-window-field-input::placeholder {
color: #73899e;
}
.signin-window-form {
width: 80%;
}
.signin-window-form-button {
background-color: #85c742;
border: none;
border-radius: 30px;
color: #0d2437;
font-family: sans-serif;
font-size: 16px;
font-weight: bold;
margin-top: 10px;
padding: 8px 11px;
width: 100%;
}
.signin-window-form-button:hover {
cursor: pointer;
}
.signin-window-password {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.signin-window-password-input {
background-color: #1f2e3c;
border: none;
border-radius: 5px 0px 0px 5px;
box-sizing: border-box;
color: white;
font-family: monospace;
font-size: 16px;
outline: none;
padding: 10px;
resize: none;
width: 90%;
}
.signin-window-password-input::placeholder {
color: #73899e;
}
.signin-window-password-show-button {
align-items: center;
background-color: #1f2e3c;
border: none;
border-radius: 0px 5px 5px 0px;
display: flex;
justify-content: center;
width: 10%;
}
.signin-window-password-show-button:hover {
cursor: pointer;
}
.signin-window-password-show-icon {
height: 16px;
width: 16px;
}
.signin-window-title {
color: #d6e0ea;
font-family: sans-serif;
font-size: 24px;
font-weight: bold;
}

View file

@ -0,0 +1,121 @@
import { useEffect, useState } from 'react';
import { SmallModal } from '../components/Modal';
import { Login, SignedIn } from '../../wailsjs/go/main/App';
import { Eye, EyeSlash, Logo } from '../assets';
import { Navigate, useNavigate } from 'react-router-dom';
import './SignIn.css';
import { NavDashboard } from '../Navigation';
function SignIn() {
const [error, setError] = useState('');
const navigate = useNavigate();
const [password, setPassword] = useState('');
const updatePassword = (event) => setPassword(event.target.value);
const [showPassword, setShowPassword] = useState(false);
const updateShowPassword = () => setShowPassword(!showPassword);
const [signingIn, setSigningIn] = useState(false);
const [username, setUsername] = useState('');
const updateUsername = (event) => setUsername(event.target.value);
// useEffect(() => {
// SignedIn()
// .then((signedIn) => {
// if (signedIn) {
// navigate(NavDashboard);
// }
// })
// .catch((error) => {
// setError(error);
// });
// }, []);
useEffect(() => {
if (signingIn) {
Login(username, password)
.then(() => {
setUsername('');
setPassword('');
navigate(NavDashboard);
})
.catch((error) => {
setError(error);
})
.finally(() => {
setSigningIn(false);
});
}
}, [signingIn]);
const signIn = () => {
setSigningIn(true);
};
return (
<>
<SmallModal
onClose={() => setError('')}
show={error !== ''}
style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }}
title={'Error'}
message={error}
submitButton={'OK'}
onSubmit={() => setError('')}
/>
<div className='signin-body'>
<div className='signin-header'>
<img className='signin-logo' src={Logo} />
</div>
<div></div>
<div className='signin-center'>
<div className='signin-window'>
<div className='signin-window-header'>
<span className='signin-window-title'>Sign in to Rumble</span>
</div>
<div className='signin-window-form'>
<div className='signin-window-field'>
<span className='signin-window-field-label'>Username</span>
<input
className='signin-window-field-input'
onChange={updateUsername}
placeholder='Username'
value={username}
/>
</div>
<div className='signin-window-field'>
<span className='signin-window-field-label'>Password</span>
<div className='signin-window-password'>
<input
className='signin-window-password-input'
onChange={updatePassword}
placeholder='Password'
type={showPassword ? 'text' : 'password'}
value={password}
/>
<button
className='signin-window-password-show-button'
onClick={updateShowPassword}
>
<img
className='signin-window-password-show-icon'
src={showPassword ? EyeSlash : Eye}
/>
</button>
</div>
</div>
<button className='signin-window-form-button' onClick={signIn}>
{signingIn ? 'Signing in...' : 'Sign In'}
</button>
</div>
<div></div>
</div>
</div>
<div className='signin-footer'>
<span className='signin-footer-description'>Rum Goggles by Tyler Travis</span>
<span className='signin-footer-description'>Follow @tylertravisty</span>
</div>
</div>
</>
);
}
export default SignIn;

Some files were not shown because too many files have changed in this diff Show more