Neopets Kadoatery tracker that looks cute and allows for splitting times.
// ==UserScript==
// @name KadWatch
// @version 0.46
// @description Neopets Kadoatery tracker that looks cute and allows for splitting times.
// @author Ryan (ext1nct)
// @match http*://*.neopets.com/games/kadoatery/*
// @icon https://itemdb.com.br/api/cache/preview/7f18f78e35daa6.png
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @namespace https://greasyfork.org/users/custom
// ==/UserScript==
/* eslint-env jquery */
/**
* Stored data keys
*/
const LAST_REFRESH_TIME_KEY = "lastRefreshTime"
const MAIN_KAD_DATA_KEY = "mainKadData"
const MAIN_HISTORY_KEY = "mainHistory"
const MINI_REFRESH_TIMES_KEY = "miniRefreshTimes"
const KAD_STATES_KEY = "kadStates"
const NOTIFICATION_OPT_IN_KEY = "notificationOptIn"
const KAD_FIRST_RUN_KEY = "kadFirstRun"
const PENDING_DROP_KEY = "pendingDrop"
// Settings Keys
const KAD_THEME_KEY = "kadTheme"
const KAD_AUDIO_PING_KEY = "kadAudioPing"
const KAD_WARNING_TIME_KEY = "kadWarningTime"
const KAD_24H_TIME_KEY = "kad24hTime"
const KAD_INCLUDE_FOOD_KEY = "kadIncludeFood"
const KAD_AUTO_CENSOR_KEY = "kadAutoCensor"
const DURATION_UNTIL_START_OF_MAIN_WINDOW_MS = 2100000 // 35 minutes
const DURATION_OF_MAIN_WINDOW_MS = 60000 // 60 seconds
const PEND_INTERVAL_MS = 420000 // 7 minutes
let notificationsTriggered = { main: 0, minis: {} };
let userIsLockedOut = false;
let editingTarget = null;
let isDraggingModal = false;
let dragTarget = null;
let dragOffsetX = 0;
let dragOffsetY = 0;
let firstClickedIndex = null; // Used for range selection
function censorFood(name) {
let doCensor = GM_getValue(KAD_AUTO_CENSOR_KEY, true);
if (!doCensor) return name;
return name
.replace(/ball/gi, "b.all")
.replace(/crack/gi, "crac.k")
.replace(/weed/gi, "w.eed")
.replace(/rape/gi, "r.ape")
.replace(/cum/gi, "c.um");
}
// Inject KadWatch CSS with Vibrant Theme Variables
const STYLES = `
<style>
:root {
--kw-primary: #f472b6;
--kw-secondary: #dbeafe;
--kw-accent: #60a5fa;
--kw-bg: #ffffff;
--kw-panel: #fdf2f8;
--kw-shadow: rgba(244, 114, 182, 0.15);
--kw-text: #334155;
}
[data-kad-theme="kadwatch"] {
--kw-primary: #f472b6; --kw-secondary: #dbeafe; --kw-accent: #60a5fa; --kw-panel: #fdf2f8; --kw-shadow: rgba(244, 114, 182, 0.15);
}
[data-kad-theme="green"] {
--kw-primary: #22c55e; --kw-secondary: #bbf7d0; --kw-accent: #15803d; --kw-panel: #f0fdf4; --kw-shadow: rgba(34, 197, 94, 0.2);
}
[data-kad-theme="island"] {
--kw-primary: #8b5a2b; --kw-secondary: #e6d5b8; --kw-accent: #2e8b57; --kw-panel: #fdfaf6; --kw-shadow: rgba(139, 90, 43, 0.2);
}
[data-kad-theme="spotted"] {
--kw-primary: #171717; --kw-secondary: #fef08a; --kw-accent: #404040; --kw-panel: #fef9c3; --kw-shadow: rgba(23, 23, 23, 0.2);
}
[data-kad-theme="white"] {
--kw-primary: #cbd5e1; --kw-secondary: #f1f5f9; --kw-accent: #64748b; --kw-panel: #f8fafc; --kw-shadow: rgba(100, 116, 139, 0.1);
}
[data-kad-theme="void"] {
--kw-primary: #7c3aed; --kw-secondary: #334155; --kw-accent: #8b5cf6; --kw-bg: #0f172a; --kw-panel: #1e293b; --kw-shadow: rgba(0, 0, 0, 0.4); --kw-text: #f8fafc;
}
[data-kad-theme="rainbow"] {
--kw-primary: #3b82f6; --kw-secondary: #e2e8f0; --kw-accent: #ec4899; --kw-bg: #ffffff; --kw-panel: #f8fafc; --kw-shadow: rgba(59, 130, 246, 0.2);
}
.kad-modern-wrapper { margin: 8px auto 16px auto; max-width: 850px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; color: var(--kw-text); }
.kad-panel { background: var(--kw-bg); border: 1px solid var(--kw-secondary); border-top: 3px solid var(--kw-primary); box-shadow: 0 2px 6px var(--kw-shadow); padding: 8px 12px; display: flex; flex-direction: column; gap: 6px; border-radius: 4px; }
.kad-alert { background: #fef2f2; color: #991b1b; padding: 6px; border-radius: 4px; border: 1px solid #fecaca; margin-bottom: 6px; text-align: center; font-weight: 500; font-size: 12px; }
.kad-alert-warning { background: #fef9c3; color: #854d0e; border-color: #fde047; }
.kad-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; border-bottom: 1px solid var(--kw-secondary); padding-bottom: 8px; margin-bottom: 2px; }
.kad-toolbar-group { display: flex; align-items: center; gap: 8px; }
.kad-tracker-block { background: var(--kw-bg); border: 1px solid var(--kw-secondary); border-radius: 4px; padding: 14px 12px 12px 12px; position: relative; margin-top: 4px; display: flex; flex-direction: column; align-items: center; border-left: 3px solid var(--kw-accent); }
.kad-tracker-block.kad-main-block { background: var(--kw-panel); border-left: 3px solid var(--kw-primary); border-color: var(--kw-secondary); }
.kad-block-label { position: absolute; top: 6px; left: 10px; font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; max-width: calc(100% - 160px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--kw-text); opacity: 0.8;}
.kad-last-refresh { font-size: 11px; opacity: 0.6; margin-top: 4px; }
.kad-countdown-box { font-size: 18px; font-weight: 700; margin-top: 6px; display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; color: var(--kw-text); }
.kad-time-hl { color: #ef4444; font-size: 22px; font-variant-numeric: tabular-nums; letter-spacing: 0.5px; }
.kad-time-sub { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.7;}
.kad-divider { opacity: 0.3; font-weight: 300; margin: 0 4px; }
.kad-window-active { color: #10b981; font-size: 22px; font-weight: 700; }
.kad-btn { background: var(--kw-accent); color: #fff; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-weight: 600; transition: opacity 0.15s; font-size: 12px; }
.kad-btn:hover { opacity: 0.85; }
.kad-btn-primary { background: var(--kw-primary); color: #fff; text-transform: uppercase; letter-spacing: 0.5px; }
.kad-btn-red { background: #ef4444; padding: 2px 6px; font-size: 11px; }
.kad-btn-action { position: absolute; top: 6px; right: 28px; padding: 2px 6px; font-size: 10px; opacity: 0.9; }
.kad-btn-edit { right: 88px; background: var(--kw-text); opacity: 0.6; color: var(--kw-bg); }
.kad-btn-edit:hover { opacity: 0.8; }
.kad-btn-remove { position: absolute; top: 6px; right: 6px; padding: 2px 6px; font-size: 10px; }
.kad-input { border: 1px solid var(--kw-secondary); border-radius: 4px; padding: 3px 6px; text-align: center; font-size: 12px; width: 65px; outline: none; background: var(--kw-bg); color: var(--kw-text); transition: border 0.15s; }
.kad-input:focus { border-color: var(--kw-primary); }
.kad-select { border: 1px solid var(--kw-secondary); border-radius: 4px; padding: 4px; font-size: 12px; background: var(--kw-bg); color: var(--kw-text); outline: none; cursor: pointer; width: 100%; }
.kad-checkbox { accent-color: var(--kw-primary); cursor: pointer; margin-right: 6px; }
/* Modal Styles */
.kad-modal { background: var(--kw-bg); border: 1px solid var(--kw-secondary); border-top: 3px solid var(--kw-primary); box-shadow: 0 8px 24px rgba(0,0,0,0.15); display: flex; flex-direction: column; color: var(--kw-text); position: absolute; z-index: 9999; border-radius: 6px; }
.kad-modal-header { font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid var(--kw-secondary); padding: 10px 14px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; background: var(--kw-panel); border-top-left-radius: 4px; border-top-right-radius: 4px; }
.kad-modal-content { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.kad-modal-row { display: flex; flex-direction: column; gap: 4px; }
.kad-modal-row-group { display: flex; gap: 12px; }
.kad-modal-row-group .kad-modal-row { flex: 1; }
.kad-modal-label { font-size: 11px; font-weight: 700; text-transform: uppercase; opacity: 0.7; }
.kad-modal-textarea { border: 1px solid var(--kw-secondary); padding: 6px; font-family: system-ui; font-size: 12px; background: var(--kw-bg); color: var(--kw-text); resize: vertical; min-height: 60px; outline: none; border-radius: 4px; }
.kad-modal-textarea:focus { border-color: var(--kw-primary); }
.kad-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.kad-settings-section { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px solid var(--kw-secondary); border-radius: 4px; background: var(--kw-panel); }
.kad-settings-title { font-size: 12px; font-weight: 700; color: var(--kw-primary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
/* Rainbow Theme Specific Overrides */
[data-kad-theme="rainbow"] .kad-panel,
[data-kad-theme="rainbow"] .kad-modal {
border-top: 3px solid transparent;
border-image: linear-gradient(to right, #ef4444, #f97316, #eab308, #22c55e, #3b82f6, #a855f7, #ec4899) 1;
}
[data-kad-theme="rainbow"] .kad-tracker-block {
border-left: 3px solid transparent;
border-image: linear-gradient(to bottom, #ef4444, #eab308, #22c55e, #3b82f6, #a855f7) 1;
}
.kad-selectable-cell { cursor: crosshair !important; transition: background 0.15s; }
.kad-selectable-cell:hover { outline: 1px dotted var(--kw-primary); background: var(--kw-panel) !important; }
.kad-food-copy { cursor: pointer; color: var(--kw-text) !important; border-bottom: 1px dotted var(--kw-accent); transition: all 0.15s; }
.kad-food-copy:hover { color: var(--kw-primary) !important; border-bottom: 1px solid var(--kw-primary); }
</style>
`;
runScript()
function runScript() {
$('head').append(STYLES);
loadThemePreference();
hideHeaderText();
addIdToKadaotiesTable();
injectDashboard();
injectModals();
setupDragLogic();
checkBoardLockState();
addClickToCopyFeature();
setupNotificationSupport();
checkForKadaotieRefresh();
setInterval(tickTimers, 1000);
tickTimers();
}
function loadThemePreference() {
let savedTheme = GM_getValue(KAD_THEME_KEY, "kadwatch");
let validThemes = ['kadwatch', 'green', 'island', 'rainbow', 'spotted', 'white', 'void'];
if (!validThemes.includes(savedTheme)) {
savedTheme = 'kadwatch';
GM_setValue(KAD_THEME_KEY, savedTheme);
}
document.documentElement.setAttribute("data-kad-theme", savedTheme);
}
function hideHeaderText() {
let headerStrong = $('.content').find('strong:contains("The Kadoatery")').first();
if (headerStrong.length) {
headerStrong.hide();
let parent = headerStrong.parent();
let started = false;
parent.contents().each(function() {
if (this === headerStrong[0]) {
started = true;
return true;
}
if (started) {
if (this.nodeType === Node.ELEMENT_NODE && (this.tagName === 'DIV' || this.tagName === 'TABLE' || this.tagName === 'FORM' || this.id === 'kad-dashboard-wrapper')) {
return false;
}
if (this.nodeType === Node.TEXT_NODE) {
this.nodeValue = '';
} else {
$(this).hide();
}
}
});
}
}
function addIdToKadaotiesTable() {
$('.content div table').first().attr("id","kadaotiesTable");
}
function injectDashboard() {
let dashboardHtml = `
<div id="kad-dashboard-wrapper" class="kad-modern-wrapper">
<div id='alreadyFedAlert' class='kad-alert kad-alert-warning' style='display:none;'>
⚠️ <strong>You are locked out!</strong> A Mini refreshed, but your previously fed Kad is still on the board. Do not buy!
</div>
<div id='pendingDropAlert' class='kad-alert kad-alert-warning' style='display:none;'></div>
<div id="kad-dashboard" class="kad-panel">
<div class="kad-toolbar">
<div class="kad-toolbar-group kad-quick-links">
<button id="openSettingsBtn" class="kad-btn" title="Settings" style="padding: 4px 6px; font-size: 14px; background: transparent; color: var(--kw-text); border: 1px solid var(--kw-secondary);">⚙️</button>
• <a href="/market.phtml?type=wizard" target="_blank" style="color: inherit; font-weight: 600;">SW</a>
• <a href="/safetydeposit.phtml" target="_blank" style="color: inherit; font-weight: 600;">SDB</a>
</div>
<div class="kad-toolbar-group" style="border-left: 1px solid var(--kw-secondary); border-right: 1px solid var(--kw-secondary); padding: 0 10px;">
<input type="text" id="manualTimeInput" class="kad-input" placeholder="HH:MM" />
<button id="setMainBtn" class="kad-btn">Log Main</button>
<button id="addMiniBtn" class="kad-btn">Log Mini</button>
</div>
<div class="kad-toolbar-group">
<button id="toggleNotifyBtn" class="kad-btn" style="display: none;"></button>
<button id="copyBoardBtn" class="kad-btn kad-btn-primary">📋 Copy Post</button>
</div>
</div>
<div id="mainContainer"></div>
<div id="minisContainer" style="display: flex; flex-direction: column; gap: 4px;"></div>
</div>
</div>
`;
$(dashboardHtml).insertBefore('#kadaotiesTable');
$('#openSettingsBtn').on('click', function(e) { openSettingsModal(e); });
$("#setMainBtn").on("click", () => handleManualTime('main'));
$("#addMiniBtn").on("click", () => handleManualTime('mini'));
$("#copyBoardBtn").on("click", copyBoardTimes);
$(document).on("click", ".demote-main-btn", demoteMainToMini);
$(document).on("click", ".promote-mini-btn", function() { promoteMiniToMain($(this).data('index')); });
$(document).on("click", ".remove-main-btn", function() { GM_setValue(LAST_REFRESH_TIME_KEY, 0); tickTimers(); });
$(document).on("click", ".remove-mini-btn", function() { removeMini($(this).data('index')); });
$(document).on("click", ".edit-main-btn", function(e) { openEditModal('main', 0, e); });
$(document).on("click", ".edit-mini-btn", function(e) { openEditModal('mini', $(this).data('index'), e); });
$(document).on("click", ".merge-main-btn", function() { mergePendingDrop('main'); });
$(document).on("click", ".merge-mini-btn", function() { mergePendingDrop('mini', $(this).data('index')); });
$(document).on("click", "#discardDropBtn", discardPendingDrop);
}
function injectModals() {
let editModalHtml = `
<div id="kadEditModal" class="kad-modal" style="display:none; width: 420px;">
<div class="kad-modal-header kad-modal-drag">
<div>
<span id="editModalTitle">Data Override</span>
<span id="editModalHeaderHint" style="font-size: 10px; color: var(--kw-accent); font-weight: 600; margin-left: 8px;">[Click board to replace]</span>
</div>
<button class="minimize-modal-btn" data-target="#kadEditModalContent" style="background:transparent; border:none; color:var(--kw-text); font-weight:900; font-size:16px; cursor:pointer; line-height: 1;">−</button>
</div>
<div id="kadEditModalContent" class="kad-modal-content">
<div class="kad-modal-row-group">
<div class="kad-modal-row">
<label class="kad-modal-label">Count</label>
<input type="number" id="editCount" class="kad-input" style="width: 100%;" min="1" max="20" />
</div>
<div class="kad-modal-row">
<label class="kad-modal-label">Seconds (0-59)</label>
<input type="number" id="editSeconds" class="kad-input" style="width: 100%;" min="0" max="59" />
</div>
</div>
<div class="kad-modal-row">
<label class="kad-modal-label">Kad Names (Comma Separated)</label>
<textarea id="editNames" class="kad-modal-textarea" placeholder="Select a range on the board..."></textarea>
</div>
<div class="kad-modal-row">
<label class="kad-modal-label">Foods (Comma Separated)</label>
<textarea id="editFoods" class="kad-modal-textarea"></textarea>
</div>
<div class="kad-modal-actions">
<button id="cancelEditBtn" class="kad-btn" style="background: transparent; color: inherit; border: 1px solid var(--kw-secondary);">Cancel</button>
<button id="saveEditBtn" class="kad-btn kad-btn-primary">Save Changes</button>
</div>
</div>
</div>
`;
let currentTheme = GM_getValue(KAD_THEME_KEY, "kadwatch");
let warnTime = GM_getValue(KAD_WARNING_TIME_KEY, 10);
let settingsModalHtml = `
<div id="kadSettingsModal" class="kad-modal" style="display:none; width: 380px;">
<div class="kad-modal-header kad-modal-drag">
<span>KadWatch Settings</span>
<button class="minimize-modal-btn" data-target="#kadSettingsModalContent" style="background:transparent; border:none; color:var(--kw-text); font-weight:900; font-size:16px; cursor:pointer; line-height: 1;">−</button>
</div>
<div id="kadSettingsModalContent" class="kad-modal-content">
<div class="kad-settings-section">
<div class="kad-settings-title">Appearance</div>
<div class="kad-modal-row">
<select id="settingTheme" class="kad-select">
<option value="kadwatch" ${currentTheme === 'kadwatch' ? 'selected' : ''}>🎀 KadWatch</option>
<option value="green" ${currentTheme === 'green' ? 'selected' : ''}>🐢 Green Kad</option>
<option value="island" ${currentTheme === 'island' ? 'selected' : ''}>🏝️ Island Kad</option>
<option value="rainbow" ${currentTheme === 'rainbow' ? 'selected' : ''}>🌈 Rainbow Kad</option>
<option value="spotted" ${currentTheme === 'spotted' ? 'selected' : ''}>🐆 Spotted Kad</option>
<option value="white" ${currentTheme === 'white' ? 'selected' : ''}>☁️ White Kad</option>
<option value="void" ${currentTheme === 'void' ? 'selected' : ''}>🌌 Void</option>
</select>
</div>
</div>
<div class="kad-settings-section">
<div class="kad-settings-title">Alerts</div>
<label style="display: flex; align-items: center; font-size: 12px; cursor: pointer;">
<input type="checkbox" id="settingDesktopNotif" class="kad-checkbox" ${GM_getValue(NOTIFICATION_OPT_IN_KEY, false) ? 'checked' : ''} />
Desktop Notifications
</label>
<label style="display: flex; align-items: center; font-size: 12px; cursor: pointer; margin-top: 6px;">
<input type="checkbox" id="settingAudioPing" class="kad-checkbox" ${GM_getValue(KAD_AUDIO_PING_KEY, false) ? 'checked' : ''} />
Play Audio Ping
</label>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 4px;">
<span style="font-size: 12px;">Warning Time:</span>
<select id="settingWarnTime" class="kad-select" style="width: auto;">
<option value="10" ${warnTime === 10 ? 'selected' : ''}>10 Seconds</option>
<option value="30" ${warnTime === 30 ? 'selected' : ''}>30 Seconds</option>
<option value="60" ${warnTime === 60 ? 'selected' : ''}>60 Seconds</option>
</select>
</div>
</div>
<div class="kad-settings-section">
<div class="kad-settings-title">Clipboard Options</div>
<label style="display: flex; align-items: center; font-size: 12px; cursor: pointer;">
<input type="checkbox" id="setting24h" class="kad-checkbox" ${GM_getValue(KAD_24H_TIME_KEY, false) ? 'checked' : ''} />
Use 24-Hour Time
</label>
<label style="display: flex; align-items: center; font-size: 12px; cursor: pointer; margin-top: 6px;">
<input type="checkbox" id="settingIncludeFood" class="kad-checkbox" ${GM_getValue(KAD_INCLUDE_FOOD_KEY, true) ? 'checked' : ''} />
Include Food List in Copy
</label>
<label style="display: flex; align-items: center; font-size: 12px; cursor: pointer; margin-top: 6px;">
<input type="checkbox" id="settingAutoCensor" class="kad-checkbox" ${GM_getValue(KAD_AUTO_CENSOR_KEY, true) ? 'checked' : ''} />
Auto-Censor Words (e.g. crac.k)
</label>
</div>
<div class="kad-settings-section" style="border-color: #ef4444; background: #fef2f2;">
<div class="kad-settings-title" style="color: #ef4444;">Danger Zone</div>
<button id="nukeBoardBtn" class="kad-btn kad-btn-red" style="width: 100%; padding: 6px;">Clear All Timers</button>
</div>
<div class="kad-modal-actions">
<button id="closeSettingsBtn" class="kad-btn kad-btn-primary" style="width: 100%;">Done</button>
</div>
</div>
</div>
`;
$('body').append(editModalHtml).append(settingsModalHtml);
// Edit Modal Events
$('#cancelEditBtn').on('click', closeEditModal);
$('#saveEditBtn').on('click', saveEditModal);
// Settings Modal Events
$('#closeSettingsBtn').on('click', () => $('#kadSettingsModal').hide());
$('#settingTheme').on('change', function() {
let val = $(this).val();
GM_setValue(KAD_THEME_KEY, val);
document.documentElement.setAttribute("data-kad-theme", val);
});
$('#settingAudioPing').on('change', function() { GM_setValue(KAD_AUDIO_PING_KEY, this.checked); });
$('#setting24h').on('change', function() { GM_setValue(KAD_24H_TIME_KEY, this.checked); tickTimers(); });
$('#settingIncludeFood').on('change', function() { GM_setValue(KAD_INCLUDE_FOOD_KEY, this.checked); });
$('#settingAutoCensor').on('change', function() { GM_setValue(KAD_AUTO_CENSOR_KEY, this.checked); });
$('#settingWarnTime').on('change', function() { GM_setValue(KAD_WARNING_TIME_KEY, parseInt($(this).val(), 10)); });
$('#nukeBoardBtn').on('click', function() {
if(confirm("Are you sure you want to wipe all tracked timers? This cannot be undone.")) {
GM_setValue(LAST_REFRESH_TIME_KEY, 0);
GM_setValue(MAIN_KAD_DATA_KEY, "{}");
GM_setValue(MINI_REFRESH_TIMES_KEY, "[]");
GM_setValue(PENDING_DROP_KEY, "{}");
$('#pendingDropAlert').hide().empty();
$('#kadSettingsModal').hide();
tickTimers();
}
});
// Universal Minimize
$('.minimize-modal-btn').on('click', function() {
let target = $($(this).data('target'));
target.slideToggle(200);
});
// Range Selection logic for Edit Modal
$(document).on('click', '#kadaotiesTable td', function(e) {
if ($('#kadEditModal').is(':visible')) {
e.preventDefault();
e.stopPropagation();
let targetCell = $(this).closest('td');
let idx = $("#kadaotiesTable td").index(targetCell);
if (idx === -1) return;
if (firstClickedIndex === null) {
// First click sets the anchor
firstClickedIndex = idx;
let kadName = targetCell.find('strong').first().text().trim() || "[Blank]";
$('#editNames').val(kadName);
$('#editCount').val(1);
$('#kadaotiesTable td').css({'outline': '', 'background': ''});
targetCell.css({'outline': '2px solid var(--kw-primary)', 'background': 'var(--kw-panel)'});
$('#editModalTitle').text("Select last Kad...");
$('#editModalHeaderHint').text("[Click last Kad]");
} else {
// Second click selects the range
let start = Math.min(firstClickedIndex, idx);
let end = Math.max(firstClickedIndex, idx);
let names = [];
$('#kadaotiesTable td').css({'outline': '', 'background': ''});
$("#kadaotiesTable td").slice(start, end + 1).each(function() {
let kName = $(this).find('strong').first().text().trim() || "[Blank]";
names.push(kName);
$(this).css({'outline': '2px solid var(--kw-primary)', 'background': 'var(--kw-panel)'});
});
$('#editNames').val(names.join(', '));
$('#editCount').val(names.length);
firstClickedIndex = null;
$('#editModalTitle').text("Data Override");
$('#editModalHeaderHint').text("[Range selected]");
setTimeout(() => {
if (firstClickedIndex === null && $('#kadEditModal').is(':visible')) {
$('#editModalHeaderHint').text("[Click board to replace]");
}
}, 2000);
}
}
});
}
function setupDragLogic() {
$(document).on('mousedown', '.kad-modal-drag', function(e) {
if ($(e.target).is('button') || $(e.target).closest('button').length) return;
isDraggingModal = true;
dragTarget = $(this).closest('.kad-modal');
dragOffsetX = e.pageX - dragTarget.offset().left;
dragOffsetY = e.pageY - dragTarget.offset().top;
$('body').css('user-select', 'none');
});
$(document).on('mousemove', function(e) {
if (isDraggingModal && dragTarget) {
dragTarget.css({
left: (e.pageX - dragOffsetX) + 'px',
top: (e.pageY - dragOffsetY) + 'px'
});
}
});
$(document).on('mouseup', function() {
if (isDraggingModal) {
isDraggingModal = false;
dragTarget = null;
$('body').css('user-select', '');
}
});
}
function openSettingsModal(e) {
if (e && e.currentTarget) {
let btnOffset = $(e.currentTarget).offset();
$('#kadSettingsModal').css({
top: (btnOffset.top + 30) + 'px',
left: btnOffset.left + 'px',
display: 'flex'
});
} else {
$('#kadSettingsModal').css({ top: '20%', left: '50%', display: 'flex' });
}
$('#kadSettingsModalContent').show();
}
function openEditModal(type, index = 0, e = null) {
editingTarget = { type, index };
let data = {};
let timeMs = 0;
if (type === 'main') {
timeMs = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
try { data = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(err){}
$('#editModalTitle').text("Data Override // MAIN");
} else {
let minis = getMinis();
if (minis[index]) {
timeMs = minis[index].time;
data = minis[index];
}
$('#editModalTitle').text(`Data Override // MINI ${index + 1}`);
}
firstClickedIndex = null;
$('#editModalHeaderHint').text("[Click board to replace]");
let d = new Date(timeMs);
let seconds = timeMs > 0 ? d.getSeconds() : 0;
let names = data.names || [];
let foods = data.foods || [];
$('#editCount').val(data.count || 0);
$('#editSeconds').val(seconds);
$('#editNames').val(names.join(', '));
$('#editFoods').val(foods.join(', '));
$('#kadaotiesTable td').each(function() {
let cellName = $(this).find('strong').first().text().trim() || "[Blank]";
if (names.includes(cellName)) {
$(this).css({'outline': '2px solid var(--kw-primary)', 'background': 'var(--kw-panel)'});
} else {
$(this).css({'outline': '', 'background': ''});
}
$(this).addClass('kad-selectable-cell');
});
if (e && e.currentTarget) {
let targetBtn = $(e.currentTarget);
let btnOffset = targetBtn.offset();
let modalLeft = btnOffset.left - 420 + targetBtn.outerWidth();
if (modalLeft < 10) modalLeft = 10;
$('#kadEditModal').css({
top: (btnOffset.top + 30) + 'px',
left: modalLeft + 'px',
display: 'flex'
});
} else {
$('#kadEditModal').css({ top: '20%', left: '50%', display: 'flex' });
}
$('#kadEditModalContent').show();
}
function closeEditModal() {
$('#kadEditModal').hide();
firstClickedIndex = null;
$('#kadaotiesTable td').css({'outline': '', 'background': ''}).removeClass('kad-selectable-cell');
}
function checkBoardLockState() {
let username = "";
let modernNav = $('.nav-profile-dropdown-text').first().text().trim();
if (modernNav) username = modernNav;
else {
let classicHref = $("a[href^='/userlookup.phtml?user=']").first().attr("href");
if (classicHref) {
let match = classicHref.match(/user=([^&]+)/);
if (match) username = match[1];
}
}
userIsLockedOut = false;
if (username) {
let fedItText = username + " has been fed";
let hasHungryKads = false;
$("#kadaotiesTable td").each(function() {
let text = $(this).text();
if (text.includes(fedItText)) userIsLockedOut = true;
if (text.includes("is very sad")) hasHungryKads = true;
});
if (userIsLockedOut && hasHungryKads) $('#alreadyFedAlert').show();
else $('#alreadyFedAlert').hide();
}
}
function formatKadLabel(type, count, first, last) {
if (!count || count === 0) return type;
if (count === 1) return `${type} (1) ${first}`;
return `${type} (${count}) ${first} › ${last}`;
}
function getCurrentKadStates() {
let states = {};
$("#kadaotiesTable td").slice(0, 20).each(function(index) {
let text = $(this).text();
let strongs = $(this).find('strong');
let kadName = strongs.first().text().trim() || "[Blank]";
if (text.includes("is very sad")) {
let foodName = strongs.length > 1 ? strongs.last().text().trim() : "";
states[index] = { name: kadName, food: foodName, status: "sad" };
} else if (text.includes("has been fed")) {
states[index] = { name: kadName, food: "", status: "fed" };
}
});
return states;
}
function checkForKadaotieRefresh() {
let currentState = getCurrentKadStates();
let prevStateString = GM_getValue(KAD_STATES_KEY, "{}");
let prevState = JSON.parse(prevStateString);
let isInitialLoad = GM_getValue(KAD_FIRST_RUN_KEY, true);
GM_setValue(KAD_FIRST_RUN_KEY, false);
let minIdx = 99;
let maxIdx = -1;
for (let i = 0; i < 20; i++) {
if (currentState[i] && currentState[i].status === "sad") {
let isNew = !prevState[i] || prevState[i].status !== "sad" || prevState[i].name !== currentState[i].name;
if (isNew) {
if (i < minIdx) minIdx = i;
if (i > maxIdx) maxIdx = i;
}
}
}
GM_setValue(KAD_STATES_KEY, JSON.stringify(currentState));
if (isInitialLoad || minIdx > maxIdx) {
return;
}
let now = new Date().getTime();
// 5-minute buffer check to prevent false splits immediately after feeding
let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
let minis = getMinis();
let highestLoggedTime = mainTime;
for (let m of minis) {
if (m.time > highestLoggedTime) highestLoggedTime = m.time;
}
if (now - highestLoggedTime < 300000) {
return;
}
let activeDrop = {};
try { activeDrop = JSON.parse(GM_getValue(PENDING_DROP_KEY, "{}")); } catch(e) {}
if (activeDrop.time && (now - activeDrop.time < 300000)) {
return;
}
let dropNames = [];
let dropFoods = [];
for (let i = minIdx; i <= maxIdx; i++) {
dropNames.push(currentState[i].name);
dropFoods.push(currentState[i].food || "Fed");
}
let count = (maxIdx - minIdx) + 1;
GM_setValue(PENDING_DROP_KEY, JSON.stringify({
time: now,
count: count,
names: dropNames,
foods: dropFoods,
indices: [minIdx, maxIdx]
}));
renderPendingDrop();
}
function renderPendingDrop() {
let drop = {};
try { drop = JSON.parse(GM_getValue(PENDING_DROP_KEY, "{}")); } catch(e) {}
let alert = $('#pendingDropAlert');
if (!drop.time) { alert.hide().empty(); return; }
let minis = getMinis();
let mergeButtons = '';
mergeButtons += `<button class="kad-btn merge-main-btn" style="margin: 0 3px;" title="Assign to Main">→ Main</button>`;
minis.forEach((mini, i) => {
mergeButtons += `<button class="kad-btn merge-mini-btn" data-index="${i}" style="margin: 0 3px;" title="Assign to Mini ${i+1}">→ Mini ${i+1}</button>`;
});
let firstKad = drop.names[0] || '';
let lastKad = drop.count > 1 ? ` › ${drop.names[drop.count - 1]}` : '';
let kadSummary = `${drop.count} Kad${drop.count !== 1 ? 's' : ''} (${firstKad}${lastKad})`;
alert.html(`
🐱 <strong>New drop detected:</strong> ${kadSummary}
${mergeButtons}
<button id="discardDropBtn" class="kad-btn kad-btn-red" style="margin: 0 3px;">Discard</button>
`).show();
}
function pushMainToHistory() {
let currentMainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
if (currentMainTime <= 0) return;
let history = JSON.parse(GM_getValue(MAIN_HISTORY_KEY, "[]"));
let currentData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}"));
history.push({ time: currentMainTime, data: currentData });
if (history.length > 5) history.shift();
GM_setValue(MAIN_HISTORY_KEY, JSON.stringify(history));
}
function mergePendingDrop(type, miniIndex) {
let drop = {};
try { drop = JSON.parse(GM_getValue(PENDING_DROP_KEY, "{}")); } catch(e) {}
if (!drop.time) return;
if (type === 'main') {
pushMainToHistory();
GM_setValue(LAST_REFRESH_TIME_KEY, drop.time);
GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
count: drop.count,
first: drop.names[0],
last: drop.names[drop.count - 1],
names: drop.names,
foods: drop.foods
}));
} else {
let minis = getMinis();
let mini = minis[miniIndex];
if (!mini) return;
minis[miniIndex] = {
...mini,
time: drop.time,
count: drop.count,
first: drop.names[0],
last: drop.names[drop.count - 1],
names: drop.names,
foods: drop.foods
};
saveMinis(minis);
}
GM_setValue(PENDING_DROP_KEY, "{}");
renderPendingDrop();
tickTimers();
}
function discardPendingDrop() {
GM_setValue(PENDING_DROP_KEY, "{}");
renderPendingDrop();
}
function saveEditModal() {
if (!editingTarget) return;
let count = parseInt($('#editCount').val(), 10) || 0;
let seconds = parseInt($('#editSeconds').val(), 10) || 0;
let namesRaw = $('#editNames').val();
let foodsRaw = $('#editFoods').val();
let names = namesRaw ? namesRaw.split(',').map(s => s.trim()).filter(s => s) : [];
let foods = foodsRaw ? foodsRaw.split(',').map(s => s.trim()).filter(s => s) : [];
let first = names.length > 0 ? names[0] : "";
let last = names.length > 0 ? names[names.length - 1] : "";
if (editingTarget.type === 'main') {
let timeMs = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
if (timeMs > 0) {
let d = new Date(timeMs);
d.setSeconds(seconds);
GM_setValue(LAST_REFRESH_TIME_KEY, d.getTime());
}
GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({ count, first, last, names, foods }));
} else {
let minis = getMinis();
let m = minis[editingTarget.index];
if (m) {
let d = new Date(m.time);
d.setSeconds(seconds);
minis[editingTarget.index] = { ...m, time: d.getTime(), count, first, last, names, foods };
saveMinis(minis);
}
}
closeEditModal();
tickTimers();
}
function demoteMainToMini() {
let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
if (!mainTime) return;
let mainData = {};
try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e){}
addMini(mainTime, mainData.count || 0, mainData.first || "", mainData.last || "", mainData.names || [], mainData.foods || []);
let history = JSON.parse(GM_getValue(MAIN_HISTORY_KEY, "[]"));
if (history.length > 0) {
let previous = history.pop();
GM_setValue(MAIN_HISTORY_KEY, JSON.stringify(history));
GM_setValue(LAST_REFRESH_TIME_KEY, previous.time);
GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify(previous.data));
} else {
GM_setValue(LAST_REFRESH_TIME_KEY, 0);
GM_setValue(MAIN_KAD_DATA_KEY, "{}");
}
tickTimers();
}
function promoteMiniToMain(index) {
let minis = getMinis();
if (!minis[index]) return;
pushMainToHistory();
let mini = minis[index];
minis.splice(index, 1);
saveMinis(minis);
GM_setValue(LAST_REFRESH_TIME_KEY, mini.time);
GM_setValue(MAIN_KAD_DATA_KEY, JSON.stringify({
count: mini.count,
first: mini.first,
last: mini.last,
names: mini.names || [],
foods: mini.foods || []
}));
tickTimers();
}
function getMinis() {
try {
let minis = JSON.parse(GM_getValue(MINI_REFRESH_TIMES_KEY, "[]"));
if (!Array.isArray(minis)) return [];
return minis.map(m => (typeof m === 'number') ? { time: m, count: 0, first: "", last: "", names: [], foods: [] } : m);
} catch(e) { return []; }
}
function saveMinis(minisArr) {
GM_setValue(MINI_REFRESH_TIMES_KEY, JSON.stringify(minisArr));
tickTimers();
}
function addMini(timeMs, count = 0, first = "", last = "", names = [], foods = []) {
let minis = getMinis();
minis.push({ time: timeMs, count, first, last, names, foods });
minis.sort((a, b) => a.time - b.time);
saveMinis(minis);
}
function removeMini(index) {
let minis = getMinis();
minis.splice(index, 1);
saveMinis(minis);
}
function tickTimers() {
let nowTime = new Date().getTime();
updateMainUI(nowTime);
renderMinis(nowTime);
}
function getTimerState(lastRefreshTime, currentTime) {
if (!lastRefreshTime || lastRefreshTime === 0) return { status: "expired" };
let mainWindowStart = lastRefreshTime + DURATION_UNTIL_START_OF_MAIN_WINDOW_MS;
let timeSinceMainStart = currentTime - mainWindowStart;
if (timeSinceMainStart < 0) {
return { status: "waiting", nextStart: mainWindowStart, countdown: mainWindowStart - currentTime };
}
let currentCycle = Math.floor(timeSinceMainStart / PEND_INTERVAL_MS);
let currentWindowStart = mainWindowStart + (currentCycle * PEND_INTERVAL_MS);
let currentWindowEnd = currentWindowStart + DURATION_OF_MAIN_WINDOW_MS;
if (currentTime >= currentWindowStart && currentTime < currentWindowEnd) {
return { status: "active", timeRemaining: currentWindowEnd - currentTime };
}
let nextWindowStart = mainWindowStart + ((currentCycle + 1) * PEND_INTERVAL_MS);
return { status: "waiting", nextStart: nextWindowStart, countdown: nextWindowStart - currentTime };
}
function renderTimerHTML(state) {
if (state.status === "expired") return '<span style="color: #ef4444; font-size: 14px;">Log time to start.</span>';
if (state.status === "waiting") return `<span class="kad-time-sub">Next:</span> <span>${formatWindowTime(new Date(state.nextStart))}</span> <span class="kad-divider">|</span> <span class="kad-time-sub">In:</span> <span class="kad-time-hl">${formatCountdown(new Date(state.countdown))}</span>`;
if (state.status === "active") return `<span class="kad-window-active">WINDOW ACTIVE!</span> <span class="kad-divider">|</span> <span class="kad-time-sub">Closes in:</span> <span class="kad-window-active">${Math.round(state.timeRemaining / 1000)}s</span>`;
return '<span style="font-size: 14px; opacity: 0.6;">Unknown state.</span>';
}
function updateMainUI(nowTime) {
let lastTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
let state = getTimerState(lastTime, nowTime);
let container = $('#mainContainer');
if (state.status === "expired" && lastTime === 0) {
container.empty();
return;
}
let label = "Main";
try {
let mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}"));
label = formatKadLabel("Main", mainData.count, mainData.first, mainData.last);
} catch(e) {}
let lastRefreshStr = formatLastRefresh(lastTime);
container.html(`
<div class="kad-tracker-block kad-main-block">
<div class="kad-block-label" title="${label}">${label}</div>
<div class="kad-countdown-box">${renderTimerHTML(state)}</div>
<div class="kad-last-refresh">Last refresh: ${lastRefreshStr}</div>
<button class="kad-btn kad-btn-action kad-btn-edit edit-main-btn" title="Edit Properties">✏️ Edit</button>
<button class="kad-btn kad-btn-action demote-main-btn" title="Convert to Mini">↓ Demote</button>
<button class="kad-btn kad-btn-red kad-btn-remove remove-main-btn" title="Remove Main">✕</button>
</div>
`);
}
function renderMinis(nowTime) {
let minis = getMinis();
let container = $('#minisContainer');
container.empty();
minis.forEach((miniObj, index) => {
let state = getTimerState(miniObj.time, nowTime);
if (state.status === "expired") return;
let label = formatKadLabel(`Mini ${index + 1}`, miniObj.count, miniObj.first, miniObj.last);
let miniLastStr = formatLastRefresh(miniObj.time);
container.append(`
<div class="kad-tracker-block">
<div class="kad-block-label" title="${label}">${label}</div>
<div class="kad-countdown-box">${renderTimerHTML(state)}</div>
<div class="kad-last-refresh">Last refresh: ${miniLastStr}</div>
<button class="kad-btn kad-btn-action kad-btn-edit edit-mini-btn" data-index="${index}" title="Edit Properties">✏️ Edit</button>
<button class="kad-btn kad-btn-action promote-mini-btn" data-index="${index}" title="Set as Main">↑ Promote</button>
<button class="kad-btn kad-btn-red kad-btn-remove remove-mini-btn" data-index="${index}" title="Remove Mini">✕</button>
</div>
`);
});
}
function handleManualTime(type) {
let val = $('#manualTimeInput').val();
let newTime = parseTimeInput(val);
if(newTime) {
if (type === 'main') {
pushMainToHistory();
GM_setValue(LAST_REFRESH_TIME_KEY, newTime.getTime());
GM_setValue(MAIN_KAD_DATA_KEY, "{}");
} else {
addMini(newTime.getTime());
}
$('#manualTimeInput').val('');
tickTimers();
}
}
function parseTimeInput(val) {
let inputtedTimes = val.split(":");
if (inputtedTimes.length < 2) return null;
let hour = parseInt(inputtedTimes[0], 10);
let min = parseInt(inputtedTimes[1], 10);
let sec = inputtedTimes.length > 2 ? parseInt(inputtedTimes[2], 10) : 0;
if (isNaN(hour) || isNaN(min) || isNaN(sec)) return null;
let now = new Date();
let formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Los_Angeles',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
});
let parts = formatter.formatToParts(now);
let p = {};
for (let part of parts) {
p[part.type] = part.value;
}
let currentNstHour = parseInt(p.hour, 10);
if (currentNstHour === 24) currentNstHour = 0;
if (hour < 12 && currentNstHour >= 12) {
if (Math.abs((hour + 12) - currentNstHour) < Math.abs(hour - currentNstHour)) {
hour += 12;
}
}
let pad = (num) => String(num).padStart(2, '0');
let nstIsoString = `${p.year}-${p.month}-${p.day}T${pad(currentNstHour)}:${p.minute}:${p.second}`;
let nstSimulatedNow = new Date(nstIsoString);
let nstSimulatedTarget = new Date(nstIsoString);
nstSimulatedTarget.setHours(hour, min, sec, 0);
if (currentNstHour <= 1 && hour >= 22) {
nstSimulatedTarget.setDate(nstSimulatedTarget.getDate() - 1);
} else if (currentNstHour >= 22 && hour <= 1) {
nstSimulatedTarget.setDate(nstSimulatedTarget.getDate() + 1);
}
let diffMs = nstSimulatedTarget.getTime() - nstSimulatedNow.getTime();
return new Date(now.getTime() + diffMs);
}
function generateBoardString(label, lastTimeMs, foodNames, now) {
if (!lastTimeMs || lastTimeMs === 0) return null;
let last = new Date(lastTimeMs);
let mainWindowStart = lastTimeMs + DURATION_UNTIL_START_OF_MAIN_WINDOW_MS;
let timesToDisplay = [];
let timeSinceMainStart = now - mainWindowStart;
let currentCycle = Math.floor(timeSinceMainStart / PEND_INTERVAL_MS);
if (currentCycle < 0) currentCycle = 0;
for (let i = currentCycle; i <= currentCycle + 10; i++) {
let windowStart = mainWindowStart + (i * PEND_INTERVAL_MS);
if (now < windowStart + DURATION_OF_MAIN_WINDOW_MS) {
timesToDisplay.push(new Date(windowStart));
}
if (timesToDisplay.length >= 6) break;
}
if (timesToDisplay.length === 0) return null;
let lastStr = formatWindowTime(last);
let nextStr = formatWindowTime(timesToDisplay[0]);
let pends = [];
for(let i = 1; i < timesToDisplay.length; i++) {
pends.push(":" + formatTwoDigits(timesToDisplay[i].getMinutes()));
}
let pendsStr = pends.length > 0 ? " / " + pends.join(" / ") : "";
let includeFood = GM_getValue(KAD_INCLUDE_FOOD_KEY, true);
let foodLine = (includeFood && foodNames && foodNames.length > 0)
? "\n\n" + foodNames.map(censorFood).reduce((acc, food, i) => acc + (i > 0 ? (i % 4 === 0 ? "\n\n" : "\n") : "") + food, "")
: '';
return `${label} @ ${lastStr}\nNext ${nextStr}${pendsStr}${foodLine}`;
}
function copyBoardTimes() {
let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
let minis = getMinis();
let now = new Date().getTime();
let postArray = [];
let mainState = getTimerState(mainTime, now);
if(mainState.status !== "expired" && mainTime > 0) {
let mainData = {};
try { mainData = JSON.parse(GM_getValue(MAIN_KAD_DATA_KEY, "{}")); } catch(e){}
let label = formatKadLabel("Main", mainData.count, mainData.first, mainData.last);
postArray.push(generateBoardString(label, mainTime, mainData.foods || [], now));
}
minis.forEach((miniObj, index) => {
let state = getTimerState(miniObj.time, now);
if(state.status !== "expired") {
let label = formatKadLabel(`Mini ${index + 1}`, miniObj.count, miniObj.first, miniObj.last);
postArray.push(generateBoardString(label, miniObj.time, miniObj.foods || [], now));
}
});
if(postArray.length === 0) {
let btn = $('#copyBoardBtn');
btn.text("Nothing to copy");
setTimeout(() => btn.text("📋 Copy Post"), 1800);
return;
}
navigator.clipboard.writeText(postArray.join("\n\n")).then(() => {
let btn = $('#copyBoardBtn');
let oldText = btn.text();
btn.text("✅ Copied!");
setTimeout(() => { btn.text(oldText); }, 1500);
});
}
function addClickToCopyFeature() {
$("#kadaotiesTable td:contains('is very sad')").each(function() {
let foodTag = $(this).find('strong').last();
if (foodTag.length && !foodTag.hasClass('kad-food-copy')) {
foodTag.addClass('kad-food-copy');
foodTag.attr('title', userIsLockedOut ? 'You are locked out!' : 'Click to copy to clipboard');
foodTag.on('click', function(e) {
if ($('#kadEditModal').is(':visible') && $('#kadEditModalContent').is(':visible')) return;
e.preventDefault();
if (userIsLockedOut) {
let originalText = $(this).text();
$(this).text('Locked!');
$(this).css({ 'color': '#ef4444', 'border-color': '#ef4444' });
setTimeout(() => {
$(this).text(originalText);
$(this).css({ 'color': '', 'border-color': '' });
}, 1000);
return;
}
let foodName = $(this).text();
navigator.clipboard.writeText(foodName).then(() => {
$(this).text('Copied!');
$(this).css({ 'color': '#10b981', 'border-color': '#10b981' });
setTimeout(() => {
$(this).text(foodName);
$(this).css({ 'color': '', 'border-color': '' });
}, 800);
});
});
}
});
}
function updateNotifyUI() {
let optedIn = GM_getValue(NOTIFICATION_OPT_IN_KEY, false);
$('#settingDesktopNotif').prop('checked', optedIn);
if (optedIn) {
$('#toggleNotifyBtn').text('🔔 Alerts: ON').css({'background':'#10b981', 'color':'#fff', 'border':'none'}).show();
} else {
$('#toggleNotifyBtn').text('🔕 Alerts: OFF').css({'background':'transparent', 'color':'var(--kw-text)', 'border':'1px solid var(--kw-secondary)'}).show();
}
}
function setupNotificationSupport() {
if ("Notification" in window) {
updateNotifyUI();
$('#toggleNotifyBtn').off('click').on('click', function() {
let current = GM_getValue(NOTIFICATION_OPT_IN_KEY, false);
if (!current) {
if (Notification.permission === "granted") {
GM_setValue(NOTIFICATION_OPT_IN_KEY, true);
updateNotifyUI();
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
GM_setValue(NOTIFICATION_OPT_IN_KEY, true);
updateNotifyUI();
}
});
} else {
alert("You have blocked notifications for Neopets in your browser settings. Please enable them to use this feature.");
}
} else {
GM_setValue(NOTIFICATION_OPT_IN_KEY, false);
updateNotifyUI();
}
});
$('#settingDesktopNotif').off('change').on('change', function() {
if (this.checked) {
if (Notification.permission === "granted") {
GM_setValue(NOTIFICATION_OPT_IN_KEY, true);
updateNotifyUI();
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
GM_setValue(NOTIFICATION_OPT_IN_KEY, true);
} else {
GM_setValue(NOTIFICATION_OPT_IN_KEY, false);
$(this).prop('checked', false);
}
updateNotifyUI();
});
} else {
alert("Notifications are blocked in your browser settings.");
$(this).prop('checked', false);
}
} else {
GM_setValue(NOTIFICATION_OPT_IN_KEY, false);
updateNotifyUI();
}
});
}
setInterval(() => {
let nowTime = new Date().getTime();
let mainTime = GM_getValue(LAST_REFRESH_TIME_KEY, 0);
let minis = getMinis();
let state = getTimerState(mainTime, nowTime);
if (state.status === "waiting") checkAndTriggerNotification('main', state.countdown);
minis.forEach((miniObj, index) => {
let mState = getTimerState(miniObj.time, nowTime);
if (mState.status === "waiting") checkAndTriggerNotification(`mini_${index}`, mState.countdown);
});
}, 1000);
}
function checkAndTriggerNotification(type, countdownRemainingMs) {
if (!GM_getValue(NOTIFICATION_OPT_IN_KEY, false)) return;
let warningTimeMs = GM_getValue(KAD_WARNING_TIME_KEY, 10) * 1000;
if (countdownRemainingMs <= warningTimeMs && countdownRemainingMs > (warningTimeMs - 1000)) {
let nowMs = new Date().getTime();
if (nowMs - (notificationsTriggered[type] || 0) > 240000) {
notificationsTriggered[type] = nowMs;
let title = type === 'main' ? "Main Refresh Incoming!" : "Mini Refresh Incoming!";
if ("Notification" in window && Notification.permission === "granted") {
showNotification(title, `Window starts in ${warningTimeMs / 1000} seconds.`);
}
if (GM_getValue(KAD_AUDIO_PING_KEY, false)) {
let ping = new Audio('https://actions.google.com/sounds/v1/alarms/beep_short.ogg');
ping.volume = 0.5;
ping.play().catch(()=>{});
}
}
}
}
function showNotification(title, body) {
let notification = new Notification(title, {
body: body,
icon: "https://itemdb.com.br/api/cache/preview/7f18f78e35daa6.png"
});
notification.onclick = () => {
notification.close();
window.focus();
}
}
function formatTwoDigits(n) { return n < 10 ? '0' + n : n; }
function formatCountdown(d) { return formatTwoDigits(d.getMinutes()) + ":" + formatTwoDigits(d.getSeconds()); }
function formatWindowTime(d) {
let is24h = GM_getValue(KAD_24H_TIME_KEY, false);
let nstStr = d.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: !is24h });
return nstStr.replace(/ /g, '').toLowerCase();
}
function formatLastRefresh(ms) {
if (!ms || ms === 0) return '';
let d = new Date(ms);
let is24h = GM_getValue(KAD_24H_TIME_KEY, false);
let nstStr = d.toLocaleString("en-US", { timeZone: "America/Los_Angeles", hour: "numeric", minute: "2-digit", second: "2-digit", hour12: !is24h });
return nstStr.replace(/ /g, '').toLowerCase();
}