Track your level progress
// ==UserScript==
// @name Waze Editor Level Tracker
// @name:vi Trình theo dõi cấp độ Waze Editor
// @version 1.0.1
// @description Track your level progress
// @description:vi Theo dõi tiến trình cấp độ của bạn
// @author vdt2210
// @namespace https://greatest.deepsurf.us/en/users/1603731-vdt2210
// @license MIT
// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @grant none
// ==/UserScript==
(function () {
'use strict';
let wmeSDK = null;
let userName = '';
let cachedProfile = null;
const WAZE_PRIMARY_COLOR = 'var(--wz-button-background-color, var(--primary, #0099ff))';
const WAZE_LEVEL_COLORS = {
1: '#9ca3af',
2: '#34d399',
3: '#60a5fa',
4: '#fbbf24',
5: '#ff8533',
6: '#f43f5e',
7: WAZE_PRIMARY_COLOR,
};
const WAZE_LEVEL_TARGETS = {
1: 3000,
2: 25000,
3: 100000,
4: 250000,
};
const TRANSLATIONS = {
vi: {
level: 'Cấp độ',
staff: 'Nhân viên',
edits: 'Số chỉnh sửa',
target: 'Mục tiêu',
remaining: 'Còn lại',
progress: 'Tiến độ',
readyToUpgrade: 'Đủ điểm lên cấp',
maxLevelAchieved: 'Bạn đã hoàn thành mục tiêu!',
},
en: {
level: 'Level',
staff: 'Staff',
edits: 'Edits',
target: 'Target',
remaining: 'Remaining',
progress: 'Progress',
readyToUpgrade: 'Ready to upgrade to level',
maxLevelAchieved: 'You have achieved the final target!',
},
};
function getLocale() {
let wmeLang = 'en';
if (typeof I18n !== 'undefined' && I18n.currentLocale) {
wmeLang = I18n.currentLocale();
}
return TRANSLATIONS[wmeLang] || TRANSLATIONS.en;
}
function formatToK(num) {
if (!num || isNaN(num)) return '0';
if (num >= 1000) {
return num / 1000 + 'k';
}
return num.toString();
}
function getWmeUserData() {
const userState = wmeSDK && wmeSDK.State ? wmeSDK.State.getUserInfo() : null;
const isStaff = userState && userState.rank !== undefined && userState.rank === 6;
const displayLevel = userState && userState.rank !== undefined ? userState.rank + 1 : 1;
const currentLvlColor = WAZE_LEVEL_COLORS[displayLevel] || WAZE_LEVEL_COLORS[1];
return {
displayLevel,
isStaff,
currentLvlColor,
};
}
function calculateProgressMetrics(displayLevel, totalEdits, currentLvlColor) {
const targetCount = WAZE_LEVEL_TARGETS[displayLevel] || 0;
const nextLevel = displayLevel + 1;
const nextLvlColor = WAZE_LEVEL_COLORS[nextLevel] || currentLvlColor;
const editsNeeded = targetCount - totalEdits;
const currentLevelBase = WAZE_LEVEL_TARGETS[displayLevel - 1] || 0;
const totalLevelRange = targetCount - currentLevelBase;
const currentRangeProgress = totalEdits - currentLevelBase;
const progressNum = Math.min(
100,
Math.max(0, (currentRangeProgress / (totalLevelRange || 1)) * 100),
);
return {
nextLevel,
targetCount,
nextLvlColor,
editsNeeded,
finalEditsNeeded: Math.max(0, editsNeeded),
progressNum,
progressPercentStr: progressNum.toFixed(2).replace('.', ','),
barGradientStyle: `linear-gradient(to right, ${currentLvlColor} 0%, ${nextLvlColor} 100%)`,
bgWidthFactor: (100 / (progressNum || 1)) * 100,
};
}
function positionPopover(anchorWidget, popoverBox) {
const rect = anchorWidget.getBoundingClientRect();
popoverBox.style.display = 'block';
const popoverWidth = popoverBox.offsetWidth;
const centeredLeft = rect.left + rect.width / 2 - popoverWidth / 2;
popoverBox.style.left = centeredLeft + 'px';
popoverBox.style.top = rect.bottom + 6 + 'px';
}
function injectTopBarUI() {
if (document.getElementById('wme-progress-topbar')) return;
const styleBlock = document.createElement('style');
styleBlock.textContent = `
@keyframes wmeProgressSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(styleBlock);
const toolbar = document.querySelector('.secondary-toolbar');
const saveButton = document.querySelector('#save-button');
const saveContainerElement =
saveButton?.closest('.container--VcOZy')?.parentElement || saveButton?.closest('div');
if (toolbar && saveContainerElement) {
const progressWidget = document.createElement('span');
progressWidget.id = 'wme-progress-topbar';
progressWidget.style.display = 'inline-flex';
progressWidget.style.alignItems = 'center';
progressWidget.style.fontSize = '13px';
progressWidget.style.fontWeight = '500';
progressWidget.style.verticalAlign = 'middle';
progressWidget.style.whiteSpace = 'nowrap';
progressWidget.style.cursor = 'pointer';
progressWidget.style.userSelect = 'none';
progressWidget.innerHTML = `
<span id="topbar-ui-fraction" style="font-weight: bold; display: inline-flex; align-items: center;">
<div style="width: 14px; height: 14px; border: 2px solid rgba(0, 0, 0, 0.1); border-top: 2px solid ${WAZE_PRIMARY_COLOR}; border-radius: 50%; animation: wmeProgressSpin 0.8s linear infinite;"></div>
</span>
`;
const popoverBox = document.createElement('div');
popoverBox.id = 'wme-progress-popover';
popoverBox.style.display = 'none';
popoverBox.style.position = 'fixed';
popoverBox.style.zIndex = '9999';
popoverBox.style.backgroundColor = 'rgba(0,0,0,.7)';
popoverBox.style.backdropFilter = 'blur(6px)';
popoverBox.style.webkitBackdropFilter = 'blur(6px)';
popoverBox.style.borderRadius = '8px';
popoverBox.style.padding = '14px';
popoverBox.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.25)';
popoverBox.style.fontSize = '13px';
popoverBox.style.color = '#ffffff';
popoverBox.style.lineHeight = '1.6';
popoverBox.style.pointerEvents = 'none';
document.body.appendChild(popoverBox);
progressWidget.addEventListener('mouseenter', () => {
if (!cachedProfile) return;
updatePopoverContent();
positionPopover(progressWidget, popoverBox);
});
progressWidget.addEventListener('mouseleave', () => {
popoverBox.style.display = 'none';
});
toolbar.insertBefore(progressWidget, saveContainerElement);
updateEditCount();
} else {
setTimeout(injectTopBarUI, 300);
}
}
function updatePopoverContent() {
const popoverBox = document.getElementById('wme-progress-popover');
if (!popoverBox || !wmeSDK || !cachedProfile) return;
const { displayLevel, isStaff, currentLvlColor } = getWmeUserData();
const locale = getLocale();
const totalEdits =
cachedProfile.totalEditCount !== undefined ? cachedProfile.totalEditCount : 0;
let popoverHTML = `
<div style="font-weight: 500; border-bottom: 1px solid rgba(255, 255, 255, 0.12); padding-bottom: 6px; margin-bottom: 8px; color: ${currentLvlColor}; font-size: 14px; text-align: center;">
${userName}
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; gap: 16px;">
<span style="color: #a1a1aa;">${locale.level}:</span>
<span style="font-weight: bold; color: ${currentLvlColor};">${isStaff ? locale.staff : displayLevel}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; gap: 16px;">
<span style="color: #a1a1aa;">${locale.edits}:</span>
<span style="font-weight: bold;">${totalEdits.toLocaleString()}</span>
</div>
`;
if (displayLevel < 5) {
const {
nextLevel,
targetCount,
nextLvlColor,
editsNeeded,
finalEditsNeeded,
progressNum,
progressPercentStr,
barGradientStyle,
bgWidthFactor,
} = calculateProgressMetrics(displayLevel, totalEdits, currentLvlColor);
if (targetCount) {
popoverHTML += `
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; gap: 16px;">
<span style="color: #a1a1aa;">${locale.target}:</span>
<span style="font-weight: bold; color: ${nextLvlColor};">${formatToK(targetCount)} (${locale.level} ${nextLevel})</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px; gap: 16px;">
<span style="color: #a1a1aa;">${locale.remaining}:</span>
<span style="font-weight: bold; color: #f87171;">${finalEditsNeeded.toLocaleString()}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px; gap: 16px;">
<span style="color: #a1a1aa;">${locale.progress}:</span>
<span style="font-weight: bold; color: #60a5fa;">${progressPercentStr}%</span>
</div>
<div style="width: 100%; height: 10px; background-color: rgba(255, 255, 255, 0.15); border-radius: 6px; overflow: hidden;">
<div style="width: ${progressNum}%; height: 100%; background: ${barGradientStyle}; background-size: ${bgWidthFactor}% 100%; border-radius: 6px; transition: width 0.4s ease;"></div>
</div>
`;
if (editsNeeded <= 0) {
popoverHTML += `
<div style="border-top: 1px dashed rgba(255, 255, 255, 0.12); padding-top: 6px; margin-top: 6px; text-align: center; color: #fbbf24; font-weight: bold;">
🎉 ${locale.readyToUpgrade} ${nextLevel}!
</div>
`;
}
}
} else if (displayLevel == 5) {
popoverHTML += `
<div style="text-align: center; font-weight: bold;">
${locale.maxLevelAchieved}
</div>
`;
}
popoverBox.innerHTML = popoverHTML;
}
function updateEditCount() {
if (!wmeSDK || !userName) return;
wmeSDK.DataModel.Users.getUserProfile({ userName })
.then((profile) => {
if (profile) {
cachedProfile = profile;
const totalEdits = profile.totalEditCount !== undefined ? profile.totalEditCount : 0;
const fractionEl = document.getElementById('topbar-ui-fraction');
if (fractionEl) {
const { displayLevel } = getWmeUserData();
if (displayLevel < 5) {
const targetCount = WAZE_LEVEL_TARGETS[displayLevel];
const targetText = `/${formatToK(targetCount)}`;
fractionEl.textContent = `${totalEdits.toLocaleString()}${targetText}`;
} else {
fractionEl.textContent = totalEdits.toLocaleString();
}
}
const popoverBox = document.getElementById('wme-progress-popover');
if (popoverBox && popoverBox.style.display === 'block') {
updatePopoverContent();
}
}
})
.catch((err) => {
console.error('❌ UserProfile from SDK:', err);
});
}
function initScript() {
try {
wmeSDK = window.getWmeSdk({
scriptId: 'waze-editor-level-tracker',
scriptName: 'Waze Editor Level Tracker',
});
wmeSDK.Events.once({ eventName: 'wme-ready' }).then(() => {
userName = wmeSDK.State.getUserInfo().userName;
injectTopBarUI();
wmeSDK.Events.on({
eventName: 'wme-save-finished',
eventHandler: function (result) {
if (result && result.success) {
setTimeout(updateEditCount, 1000);
}
},
});
});
} catch (error) {
console.error('❌ SDK:', error);
}
}
if (window.SDK_INITIALIZED) {
window.SDK_INITIALIZED.then(initScript);
} else {
const checkPromise = setInterval(() => {
if (window.SDK_INITIALIZED) {
clearInterval(checkPromise);
window.SDK_INITIALIZED.then(initScript);
}
}, 100);
}
})();