Last active
August 26, 2024 18:29
-
-
Save hotdogee/e38142da3376da676aa17a57ec6a528d to your computer and use it in GitHub Desktop.
Google Cloud Skill Boost Lab - Quota Display
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Google Cloud Skill Boost Lab - Quota Display | |
// @namespace Violentmonkey Scripts | |
// @version 1.2 | |
// @description Displays a countdown timer for the amount of time needed for the next lab to leave the 24 hour line, the number of labs that can still be started, and the number labs that were started within the past 24 hours | |
// @match https://www.cloudskillsboost.google/profile/activity | |
// @grant GM_addStyle | |
// @author Han Lin (hotdogee[at]gmail[dot]com) | |
// ==/UserScript== | |
/** | |
* @changelog | |
* 1.2 - Don't count labs that have failed to start towards the quota | |
* 1.1 - Fix NaN issue when no labs have been started | |
* 1.0 - Initial release | |
*/ | |
(function () { | |
'use strict'; | |
const data = JSON.parse(document.querySelector('ql-table').getAttribute('data')); | |
// Add Material Icons font | |
GM_addStyle(` | |
@import url('https://fonts.googleapis.com/icon?family=Material+Icons'); | |
`); | |
// Create the panel | |
const panel = document.createElement('div'); | |
panel.id = 'floatingPanel'; | |
panel.style.cssText = ` | |
position: fixed; | |
top: 119px; | |
left: 12px; | |
width: 370px; | |
height: 312px; | |
background-color: #FFFFFF; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px -1px rgba(0,0,0,0.2), 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12); | |
z-index: 9999; | |
display: flex; | |
flex-direction: column; | |
font-family: 'Roboto', Arial, sans-serif; | |
overflow: hidden; | |
`; | |
// Create the header | |
const header = document.createElement('div'); | |
header.style.cssText = ` | |
padding: 6px 16px; | |
background-color: #21751B; | |
color: #FFFFFF; | |
cursor: move; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
font-size: 20px; | |
font-weight: 500; | |
`; | |
header.textContent = 'Labs Quota Info'; | |
// Create button container | |
const buttonContainer = document.createElement('div'); | |
buttonContainer.style.cssText = ` | |
display: flex; | |
gap: 8px; | |
`; | |
// Create minimize button | |
const minimizeBtn = document.createElement('button'); | |
minimizeBtn.innerHTML = | |
'<span class="material-icons" style="font-size: 24px;">expand_more</span>'; | |
minimizeBtn.style.cssText = ` | |
background: none; | |
border: none; | |
color: #FFFFFF; | |
cursor: pointer; | |
padding: 4px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
width: 36px; | |
height: 36px; | |
border-radius: 50%; | |
transition: background-color 0.3s; | |
`; | |
minimizeBtn.addEventListener('mouseover', () => { | |
minimizeBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; | |
}); | |
minimizeBtn.addEventListener('mouseout', () => { | |
minimizeBtn.style.backgroundColor = 'transparent'; | |
}); | |
buttonContainer.appendChild(minimizeBtn); | |
header.appendChild(buttonContainer); | |
// Append elements to the panel | |
panel.appendChild(header); | |
// Add the panel to the document | |
document.body.appendChild(panel); | |
// Drag functionality | |
let isDragging = false; | |
let startX, startY; | |
header.addEventListener('mousedown', (e) => { | |
isDragging = true; | |
startX = e.clientX - panel.offsetLeft; | |
startY = e.clientY - panel.offsetTop; | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isDragging) return; | |
e.preventDefault(); | |
panel.style.left = e.clientX - startX + 'px'; | |
panel.style.top = e.clientY - startY + 'px'; | |
}); | |
document.addEventListener('mouseup', () => { | |
isDragging = false; | |
}); | |
// Enhanced resize functionality | |
const resizers = { | |
bottom: createResizer('bottom'), | |
left: createResizer('left'), | |
right: createResizer('right'), | |
bottomLeft: createResizer('bottom-left'), | |
bottomRight: createResizer('bottom-right'), | |
}; | |
function createResizer(position) { | |
const resizer = document.createElement('div'); | |
resizer.classList.add('resizer', `resizer-${position}`); | |
resizer.style.cssText = ` | |
position: absolute; | |
background-color: #21751B; | |
transition: opacity 0.3s; | |
opacity: 0; | |
`; | |
switch (position) { | |
case 'bottom': | |
resizer.style.bottom = '0'; | |
resizer.style.left = '8px'; | |
resizer.style.right = '8px'; | |
resizer.style.height = '4px'; | |
resizer.style.cursor = 'ns-resize'; | |
break; | |
case 'left': | |
resizer.style.left = '0'; | |
resizer.style.top = '8px'; | |
resizer.style.bottom = '8px'; | |
resizer.style.width = '4px'; | |
resizer.style.cursor = 'ew-resize'; | |
break; | |
case 'right': | |
resizer.style.right = '0'; | |
resizer.style.top = '8px'; | |
resizer.style.bottom = '8px'; | |
resizer.style.width = '4px'; | |
resizer.style.cursor = 'ew-resize'; | |
break; | |
case 'bottom-left': | |
resizer.style.bottom = '0'; | |
resizer.style.left = '0'; | |
resizer.style.width = '8px'; | |
resizer.style.height = '8px'; | |
resizer.style.cursor = 'nesw-resize'; | |
resizer.style.borderRadius = '0 4px 0 0'; | |
break; | |
case 'bottom-right': | |
resizer.style.bottom = '0'; | |
resizer.style.right = '0'; | |
resizer.style.width = '8px'; | |
resizer.style.height = '8px'; | |
resizer.style.cursor = 'nwse-resize'; | |
resizer.style.borderRadius = '4px 0 0 0'; | |
break; | |
} | |
panel.appendChild(resizer); | |
return resizer; | |
} | |
// Show/hide resizers on hover | |
panel.addEventListener('mouseover', () => { | |
Object.values(resizers).forEach((resizer) => (resizer.style.opacity = '1')); | |
}); | |
panel.addEventListener('mouseout', () => { | |
Object.values(resizers).forEach((resizer) => (resizer.style.opacity = '0')); | |
}); | |
// Resize functionality | |
let isResizing = false; | |
let resizeStartWidth, | |
resizeStartHeight, | |
resizeStartX, | |
resizeStartY, | |
resizeStartLeft, | |
resizeStartTop; | |
Object.values(resizers).forEach((resizer) => { | |
resizer.addEventListener('mousedown', (e) => { | |
isResizing = true; | |
resizeStartWidth = parseInt(document.defaultView.getComputedStyle(panel).width, 10); | |
resizeStartHeight = parseInt(document.defaultView.getComputedStyle(panel).height, 10); | |
resizeStartX = e.clientX; | |
resizeStartY = e.clientY; | |
resizeStartLeft = panel.offsetLeft; | |
resizeStartTop = panel.offsetTop; | |
currentResizer = e.target; | |
}); | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isResizing) return; | |
e.preventDefault(); | |
const minWidth = 200; | |
const minHeight = 100; | |
let newWidth, newHeight, newLeft, newTop; | |
switch (currentResizer.classList[1]) { | |
case 'resizer-bottom': | |
newHeight = resizeStartHeight + (e.clientY - resizeStartY); | |
if (newHeight > minHeight) panel.style.height = newHeight + 'px'; | |
break; | |
case 'resizer-right': | |
newWidth = resizeStartWidth + (e.clientX - resizeStartX); | |
if (newWidth > minWidth) panel.style.width = newWidth + 'px'; | |
break; | |
case 'resizer-left': | |
newWidth = resizeStartWidth - (e.clientX - resizeStartX); | |
newLeft = resizeStartLeft + (e.clientX - resizeStartX); | |
if (newWidth > minWidth) { | |
panel.style.width = newWidth + 'px'; | |
panel.style.left = newLeft + 'px'; | |
} | |
break; | |
case 'resizer-bottom-right': | |
newWidth = resizeStartWidth + (e.clientX - resizeStartX); | |
newHeight = resizeStartHeight + (e.clientY - resizeStartY); | |
if (newWidth > minWidth) panel.style.width = newWidth + 'px'; | |
if (newHeight > minHeight) panel.style.height = newHeight + 'px'; | |
break; | |
case 'resizer-bottom-left': | |
newWidth = resizeStartWidth - (e.clientX - resizeStartX); | |
newHeight = resizeStartHeight + (e.clientY - resizeStartY); | |
newLeft = resizeStartLeft + (e.clientX - resizeStartX); | |
if (newWidth > minWidth) { | |
panel.style.width = newWidth + 'px'; | |
panel.style.left = newLeft + 'px'; | |
} | |
if (newHeight > minHeight) panel.style.height = newHeight + 'px'; | |
break; | |
} | |
}); | |
document.addEventListener('mouseup', () => { | |
isResizing = false; | |
currentResizer = null; | |
}); | |
// Keep track of the current resizer being used | |
let currentResizer = null; | |
Object.values(resizers).forEach((resizer) => { | |
resizer.addEventListener('mousedown', (e) => { | |
currentResizer = e.target; | |
}); | |
}); | |
// Create the content container | |
const content = document.createElement('div'); | |
content.style.cssText = ` | |
padding: 16px; | |
display: flex; | |
flex-direction: column; | |
gap: 16px; | |
flex-grow: 1; | |
overflow-y: auto; | |
`; | |
panel.appendChild(content); | |
// Create the timer section | |
const timerSection = document.createElement('div'); | |
timerSection.style.cssText = ` | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
background-color: #E8F5E9; | |
border-radius: 8px; | |
padding: 16px; | |
`; | |
content.appendChild(timerSection); | |
const timerLabel = document.createElement('div'); | |
timerLabel.textContent = 'Next Lab Available In'; | |
timerLabel.style.cssText = ` | |
font-size: 16px; | |
color: #2E7D32; | |
margin-bottom: 8px; | |
`; | |
timerSection.appendChild(timerLabel); | |
const timerDisplay = document.createElement('div'); | |
timerDisplay.style.cssText = ` | |
font-size: 36px; | |
font-weight: bold; | |
color: #1B5E20; | |
`; | |
timerSection.appendChild(timerDisplay); | |
// Create the lab stats section | |
const statsSection = document.createElement('div'); | |
statsSection.style.cssText = ` | |
display: flex; | |
justify-content: space-between; | |
gap: 16px; | |
`; | |
content.appendChild(statsSection); | |
function createStatCard(icon, label) { | |
const card = document.createElement('div'); | |
card.style.cssText = ` | |
flex: 1; | |
background-color: #FFFFFF; | |
border-radius: 8px; | |
padding: 16px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
text-align: center; | |
`; | |
const iconElement = document.createElement('span'); | |
iconElement.className = 'material-icons'; | |
iconElement.textContent = icon; | |
iconElement.style.cssText = ` | |
font-size: 36px; | |
color: #21751B; | |
margin-bottom: 8px; | |
`; | |
card.appendChild(iconElement); | |
const labelElement = document.createElement('div'); | |
labelElement.textContent = label; | |
labelElement.style.cssText = ` | |
font-size: 14px; | |
color: #424242; | |
margin-bottom: 4px; | |
`; | |
card.appendChild(labelElement); | |
const valueElement = document.createElement('div'); | |
valueElement.style.cssText = ` | |
font-size: 24px; | |
font-weight: bold; | |
color: #212121; | |
`; | |
card.appendChild(valueElement); | |
return { card, valueElement }; | |
} | |
const availableLabsCard = createStatCard('play_circle', 'Available Labs'); | |
const startedLabsCard = createStatCard('history', 'Labs Started (24h)'); | |
statsSection.appendChild(availableLabsCard.card); | |
statsSection.appendChild(startedLabsCard.card); | |
// Function to update the displayed information | |
let labQuota = 15; | |
function updateInfo() { | |
const now = new Date(); | |
const twentyFourHoursAgo = new Date(now - 24 * 60 * 60 * 1000); | |
// Calculate the time until the next lab becomes available | |
const labStartTimes = data | |
.filter((item) => new Date(item.started) > twentyFourHoursAgo && item.ended) | |
.map((item) => new Date(item.started)); | |
const nextAvailableTime = new Date(Math.min(...labStartTimes) + 24 * 60 * 60 * 1000); | |
const timeRemaining = nextAvailableTime - now; | |
// console.log(labStartTimes, nextAvailableTime, timeRemaining); | |
// Update timer display | |
if (isNaN(timeRemaining) || timeRemaining <= 0) { | |
timerDisplay.textContent = '00:00:00'; | |
} else { | |
const hours = Math.floor(timeRemaining / (60 * 60 * 1000)); | |
const minutes = Math.floor((timeRemaining % (60 * 60 * 1000)) / (60 * 1000)); | |
const seconds = Math.floor((timeRemaining % (60 * 1000)) / 1000); | |
timerDisplay.textContent = `${hours.toString().padStart(2, '0')}:${minutes | |
.toString() | |
.padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
} | |
// Update available labs count | |
const availableLabs = labQuota - labStartTimes.length; | |
availableLabsCard.valueElement.textContent = availableLabs; | |
// Update started labs count | |
const startedLabs = labStartTimes.length; | |
startedLabsCard.valueElement.textContent = startedLabs; | |
} | |
// Update info immediately and then every second | |
updateInfo(); | |
setInterval(updateInfo, 1000); | |
// Minimize functionality | |
let isMinimized = false; | |
minimizeBtn.addEventListener('click', () => { | |
if (isMinimized) { | |
panel.style.height = '300px'; | |
content.style.display = 'block'; | |
minimizeBtn.innerHTML = | |
'<span class="material-icons" style="font-size: 24px;">expand_more</span>'; | |
Object.values(resizers).forEach((resizer) => (resizer.style.display = 'block')); | |
} else { | |
panel.style.height = 'auto'; | |
content.style.display = 'none'; | |
minimizeBtn.innerHTML = | |
'<span class="material-icons" style="font-size: 24px;">expand_less</span>'; | |
Object.values(resizers).forEach((resizer) => (resizer.style.display = 'none')); | |
} | |
isMinimized = !isMinimized; | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment