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(
|
||||
models.WithDatabase(db),
|
||||
models.WithAccountService(),
|
||||
models.WithChannelService(),
|
||||
models.WithAccountChannelService(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -100,6 +102,84 @@ func (a *App) shutdown(ctx context.Context) {
|
|||
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 {
|
||||
var err error
|
||||
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.")
|
||||
}
|
||||
if act == nil {
|
||||
act = &models.Account{nil, &username, &cookiesS}
|
||||
act = &models.Account{nil, nil, &username, &cookiesS, nil, nil}
|
||||
err = a.services.AccountS.Create(act)
|
||||
if err != nil {
|
||||
a.logError.Println("error creating account:", err)
|
||||
|
@ -143,10 +223,54 @@ func (a *App) Login(username string, password string) error {
|
|||
act.Cookies = &cookiesS
|
||||
err = a.services.AccountS.Update(act)
|
||||
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 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 { MemoryRouter as Router, Route, Routes, Link } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import { NavSignIn } from './Navigation';
|
||||
import { NavDashboard, NavSignIn } from './Navigation';
|
||||
import Dashboard from './screens/Dashboard';
|
||||
import SignIn from './screens/SignIn';
|
||||
|
||||
function App() {
|
||||
|
@ -9,6 +10,7 @@ function App() {
|
|||
<Router>
|
||||
<Routes>
|
||||
<Route path={NavSignIn} element={<SignIn />} />
|
||||
<Route path={NavDashboard} element={<Dashboard />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const NavDashboard = '/dashboard';
|
||||
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_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 logo from './logo/logo.png';
|
||||
|
||||
export const ChevronRight = chevron_right;
|
||||
export const CircleGreenBackground = circle_green_background;
|
||||
export const Eye = eye;
|
||||
export const EyeSlash = eye_slash;
|
||||
export const Heart = heart;
|
||||
export const Logo = logo;
|
||||
export const PlusCircle = plus_circle;
|
||||
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 {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
/* background-color: transparent; */
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
|
@ -29,6 +30,7 @@
|
|||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
/* width: 20%; */
|
||||
height: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
|
@ -36,12 +38,14 @@
|
|||
background-color: transparent;
|
||||
border: 1px solid #495a6a;
|
||||
border-radius: 5px;
|
||||
color: #495a6a;
|
||||
/* color: #495a6a; */
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
/* width: 20%; */
|
||||
height: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
|
@ -55,6 +59,7 @@
|
|||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
/* width: 20%; */
|
||||
height: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
|
@ -79,8 +84,8 @@
|
|||
|
||||
.modal-container {
|
||||
align-items: center;
|
||||
background-color: rgba(6,23,38,1);
|
||||
border: 1px solid #495a6a;
|
||||
background-color: #1f2e3c;
|
||||
/* border: 1px solid #495a6a; */
|
||||
border-radius: 15px;
|
||||
color: black;
|
||||
display: flex;
|
||||
|
@ -133,7 +138,7 @@
|
|||
align-items: center;
|
||||
/* background-color: rgba(6,23,38,1); */
|
||||
background-color: white;
|
||||
border: 1px solid #495a6a;
|
||||
/* border: 1px solid #495a6a; */
|
||||
/* border: 1px solid black; */
|
||||
border-radius: 15px;
|
||||
color: black;
|
||||
|
|
|
@ -6,7 +6,7 @@ export function Modal(props) {
|
|||
<div
|
||||
className='modal-background'
|
||||
onClick={props.onClose}
|
||||
style={{ zIndex: props.show ? 10 : -10 }}
|
||||
style={{ zIndex: props.show ? 8 : -8 }}
|
||||
>
|
||||
<div
|
||||
className='modal-container'
|
||||
|
@ -33,7 +33,12 @@ export function Modal(props) {
|
|||
)}
|
||||
{props.submitButton && (
|
||||
<button className='modal-button' onClick={props.onSubmit}>
|
||||
{props.submitButton}
|
||||
{/* {props.submitButton} */}
|
||||
{props.submitLoading ? (
|
||||
<div className='loader'></div>
|
||||
) : (
|
||||
props.submitButton
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</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 { 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 { 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);
|
||||
|
@ -14,12 +17,25 @@ function SignIn() {
|
|||
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);
|
||||
|
|
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"
|
||||
configDirWin = "RumGoggles"
|
||||
|
||||
imageDir = "images"
|
||||
|
||||
logFile = "rumgoggles.log"
|
||||
sqlFile = "rumgoggles.db"
|
||||
)
|
||||
|
@ -32,6 +34,22 @@ func Database() (string, error) {
|
|||
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
|
||||
// Rotate log file every week?
|
||||
// Keep most recent 4 logs?
|
||||
|
|
|
@ -6,36 +6,59 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
accountColumns = "id, username, cookies"
|
||||
accountColumns = "id, uid, username, cookies, profile_image, api_key"
|
||||
accountTable = "account"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
ID *int64
|
||||
Username *string
|
||||
Cookies *string
|
||||
ID *int64 `json:"id"`
|
||||
UID *string `json:"uid"`
|
||||
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 {
|
||||
id sql.NullInt64
|
||||
uid sql.NullString
|
||||
username sql.NullString
|
||||
cookies sql.NullString
|
||||
profileImage sql.NullString
|
||||
apiKey sql.NullString
|
||||
}
|
||||
|
||||
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 {
|
||||
var a Account
|
||||
a.ID = toInt64(sa.id)
|
||||
a.UID = toString(sa.uid)
|
||||
a.Username = toString(sa.username)
|
||||
a.Cookies = toString(sa.cookies)
|
||||
a.ProfileImage = toString(sa.profileImage)
|
||||
a.ApiKey = toString(sa.apiKey)
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
type AccountService interface {
|
||||
All() ([]Account, error)
|
||||
AutoMigrate() error
|
||||
ByUsername(username string) (*Account, error)
|
||||
Create(a *Account) error
|
||||
|
@ -55,10 +78,41 @@ type accountService struct {
|
|||
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 {
|
||||
err := as.createAccountTable()
|
||||
if err != nil {
|
||||
return err
|
||||
return pkgErr(fmt.Sprintf("error creating %s table", accountTable), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -68,14 +122,17 @@ func (as *accountService) createAccountTable() error {
|
|||
createQ := fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS "%s" (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
uid TEXT UNIQUE,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
cookies TEXT
|
||||
cookies TEXT,
|
||||
profile_image TEXT,
|
||||
api_key TEXT
|
||||
)
|
||||
`, accountTable)
|
||||
|
||||
_, err := as.Database.Exec(createQ)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating table: %v", err)
|
||||
return fmt.Errorf("error executing create query: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -103,7 +160,7 @@ func (as *accountService) ByUsername(username string) (*Account, error) {
|
|||
if err == sql.ErrNoRows {
|
||||
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
|
||||
|
@ -124,22 +181,31 @@ func (as *accountService) Create(a *Account) error {
|
|||
VALUES (%s)
|
||||
`, accountTable, columns, values(columns))
|
||||
|
||||
_, err = as.Database.Exec(insertQ, a.Username, a.Cookies)
|
||||
_, err = as.Database.Exec(insertQ, a.valuesNoID()...)
|
||||
if err != nil {
|
||||
return pkgErr(fmt.Sprintf("error inserting %s", accountTable), err)
|
||||
return pkgErr("error executing insert query", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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(`
|
||||
DROP TABLE IF EXISTS "%s"
|
||||
`, accountTable)
|
||||
|
||||
_, err := as.Database.Exec(dropQ)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dropping table: %v", err)
|
||||
return fmt.Errorf("error executing drop query: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -162,9 +228,9 @@ func (as *accountService) Update(a *Account) error {
|
|||
WHERE id=?
|
||||
`, accountTable, set(columns))
|
||||
|
||||
_, err = as.Database.Exec(updateQ, a.Username, a.Cookies, a.ID)
|
||||
_, err = as.Database.Exec(updateQ, a.valuesEndID()...)
|
||||
if err != nil {
|
||||
return pkgErr(fmt.Sprintf("error updating %s", accountTable), err)
|
||||
return pkgErr(fmt.Sprintf("error executing update query", accountTable), err)
|
||||
}
|
||||
|
||||
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"
|
||||
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 {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
type migrationFunc func() error
|
||||
|
||||
type service struct {
|
||||
type table struct {
|
||||
name string
|
||||
automigrate migrationFunc
|
||||
destructivereset migrationFunc
|
||||
|
@ -15,15 +15,19 @@ type service struct {
|
|||
|
||||
type Services struct {
|
||||
AccountS AccountService
|
||||
AccountChannelS AccountChannelService
|
||||
ChannelS ChannelService
|
||||
Database *sql.DB
|
||||
services []service
|
||||
tables []table
|
||||
}
|
||||
|
||||
func (s *Services) AutoMigrate() error {
|
||||
for _, service := range s.services {
|
||||
err := service.automigrate()
|
||||
for _, table := range s.tables {
|
||||
if table.automigrate != nil {
|
||||
err := table.automigrate()
|
||||
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 {
|
||||
for _, service := range s.services {
|
||||
err := service.destructivereset()
|
||||
for _, table := range s.tables {
|
||||
if table.destructivereset != nil {
|
||||
err := table.destructivereset()
|
||||
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 {
|
||||
return func(s *Services) error {
|
||||
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
|
||||
}
|
||||
|
|
13
v1/main.go
13
v1/main.go
|
@ -2,7 +2,10 @@ package main
|
|||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tylertravisty/rum-goggles/v1/internal/config"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
|
@ -22,6 +25,7 @@ func main() {
|
|||
Height: 768,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
Handler: http.HandlerFunc(GetImage),
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
OnShutdown: app.shutdown,
|
||||
|
@ -35,3 +39,12 @@ func main() {
|
|||
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