Fixed UI issues; added footer and controls
23
app.go
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/tylertravisty/go-utils/random"
|
"github.com/tylertravisty/go-utils/random"
|
||||||
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// App struct
|
// App struct
|
||||||
|
@ -35,15 +34,15 @@ func (a *App) Greet(name string) string {
|
||||||
return fmt.Sprintf("Hello %s, It's show time!", random)
|
return fmt.Sprintf("Hello %s, It's show time!", random)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) QueryAPI(url string) (*rumblelivestreamlib.Followers, error) {
|
// func (a *App) QueryAPI(url string) (*rumblelivestreamlib.Followers, error) {
|
||||||
fmt.Println("QueryAPI")
|
// fmt.Println("QueryAPI")
|
||||||
client := rumblelivestreamlib.Client{StreamKey: url}
|
// client := rumblelivestreamlib.Client{StreamKey: url}
|
||||||
resp, err := client.Request()
|
// resp, err := client.Request()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
// TODO: log error
|
// // TODO: log error
|
||||||
fmt.Println("client.Request err:", err)
|
// fmt.Println("client.Request err:", err)
|
||||||
return nil, fmt.Errorf("API request failed")
|
// 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 from './eye.png';
|
||||||
import eye_slash from './eye-slash.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 Eye = eye;
|
||||||
export const EyeSlash = eye_slash;
|
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 React from 'react';
|
||||||
import {createRoot} from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import './style.css'
|
import './style.css';
|
||||||
import App from './App'
|
import App from './App';
|
||||||
|
|
||||||
const container = document.getElementById('root')
|
const container = document.getElementById('root');
|
||||||
|
|
||||||
const root = createRoot(container)
|
const root = createRoot(container);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
// <React.StrictMode>
|
||||||
<App/>
|
<App />
|
||||||
</React.StrictMode>
|
// </React.StrictMode>
|
||||||
)
|
);
|
||||||
|
|
|
@ -1,82 +1,56 @@
|
||||||
#Dashboard {
|
#Dashboard {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #f3f5f8;
|
background-color: #000312;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.followers {
|
.header {
|
||||||
background-color: #061726;
|
align-items: center;
|
||||||
height: 100%;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 62px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.followers-list {
|
.header-left {
|
||||||
overflow-y: auto;
|
width: 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.followers-list-follower {
|
.header-right {
|
||||||
border-bottom: 1px solid #82b1ff;
|
width: 20%;
|
||||||
color: white;
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
border-bottom: 1px solid #495a6a;
|
||||||
|
border-top: 1px solid #495a6a;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
font-family: sans-serif;
|
height: calc(100vh - 83px - 179px);
|
||||||
justify-content: space-between;
|
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%;
|
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;
|
align-items: center;
|
||||||
color: white;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: #75a54b;
|
flex-direction: row;
|
||||||
border-radius: 0.5rem;
|
justify-content: space-evenly;
|
||||||
flex-direction: column;
|
height: 50px;
|
||||||
font-family: sans-serif;
|
width: 60%;
|
||||||
font-weight: bold;
|
|
||||||
min-width: 50px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.followers-header-highlight-count {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.followers-header-highlight-description {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
|
@ -1,162 +1,132 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
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 './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() {
|
function Dashboard() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
const [streamKey, setStreamKey] = useState(location.state.streamKey);
|
const [streamKey, setStreamKey] = useState(location.state.streamKey);
|
||||||
|
const [channelName, setChannelName] = useState('');
|
||||||
const [followers, setFollowers] = useState({});
|
const [followers, setFollowers] = useState({});
|
||||||
const [totalFollowers, setTotalFollowers] = useState('-');
|
const [totalFollowers, setTotalFollowers] = useState(0);
|
||||||
const [channelFollowers, setChannelFollowers] = useState('-');
|
const [channelFollowers, setChannelFollowers] = useState(0);
|
||||||
const [latestFollower, setLatestFollower] = useState('-');
|
|
||||||
const [recentFollowers, setRecentFollowers] = useState([]);
|
const [recentFollowers, setRecentFollowers] = useState([]);
|
||||||
|
const [subscribers, setSubscribers] = useState({});
|
||||||
// useEffect(() => {
|
const [subscriberCount, setSubscriberCount] = useState(0);
|
||||||
// QueryAPI(streamKey)
|
const [recentSubscribers, setRecentSubscribers] = useState([]);
|
||||||
// .then((response) => {
|
const [streamCategories, setStreamCategories] = useState({
|
||||||
// console.log(response);
|
primary: { title: '' },
|
||||||
// setFollowers(response);
|
secondary: { title: '' },
|
||||||
// setChannelFollowers(response.num_followers);
|
});
|
||||||
// setTotalFollowers(response.num_followers_total);
|
const [streamLikes, setStreamLikes] = useState(0);
|
||||||
// setLatestFollower(response.latest_follower.username);
|
const [streamLive, setStreamLive] = useState(false);
|
||||||
// setRecentFollowers(response.recent_followers);
|
const [streamDislikes, setStreamDislikes] = useState(0);
|
||||||
// })
|
const [streamTitle, setStreamTitle] = useState('');
|
||||||
// .catch((e) => console.log('Error:', e));
|
const [watchingNow, setWatchingNow] = useState(0);
|
||||||
// }, []);
|
const [createdOn, setCreatedOn] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval = setInterval(() => {
|
console.log('use effect start');
|
||||||
console.log('Query API');
|
Start(streamKey);
|
||||||
QueryAPI(streamKey)
|
setActive(true);
|
||||||
.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);
|
|
||||||
|
|
||||||
return () => {
|
EventsOn('QueryResponse', (response) => {
|
||||||
clearInterval(interval);
|
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 startQuery = () => {
|
||||||
const options = { month: 'short' };
|
console.log('start');
|
||||||
let month = new Intl.DateTimeFormat('en-US', options).format(date);
|
Start(streamKey);
|
||||||
let day = date.getDay();
|
setActive(true);
|
||||||
return month + ' ' + day;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateDay = (date) => {
|
const stopQuery = () => {
|
||||||
let now = new Date();
|
console.log('stop');
|
||||||
let today = now.getDay();
|
Stop();
|
||||||
switch (date.getDay()) {
|
// EventsEmit('StopQuery');
|
||||||
case 0:
|
setActive(false);
|
||||||
return 'Sunday';
|
};
|
||||||
case 1:
|
|
||||||
return 'Monday';
|
const activityDate = (activity) => {
|
||||||
case 2:
|
if (activity.followed_on) {
|
||||||
return 'Tuesday';
|
return activity.followed_on;
|
||||||
case 3:
|
}
|
||||||
return 'Wednesday';
|
if (activity.subscribed_on) {
|
||||||
case 4:
|
return activity.subscribed_on;
|
||||||
return 'Thursday';
|
|
||||||
case 5:
|
|
||||||
return 'Friday';
|
|
||||||
case 6:
|
|
||||||
return 'Saturday';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateTime = (date) => {
|
const activityEvents = () => {
|
||||||
let now = new Date();
|
let sorted = [...recentFollowers, ...recentSubscribers].sort((a, b) =>
|
||||||
let today = now.getDay();
|
activityDate(a) < activityDate(b) ? 1 : -1
|
||||||
let day = date.getDay();
|
);
|
||||||
|
return sorted;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='Dashboard'>
|
<div id='Dashboard'>
|
||||||
<span>Dashboard:</span>
|
<div className='header'>
|
||||||
<div className='followers'>
|
<div className='header-left'></div>
|
||||||
<div className='followers-header'>
|
<div className='highlights'>
|
||||||
<span className='followers-header-title'>Followers</span>
|
{/* <Highlight description={'Session'} type={'stopwatch'} value={createdOn} /> */}
|
||||||
<div className='followers-header-highlights'>
|
<Highlight description={'Viewers'} type={'count'} value={watchingNow} />
|
||||||
<div className='followers-header-highlight'>
|
<Highlight description={'Followers'} type={'count'} value={channelFollowers} />
|
||||||
<span className='followers-header-highlight-count'>
|
<Highlight description={'Subscribers'} type={'count'} value={subscriberCount} />
|
||||||
{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>
|
</div>
|
||||||
|
<div className='header-right'></div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,5 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
height: 100vh;
|
/* height: 100vh; */
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
|
@ -4,7 +4,7 @@ go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909
|
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
|
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/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.13.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
|
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/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 h1:xrjIFqzGQXlCrCdMPpW6+SodGFSlrQ3ZNUCr3f5tF1g=
|
||||||
github.com/tylertravisty/go-utils v0.0.0-20230524204414-6893ae548909/go.mod h1:2W31Jhs9YSy7y500wsCOW0bcamGi9foQV1CKrfvfTxk=
|
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-20231218182551-5ac1d6c01910 h1:pu5jBae9XZDF/G8YkCN7D5TMxOCsCO5+Jn1/lNPsOUY=
|
||||||
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/go.mod h1:YrfW5N6xVozOzubzfNNsy+v0MIL2GPi9Kx3mTZ/Q9zI=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
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/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/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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
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=
|
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
|
"github.com/tylertravisty/rum-goggles/internal/api"
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
@ -13,6 +15,7 @@ var assets embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create an instance of the app structure
|
// Create an instance of the app structure
|
||||||
|
api := api.NewApi()
|
||||||
app := NewApp()
|
app := NewApp()
|
||||||
|
|
||||||
// Create application with options
|
// Create application with options
|
||||||
|
@ -24,9 +27,13 @@ func main() {
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
},
|
},
|
||||||
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
|
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{}{
|
Bind: []interface{}{
|
||||||
app,
|
app,
|
||||||
|
api,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|