Skip to content

Instantly share code, notes, and snippets.

@hotdogee
Last active August 26, 2024 18:29
Show Gist options
  • Save hotdogee/e38142da3376da676aa17a57ec6a528d to your computer and use it in GitHub Desktop.
Save hotdogee/e38142da3376da676aa17a57ec6a528d to your computer and use it in GitHub Desktop.
Google Cloud Skill Boost Lab - Quota Display
// ==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