Compare commits
24 commits
Author | SHA1 | Date | |
---|---|---|---|
tyler | 71fb9a28a3 | ||
tyler | daceefe548 | ||
3f29337fd6 | |||
983c031325 | |||
0bf60429ec | |||
04424b4abb | |||
f9a5ae7c01 | |||
142ced1d3a | |||
bb5098f472 | |||
aea79d55bf | |||
e1fcceb721 | |||
562b90ebf7 | |||
1e87346086 | |||
47437dbfbb | |||
14ef632e6c | |||
2906a372d5 | |||
79031a1978 | |||
419e67ff21 | |||
e7ca3cfbf0 | |||
b97012ed21 | |||
e68567c010 | |||
08d6bc3782 | |||
954af040d1 | |||
7d3c6e34ec |
34
.github/workflows/main.yml
vendored
|
@ -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
|
@ -1,8 +1 @@
|
||||||
build/bin/
|
.prettierignore
|
||||||
node_modules
|
|
||||||
frontend/dist
|
|
||||||
frontend/wailsjs
|
|
||||||
|
|
||||||
.prettierignore
|
|
||||||
|
|
||||||
config.json
|
|
93
NOTES.md
|
@ -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
|
@ -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)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.4 KiB |
|
@ -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;
|
|
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.3 KiB |
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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() {}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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() {}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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%;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export const NavSignIn = '/';
|
|
||||||
export const NavDashboard = '/dashboard';
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -1,10 +0,0 @@
|
||||||
html {
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
/* height: 100vh; */
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
165
licenses/github.com/FortAwesome/Font-Awesome/LICENSE.txt
Normal 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.**
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"react-router-dom": "^6.20.1"
|
|
||||||
}
|
|
||||||
}
|
|
6
v1/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
build/bin
|
||||||
|
build/darwin
|
||||||
|
build/windows
|
||||||
|
node_modules
|
||||||
|
frontend/dist
|
||||||
|
frontend/wailsjs
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
@ -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",
|
|
@ -1,3 +1,3 @@
|
||||||
#App {
|
#app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
|
@ -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>
|
||||||
);
|
);
|
3
v1/frontend/src/Navigation.jsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const NavDashboard = '/dashboard';
|
||||||
|
export const NavSignIn = '/signin';
|
||||||
|
export const NavStartup = '/';
|
165
v1/frontend/src/assets/icons/Font-Awesome/LICENSE.txt
Normal 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.**
|
BIN
v1/frontend/src/assets/icons/Font-Awesome/chess-rook.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
v1/frontend/src/assets/icons/Font-Awesome/robot.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
21
v1/frontend/src/assets/icons/twbs/LICENSE
Normal 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.
|
BIN
v1/frontend/src/assets/icons/twbs/chevron-down.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
v1/frontend/src/assets/icons/twbs/chevron-left.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
v1/frontend/src/assets/icons/twbs/chevron-right.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
v1/frontend/src/assets/icons/twbs/circle-green-background.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
v1/frontend/src/assets/icons/twbs/circle-red-background.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
v1/frontend/src/assets/icons/twbs/eye-red.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
v1/frontend/src/assets/icons/twbs/eye-slash.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
v1/frontend/src/assets/icons/twbs/eye.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
v1/frontend/src/assets/icons/twbs/gear-fill-white.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 6 KiB |
BIN
v1/frontend/src/assets/icons/twbs/hand-thumbs-down-fill.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
v1/frontend/src/assets/icons/twbs/hand-thumbs-up-fill.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
v1/frontend/src/assets/icons/twbs/pause-circle-green.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
v1/frontend/src/assets/icons/twbs/pause-fill.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
v1/frontend/src/assets/icons/twbs/play-circle-green.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
v1/frontend/src/assets/icons/twbs/play-fill.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
51
v1/frontend/src/assets/index.js
Normal 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;
|
BIN
v1/frontend/src/assets/logo/logo.png
Normal file
After Width: | Height: | Size: 81 KiB |
588
v1/frontend/src/components/ChatBot.css
Normal 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;
|
||||||
|
}
|
2121
v1/frontend/src/components/ChatBot.jsx
Normal file
80
v1/frontend/src/components/DropDown.css
Normal 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;
|
||||||
|
}
|
97
v1/frontend/src/components/DropDown.jsx
Normal 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;
|
||||||
|
};
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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>
|
||||||
)}
|
)}
|
356
v1/frontend/src/components/PageDetails.css
Normal 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;
|
||||||
|
}
|
803
v1/frontend/src/components/PageDetails.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
362
v1/frontend/src/components/PageSideBar.css
Normal 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% }
|
||||||
|
}
|
654
v1/frontend/src/components/PageSideBar.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
7
v1/frontend/src/screens/Dashboard.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.dashboard {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
19
v1/frontend/src/screens/Dashboard.jsx
Normal 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;
|
192
v1/frontend/src/screens/SignIn.css
Normal 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;
|
||||||
|
}
|
121
v1/frontend/src/screens/SignIn.jsx
Normal 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;
|