Implemented functionality to add accounts and channels
This commit is contained in:
parent
08d6bc3782
commit
e68567c010
128
v1/app.go
128
v1/app.go
|
@ -67,6 +67,8 @@ func (a *App) startup(ctx context.Context) {
|
||||||
services, err := models.NewServices(
|
services, err := models.NewServices(
|
||||||
models.WithDatabase(db),
|
models.WithDatabase(db),
|
||||||
models.WithAccountService(),
|
models.WithAccountService(),
|
||||||
|
models.WithChannelService(),
|
||||||
|
models.WithAccountChannelService(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -100,6 +102,84 @@ func (a *App) shutdown(ctx context.Context) {
|
||||||
a.logFileMu.Unlock()
|
a.logFileMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) AddChannel(apiKey string) error {
|
||||||
|
client := rumblelivestreamlib.Client{StreamKey: apiKey}
|
||||||
|
resp, err := client.Request()
|
||||||
|
if err != nil {
|
||||||
|
a.logError.Println("error executing api request:", err)
|
||||||
|
return fmt.Errorf("Error querying API. Verify key and try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
userKey := apiKey
|
||||||
|
channelKey := ""
|
||||||
|
if resp.Type == "channel" {
|
||||||
|
userKey = ""
|
||||||
|
channelKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.addAccountNotExist(resp.UserID, resp.Username, userKey)
|
||||||
|
if err != nil {
|
||||||
|
a.logError.Println("error adding account if not exist:", err)
|
||||||
|
return fmt.Errorf("Error adding channel. Try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Type == "channel" {
|
||||||
|
err = a.addChannelNotExist(resp.Username, fmt.Sprint(resp.ChannelID), resp.ChannelName, channelKey)
|
||||||
|
if err != nil {
|
||||||
|
a.logError.Println("error adding channel if not exist:", err)
|
||||||
|
return fmt.Errorf("Error adding channel. Try again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) addAccountNotExist(uid string, username string, apiKey string) error {
|
||||||
|
acct, err := a.services.AccountS.ByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error querying account by username: %v", err)
|
||||||
|
}
|
||||||
|
if acct == nil {
|
||||||
|
err = a.services.AccountS.Create(&models.Account{
|
||||||
|
UID: &uid,
|
||||||
|
Username: &username,
|
||||||
|
ApiKey: &apiKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating account: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) addChannelNotExist(username string, cid string, name string, apiKey string) error {
|
||||||
|
channel, err := a.services.ChannelS.ByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error querying channel by name: %v", err)
|
||||||
|
}
|
||||||
|
if channel == nil {
|
||||||
|
acct, err := a.services.AccountS.ByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error querying account by username: %v", err)
|
||||||
|
}
|
||||||
|
if acct == nil {
|
||||||
|
return fmt.Errorf("account does not exist with username: %s", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.services.ChannelS.Create(&models.Channel{
|
||||||
|
AccountID: acct.ID,
|
||||||
|
CID: &cid,
|
||||||
|
Name: &name,
|
||||||
|
ApiKey: &apiKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating channel: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Login(username string, password string) error {
|
func (a *App) Login(username string, password string) error {
|
||||||
var err error
|
var err error
|
||||||
client, exists := a.clients[username]
|
client, exists := a.clients[username]
|
||||||
|
@ -133,7 +213,7 @@ func (a *App) Login(username string, password string) error {
|
||||||
return fmt.Errorf("Error logging in. Try again.")
|
return fmt.Errorf("Error logging in. Try again.")
|
||||||
}
|
}
|
||||||
if act == nil {
|
if act == nil {
|
||||||
act = &models.Account{nil, &username, &cookiesS}
|
act = &models.Account{nil, nil, &username, &cookiesS, nil, nil}
|
||||||
err = a.services.AccountS.Create(act)
|
err = a.services.AccountS.Create(act)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logError.Println("error creating account:", err)
|
a.logError.Println("error creating account:", err)
|
||||||
|
@ -143,10 +223,54 @@ func (a *App) Login(username string, password string) error {
|
||||||
act.Cookies = &cookiesS
|
act.Cookies = &cookiesS
|
||||||
err = a.services.AccountS.Update(act)
|
err = a.services.AccountS.Update(act)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logError.Println("error updating account", err)
|
a.logError.Println("error updating account:", err)
|
||||||
return fmt.Errorf("Error logging in. Try again.")
|
return fmt.Errorf("Error logging in. Try again.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SignedIn() (bool, error) {
|
||||||
|
accounts, err := a.services.AccountS.All()
|
||||||
|
if err != nil {
|
||||||
|
a.logError.Println("error getting all accounts:", err)
|
||||||
|
return false, fmt.Errorf("Error retrieving accounts. Try restarting.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(accounts) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Account models.Account `json:"account"`
|
||||||
|
Channels []models.Channel `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) AccountList() (map[string]*Account, error) {
|
||||||
|
list := map[string]*Account{}
|
||||||
|
|
||||||
|
accountChannels, err := a.services.AccountChannelS.All()
|
||||||
|
if err != nil {
|
||||||
|
a.logError.Println("error getting all account channels:", err)
|
||||||
|
return nil, fmt.Errorf("Error retrieving accounts and channels. Try restarting.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ac := range accountChannels {
|
||||||
|
if ac.Account.Username == nil {
|
||||||
|
a.logError.Println("account-channel contains nil account username")
|
||||||
|
return nil, fmt.Errorf("Error retrieving accounts and channels. Try restarting.")
|
||||||
|
}
|
||||||
|
|
||||||
|
act, exists := list[*ac.Account.Username]
|
||||||
|
if !exists || act == nil {
|
||||||
|
act = &Account{ac.Account, []models.Channel{}}
|
||||||
|
list[*ac.Account.Username] = act
|
||||||
|
}
|
||||||
|
|
||||||
|
if ac.Channel.AccountID != nil {
|
||||||
|
act.Channels = append(act.Channels, ac.Channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { 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 { NavSignIn } from './Navigation';
|
import { NavDashboard, NavSignIn } from './Navigation';
|
||||||
|
import Dashboard from './screens/Dashboard';
|
||||||
import SignIn from './screens/SignIn';
|
import SignIn from './screens/SignIn';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -9,6 +10,7 @@ function App() {
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={NavSignIn} element={<SignIn />} />
|
<Route path={NavSignIn} element={<SignIn />} />
|
||||||
|
<Route path={NavDashboard} element={<Dashboard />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
export const NavDashboard = '/dashboard';
|
||||||
export const NavSignIn = '/';
|
export const NavSignIn = '/';
|
||||||
|
|
BIN
v1/frontend/src/assets/icons/chevron-right.png
Normal file
BIN
v1/frontend/src/assets/icons/chevron-right.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
v1/frontend/src/assets/icons/circle-green-background.png
Normal file
BIN
v1/frontend/src/assets/icons/circle-green-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
BIN
v1/frontend/src/assets/icons/circle-red-background.png
Normal file
BIN
v1/frontend/src/assets/icons/circle-red-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
v1/frontend/src/assets/icons/heart-fill.png
Normal file
BIN
v1/frontend/src/assets/icons/heart-fill.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
v1/frontend/src/assets/icons/plus-circle-fill.png
Normal file
BIN
v1/frontend/src/assets/icons/plus-circle-fill.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
|
@ -1,9 +1,17 @@
|
||||||
|
import chevron_right from './icons/chevron-right.png';
|
||||||
|
import circle_green_background from './icons/circle-green-background.png';
|
||||||
import eye from './icons/eye.png';
|
import eye from './icons/eye.png';
|
||||||
import eye_slash from './icons/eye-slash.png';
|
import eye_slash from './icons/eye-slash.png';
|
||||||
|
import heart from './icons/heart-fill.png';
|
||||||
|
import plus_circle from './icons/plus-circle-fill.png'
|
||||||
import x_lg from './icons/x-lg.png';
|
import x_lg from './icons/x-lg.png';
|
||||||
import logo from './logo/logo.png';
|
import logo from './logo/logo.png';
|
||||||
|
|
||||||
|
export const ChevronRight = chevron_right;
|
||||||
|
export const CircleGreenBackground = circle_green_background;
|
||||||
export const Eye = eye;
|
export const Eye = eye;
|
||||||
export const EyeSlash = eye_slash;
|
export const EyeSlash = eye_slash;
|
||||||
|
export const Heart = heart;
|
||||||
export const Logo = logo;
|
export const Logo = logo;
|
||||||
|
export const PlusCircle = plus_circle;
|
||||||
export const XLg = x_lg;
|
export const XLg = x_lg;
|
357
v1/frontend/src/components/ChannelSideBar.css
Normal file
357
v1/frontend/src/components/ChannelSideBar.css
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
|
||||||
|
.channel-sidebar {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #061726;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-account-list {
|
||||||
|
border-top: 2px solid #273848;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-button {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #061726;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-button-icon {
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-footer {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-icon {
|
||||||
|
height: 60px;
|
||||||
|
margin-top: 10px;
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-icon-account {
|
||||||
|
bottom: 0px;
|
||||||
|
height: 24px;
|
||||||
|
left: 36px;
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-icon-hover {
|
||||||
|
background-color: #061726;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: black;
|
||||||
|
padding: 10px;
|
||||||
|
position: fixed;
|
||||||
|
/* transform: translate(75px, -50px); */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.channel-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-icon-hover-text {
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-icon-image:hover {
|
||||||
|
border-radius: 30%;
|
||||||
|
transition: border-radius 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar-icon-initial {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-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% }
|
||||||
|
}
|
503
v1/frontend/src/components/ChannelSideBar.jsx
Normal file
503
v1/frontend/src/components/ChannelSideBar.jsx
Normal file
|
@ -0,0 +1,503 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Modal, SmallModal } from './Modal';
|
||||||
|
import { AccountList, AddChannel, Login } from '../../wailsjs/go/main/App';
|
||||||
|
|
||||||
|
import { ChevronRight, CircleGreenBackground, Eye, EyeSlash, PlusCircle } from '../assets';
|
||||||
|
import './ChannelSideBar.css';
|
||||||
|
|
||||||
|
function ChannelSideBar(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(() => {
|
||||||
|
AccountList()
|
||||||
|
.then((response) => {
|
||||||
|
setAccounts(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
});
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ModalAdd
|
||||||
|
onClose={() => setAddOpen(false)}
|
||||||
|
onRefresh={() => {
|
||||||
|
setRefresh(!refresh);
|
||||||
|
}}
|
||||||
|
show={addOpen}
|
||||||
|
/>
|
||||||
|
<div className='channel-sidebar'>
|
||||||
|
<div className='channel-sidebar-body' onScroll={handleScroll}>
|
||||||
|
{sortAccounts().map((account, index) => (
|
||||||
|
<AccountChannels
|
||||||
|
account={accounts[account]}
|
||||||
|
key={index}
|
||||||
|
scrollY={scrollY}
|
||||||
|
top={index === 0}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='channel-sidebar-footer'>
|
||||||
|
<ButtonIcon
|
||||||
|
hoverText={'Add an account/channel'}
|
||||||
|
onClick={() => setAddOpen(true)}
|
||||||
|
scrollY={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelSideBar;
|
||||||
|
|
||||||
|
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='channel-sidebar-account-list'
|
||||||
|
style={props.top ? { borderTop: 'none' } : {}}
|
||||||
|
>
|
||||||
|
<AccountIcon account={props.account.account} key={0} scrollY={props.scrollY} />
|
||||||
|
{sortChannels().map((channel, index) => (
|
||||||
|
<ChannelIcon channel={channel} key={index + 1} scrollY={props.scrollY} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountIcon(props) {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='channel-sidebar-icon'
|
||||||
|
onMouseEnter={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
>
|
||||||
|
{props.account.profile_image === null ? (
|
||||||
|
<span className='channel-sidebar-icon-initial'>
|
||||||
|
{props.account.username[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<img className='channel-sidebar-icon-image' src={props.account.profile_image} />
|
||||||
|
)}
|
||||||
|
<img className='channel-sidebar-icon-account' src={CircleGreenBackground} />
|
||||||
|
{hover && (
|
||||||
|
<HoverName name={'/user/' + props.account.username} scrollY={props.scrollY} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonIcon(props) {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='channel-sidebar-icon'
|
||||||
|
onMouseEnter={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
>
|
||||||
|
<button className='channel-sidebar-button' onClick={props.onClick}>
|
||||||
|
<img className='channel-sidebar-button-icon' src={PlusCircle} />
|
||||||
|
</button>
|
||||||
|
{hover && <HoverName name={props.hoverText} scrollY={props.scrollY} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChannelIcon(props) {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='channel-sidebar-icon'
|
||||||
|
onMouseEnter={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
>
|
||||||
|
{props.channel.profile_image === null ? (
|
||||||
|
<span className='channel-sidebar-icon-initial'>
|
||||||
|
{props.channel.name[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<img className='channel-sidebar-icon-image' src={props.channel.profile_image} />
|
||||||
|
)}
|
||||||
|
{hover && (
|
||||||
|
<HoverName
|
||||||
|
name={'/c/' + props.channel.name.replace(/\s/g, '')}
|
||||||
|
scrollY={props.scrollY}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoverName(props) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='channel-sidebar-icon-hover'
|
||||||
|
style={{ transform: 'translate(75px, -' + (50 + props.scrollY) + 'px)' }}
|
||||||
|
>
|
||||||
|
<span className='channel-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) {
|
||||||
|
AddChannel(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 (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,6 +30,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
/* width: 20%; */
|
/* width: 20%; */
|
||||||
|
height: 40px;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,12 +38,14 @@
|
||||||
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%; */
|
||||||
|
height: 40px;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +59,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
/* width: 20%; */
|
/* width: 20%; */
|
||||||
|
height: 40px;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,8 +84,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;
|
||||||
|
@ -133,7 +138,7 @@
|
||||||
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;
|
||||||
|
|
|
@ -6,7 +6,7 @@ export function Modal(props) {
|
||||||
<div
|
<div
|
||||||
className='modal-background'
|
className='modal-background'
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
style={{ zIndex: props.show ? 10 : -10 }}
|
style={{ zIndex: props.show ? 8 : -8 }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='modal-container'
|
className='modal-container'
|
||||||
|
@ -33,7 +33,12 @@ export function Modal(props) {
|
||||||
)}
|
)}
|
||||||
{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>
|
||||||
|
|
7
v1/frontend/src/screens/Dashboard.css
Normal file
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%;
|
||||||
|
}
|
14
v1/frontend/src/screens/Dashboard.jsx
Normal file
14
v1/frontend/src/screens/Dashboard.jsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { CircleGreenBackground, Heart } from '../assets';
|
||||||
|
import ChannelSideBar from '../components/ChannelSideBar';
|
||||||
|
import './Dashboard.css';
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
return (
|
||||||
|
<div className='dashboard'>
|
||||||
|
<ChannelSideBar />
|
||||||
|
<div style={{ backgroundColor: '#1f2e3c', width: '100%', height: '100%' }}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
|
@ -1,11 +1,14 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { SmallModal } from '../components/Modal';
|
import { SmallModal } from '../components/Modal';
|
||||||
import { Login } from '../../wailsjs/go/main/App';
|
import { Login, SignedIn } from '../../wailsjs/go/main/App';
|
||||||
import { Eye, EyeSlash, Logo } from '../assets';
|
import { Eye, EyeSlash, Logo } from '../assets';
|
||||||
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
import './SignIn.css';
|
import './SignIn.css';
|
||||||
|
import { NavDashboard } from '../Navigation';
|
||||||
|
|
||||||
function SignIn() {
|
function SignIn() {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const updatePassword = (event) => setPassword(event.target.value);
|
const updatePassword = (event) => setPassword(event.target.value);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
@ -14,12 +17,25 @@ function SignIn() {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const updateUsername = (event) => setUsername(event.target.value);
|
const updateUsername = (event) => setUsername(event.target.value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
SignedIn()
|
||||||
|
.then((signedIn) => {
|
||||||
|
if (signedIn) {
|
||||||
|
navigate(NavDashboard);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (signingIn) {
|
if (signingIn) {
|
||||||
Login(username, password)
|
Login(username, password)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setUsername('');
|
setUsername('');
|
||||||
setPassword('');
|
setPassword('');
|
||||||
|
navigate(NavDashboard);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
|
0
v1/frontend/src/screens/Start.css
Normal file
0
v1/frontend/src/screens/Start.css
Normal file
0
v1/frontend/src/screens/Start.jsx
Normal file
0
v1/frontend/src/screens/Start.jsx
Normal file
|
@ -11,6 +11,8 @@ const (
|
||||||
configDirNix = ".rum-goggles"
|
configDirNix = ".rum-goggles"
|
||||||
configDirWin = "RumGoggles"
|
configDirWin = "RumGoggles"
|
||||||
|
|
||||||
|
imageDir = "images"
|
||||||
|
|
||||||
logFile = "rumgoggles.log"
|
logFile = "rumgoggles.log"
|
||||||
sqlFile = "rumgoggles.db"
|
sqlFile = "rumgoggles.db"
|
||||||
)
|
)
|
||||||
|
@ -32,6 +34,22 @@ func Database() (string, error) {
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ImageDir() (string, error) {
|
||||||
|
cfgDir, err := configDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", pkgErr("error getting config directory", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Join(cfgDir, imageDir)
|
||||||
|
|
||||||
|
err = os.MkdirAll(dir, 0750)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error making directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: implement log rotation
|
// TODO: implement log rotation
|
||||||
// Rotate log file every week?
|
// Rotate log file every week?
|
||||||
// Keep most recent 4 logs?
|
// Keep most recent 4 logs?
|
||||||
|
|
|
@ -6,36 +6,59 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
accountColumns = "id, username, cookies"
|
accountColumns = "id, uid, username, cookies, profile_image, api_key"
|
||||||
accountTable = "account"
|
accountTable = "account"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID *int64
|
ID *int64 `json:"id"`
|
||||||
Username *string
|
UID *string `json:"uid"`
|
||||||
Cookies *string
|
Username *string `json:"username"`
|
||||||
|
Cookies *string `json:"cookies"`
|
||||||
|
ProfileImage *string `json:"profile_image"`
|
||||||
|
ApiKey *string `json:"api_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) values() []any {
|
||||||
|
return []any{a.ID, a.UID, a.Username, a.Cookies, a.ProfileImage, a.ApiKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) valuesNoID() []any {
|
||||||
|
return a.values()[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) valuesEndID() []any {
|
||||||
|
vals := a.values()
|
||||||
|
return append(vals[1:], vals[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
type sqlAccount struct {
|
type sqlAccount struct {
|
||||||
id sql.NullInt64
|
id sql.NullInt64
|
||||||
|
uid sql.NullString
|
||||||
username sql.NullString
|
username sql.NullString
|
||||||
cookies sql.NullString
|
cookies sql.NullString
|
||||||
|
profileImage sql.NullString
|
||||||
|
apiKey sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa *sqlAccount) scan(r Row) error {
|
func (sa *sqlAccount) scan(r Row) error {
|
||||||
return r.Scan(&sa.id, &sa.username, &sa.cookies)
|
return r.Scan(&sa.id, &sa.uid, &sa.username, &sa.cookies, &sa.profileImage, &sa.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sa sqlAccount) toAccount() *Account {
|
func (sa sqlAccount) toAccount() *Account {
|
||||||
var a Account
|
var a Account
|
||||||
a.ID = toInt64(sa.id)
|
a.ID = toInt64(sa.id)
|
||||||
|
a.UID = toString(sa.uid)
|
||||||
a.Username = toString(sa.username)
|
a.Username = toString(sa.username)
|
||||||
a.Cookies = toString(sa.cookies)
|
a.Cookies = toString(sa.cookies)
|
||||||
|
a.ProfileImage = toString(sa.profileImage)
|
||||||
|
a.ApiKey = toString(sa.apiKey)
|
||||||
|
|
||||||
return &a
|
return &a
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountService interface {
|
type AccountService interface {
|
||||||
|
All() ([]Account, error)
|
||||||
AutoMigrate() error
|
AutoMigrate() error
|
||||||
ByUsername(username string) (*Account, error)
|
ByUsername(username string) (*Account, error)
|
||||||
Create(a *Account) error
|
Create(a *Account) error
|
||||||
|
@ -55,10 +78,41 @@ type accountService struct {
|
||||||
Database *sql.DB
|
Database *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *accountService) All() ([]Account, error) {
|
||||||
|
selectQ := fmt.Sprintf(`
|
||||||
|
SELECT %s
|
||||||
|
FROM "%s"
|
||||||
|
`, accountColumns, accountTable)
|
||||||
|
|
||||||
|
rows, err := as.Database.Query(selectQ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, pkgErr("error executing select query", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
accounts := []Account{}
|
||||||
|
for rows.Next() {
|
||||||
|
sa := &sqlAccount{}
|
||||||
|
|
||||||
|
err = sa.scan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, pkgErr("error scanning row", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts = append(accounts, *sa.toAccount())
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return nil, pkgErr("error iterating over rows", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (as *accountService) AutoMigrate() error {
|
func (as *accountService) AutoMigrate() error {
|
||||||
err := as.createAccountTable()
|
err := as.createAccountTable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return pkgErr(fmt.Sprintf("error creating %s table", accountTable), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -68,14 +122,17 @@ func (as *accountService) createAccountTable() error {
|
||||||
createQ := fmt.Sprintf(`
|
createQ := fmt.Sprintf(`
|
||||||
CREATE TABLE IF NOT EXISTS "%s" (
|
CREATE TABLE IF NOT EXISTS "%s" (
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
uid TEXT UNIQUE,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
cookies TEXT
|
cookies TEXT,
|
||||||
|
profile_image TEXT,
|
||||||
|
api_key TEXT
|
||||||
)
|
)
|
||||||
`, accountTable)
|
`, accountTable)
|
||||||
|
|
||||||
_, err := as.Database.Exec(createQ)
|
_, err := as.Database.Exec(createQ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating table: %v", err)
|
return fmt.Errorf("error executing create query: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -103,7 +160,7 @@ func (as *accountService) ByUsername(username string) (*Account, error) {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, pkgErr(fmt.Sprintf("error querying \"%s\" by username", accountTable), err)
|
return nil, pkgErr("error executing select query", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sa.toAccount(), nil
|
return sa.toAccount(), nil
|
||||||
|
@ -124,22 +181,31 @@ func (as *accountService) Create(a *Account) error {
|
||||||
VALUES (%s)
|
VALUES (%s)
|
||||||
`, accountTable, columns, values(columns))
|
`, accountTable, columns, values(columns))
|
||||||
|
|
||||||
_, err = as.Database.Exec(insertQ, a.Username, a.Cookies)
|
_, err = as.Database.Exec(insertQ, a.valuesNoID()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pkgErr(fmt.Sprintf("error inserting %s", accountTable), err)
|
return pkgErr("error executing insert query", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *accountService) DestructiveReset() error {
|
func (as *accountService) DestructiveReset() error {
|
||||||
|
err := as.dropAccountTable()
|
||||||
|
if err != nil {
|
||||||
|
return pkgErr(fmt.Sprintf("error dropping %s table", accountTable), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *accountService) dropAccountTable() error {
|
||||||
dropQ := fmt.Sprintf(`
|
dropQ := fmt.Sprintf(`
|
||||||
DROP TABLE IF EXISTS "%s"
|
DROP TABLE IF EXISTS "%s"
|
||||||
`, accountTable)
|
`, accountTable)
|
||||||
|
|
||||||
_, err := as.Database.Exec(dropQ)
|
_, err := as.Database.Exec(dropQ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error dropping table: %v", err)
|
return fmt.Errorf("error executing drop query: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -162,9 +228,9 @@ func (as *accountService) Update(a *Account) error {
|
||||||
WHERE id=?
|
WHERE id=?
|
||||||
`, accountTable, set(columns))
|
`, accountTable, set(columns))
|
||||||
|
|
||||||
_, err = as.Database.Exec(updateQ, a.Username, a.Cookies, a.ID)
|
_, err = as.Database.Exec(updateQ, a.valuesEndID()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pkgErr(fmt.Sprintf("error updating %s", accountTable), err)
|
return pkgErr(fmt.Sprintf("error executing update query", accountTable), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
94
v1/internal/models/accountchannel.go
Normal file
94
v1/internal/models/accountchannel.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
accountChannelColumns = "a.id, a.uid, a.username, a.cookies, a.profile_image, a.api_key, c.id, c.account_id, c.cid, c.name, c.profile_image, c.api_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountChannel struct {
|
||||||
|
Account
|
||||||
|
Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlAccountChannel struct {
|
||||||
|
sqlAccount
|
||||||
|
sqlChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sac *sqlAccountChannel) scan(r Row) error {
|
||||||
|
return r.Scan(
|
||||||
|
&sac.sqlAccount.id,
|
||||||
|
&sac.sqlAccount.uid,
|
||||||
|
&sac.sqlAccount.username,
|
||||||
|
&sac.sqlAccount.cookies,
|
||||||
|
&sac.sqlAccount.profileImage,
|
||||||
|
&sac.sqlAccount.apiKey,
|
||||||
|
&sac.sqlChannel.id,
|
||||||
|
&sac.sqlChannel.accountID,
|
||||||
|
&sac.sqlChannel.cid,
|
||||||
|
&sac.sqlChannel.name,
|
||||||
|
&sac.sqlChannel.profileImage,
|
||||||
|
&sac.sqlChannel.apiKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sac *sqlAccountChannel) toAccountChannel() *AccountChannel {
|
||||||
|
var ac AccountChannel
|
||||||
|
|
||||||
|
ac.Account = *sac.toAccount()
|
||||||
|
ac.Channel = *sac.toChannel()
|
||||||
|
|
||||||
|
return &ac
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountChannelService interface {
|
||||||
|
All() ([]AccountChannel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountChannelService(db *sql.DB) AccountChannelService {
|
||||||
|
return &accountChannelService{
|
||||||
|
Database: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ AccountChannelService = &accountChannelService{}
|
||||||
|
|
||||||
|
type accountChannelService struct {
|
||||||
|
Database *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *accountChannelService) All() ([]AccountChannel, error) {
|
||||||
|
selectQ := fmt.Sprintf(`
|
||||||
|
SELECT %s
|
||||||
|
FROM "%s" a
|
||||||
|
LEFT JOIN "%s" c ON a.id=c.account_id
|
||||||
|
`, accountChannelColumns, accountTable, channelTable)
|
||||||
|
|
||||||
|
rows, err := as.Database.Query(selectQ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, pkgErr("error executing select query", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
accountChannels := []AccountChannel{}
|
||||||
|
for rows.Next() {
|
||||||
|
sac := &sqlAccountChannel{}
|
||||||
|
|
||||||
|
err = sac.scan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, pkgErr("error scanning row", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountChannels = append(accountChannels, *sac.toAccountChannel())
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return nil, pkgErr("error iterating over rows", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountChannels, nil
|
||||||
|
}
|
232
v1/internal/models/channel.go
Normal file
232
v1/internal/models/channel.go
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
channelColumns = "id, account_id, cid, name, profile_image, api_key"
|
||||||
|
channelTable = "channel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
ID *int64 `json:"id"`
|
||||||
|
AccountID *int64 `json:"account_id"`
|
||||||
|
CID *string `json:"cid"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
ProfileImage *string `json:"profile_image"`
|
||||||
|
ApiKey *string `json:"api_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) values() []any {
|
||||||
|
return []any{c.ID, c.AccountID, c.CID, c.Name, c.ProfileImage, c.ApiKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) valuesNoID() []any {
|
||||||
|
return c.values()[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Channel) valuesEndID() []any {
|
||||||
|
vals := c.values()
|
||||||
|
return append(vals[1:], vals[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlChannel struct {
|
||||||
|
id sql.NullInt64
|
||||||
|
accountID sql.NullInt64
|
||||||
|
cid sql.NullString
|
||||||
|
name sql.NullString
|
||||||
|
profileImage sql.NullString
|
||||||
|
apiKey sql.NullString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *sqlChannel) scan(r Row) error {
|
||||||
|
return r.Scan(&sc.id, &sc.accountID, &sc.cid, &sc.name, &sc.profileImage, &sc.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc sqlChannel) toChannel() *Channel {
|
||||||
|
var c Channel
|
||||||
|
c.ID = toInt64(sc.id)
|
||||||
|
c.AccountID = toInt64(sc.accountID)
|
||||||
|
c.CID = toString(sc.cid)
|
||||||
|
c.Name = toString(sc.name)
|
||||||
|
c.ProfileImage = toString(sc.profileImage)
|
||||||
|
c.ApiKey = toString(sc.apiKey)
|
||||||
|
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelService interface {
|
||||||
|
AutoMigrate() error
|
||||||
|
ByName(name string) (*Channel, error)
|
||||||
|
Create(c *Channel) error
|
||||||
|
DestructiveReset() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChannelService(db *sql.DB) ChannelService {
|
||||||
|
return &channelService{
|
||||||
|
Database: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ChannelService = &channelService{}
|
||||||
|
|
||||||
|
type channelService struct {
|
||||||
|
Database *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *channelService) AutoMigrate() error {
|
||||||
|
err := cs.createChannelTable()
|
||||||
|
if err != nil {
|
||||||
|
return pkgErr(fmt.Sprintf("error creating %s table", channelTable), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *channelService) createChannelTable() error {
|
||||||
|
createQ := fmt.Sprintf(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "%s" (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
cid TEXT UNIQUE NOT NULL,
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
profile_image TEXT,
|
||||||
|
api_key TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (account_id) REFERENCES "%s" (id)
|
||||||
|
)
|
||||||
|
`, channelTable, accountTable)
|
||||||
|
|
||||||
|
_, err := cs.Database.Exec(createQ)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error executing create query: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *channelService) ByName(name string) (*Channel, error) {
|
||||||
|
err := runChannelValFuncs(
|
||||||
|
&Channel{Name: &name},
|
||||||
|
channelRequireName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, pkgErr("", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectQ := fmt.Sprintf(`
|
||||||
|
SELECT %s
|
||||||
|
FROM "%s"
|
||||||
|
WHERE name=?
|
||||||
|
`, channelColumns, channelTable)
|
||||||
|
|
||||||
|
var sc sqlChannel
|
||||||
|
row := cs.Database.QueryRow(selectQ, name)
|
||||||
|
err = sc.scan(row)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, pkgErr("error executing select query", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sc.toChannel(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *channelService) Create(c *Channel) error {
|
||||||
|
err := runChannelValFuncs(
|
||||||
|
c,
|
||||||
|
channelRequireAccountID,
|
||||||
|
channelRequireApiKey,
|
||||||
|
channelRequireCID,
|
||||||
|
channelRequireName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return pkgErr("invalid channel", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := columnsNoID(channelColumns)
|
||||||
|
insertQ := fmt.Sprintf(`
|
||||||
|
INSERT INTO "%s" (%s)
|
||||||
|
VALUES (%s)
|
||||||
|
`, channelTable, columns, values(columns))
|
||||||
|
|
||||||
|
_, err = cs.Database.Exec(insertQ, c.valuesNoID()...)
|
||||||
|
if err != nil {
|
||||||
|
return pkgErr("error executing insert query", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *channelService) DestructiveReset() error {
|
||||||
|
err := cs.dropChannelTable()
|
||||||
|
if err != nil {
|
||||||
|
return pkgErr(fmt.Sprintf("error dropping %s table", channelTable), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *channelService) dropChannelTable() error {
|
||||||
|
dropQ := fmt.Sprintf(`
|
||||||
|
DROP TABLE IF EXISTS "%s"
|
||||||
|
`, channelTable)
|
||||||
|
|
||||||
|
_, err := cs.Database.Exec(dropQ)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error executing drop query: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelValFunc func(*Channel) error
|
||||||
|
|
||||||
|
func runChannelValFuncs(c *Channel, fns ...channelValFunc) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("channel cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range fns {
|
||||||
|
err := fn(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func channelRequireAccountID(c *Channel) error {
|
||||||
|
if c.AccountID == nil || *c.AccountID <= 0 {
|
||||||
|
return ErrChannelInvalidAccountID
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func channelRequireApiKey(c *Channel) error {
|
||||||
|
if c.ApiKey == nil || *c.ApiKey == "" {
|
||||||
|
return ErrChannelInvalidApiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func channelRequireCID(c *Channel) error {
|
||||||
|
if c.CID == nil || *c.CID == "" {
|
||||||
|
return ErrChannelInvalidCID
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func channelRequireName(c *Channel) error {
|
||||||
|
if c.Name == nil || *c.Name == "" {
|
||||||
|
return ErrChannelInvalidName
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -7,6 +7,11 @@ const (
|
||||||
|
|
||||||
ErrAccountInvalidUsername ValidatorError = "invalid account username"
|
ErrAccountInvalidUsername ValidatorError = "invalid account username"
|
||||||
ErrAccountInvalidID ValidatorError = "invalid account id"
|
ErrAccountInvalidID ValidatorError = "invalid account id"
|
||||||
|
|
||||||
|
ErrChannelInvalidAccountID ValidatorError = "invalid channel account id"
|
||||||
|
ErrChannelInvalidApiKey ValidatorError = "invalid channel API key"
|
||||||
|
ErrChannelInvalidCID ValidatorError = "invalid channel CID"
|
||||||
|
ErrChannelInvalidName ValidatorError = "invalid channel name"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pkgErr(prefix string, err error) error {
|
func pkgErr(prefix string, err error) error {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
type migrationFunc func() error
|
type migrationFunc func() error
|
||||||
|
|
||||||
type service struct {
|
type table struct {
|
||||||
name string
|
name string
|
||||||
automigrate migrationFunc
|
automigrate migrationFunc
|
||||||
destructivereset migrationFunc
|
destructivereset migrationFunc
|
||||||
|
@ -15,15 +15,19 @@ type service struct {
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
AccountS AccountService
|
AccountS AccountService
|
||||||
|
AccountChannelS AccountChannelService
|
||||||
|
ChannelS ChannelService
|
||||||
Database *sql.DB
|
Database *sql.DB
|
||||||
services []service
|
tables []table
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Services) AutoMigrate() error {
|
func (s *Services) AutoMigrate() error {
|
||||||
for _, service := range s.services {
|
for _, table := range s.tables {
|
||||||
err := service.automigrate()
|
if table.automigrate != nil {
|
||||||
|
err := table.automigrate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pkgErr(fmt.Sprintf("error auto-migrating %s service", service.name), err)
|
return pkgErr(fmt.Sprintf("error auto-migrating %s table", table.name), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +44,12 @@ func (s *Services) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Services) DestructiveReset() error {
|
func (s *Services) DestructiveReset() error {
|
||||||
for _, service := range s.services {
|
for _, table := range s.tables {
|
||||||
err := service.destructivereset()
|
if table.destructivereset != nil {
|
||||||
|
err := table.destructivereset()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pkgErr(fmt.Sprintf("error destructive-resetting %s service", service.name), err)
|
return pkgErr(fmt.Sprintf("error destructive-resetting %s table", table.name), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +84,24 @@ func WithDatabase(file string) ServicesInit {
|
||||||
func WithAccountService() ServicesInit {
|
func WithAccountService() ServicesInit {
|
||||||
return func(s *Services) error {
|
return func(s *Services) error {
|
||||||
s.AccountS = NewAccountService(s.Database)
|
s.AccountS = NewAccountService(s.Database)
|
||||||
s.services = append(s.services, service{accountTable, s.AccountS.AutoMigrate, s.AccountS.DestructiveReset})
|
s.tables = append(s.tables, table{accountTable, s.AccountS.AutoMigrate, s.AccountS.DestructiveReset})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAccountChannelService() ServicesInit {
|
||||||
|
return func(s *Services) error {
|
||||||
|
s.AccountChannelS = NewAccountChannelService(s.Database)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithChannelService() ServicesInit {
|
||||||
|
return func(s *Services) error {
|
||||||
|
s.ChannelS = NewChannelService(s.Database)
|
||||||
|
s.tables = append(s.tables, table{channelTable, s.ChannelS.AutoMigrate, s.ChannelS.DestructiveReset})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
13
v1/main.go
13
v1/main.go
|
@ -2,7 +2,10 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tylertravisty/rum-goggles/v1/internal/config"
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
@ -22,6 +25,7 @@ func main() {
|
||||||
Height: 768,
|
Height: 768,
|
||||||
AssetServer: &assetserver.Options{
|
AssetServer: &assetserver.Options{
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
|
Handler: http.HandlerFunc(GetImage),
|
||||||
},
|
},
|
||||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||||
OnShutdown: app.shutdown,
|
OnShutdown: app.shutdown,
|
||||||
|
@ -35,3 +39,12 @@ func main() {
|
||||||
println("Error:", err.Error())
|
println("Error:", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := strings.Replace(r.RequestURI, "wails://wails", "", 1)
|
||||||
|
prefix, err := config.ImageDir()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, prefix+path)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue