Fixed UI issues; added footer and controls
23
app.go
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/tylertravisty/go-utils/random"
|
||||
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
|
||||
)
|
||||
|
||||
// App struct
|
||||
|
@ -35,15 +34,15 @@ func (a *App) Greet(name string) string {
|
|||
return fmt.Sprintf("Hello %s, It's show time!", random)
|
||||
}
|
||||
|
||||
func (a *App) QueryAPI(url string) (*rumblelivestreamlib.Followers, error) {
|
||||
fmt.Println("QueryAPI")
|
||||
client := rumblelivestreamlib.Client{StreamKey: url}
|
||||
resp, err := client.Request()
|
||||
if err != nil {
|
||||
// TODO: log error
|
||||
fmt.Println("client.Request err:", err)
|
||||
return nil, fmt.Errorf("API request failed")
|
||||
}
|
||||
// func (a *App) QueryAPI(url string) (*rumblelivestreamlib.Followers, error) {
|
||||
// fmt.Println("QueryAPI")
|
||||
// client := rumblelivestreamlib.Client{StreamKey: url}
|
||||
// resp, err := client.Request()
|
||||
// if err != nil {
|
||||
// // TODO: log error
|
||||
// fmt.Println("client.Request err:", err)
|
||||
// return nil, fmt.Errorf("API request failed")
|
||||
// }
|
||||
|
||||
return &resp.Followers, nil
|
||||
}
|
||||
// return &resp.Followers, nil
|
||||
// }
|
||||
|
|
BIN
frontend/src/assets/icons/gear.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
frontend/src/assets/icons/hand-thumbs-down.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
frontend/src/assets/icons/hand-thumbs-up.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
frontend/src/assets/icons/heart-fill.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
frontend/src/assets/icons/house.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
|
@ -1,5 +1,21 @@
|
|||
import eye from './eye.png';
|
||||
import eye_slash from './eye-slash.png';
|
||||
import gear from './gear.png';
|
||||
import heart from './heart-fill.png';
|
||||
import house from './house.png';
|
||||
import pause from './pause-fill.png';
|
||||
import play from './play-fill.png';
|
||||
import star from './star-fill.png';
|
||||
import thumbs_down from './hand-thumbs-down.png';
|
||||
import thumbs_up from './hand-thumbs-up.png';
|
||||
|
||||
export const Eye = eye;
|
||||
export const EyeSlash = eye_slash;
|
||||
export const Gear = gear;
|
||||
export const Heart = heart;
|
||||
export const House = house;
|
||||
export const Pause = pause;
|
||||
export const Play = play;
|
||||
export const Star = star;
|
||||
export const ThumbsDown = thumbs_down;
|
||||
export const ThumbsUp = thumbs_up;
|
||||
|
|
BIN
frontend/src/assets/icons/pause-circle.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
frontend/src/assets/icons/pause-fill.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
frontend/src/assets/icons/play-circle.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
frontend/src/assets/icons/play-fill.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
frontend/src/assets/icons/star-fill.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
24
frontend/src/components/Highlight.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.highlight {
|
||||
align-items: start;
|
||||
color: white;
|
||||
display: flex;
|
||||
background-color: #75a54b;
|
||||
border-radius: 0.5rem;
|
||||
flex-direction: column;
|
||||
font-family: sans-serif;
|
||||
font-weight: bold;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
min-width: 90px;
|
||||
padding: 5px 10px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.highlight-value {
|
||||
font-family: monospace;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.highlight-description {
|
||||
font-size: 12px;
|
||||
}
|
74
frontend/src/components/Highlight.jsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import './Highlight.css';
|
||||
|
||||
function Highlight(props) {
|
||||
const countString = () => {
|
||||
switch (true) {
|
||||
case props.value <= 0:
|
||||
return '-';
|
||||
case props.value < 1000:
|
||||
return props.value;
|
||||
case props.value < 1000000:
|
||||
return (props.value / 1000).toFixed(3).slice(0, -2) + 'K';
|
||||
case props.value < 1000000000:
|
||||
return (props.value / 1000000).toFixed(6).slice(0, -5) + 'M';
|
||||
default:
|
||||
return 'Inf';
|
||||
}
|
||||
};
|
||||
|
||||
const stopwatchString = () => {
|
||||
console.log(props.value);
|
||||
if (isNaN(Date.parse(props.value))) {
|
||||
return '--:--';
|
||||
}
|
||||
let now = new Date();
|
||||
let date = new Date(props.value);
|
||||
console.log(date);
|
||||
let diff = now - date;
|
||||
|
||||
let msMinute = 1000 * 60;
|
||||
let msHour = msMinute * 60;
|
||||
let msDay = msHour * 24;
|
||||
|
||||
let days = Math.floor(diff / msDay);
|
||||
let hours = Math.floor((diff - days * msDay) / msHour);
|
||||
let minutes = Math.floor((diff - days * msDay - hours * msHour) / msMinute);
|
||||
|
||||
if (diff >= 100 * msDay) {
|
||||
return days + 'd';
|
||||
}
|
||||
if (diff >= msDay) {
|
||||
return days + 'd ' + hours + 'h';
|
||||
}
|
||||
|
||||
if (hours < 10) {
|
||||
hours = '0' + hours;
|
||||
}
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = '0' + minutes;
|
||||
}
|
||||
|
||||
return hours + ':' + minutes;
|
||||
};
|
||||
|
||||
const valueString = () => {
|
||||
switch (props.type) {
|
||||
case 'count':
|
||||
return countString();
|
||||
case 'stopwatch':
|
||||
return stopwatchString();
|
||||
default:
|
||||
return props.value;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='highlight'>
|
||||
<span className='highlight-value'>{valueString()}</span>
|
||||
<span className='highlight-description'>{props.description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Highlight;
|
24
frontend/src/components/StreamActivity.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.stream-activity {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stream-activity-header {
|
||||
text-align: left;
|
||||
background-color: rgba(6,23,38,1);
|
||||
border-bottom: 1px solid #495a6a;
|
||||
height: 19px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.stream-activity-title {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-activity-list {
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 84px - 40px - 179px);
|
||||
}
|
20
frontend/src/components/StreamActivity.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import StreamEvent from './StreamEvent';
|
||||
|
||||
import './StreamActivity.css';
|
||||
|
||||
function StreamActivity(props) {
|
||||
return (
|
||||
<div className='stream-activity'>
|
||||
<div className='stream-activity-header'>
|
||||
<span className='stream-activity-title'>{props.title}</span>
|
||||
</div>
|
||||
<div className='stream-activity-list'>
|
||||
{props.events.map((event, index) => (
|
||||
<StreamEvent event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StreamActivity;
|
19
frontend/src/components/StreamChat.css
Normal file
|
@ -0,0 +1,19 @@
|
|||
.stream-chat {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stream-chat-header {
|
||||
text-align: left;
|
||||
background-color: rgba(6,23,38,1);
|
||||
border-bottom: 1px solid #495a6a;
|
||||
height: 19px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.stream-chat-title {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
13
frontend/src/components/StreamChat.jsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import './StreamChat.css';
|
||||
|
||||
function StreamChat(props) {
|
||||
return (
|
||||
<div className='stream-chat'>
|
||||
<div className='stream-chat-header'>
|
||||
<span className='stream-chat-title'>{props.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StreamChat;
|
42
frontend/src/components/StreamEvent.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
.stream-event {
|
||||
border-bottom: 1px solid #82b1ff;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: sans-serif;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.stream-event-left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.stream-event-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.stream-event-left-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stream-event-username {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-event-description {
|
||||
color: #88a0b8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stream-event-date {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-family: monospace;
|
||||
}
|
108
frontend/src/components/StreamEvent.jsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { Heart, Star } from '../assets/icons';
|
||||
|
||||
import './StreamEvent.css';
|
||||
|
||||
function StreamEvent(props) {
|
||||
const dateDate = (date) => {
|
||||
const options = { month: 'short' };
|
||||
let month = new Intl.DateTimeFormat('en-US', options).format(date);
|
||||
let day = date.getDay();
|
||||
return month + ' ' + day;
|
||||
};
|
||||
|
||||
const dateDay = (date) => {
|
||||
let now = new Date();
|
||||
let today = now.getDay();
|
||||
switch (date.getDay()) {
|
||||
case 0:
|
||||
return 'Sunday';
|
||||
case 1:
|
||||
return 'Monday';
|
||||
case 2:
|
||||
return 'Tuesday';
|
||||
case 3:
|
||||
return 'Wednesday';
|
||||
case 4:
|
||||
return 'Thursday';
|
||||
case 5:
|
||||
return 'Friday';
|
||||
case 6:
|
||||
return 'Saturday';
|
||||
}
|
||||
};
|
||||
|
||||
const dateTime = (date) => {
|
||||
let now = new Date();
|
||||
let today = now.getDay();
|
||||
let day = date.getDay();
|
||||
|
||||
if (today !== day) {
|
||||
return dateDay(date);
|
||||
}
|
||||
|
||||
let hours24 = date.getHours();
|
||||
let hours = hours24 % 12 || 12;
|
||||
|
||||
let minutes = date.getMinutes();
|
||||
|
||||
let mer = 'pm';
|
||||
if (hours24 < 12) {
|
||||
mer = 'am';
|
||||
}
|
||||
|
||||
return hours + ':' + minutes + ' ' + mer;
|
||||
};
|
||||
|
||||
const dateString = (d) => {
|
||||
if (isNaN(Date.parse(d))) {
|
||||
return 'Who knows?';
|
||||
}
|
||||
|
||||
let now = new Date();
|
||||
let date = new Date(d);
|
||||
// Fix Rumble's timezone problem
|
||||
date.setHours(date.getHours() - 4);
|
||||
let diff = now - date;
|
||||
switch (true) {
|
||||
case diff < 0:
|
||||
return 'In the future!?';
|
||||
case diff < 60000:
|
||||
return 'Now';
|
||||
case diff < 3600000:
|
||||
let minutes = Math.floor(diff / 1000 / 60);
|
||||
let postfix = ' minutes ago';
|
||||
if (minutes == 1) {
|
||||
postfix = ' minute ago';
|
||||
}
|
||||
return minutes + postfix;
|
||||
case diff < 86400000:
|
||||
return dateTime(date);
|
||||
case diff < 604800000:
|
||||
return dateDay(date);
|
||||
default:
|
||||
return dateDate(date);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='stream-event'>
|
||||
<div className='stream-event-left'>
|
||||
{props.event.followed_on && <img className='stream-event-icon' src={Heart}></img>}
|
||||
{props.event.subscribed_on && <img className='stream-event-icon' src={Star}></img>}
|
||||
<div className='stream-event-left-text'>
|
||||
<span className='stream-event-username'>{props.event.username}</span>
|
||||
<span className='stream-event-description'>
|
||||
{props.event.followed_on && 'Followed you'}
|
||||
{props.event.subscribed_on && 'Subscribed'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className='stream-event-date'>
|
||||
{props.event.followed_on && dateString(props.event.followed_on)}
|
||||
{props.event.subscribed_on && dateString(props.event.subscribed_on)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StreamEvent;
|
120
frontend/src/components/StreamInfo.css
Normal file
|
@ -0,0 +1,120 @@
|
|||
.stream-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* padding: 20px 0px; */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stream-info-title {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-info-categories {
|
||||
padding: 5px 0px;
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.stream-info-category {
|
||||
background-color: rgba(6,23,38,1);
|
||||
border: 1px solid white;
|
||||
border-radius: 30px;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
margin-right: 5px;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
|
||||
.stream-info-channel {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.stream-info-controls {
|
||||
align-items: center;
|
||||
border: 1px solid white;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 5px 0px 20px 0px;
|
||||
}
|
||||
|
||||
.stream-info-control {
|
||||
height: 32px;
|
||||
padding: 5px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.stream-info-control-button {
|
||||
background-color: #000312;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.stream-info-control-button:hover {
|
||||
background-color: rgba(6,23,38,1);
|
||||
}
|
||||
|
||||
.stream-info-footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stream-info-likes {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
|
||||
.stream-info-likes-count {
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.stream-info-likes-left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: rgba(6,23,38,1);
|
||||
border: 1px solid white;
|
||||
border-radius: 10rem 0rem 0rem 10rem;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.stream-info-likes-right {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: rgba(6,23,38,1);
|
||||
border: 1px solid white;
|
||||
border-radius: 0rem 10rem 10rem 0rem;
|
||||
}
|
||||
|
||||
.stream-info-likes-icon {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 16px;
|
||||
padding: 5px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.stream-info-live {
|
||||
padding: 10px 20px 5px 20px;
|
||||
}
|
||||
|
||||
.stream-info-subtitle {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
77
frontend/src/components/StreamInfo.jsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { Gear, House, Pause, Play, ThumbsDown, ThumbsUp } from '../assets/icons';
|
||||
import './StreamInfo.css';
|
||||
|
||||
function StreamInfo(props) {
|
||||
const likesString = (likes) => {
|
||||
switch (true) {
|
||||
case likes <= 0:
|
||||
return '0';
|
||||
case likes < 1000:
|
||||
return likes;
|
||||
case likes < 1000000:
|
||||
return (likes / 1000).toFixed(3).slice(0, -2) + 'K';
|
||||
case likes < 1000000000:
|
||||
return (likes / 1000000).toFixed(6).slice(0, -5) + 'M';
|
||||
default:
|
||||
return 'Inf';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='stream-info'>
|
||||
<div className='stream-info-live'>
|
||||
<div className='stream-info-title'>
|
||||
<span>{props.live ? props.title : '-'}</span>
|
||||
</div>
|
||||
<div className='stream-info-subtitle'>
|
||||
<div className='stream-info-categories'>
|
||||
<span className='stream-info-category'>
|
||||
{props.live ? props.categories.primary.title : 'none'}
|
||||
</span>
|
||||
<span className='stream-info-category'>
|
||||
{props.live ? props.categories.secondary.title : 'none'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='stream-info-likes'>
|
||||
<div className='stream-info-likes-left'>
|
||||
<img className='stream-info-likes-icon' src={ThumbsUp} />
|
||||
<span className='stream-info-likes-count'>
|
||||
{props.live ? likesString(props.likes) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='stream-info-likes-right'>
|
||||
<img className='stream-info-likes-icon' src={ThumbsDown} />
|
||||
<span className='stream-info-likes-count'>
|
||||
{props.live ? likesString(props.dislikes) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='stream-info-channel'>
|
||||
<span>Channel: {props.channel}</span>
|
||||
</div>
|
||||
<div className='stream-info-footer'>
|
||||
<div></div>
|
||||
<div className='stream-info-controls'>
|
||||
<button className='stream-info-control-button'>
|
||||
<img className='stream-info-control' src={House} />
|
||||
</button>
|
||||
<button className='stream-info-control-button'>
|
||||
<img
|
||||
onClick={props.active ? props.pause : props.play}
|
||||
className='stream-info-control'
|
||||
src={props.active ? Pause : Play}
|
||||
/>
|
||||
</button>
|
||||
<button className='stream-info-control-button'>
|
||||
<img className='stream-info-control' src={Gear} />
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StreamInfo;
|
|
@ -1,14 +1,14 @@
|
|||
import React from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './style.css'
|
||||
import App from './App'
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './style.css';
|
||||
import App from './App';
|
||||
|
||||
const container = document.getElementById('root')
|
||||
const container = document.getElementById('root');
|
||||
|
||||
const root = createRoot(container)
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
||||
// <React.StrictMode>
|
||||
<App />
|
||||
// </React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -1,82 +1,56 @@
|
|||
#Dashboard {
|
||||
align-items: center;
|
||||
background-color: #f3f5f8;
|
||||
background-color: #000312;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.followers {
|
||||
background-color: #061726;
|
||||
height: 100%;
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 62px;
|
||||
justify-content: center;
|
||||
padding: 10px 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.followers-list {
|
||||
overflow-y: auto;
|
||||
.header-left {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.followers-list-follower {
|
||||
border-bottom: 1px solid #82b1ff;
|
||||
color: white;
|
||||
.header-right {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.main {
|
||||
border-bottom: 1px solid #495a6a;
|
||||
border-top: 1px solid #495a6a;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: sans-serif;
|
||||
height: calc(100vh - 83px - 179px);
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.followers-list-follower-username {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.followers-list-follower-date {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.followers-header {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #82b1ff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.followers-header-title {
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.followers-header-highlights {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.followers-header-highlight {
|
||||
.main-left {
|
||||
border-right: 1px solid #495a6a;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-right {
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.highlights {
|
||||
align-items: center;
|
||||
color: white;
|
||||
display: flex;
|
||||
background-color: #75a54b;
|
||||
border-radius: 0.5rem;
|
||||
flex-direction: column;
|
||||
font-family: sans-serif;
|
||||
font-weight: bold;
|
||||
min-width: 50px;
|
||||
padding: 10px;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
height: 50px;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.followers-header-highlight-count {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.followers-header-highlight-description {
|
||||
font-size: 12px;
|
||||
}
|
|
@ -1,162 +1,132 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { QueryAPI } from '../../wailsjs/go/main/App';
|
||||
import { Start, Stop } from '../../wailsjs/go/api/Api';
|
||||
|
||||
import './Dashboard.css';
|
||||
import { EventsEmit, EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { Heart, Star } from '../assets/icons';
|
||||
import Highlight from '../components/Highlight';
|
||||
import StreamEvent from '../components/StreamEvent';
|
||||
import StreamActivity from '../components/StreamActivity';
|
||||
import StreamChat from '../components/StreamChat';
|
||||
import StreamInfo from '../components/StreamInfo';
|
||||
|
||||
function Dashboard() {
|
||||
const location = useLocation();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [active, setActive] = useState(false);
|
||||
const [streamKey, setStreamKey] = useState(location.state.streamKey);
|
||||
const [channelName, setChannelName] = useState('');
|
||||
const [followers, setFollowers] = useState({});
|
||||
const [totalFollowers, setTotalFollowers] = useState('-');
|
||||
const [channelFollowers, setChannelFollowers] = useState('-');
|
||||
const [latestFollower, setLatestFollower] = useState('-');
|
||||
const [totalFollowers, setTotalFollowers] = useState(0);
|
||||
const [channelFollowers, setChannelFollowers] = useState(0);
|
||||
const [recentFollowers, setRecentFollowers] = useState([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// QueryAPI(streamKey)
|
||||
// .then((response) => {
|
||||
// console.log(response);
|
||||
// setFollowers(response);
|
||||
// setChannelFollowers(response.num_followers);
|
||||
// setTotalFollowers(response.num_followers_total);
|
||||
// setLatestFollower(response.latest_follower.username);
|
||||
// setRecentFollowers(response.recent_followers);
|
||||
// })
|
||||
// .catch((e) => console.log('Error:', e));
|
||||
// }, []);
|
||||
const [subscribers, setSubscribers] = useState({});
|
||||
const [subscriberCount, setSubscriberCount] = useState(0);
|
||||
const [recentSubscribers, setRecentSubscribers] = useState([]);
|
||||
const [streamCategories, setStreamCategories] = useState({
|
||||
primary: { title: '' },
|
||||
secondary: { title: '' },
|
||||
});
|
||||
const [streamLikes, setStreamLikes] = useState(0);
|
||||
const [streamLive, setStreamLive] = useState(false);
|
||||
const [streamDislikes, setStreamDislikes] = useState(0);
|
||||
const [streamTitle, setStreamTitle] = useState('');
|
||||
const [watchingNow, setWatchingNow] = useState(0);
|
||||
const [createdOn, setCreatedOn] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let interval = setInterval(() => {
|
||||
console.log('Query API');
|
||||
QueryAPI(streamKey)
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
setFollowers(response);
|
||||
setChannelFollowers(response.num_followers);
|
||||
setTotalFollowers(response.num_followers_total);
|
||||
setLatestFollower(response.latest_follower.username);
|
||||
setRecentFollowers(response.recent_followers);
|
||||
})
|
||||
.catch((e) => console.log('Error:', e));
|
||||
}, 10000);
|
||||
console.log('use effect start');
|
||||
Start(streamKey);
|
||||
setActive(true);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
EventsOn('QueryResponse', (response) => {
|
||||
console.log('query response received');
|
||||
setRefresh(!refresh);
|
||||
setActive(true);
|
||||
setChannelName(response.channel_name);
|
||||
setFollowers(response.followers);
|
||||
setChannelFollowers(response.followers.num_followers);
|
||||
setTotalFollowers(response.followers.num_followers_total);
|
||||
setRecentFollowers(response.followers.recent_followers);
|
||||
setSubscribers(response.subscribers);
|
||||
setSubscriberCount(response.subscribers.num_subscribers);
|
||||
setRecentSubscribers(response.subscribers.recent_subscribers);
|
||||
if (response.livestreams.length > 0) {
|
||||
setStreamLive(true);
|
||||
setStreamCategories(response.livestreams[0].categories);
|
||||
setStreamLikes(response.livestreams[0].likes);
|
||||
setStreamDislikes(response.livestreams[0].dislikes);
|
||||
setStreamTitle(response.livestreams[0].title);
|
||||
setCreatedOn(response.livestreams[0].created_on);
|
||||
setWatchingNow(response.livestreams[0].watching_now);
|
||||
} else {
|
||||
setStreamLive(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const dateDate = (date) => {
|
||||
const options = { month: 'short' };
|
||||
let month = new Intl.DateTimeFormat('en-US', options).format(date);
|
||||
let day = date.getDay();
|
||||
return month + ' ' + day;
|
||||
const startQuery = () => {
|
||||
console.log('start');
|
||||
Start(streamKey);
|
||||
setActive(true);
|
||||
};
|
||||
|
||||
const dateDay = (date) => {
|
||||
let now = new Date();
|
||||
let today = now.getDay();
|
||||
switch (date.getDay()) {
|
||||
case 0:
|
||||
return 'Sunday';
|
||||
case 1:
|
||||
return 'Monday';
|
||||
case 2:
|
||||
return 'Tuesday';
|
||||
case 3:
|
||||
return 'Wednesday';
|
||||
case 4:
|
||||
return 'Thursday';
|
||||
case 5:
|
||||
return 'Friday';
|
||||
case 6:
|
||||
return 'Saturday';
|
||||
const stopQuery = () => {
|
||||
console.log('stop');
|
||||
Stop();
|
||||
// EventsEmit('StopQuery');
|
||||
setActive(false);
|
||||
};
|
||||
|
||||
const activityDate = (activity) => {
|
||||
if (activity.followed_on) {
|
||||
return activity.followed_on;
|
||||
}
|
||||
if (activity.subscribed_on) {
|
||||
return activity.subscribed_on;
|
||||
}
|
||||
};
|
||||
|
||||
const dateTime = (date) => {
|
||||
let now = new Date();
|
||||
let today = now.getDay();
|
||||
let day = date.getDay();
|
||||
|
||||
if (today !== day) {
|
||||
return dateDay(date);
|
||||
}
|
||||
|
||||
let hours24 = date.getHours();
|
||||
let hours = hours24 % 12 || 12;
|
||||
|
||||
let minutes = date.getMinutes();
|
||||
|
||||
let mer = 'pm';
|
||||
if (hours24 < 12) {
|
||||
mer = 'am';
|
||||
}
|
||||
|
||||
return hours + ':' + minutes + ' ' + mer;
|
||||
};
|
||||
|
||||
const dateString = (d) => {
|
||||
let now = new Date();
|
||||
let date = new Date(d);
|
||||
// Fix Rumble's timezone problem
|
||||
date.setHours(date.getHours() - 4);
|
||||
let diff = now - date;
|
||||
switch (true) {
|
||||
case diff < 60000:
|
||||
return 'Now';
|
||||
case diff < 3600000:
|
||||
let minutes = Math.floor(diff / 1000 / 60);
|
||||
let postfix = ' minutes ago';
|
||||
if (minutes == 1) {
|
||||
postfix = ' minute ago';
|
||||
}
|
||||
return minutes + postfix;
|
||||
case diff < 86400000:
|
||||
return dateTime(date);
|
||||
case diff < 604800000:
|
||||
return dateDay(date);
|
||||
default:
|
||||
return dateDate(date);
|
||||
}
|
||||
console.log('Diff:', diff);
|
||||
return d;
|
||||
const activityEvents = () => {
|
||||
let sorted = [...recentFollowers, ...recentSubscribers].sort((a, b) =>
|
||||
activityDate(a) < activityDate(b) ? 1 : -1
|
||||
);
|
||||
return sorted;
|
||||
};
|
||||
|
||||
return (
|
||||
<div id='Dashboard'>
|
||||
<span>Dashboard:</span>
|
||||
<div className='followers'>
|
||||
<div className='followers-header'>
|
||||
<span className='followers-header-title'>Followers</span>
|
||||
<div className='followers-header-highlights'>
|
||||
<div className='followers-header-highlight'>
|
||||
<span className='followers-header-highlight-count'>
|
||||
{channelFollowers}
|
||||
</span>
|
||||
<span className='followers-header-highlight-description'>Channel</span>
|
||||
</div>
|
||||
<div className='followers-header-highlight'>
|
||||
<span className='followers-header-highlight-count'>
|
||||
{totalFollowers}
|
||||
</span>
|
||||
<span className='followers-header-highlight-description'>Total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='followers-list'>
|
||||
{recentFollowers.map((follower, index) => (
|
||||
<div className='followers-list-follower'>
|
||||
<span className='followers-list-follower-username'>
|
||||
{follower.username}
|
||||
</span>
|
||||
<span className='followers-list-follower-date'>
|
||||
{dateString(follower.followed_on)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className='header'>
|
||||
<div className='header-left'></div>
|
||||
<div className='highlights'>
|
||||
{/* <Highlight description={'Session'} type={'stopwatch'} value={createdOn} /> */}
|
||||
<Highlight description={'Viewers'} type={'count'} value={watchingNow} />
|
||||
<Highlight description={'Followers'} type={'count'} value={channelFollowers} />
|
||||
<Highlight description={'Subscribers'} type={'count'} value={subscriberCount} />
|
||||
</div>
|
||||
<div className='header-right'></div>
|
||||
</div>
|
||||
<div className='main'>
|
||||
<div className='main-left'>
|
||||
<StreamActivity title={'Stream Activity'} events={activityEvents()} />
|
||||
</div>
|
||||
<div className='main-right'>
|
||||
<StreamChat title={'Stream Chat'} />
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<StreamInfo
|
||||
active={active}
|
||||
channel={channelName}
|
||||
title={streamTitle}
|
||||
categories={streamCategories}
|
||||
likes={streamLikes}
|
||||
live={streamLive}
|
||||
dislikes={streamDislikes}
|
||||
play={startQuery}
|
||||
pause={stopQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,5 +6,5 @@ body {
|
|||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
/* height: 100vh; */
|
||||
}
|
||||
|
|
4
go.mod
|
@ -4,7 +4,7 @@ go 1.19
|
|||
|
||||
require (
|
||||
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
|
||||
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231213162428-b33f413975bb
|
||||
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910
|
||||
github.com/wailsapp/wails/v2 v2.7.1
|
||||
)
|
||||
|
||||
|
@ -36,7 +36,7 @@ require (
|
|||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
)
|
||||
|
||||
|
|
8
go.sum
|
@ -55,8 +55,8 @@ github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQ
|
|||
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
|
||||
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk=
|
||||
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231213162428-b33f413975bb h1:nqq0eTr8ocCaENdoryAt9T3g5xJgwcXMW7pYAztb1lc=
|
||||
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231213162428-b33f413975bb/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
|
||||
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910 h1:pu5jBae9XZDF/G8YkCN7D5TMxOCsCO5+Jn1/lNPsOUY=
|
||||
github.com/tylertravisty/rumble-livestream-lib-go v0.0.0-20231218182551-5ac1d6c01910/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
|
@ -89,8 +89,8 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
|||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
|
|
111
internal/api/api.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
type Api struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
cancelMu sync.Mutex
|
||||
querying bool
|
||||
queryingMu sync.Mutex
|
||||
queryInterval time.Duration
|
||||
queryIntervalMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewApi() *Api {
|
||||
return &Api{queryInterval: 10 * time.Second}
|
||||
}
|
||||
|
||||
func (a *Api) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
runtime.EventsOn(ctx, "StopQuery", func(optionalData ...interface{}) {
|
||||
a.Stop()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Api) Start(url string) error {
|
||||
fmt.Println("Api.Start")
|
||||
if url == "" {
|
||||
return fmt.Errorf("empty stream key")
|
||||
}
|
||||
|
||||
a.queryingMu.Lock()
|
||||
start := !a.querying
|
||||
a.querying = true
|
||||
a.queryingMu.Unlock()
|
||||
|
||||
if start {
|
||||
fmt.Println("Starting querying")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.cancelMu.Lock()
|
||||
a.cancel = cancel
|
||||
a.cancelMu.Unlock()
|
||||
a.start(ctx, url)
|
||||
} else {
|
||||
fmt.Println("Querying already started")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Api) Stop() {
|
||||
fmt.Println("stop querying")
|
||||
a.cancelMu.Lock()
|
||||
if a.cancel != nil {
|
||||
a.cancel()
|
||||
}
|
||||
a.cancelMu.Unlock()
|
||||
}
|
||||
|
||||
func (a *Api) start(ctx context.Context, url string) {
|
||||
for {
|
||||
a.query(url)
|
||||
a.queryIntervalMu.Lock()
|
||||
interval := a.queryInterval
|
||||
a.queryIntervalMu.Unlock()
|
||||
timer := time.NewTimer(interval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
a.queryingMu.Lock()
|
||||
a.querying = false
|
||||
a.queryingMu.Unlock()
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Api) query(url string) {
|
||||
fmt.Println("QueryAPI")
|
||||
client := rumblelivestreamlib.Client{StreamKey: url}
|
||||
resp, err := client.Request()
|
||||
if err != nil {
|
||||
// TODO: log error
|
||||
fmt.Println("client.Request err:", err)
|
||||
// a.Stop()
|
||||
}
|
||||
|
||||
// resp := &rumblelivestreamlib.LivestreamResponse{}
|
||||
|
||||
// resp.Followers.RecentFollowers = append(resp.Followers.RecentFollowers, rumblelivestreamlib.Follower{"tyler-follow", "2023-12-12T21:53:34-04:00"})
|
||||
// resp.Subscribers.RecentSubscribers = append(resp.Subscribers.RecentSubscribers, rumblelivestreamlib.Subscriber{"tyler-sub", "tyler-sub", 500, 5, "2023-12-14T21:53:34-04:00"})
|
||||
// resp.Subscribers.RecentSubscribers = append(resp.Subscribers.RecentSubscribers, rumblelivestreamlib.Subscriber{"tyler-sub", "tyler-sub", 500, 5, "2023-12-13T21:53:34-04:00"})
|
||||
// resp.Subscribers.RecentSubscribers = append(resp.Subscribers.RecentSubscribers, rumblelivestreamlib.Subscriber{"tyler-sub", "tyler-sub", 500, 5, "2023-11-13T21:53:34-04:00"})
|
||||
// resp.Livestreams = []rumblelivestreamlib.Livestream{
|
||||
// {
|
||||
// CreatedOn: "2023-12-16T16:13:30+00:00",
|
||||
// WatchingNow: 4},
|
||||
// }
|
||||
runtime.EventsEmit(a.ctx, "QueryResponse", &resp)
|
||||
}
|
||||
|
||||
// TODO: if start errors, send event
|
9
main.go
|
@ -1,8 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
|
||||
"github.com/tylertravisty/rum-goggles/internal/api"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
|
@ -13,6 +15,7 @@ var assets embed.FS
|
|||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
api := api.NewApi()
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
|
@ -24,9 +27,13 @@ func main() {
|
|||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
OnStartup: app.startup,
|
||||
OnStartup: func(ctx context.Context) {
|
||||
app.startup(ctx)
|
||||
api.Startup(ctx)
|
||||
},
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
api,
|
||||
},
|
||||
})
|
||||
|
||||
|
|