This commit is contained in:
0815Cracky
2024-02-27 11:46:37 +01:00
parent 6053e255ad
commit ff22f47b90
60 changed files with 7183 additions and 0 deletions

BIN
assets/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

209
assets/charts.html Normal file
View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Twitch-Channel-Points-Miner-v2</title>
<link rel="icon" type="image/png" sizes="32x32"
href="https://static.twitchcdn.net/assets/favicon-32-e29e246c157142c94346.png">
<link rel="icon" type="image/png" sizes="16x16"
href="https://static.twitchcdn.net/assets/favicon-16-52e571ffea063af7a7f4.png">
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.42.0"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"
integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link href="{{url_for('static', filename='dark-theme.css')}}" rel="stylesheet" />
<link href="{{url_for('static', filename='style.css')}}" rel="stylesheet" />
</head>
<body style="height: 100vh;">
<div class="container has-text-centered" style="height: 100vh;">
<div class="columns" id="header">
<div class="column has-text-right">
<a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2" target="_blank"><img
style="margin-top: -20px;" width="600px" src="{{url_for('static', filename='banner.png')}}"
alt="banner"></a>
</div>
<div class="column is-4 has-text-left" style="margin-top: 35px;">
<table>
<tr>
<td><a href="https://github.com/Tkd-Alex" target="_blank">Tkd-Alex</a></td>
<td><a href="https://github.com/rdavydov" target="_blank">rdavydov</a></td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/LICENSE">
<img alt="License"
src="https://img.shields.io/github/license/Tkd-Alex/Twitch-Channel-Points-Miner-v2" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/blob/master/LICENSE">
<img alt="License"
src="https://img.shields.io/github/license/rdavydov/Twitch-Channel-Points-Miner-v2" /></a>
</td>
</tr>
<tr>
<td><a href="https://www.python.org/downloads/release/python-360/">
<img alt="Python3"
src="https://img.shields.io/badge/built%20for-Python≥3.6-red.svg?style=flat"></a>
</td>
<td><a href="https://www.python.org/downloads/release/python-360/">
<img alt="Python3"
src="https://img.shields.io/badge/built%20for-Python≥3.6-red.svg?style=flat"></a>
</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pulls">
<img alt="PRsWelcome"
src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/pulls">
<img alt="PRsWelcome"
src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/stargazers">
<img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/Tkd-Alex/Twitch-Channel-Points-Miner-v2" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/stargazers">
<img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/rdavydov/Twitch-Channel-Points-Miner-v2" /></a>
</td>
</tr>
<tr>
<td><a
href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues?q=is%3Aissue+is%3Aclosed">
<img alt="GitHub closed issues"
src="https://img.shields.io/github/issues-closed/Tkd-Alex/Twitch-Channel-Points-Miner-v2"></a>
</td>
<td><a
href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/issues?q=is%3Aissue+is%3Aclosed">
<img alt="GitHub closed issues"
src="https://img.shields.io/github/issues-closed/rdavydov/Twitch-Channel-Points-Miner-v2"></a>
</td>
</tr>
<tr>
<td><a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2">
<img alt="GitHub last commit"
src="https://img.shields.io/github/last-commit/Tkd-Alex/Twitch-Channel-Points-Miner-v2" /></a>
</td>
<td><a href="https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2">
<img alt="GitHub last commit"
src="https://img.shields.io/github/last-commit/rdavydov/Twitch-Channel-Points-Miner-v2" /></a>
</td>
</tr>
</table>
</div>
</div>
<div class="columns">
<div class="column is-narrow has-text-center">
<div class="dropdown">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span id="sorting-by"></span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Name ascending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Name descending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Points ascending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Points descending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Last activity ascending
</a>
<a href="#" class="dropdown-item" onClick="changeSortBy(this)">
Last activity descending
</a>
</div>
</div>
</div>
</div>
<div class="column is-2 has-text-left">
<input class="input" type="date" id="startDate">
</div>
<div class="column is-2 has-text-left">
<input class="input" type="date" id="endDate">
</div>
<div class="column has-text-right" style="margin: auto">
<button id="auto-update-log">⏸️</button>
<label class="checkbox checkbox-label">
Log
<input type="checkbox" id="log">
</label>
<label class="checkbox checkbox-label">
Annotations
<input type="checkbox" checked="true" id="annotations">
</label>
<label class="checkbox checkbox-label">
Dark mode
<input type="checkbox" checked="true" id="dark-mode">
</label>
<label class="checkbox checkbox-label">
Header
<input type="checkbox" checked="true" id="toggle-header">
</label>
</div>
</div>
<div class="columns">
<div class="column is-narrow">
<div class="tabs">
<ul id="streamers-list"></ul>
</div>
</div>
<div class="column">
<div class="box" id="chart" style="padding: 0.30rem;"></div>
</div>
</div>
<div class="columns">
<div class="column is-12">
<div class="box" id="log-box" style="padding: 0.30rem; display: none;">
<pre id="log-content">In your run.py file set save=True in logger_settings to save logs to a file.&#10;&#13;</pre>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript">
// n.b. Seems that we are unable to call Flask variable in JS code imported from JS file.
// That's why "link[href='{{url_for('static', filename='dark-theme.css')}}']", {{ refresh }}, {{ daysAgo }} are here.
function toggleDarkMode() {
var darkMode = $('#dark-mode').prop("checked")
$("link[href='{{url_for('static', filename='dark-theme.css')}}']").prop("disabled", !darkMode);
chart.updateOptions({
colors: darkMode === true ? ["#f9826c"] : ['#008ffb'],
chart: {
foreColor: darkMode === true ? "#fff" : '#373d3f'
},
tooltip: {
theme: darkMode === true ? "dark" : "light"
}
})
}
var refresh = parseInt("{{ refresh }}");
var daysAgo = parseInt("{{ daysAgo }}");
</script>
<script type="text/javascript" src="{{url_for('static', filename='script.js')}}"></script>
</html>

39
assets/dark-theme.css Normal file
View File

@ -0,0 +1,39 @@
body, .dropdown *, .input {
background: #343E59;
color: #fff;
}
a {
color: #fff;
}
a:hover {
color: #f9826c;
}
.box {
background-color: #2B2D3E;
}
.tabs a {
border-bottom-color: #dbdbdb;
color: #fff;
border-bottom-style: none;
}
.tabs li.is-active a {
border-bottom-color: #f9826c;
color: #fff;
border-bottom-style: solid;
}
.tabs a:hover {
border-bottom-color: #dbdbdb;
color: #dbdbdb;
border-bottom-style: solid;
}
.tabs ul{
margin-bottom: 5px;
border-bottom-style: none;
}
.checkbox:hover{
color: #f9826c;
}
#log-content {
color: #fff;
background-color: #2B2D3E;
}

BIN
assets/prediction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

389
assets/script.js Normal file
View File

@ -0,0 +1,389 @@
// https://apexcharts.com/javascript-chart-demos/line-charts/zoomable-timeseries/
var options = {
series: [],
chart: {
type: 'area',
stacked: false,
height: 490,
zoom: {
type: 'x',
enabled: true,
autoScaleYaxis: true
},
// background: '#2B2D3E',
foreColor: '#fff'
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
},
markers: {
size: 0,
},
title: {
text: 'Channel points (dates are displayed in UTC)',
align: 'left'
},
colors: ["#f9826c"],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
inverseColors: false,
opacityFrom: 0.5,
opacityTo: 0,
stops: [0, 90, 100]
},
},
yaxis: {
title: {
text: 'Channel points'
},
},
xaxis: {
type: 'datetime',
labels: {
datetimeUTC: false
}
},
tooltip: {
theme: 'dark',
shared: false,
x: {
show: true,
format: 'HH:mm:ss dd MMM',
},
custom: ({
series,
seriesIndex,
dataPointIndex,
w
}) => {
return (`<div class="apexcharts-active">
<div class="apexcharts-tooltip-title">${w.globals.seriesNames[seriesIndex]}</div>
<div class="apexcharts-tooltip-series-group apexcharts-active" style="order: 1; display: flex; padding-bottom: 0px !important;">
<div class="apexcharts-tooltip-text">
<div class="apexcharts-tooltip-y-group">
<span class="apexcharts-tooltip-text-label"><b>Points</b>: ${series[seriesIndex][dataPointIndex]}</span><br>
<span class="apexcharts-tooltip-text-label"><b>Reason</b>: ${w.globals.seriesZ[seriesIndex][dataPointIndex] ? w.globals.seriesZ[seriesIndex][dataPointIndex] : ''}</span>
</div>
</div>
</div>
</div>`)
}
},
noData: {
text: 'Loading...'
}
};
var chart = new ApexCharts(document.querySelector("#chart"), options);
var currentStreamer = null;
var annotations = [];
var streamersList = [];
var sortBy = "Name ascending";
var sortField = 'name';
var startDate = new Date();
startDate.setDate(startDate.getDate() - daysAgo);
var endDate = new Date();
$(document).ready(function () {
// Variable to keep track of whether log checkbox is checked
var isLogCheckboxChecked = $('#log').prop('checked');
// Variable to keep track of whether auto-update log is active
var autoUpdateLog = true;
// Variable to keep track of the last received log index
var lastReceivedLogIndex = 0;
$('#auto-update-log').click(() => {
autoUpdateLog = !autoUpdateLog;
$('#auto-update-log').text(autoUpdateLog ? '⏸️' : '▶️');
if (autoUpdateLog) {
getLog();
}
});
// Function to get the full log content
function getLog() {
if (isLogCheckboxChecked) {
$.get(`/log?lastIndex=${lastReceivedLogIndex}`, function (data) {
// Process and display the new log entries received
$("#log-content").append(data);
// Scroll to the bottom of the log content
$("#log-content").scrollTop($("#log-content")[0].scrollHeight);
// Update the last received log index
lastReceivedLogIndex += data.length;
if (autoUpdateLog) {
// Call getLog() again after a certain interval (e.g., 1 second)
setTimeout(getLog, 1000);
}
});
}
}
// Retrieve the saved header visibility preference from localStorage
var headerVisibility = localStorage.getItem('headerVisibility');
// Set the initial header visibility based on the saved preference or default to 'visible'
if (headerVisibility === 'hidden') {
$('#toggle-header').prop('checked', false);
$('#header').hide();
} else {
$('#toggle-header').prop('checked', true);
$('#header').show();
}
// Handle the toggle header change event
$('#toggle-header').change(function () {
if (this.checked) {
$('#header').show();
// Save the header visibility preference as 'visible' in localStorage
localStorage.setItem('headerVisibility', 'visible');
} else {
$('#header').hide();
// Save the header visibility preference as 'hidden' in localStorage
localStorage.setItem('headerVisibility', 'hidden');
}
});
chart.render();
if (!localStorage.getItem("annotations")) localStorage.setItem("annotations", true);
if (!localStorage.getItem("dark-mode")) localStorage.setItem("dark-mode", true);
if (!localStorage.getItem("sort-by")) localStorage.setItem("sort-by", "Name ascending");
// Restore settings from localStorage on page load
$('#annotations').prop("checked", localStorage.getItem("annotations") === "true");
$('#dark-mode').prop("checked", localStorage.getItem("dark-mode") === "true");
// Handle the annotation toggle click event
$('#annotations').click(() => {
var isChecked = $('#annotations').prop("checked");
localStorage.setItem("annotations", isChecked);
updateAnnotations();
});
// Handle the dark mode toggle click event
$('#dark-mode').click(() => {
var isChecked = $('#dark-mode').prop("checked");
localStorage.setItem("dark-mode", isChecked);
toggleDarkMode();
});
$('#startDate').val(formatDate(startDate));
$('#endDate').val(formatDate(endDate));
sortBy = localStorage.getItem("sort-by");
if (sortBy.includes("Points")) sortField = 'points';
else if (sortBy.includes("Last activity")) sortField = 'last_activity';
else sortField = 'name';
$('#sorting-by').text(sortBy);
getStreamers();
updateAnnotations();
toggleDarkMode();
// Retrieve log checkbox state from localStorage and update UI accordingly
var logCheckboxState = localStorage.getItem('logCheckboxState');
$('#log').prop('checked', logCheckboxState === 'true');
if (logCheckboxState === 'true') {
isLogCheckboxChecked = true;
$('#auto-update-log').show();
$('#log-box').show();
// Start continuously updating the log content
getLog();
}
// Handle the log checkbox change event
$('#log').change(function () {
isLogCheckboxChecked = $(this).prop('checked');
localStorage.setItem('logCheckboxState', isLogCheckboxChecked);
if (isLogCheckboxChecked) {
$('#log-box').show();
$('#auto-update-log').show();
getLog();
$('html, body').scrollTop($(document).height());
} else {
$('#log-box').hide();
$('#auto-update-log').hide();
// Clear log content when checkbox is unchecked
// $("#log-content").text('');
}
});
});
function formatDate(date) {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear();
if (month.length < 2) month = '0' + month;
if (day.length < 2) day = '0' + day;
return [year, month, day].join('-');
}
function changeStreamer(streamer, index) {
$("li").removeClass("is-active")
$("li").eq(index - 1).addClass('is-active');
currentStreamer = streamer;
// Update the chart title with the current streamer's name
options.title.text = `${streamer.replace(".json", "")}'s channel points (dates are displayed in UTC)`;
chart.updateOptions(options);
// Save the selected streamer in localStorage
localStorage.setItem("selectedStreamer", currentStreamer);
getStreamerData(streamer);
}
function getStreamerData(streamer) {
if (currentStreamer == streamer) {
$.getJSON(`./json/${streamer}`, {
startDate: formatDate(startDate),
endDate: formatDate(endDate)
}, function (response) {
chart.updateSeries([{
name: streamer.replace(".json", ""),
data: response["series"]
}], true)
clearAnnotations();
annotations = response["annotations"];
updateAnnotations();
setTimeout(function () {
getStreamerData(streamer);
}, 300000); // 5 minutes
});
}
}
function getAllStreamersData() {
$.getJSON(`./json_all`, function (response) {
for (var i in response) {
chart.appendSeries({
name: response[i]["name"].replace(".json", ""),
data: response[i]["data"]["series"]
}, true)
}
});
}
function getStreamers() {
$.getJSON('streamers', function (response) {
streamersList = response;
sortStreamers();
// Restore the selected streamer from localStorage on page load
var selectedStreamer = localStorage.getItem("selectedStreamer");
if (selectedStreamer) {
currentStreamer = selectedStreamer;
} else {
// If no selected streamer is found, default to the first streamer in the list
currentStreamer = streamersList.length > 0 ? streamersList[0].name : null;
}
// Ensure the selected streamer is still active and scrolled into view
renderStreamers();
});
}
function renderStreamers() {
$("#streamers-list").empty();
var promised = new Promise((resolve, reject) => {
streamersList.forEach((streamer, index, array) => {
displayname = streamer.name.replace(".json", "");
if (sortField == 'points') displayname = "<font size='-2'>" + streamer['points'] + "</font>&nbsp;" + displayname;
else if (sortField == 'last_activity') displayname = "<font size='-2'>" + formatDate(streamer['last_activity']) + "</font>&nbsp;" + displayname;
var isActive = currentStreamer === streamer.name;
if (!isActive && localStorage.getItem("selectedStreamer") === null && index === 0) {
isActive = true;
currentStreamer = streamer.name;
}
var activeClass = isActive ? 'is-active' : '';
var listItem = `<li id="streamer-${streamer.name}" class="${activeClass}"><a onClick="changeStreamer('${streamer.name}', ${index + 1}); return false;">${displayname}</a></li>`;
$("#streamers-list").append(listItem);
if (isActive) {
// Scroll the selected streamer into view
document.getElementById(`streamer-${streamer.name}`).scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
if (index === array.length - 1) resolve();
});
});
promised.then(() => {
changeStreamer(currentStreamer, streamersList.findIndex(streamer => streamer.name === currentStreamer) + 1);
});
}
function sortStreamers() {
streamersList = streamersList.sort((a, b) => {
return (a[sortField] > b[sortField] ? 1 : -1) * (sortBy.includes("ascending") ? 1 : -1);
});
}
function changeSortBy(option) {
sortBy = option.innerText.trim();
if (sortBy.includes("Points")) sortField = 'points'
else if (sortBy.includes("Last activity")) sortField = 'last_activity'
else sortField = 'name';
sortStreamers();
renderStreamers();
$('#sorting-by').text(sortBy);
localStorage.setItem("sort-by", sortBy);
}
function updateAnnotations() {
if ($('#annotations').prop("checked") === true) {
clearAnnotations()
if (annotations && annotations.length > 0)
annotations.forEach((annotation, index) => {
annotations[index]['id'] = `id-${index}`
chart.addXaxisAnnotation(annotation, true)
})
} else clearAnnotations()
}
function clearAnnotations() {
if (annotations && annotations.length > 0)
annotations.forEach((annotation, index) => {
chart.removeAnnotation(annotation['id'])
})
chart.clearAnnotations();
}
// Toggle
$('#annotations').click(() => {
updateAnnotations();
});
$('#dark-mode').click(() => {
toggleDarkMode();
});
$('.dropdown').click(() => {
$('.dropdown').toggleClass('is-active');
});
// Input date
$('#startDate').change(() => {
startDate = new Date($('#startDate').val());
getStreamerData(currentStreamer);
});
$('#endDate').change(() => {
endDate = new Date($('#endDate').val());
getStreamerData(currentStreamer);
});

70
assets/style.css Normal file
View File

@ -0,0 +1,70 @@
html,
body {
overflow-x: hidden;
overflow-y: auto;
}
a {
text-decoration: none;
}
.tabs ul {
-webkit-flex-direction: column;
flex-direction: column;
flex-grow: 0px;
}
.tabs {
max-height: 500px;
overflow-y: auto;
direction: rtl;
}
.checkbox-label {
font-size: 20px;
padding-left: 15px;
font-weight: bold
}
::-webkit-scrollbar {
width: 20px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
border: 6px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background-color: #a8bbbf;
}
#log-content {
text-align: left;
white-space: pre-wrap;
max-height: 400px;
padding: 0;
}
#auto-update-log {
display: none;
background-color: transparent;
font-size: 20px;
padding: 0;
border-radius: 5px;
}