Implemented functionality to add accounts and channels

This commit is contained in:
tyler 2024-03-20 12:36:45 -04:00
parent 08d6bc3782
commit e68567c010
25 changed files with 1536 additions and 43 deletions

128
v1/app.go
View file

@ -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
}

View file

@ -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>
);

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -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;

View 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% }
}

View 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>
);
}

View file

@ -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;
@ -173,4 +178,4 @@
color: black;
font-family: sans-serif;
font-size: 24px;
}
}

View file

@ -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>

View file

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

View 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;

View file

@ -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);

View file

View file

View 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?

View file

@ -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
username sql.NullString
cookies sql.NullString
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

View 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
}

View 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
}

View file

@ -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 {

View file

@ -7,23 +7,27 @@ import (
type migrationFunc func() error
type service struct {
type table struct {
name string
automigrate migrationFunc
destructivereset migrationFunc
}
type Services struct {
AccountS AccountService
Database *sql.DB
services []service
AccountS AccountService
AccountChannelS AccountChannelService
ChannelS ChannelService
Database *sql.DB
tables []table
}
func (s *Services) AutoMigrate() error {
for _, service := range s.services {
err := service.automigrate()
if err != nil {
return pkgErr(fmt.Sprintf("error auto-migrating %s service", service.name), err)
for _, table := range s.tables {
if table.automigrate != nil {
err := table.automigrate()
if err != nil {
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()
if err != nil {
return pkgErr(fmt.Sprintf("error destructive-resetting %s service", service.name), err)
for _, table := range s.tables {
if table.destructivereset != nil {
err := table.destructivereset()
if err != nil {
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
}

View file

@ -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"
@ -21,7 +24,8 @@ func main() {
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
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)
}