Waze Editor Level Tracker

Track your level progress

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name            Waze Editor Level Tracker
// @name:vi         Trình theo dõi cấp độ Waze Editor
// @version         1.0.2
// @description     Track your level progress
// @description:vi  Theo dõi tiến trình cấp độ của bạn
// @author          vdt2210
// @namespace       https://greasyfork.org/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 cachedProfile = null;
  let isShowPopover = false;

  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];

    const userName = userState ? userState.userName : 'Unknown';
    const profileUrl = wmeSDK ? wmeSDK.DataModel.Users.getUserProfileLink({ userName }) : '#';

    return {
      currentLvlColor,
      displayLevel,
      isStaff,
      profileUrl,
      userName,
    };
  }

  function calculateProgressMetrics(totalEdits, currentLvlColor) {
    const { displayLevel } = getWmeUserData();
    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';
    popoverBox.style.top = rect.bottom + '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('div');
      progressWidget.id = 'wme-progress-topbar';
      progressWidget.style.userSelect = 'none';
      progressWidget.style.position = 'relative';

      progressWidget.innerHTML = `
        <span id="topbar-ui-fraction" style="font-weight: bold; display: inline-flex; align-items: center; white-space: nowrap;">
          <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 = 'absolute';
      popoverBox.style.left = '50%';
      popoverBox.style.transform = 'translateX(-50%)';
      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.color = '#ffffff';

      progressWidget.appendChild(popoverBox);

      const topbarFraction = progressWidget.querySelector('#topbar-ui-fraction');

      topbarFraction.addEventListener('mouseenter', () => {
        if (!cachedProfile || isShowPopover) return;
        updatePopoverContent();
        positionPopover(progressWidget, popoverBox);
      });

      topbarFraction.addEventListener('mouseleave', () => {
        if (isShowPopover) return;
        popoverBox.style.display = 'none';
      });

      topbarFraction.addEventListener('click', () => {
        if (!cachedProfile) return;
        isShowPopover = !isShowPopover;

        if (isShowPopover) {
          updatePopoverContent();
          positionPopover(progressWidget, popoverBox);
        } else {
          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 { userName, displayLevel, isStaff, currentLvlColor, profileUrl } = getWmeUserData();
    const locale = getLocale();

    const totalEdits =
      cachedProfile.totalEditCount !== undefined ? cachedProfile.totalEditCount : 0;

    let popoverHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255, 255, 255, 0.12);  padding-bottom: 6px; margin-bottom: 8px;">
              <wz-button id="wme-progress-popover-user-info" color="clear-icon" size="md" type="button">
                <i class="w-icon w-icon-info" style="color: white"></i>
              </wz-button>

              <div style="font-weight: 500; color: ${currentLvlColor}; text-align: center;">
                  ${userName}
              </div>

              <wz-button id="wme-progress-popover-close" color="clear-icon" size="md" type="button">
                <i class="w-icon w-icon-x" style="color: white"></i>
              </wz-button>
            </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(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;

    popoverBox.querySelector('#wme-progress-popover-user-info').onclick = () => {
      if (wmeSDK) {
        window.open(profileUrl, '_blank');
      }
    };

    popoverBox.querySelector('#wme-progress-popover-close').onclick = () => {
      isShowPopover = false;
      popoverBox.style.display = 'none';
    };
  }

  function updateEditCount() {
    const { userName } = getWmeUserData();

    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();
            }

            fractionEl.style.cursor = 'pointer';
          }

          const popoverBox = document.getElementById('wme-progress-popover');
          if (popoverBox && (isShowPopover || 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(() => {
        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);
  }
})();