Files
stream_deck_notion_timer/com.pdma.notion-timer.sdPlugin/ui/property-inspector.html
pdmarf ccc6d90578 v1.0.29: fix userId not restoring due to race condition
onSendToPropertyInspector (users list) arrives before
onDidReceiveGlobalSettings (saved userId) in most cases, leaving
globalUserId empty when populateUsers runs. Now cachedUsers stores
the list, and onDidReceiveGlobalSettings re-populates if users
already arrived — handles both orderings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 08:53:15 +01:00

322 lines
9.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 13px;
color: #ccc;
background: transparent;
padding: 8px;
}
.section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #888;
margin: 14px 0 8px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 4px;
}
.section-title:first-child { margin-top: 0; }
.section-title .arrow { font-size: 9px; }
.collapsible { overflow: hidden; }
.collapsible.collapsed { display: none; }
.row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
label {
width: 90px;
flex-shrink: 0;
color: #999;
font-size: 12px;
}
select, input[type="text"], input[type="password"] {
flex: 1;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 4px;
color: #eee;
padding: 5px 8px;
font-size: 12px;
outline: none;
}
.hint {
font-size: 11px;
color: #555;
margin-top: -4px;
margin-bottom: 8px;
padding-left: 98px;
line-height: 1.4;
}
.divider {
border: none;
border-top: 1px solid #333;
margin: 10px 0;
}
#statusText {
font-size: 11px;
color: #888;
text-align: center;
padding-top: 4px;
min-height: 16px;
}
#statusText.running { color: #4caf50; }
#statusText.error { color: #e57373; }
#versionText {
font-size: 12px;
color: #fff;
text-align: center;
padding-top: 8px;
}
#credStatus {
font-size: 11px;
color: #888;
text-align: center;
padding-top: 2px;
margin-bottom: 6px;
min-height: 14px;
}
#credStatus.ok { color: #4caf50; }
#credStatus.error { color: #e57373; }
#updateBtn {
display: block;
width: 100%;
margin-top: 10px;
padding: 6px;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 4px;
color: #ccc;
font-size: 12px;
cursor: pointer;
}
#updateBtn:hover { background: #333; }
#updateStatus {
font-size: 11px;
color: #888;
text-align: center;
margin-top: 4px;
min-height: 14px;
}
</style>
</head>
<body>
<!-- Credentials (collapsed by default once configured) -->
<p class="section-title" id="credToggle">
<span class="arrow" id="credArrow"></span> Notion Credentials
</p>
<div class="collapsible collapsed" id="credSection">
<div class="row">
<label>API Token</label>
<input type="password" id="notionToken" placeholder="ntn_…">
</div>
<div class="row">
<label>Your Name</label>
<select id="userId">
<option value="">— Select your name —</option>
</select>
</div>
<p class="hint">Shared across all buttons. Select once per device.</p>
<p id="credStatus"></p>
<hr class="divider">
</div>
<!-- Per-button settings -->
<p class="section-title" style="cursor:default;">
<span class="arrow" style="visibility:hidden;"></span> Button
</p>
<div class="row">
<label>Project</label>
<select id="projectSelect">
<option value="">Loading projects…</option>
</select>
</div>
<p id="statusText"></p>
<p id="versionText"></p>
<button id="updateBtn">Check for Updates</button>
<p id="updateStatus"></p>
<script src="libs/constants.js"></script>
<script src="libs/prototypes.js"></script>
<script src="libs/timers.js"></script>
<script src="libs/utils.js"></script>
<script src="libs/events.js"></script>
<script src="libs/api.js"></script>
<script src="libs/property-inspector.js"></script>
<script>
var ACTION_UUID = "com.pdma.notion-timer.toggle";
var currentSettings = {};
var credSaveTimer = null;
var credConfigured = false;
var globalUserId = "";
var cachedUsers = [];
function setStatus(msg, cls) {
var el = document.getElementById("statusText");
el.textContent = msg;
el.className = cls || "";
}
function setCredStatus(msg, cls) {
var el = document.getElementById("credStatus");
el.textContent = msg;
el.className = cls || "";
}
// Collapsible credentials section
document.getElementById("credToggle").addEventListener("click", function() {
var section = document.getElementById("credSection");
var arrow = document.getElementById("credArrow");
if (section.classList.contains("collapsed")) {
section.classList.remove("collapsed");
arrow.textContent = "▼";
} else {
section.classList.add("collapsed");
arrow.textContent = "▶";
}
});
function saveCredentials() {
var creds = {
notionToken: document.getElementById("notionToken").value.trim(),
userId: document.getElementById("userId").value,
};
$PI.setGlobalSettings(creds);
setCredStatus("Credentials saved.", "ok");
if (creds.notionToken) {
setStatus("Loading…", "");
$PI.sendToPlugin({ event: "refreshProjects", token: creds.notionToken });
}
}
function populateUsers(users, userId) {
cachedUsers = users;
var sel = document.getElementById("userId");
sel.innerHTML = '<option value="">— Select your name —</option>';
users.forEach(function(u) {
var opt = document.createElement("option");
opt.value = u.id;
opt.textContent = u.name;
if (u.id === userId) opt.selected = true;
sel.appendChild(opt);
});
}
function scheduleCredSave() {
clearTimeout(credSaveTimer);
credSaveTimer = setTimeout(saveCredentials, 600);
}
function save() {
var sel = document.getElementById("projectSelect");
var opt = sel.options[sel.selectedIndex];
currentSettings.projectId = sel.value;
currentSettings.projectName = sel.value ? opt.textContent.trim() : "";
$PI.setSettings(currentSettings);
$PI.sendToPlugin({ event: "saveSettings", settings: currentSettings });
setStatus(currentSettings.projectName ? "Saved: " + currentSettings.projectName : "", "");
}
function populateProjects(projects) {
var sel = document.getElementById("projectSelect");
sel.innerHTML = '<option value="">— Select project —</option>';
projects.forEach(function(p) {
var opt = document.createElement("option");
opt.value = p.id;
opt.textContent = p.name;
if (currentSettings.projectId === p.id) opt.selected = true;
sel.appendChild(opt);
});
if (!currentSettings.projectId) {
setStatus("Select a project to get started.", "");
} else if (currentSettings.activeEntryId) {
setStatus("⏱ Timer running", "running");
} else {
setStatus("", "");
}
}
$PI.onConnected(function(jsn) {
currentSettings = jsn.actionInfo.payload.settings || {};
$PI.getSettings();
$PI.getGlobalSettings();
document.getElementById("projectSelect").addEventListener("change", save);
document.getElementById("notionToken").addEventListener("input", scheduleCredSave);
document.getElementById("userId").addEventListener("change", saveCredentials);
document.getElementById("updateBtn").addEventListener("click", function() {
document.getElementById("updateStatus").textContent = "";
$PI.sendToPlugin({ event: "checkForUpdates" });
});
});
$PI.onDidReceiveGlobalSettings(function(jsn) {
var s = jsn.payload.settings || {};
document.getElementById("notionToken").value = s.notionToken || "";
globalUserId = s.userId || "";
if (globalUserId && cachedUsers.length > 0) {
// Users already loaded — re-populate with correct selection
populateUsers(cachedUsers, globalUserId);
}
credConfigured = !!(s.notionToken && s.userId);
// Auto-collapse if already configured, expand if not
var section = document.getElementById("credSection");
var arrow = document.getElementById("credArrow");
if (credConfigured) {
section.classList.add("collapsed");
arrow.textContent = "▶";
} else {
section.classList.remove("collapsed");
arrow.textContent = "▼";
}
});
$PI.onDidReceiveSettings(ACTION_UUID, function(jsn) {
currentSettings = jsn.payload.settings || {};
var sel = document.getElementById("projectSelect");
if (currentSettings.projectId && sel.options.length > 1) {
sel.value = currentSettings.projectId;
}
if (currentSettings.activeEntryId) {
setStatus("⏱ Timer running", "running");
}
});
$PI.onSendToPropertyInspector(ACTION_UUID, function(jsn) {
var payload = jsn.payload;
if (payload.event === "updateStatus") {
document.getElementById("updateStatus").textContent = payload.message;
}
if (payload.event === "projects") {
if (payload.version) {
document.getElementById("versionText").textContent = "v" + payload.version;
}
if (payload.users) {
populateUsers(payload.users, globalUserId);
}
if (payload.error) {
setStatus(payload.error, "error");
} else {
populateProjects(payload.data);
}
}
});
</script>
</body>
</html>