diff --git a/app.go b/app.go index d5d9339..cba1dda 100644 --- a/app.go +++ b/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 +// } diff --git a/frontend/src/assets/icons/gear.png b/frontend/src/assets/icons/gear.png new file mode 100644 index 0000000..c192495 Binary files /dev/null and b/frontend/src/assets/icons/gear.png differ diff --git a/frontend/src/assets/icons/hand-thumbs-down.png b/frontend/src/assets/icons/hand-thumbs-down.png new file mode 100644 index 0000000..27eace7 Binary files /dev/null and b/frontend/src/assets/icons/hand-thumbs-down.png differ diff --git a/frontend/src/assets/icons/hand-thumbs-up.png b/frontend/src/assets/icons/hand-thumbs-up.png new file mode 100644 index 0000000..75eddf3 Binary files /dev/null and b/frontend/src/assets/icons/hand-thumbs-up.png differ diff --git a/frontend/src/assets/icons/heart-fill.png b/frontend/src/assets/icons/heart-fill.png new file mode 100644 index 0000000..293d511 Binary files /dev/null and b/frontend/src/assets/icons/heart-fill.png differ diff --git a/frontend/src/assets/icons/house.png b/frontend/src/assets/icons/house.png new file mode 100644 index 0000000..982d5ea Binary files /dev/null and b/frontend/src/assets/icons/house.png differ diff --git a/frontend/src/assets/icons/index.jsx b/frontend/src/assets/icons/index.jsx index c4c8028..e506d73 100644 --- a/frontend/src/assets/icons/index.jsx +++ b/frontend/src/assets/icons/index.jsx @@ -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; diff --git a/frontend/src/assets/icons/pause-circle.png b/frontend/src/assets/icons/pause-circle.png new file mode 100644 index 0000000..686a73d Binary files /dev/null and b/frontend/src/assets/icons/pause-circle.png differ diff --git a/frontend/src/assets/icons/pause-fill.png b/frontend/src/assets/icons/pause-fill.png new file mode 100644 index 0000000..a54d600 Binary files /dev/null and b/frontend/src/assets/icons/pause-fill.png differ diff --git a/frontend/src/assets/icons/play-circle.png b/frontend/src/assets/icons/play-circle.png new file mode 100644 index 0000000..def567b Binary files /dev/null and b/frontend/src/assets/icons/play-circle.png differ diff --git a/frontend/src/assets/icons/play-fill.png b/frontend/src/assets/icons/play-fill.png new file mode 100644 index 0000000..2cbb56c Binary files /dev/null and b/frontend/src/assets/icons/play-fill.png differ diff --git a/frontend/src/assets/icons/star-fill.png b/frontend/src/assets/icons/star-fill.png new file mode 100644 index 0000000..3478d59 Binary files /dev/null and b/frontend/src/assets/icons/star-fill.png differ diff --git a/frontend/src/components/Highlight.css b/frontend/src/components/Highlight.css new file mode 100644 index 0000000..8a67f50 --- /dev/null +++ b/frontend/src/components/Highlight.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/Highlight.jsx b/frontend/src/components/Highlight.jsx new file mode 100644 index 0000000..5d355ca --- /dev/null +++ b/frontend/src/components/Highlight.jsx @@ -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 ( +
+ {valueString()} + {props.description} +
+ ); +} + +export default Highlight; diff --git a/frontend/src/components/StreamActivity.css b/frontend/src/components/StreamActivity.css new file mode 100644 index 0000000..7010da7 --- /dev/null +++ b/frontend/src/components/StreamActivity.css @@ -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); +} \ No newline at end of file diff --git a/frontend/src/components/StreamActivity.jsx b/frontend/src/components/StreamActivity.jsx new file mode 100644 index 0000000..30f48f0 --- /dev/null +++ b/frontend/src/components/StreamActivity.jsx @@ -0,0 +1,20 @@ +import StreamEvent from './StreamEvent'; + +import './StreamActivity.css'; + +function StreamActivity(props) { + return ( +
+
+ {props.title} +
+
+ {props.events.map((event, index) => ( + + ))} +
+
+ ); +} + +export default StreamActivity; diff --git a/frontend/src/components/StreamChat.css b/frontend/src/components/StreamChat.css new file mode 100644 index 0000000..b70cb73 --- /dev/null +++ b/frontend/src/components/StreamChat.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/StreamChat.jsx b/frontend/src/components/StreamChat.jsx new file mode 100644 index 0000000..bb9e3c1 --- /dev/null +++ b/frontend/src/components/StreamChat.jsx @@ -0,0 +1,13 @@ +import './StreamChat.css'; + +function StreamChat(props) { + return ( +
+
+ {props.title} +
+
+ ); +} + +export default StreamChat; diff --git a/frontend/src/components/StreamEvent.css b/frontend/src/components/StreamEvent.css new file mode 100644 index 0000000..0347ea4 --- /dev/null +++ b/frontend/src/components/StreamEvent.css @@ -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; +} diff --git a/frontend/src/components/StreamEvent.jsx b/frontend/src/components/StreamEvent.jsx new file mode 100644 index 0000000..436f982 --- /dev/null +++ b/frontend/src/components/StreamEvent.jsx @@ -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 ( +
+
+ {props.event.followed_on && } + {props.event.subscribed_on && } +
+ {props.event.username} + + {props.event.followed_on && 'Followed you'} + {props.event.subscribed_on && 'Subscribed'} + +
+
+ + {props.event.followed_on && dateString(props.event.followed_on)} + {props.event.subscribed_on && dateString(props.event.subscribed_on)} + +
+ ); +} + +export default StreamEvent; diff --git a/frontend/src/components/StreamInfo.css b/frontend/src/components/StreamInfo.css new file mode 100644 index 0000000..3cceb22 --- /dev/null +++ b/frontend/src/components/StreamInfo.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/StreamInfo.jsx b/frontend/src/components/StreamInfo.jsx new file mode 100644 index 0000000..ba0b68b --- /dev/null +++ b/frontend/src/components/StreamInfo.jsx @@ -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 ( +
+
+
+ {props.live ? props.title : '-'} +
+
+
+ + {props.live ? props.categories.primary.title : 'none'} + + + {props.live ? props.categories.secondary.title : 'none'} + +
+
+
+ + + {props.live ? likesString(props.likes) : '-'} + +
+
+ + + {props.live ? likesString(props.dislikes) : '-'} + +
+
+
+
+
+ Channel: {props.channel} +
+
+
+
+ + + +
+
+
+
+ ); +} + +export default StreamInfo; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index e50e105..5a6262c 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -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( - - - -) + // + + // +); diff --git a/frontend/src/screens/Dashboard.css b/frontend/src/screens/Dashboard.css index fba8777..0f135af 100644 --- a/frontend/src/screens/Dashboard.css +++ b/frontend/src/screens/Dashboard.css @@ -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; -} \ No newline at end of file diff --git a/frontend/src/screens/Dashboard.jsx b/frontend/src/screens/Dashboard.jsx index 599e0e6..04180ef 100644 --- a/frontend/src/screens/Dashboard.jsx +++ b/frontend/src/screens/Dashboard.jsx @@ -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 (
- Dashboard: -
-
- Followers -
-
- - {channelFollowers} - - Channel -
-
- - {totalFollowers} - - Total -
-
-
-
- {recentFollowers.map((follower, index) => ( -
- - {follower.username} - - - {dateString(follower.followed_on)} - -
- ))} +
+
+
+ {/* */} + + +
+
+
+
+ +
+
+ +
+
+
+
); } diff --git a/frontend/src/style.css b/frontend/src/style.css index 38bdf56..16aff13 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -6,5 +6,5 @@ body { } #app { - height: 100vh; + /* height: 100vh; */ } diff --git a/go.mod b/go.mod index f4e2f55..5a1e8b7 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 8457545..adf327d 100644 --- a/go.sum +++ b/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= diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..c6966fb --- /dev/null +++ b/internal/api/api.go @@ -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 diff --git a/main.go b/main.go index e9600bb..a848514 100644 --- a/main.go +++ b/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, }, })