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