Buttons can carry a Timer Tags select value and prompt for an optional description before starting. Tag options and the prompt toggle are configured per-button in the property inspector; startTimer writes the tag to the Timer Tags select property and the description to the Description rich_text property when set.
392 lines
12 KiB
HTML
392 lines
12 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;
|
|
}
|
|
.info-icon {
|
|
color: #888;
|
|
font-size: 13px;
|
|
cursor: help;
|
|
}
|
|
.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>
|
|
<div class="row">
|
|
<label>Ask description (optional)</label>
|
|
<input type="checkbox" id="askDescription" style="flex: none; width: auto;">
|
|
<span class="info-icon" title="Prompt for a description when starting this button's timer.">ⓘ</span>
|
|
</div>
|
|
<div class="row">
|
|
<label>Use tag (Pete only)</label>
|
|
<input type="checkbox" id="tagEnabled" style="flex: none; width: auto;">
|
|
</div>
|
|
<div class="row" id="tagRow">
|
|
<label></label>
|
|
<select id="tagSelect">
|
|
<option value="">— No tag —</option>
|
|
</select>
|
|
</div>
|
|
<p class="hint" id="tagHint">Recorded as the entry's Tag in Notion and shown on the button.</p>
|
|
|
|
<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 = [];
|
|
var cachedTags = [];
|
|
|
|
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);
|
|
globalUserId = creds.userId;
|
|
setCredStatus("Credentials saved.", "ok");
|
|
}
|
|
|
|
function scheduleCredSave() {
|
|
clearTimeout(credSaveTimer);
|
|
credSaveTimer = setTimeout(function() {
|
|
saveCredentials();
|
|
var token = document.getElementById("notionToken").value.trim();
|
|
if (token) {
|
|
setStatus("Loading…", "");
|
|
$PI.sendToPlugin({ event: "refreshProjects", token: token });
|
|
}
|
|
}, 600);
|
|
}
|
|
|
|
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 populateTags(tags) {
|
|
cachedTags = tags;
|
|
var sel = document.getElementById("tagSelect");
|
|
sel.innerHTML = '<option value="">— No tag —</option>';
|
|
var values = tags.slice();
|
|
// A tag saved on this button before it existed as a Notion select option
|
|
// (or since removed there) must still show up, selected, instead of
|
|
// silently reverting to "No tag".
|
|
if (currentSettings.tag && values.indexOf(currentSettings.tag) === -1) {
|
|
values.push(currentSettings.tag);
|
|
}
|
|
values.forEach(function(t) {
|
|
var opt = document.createElement("option");
|
|
opt.value = t;
|
|
opt.textContent = t;
|
|
if (currentSettings.tag === t) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
function setTagRowVisible(visible) {
|
|
document.getElementById("tagRow").style.display = visible ? "" : "none";
|
|
document.getElementById("tagHint").style.display = visible ? "" : "none";
|
|
}
|
|
|
|
function save() {
|
|
var sel = document.getElementById("projectSelect");
|
|
var opt = sel.options[sel.selectedIndex];
|
|
currentSettings.projectId = sel.value;
|
|
currentSettings.projectName = sel.value ? opt.textContent.trim() : "";
|
|
currentSettings.tagEnabled = document.getElementById("tagEnabled").checked;
|
|
currentSettings.tag = currentSettings.tagEnabled ? document.getElementById("tagSelect").value : "";
|
|
currentSettings.askDescription = document.getElementById("askDescription").checked;
|
|
$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 || {};
|
|
document.getElementById("tagEnabled").checked = !!currentSettings.tagEnabled;
|
|
setTagRowVisible(!!currentSettings.tagEnabled);
|
|
document.getElementById("askDescription").checked = !!currentSettings.askDescription;
|
|
$PI.getSettings();
|
|
$PI.getGlobalSettings();
|
|
|
|
document.getElementById("projectSelect").addEventListener("change", save);
|
|
document.getElementById("tagEnabled").addEventListener("change", function() {
|
|
setTagRowVisible(this.checked);
|
|
save();
|
|
});
|
|
document.getElementById("tagSelect").addEventListener("change", save);
|
|
document.getElementById("askDescription").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;
|
|
}
|
|
document.getElementById("tagEnabled").checked = !!currentSettings.tagEnabled;
|
|
setTagRowVisible(!!currentSettings.tagEnabled);
|
|
populateTags(cachedTags);
|
|
document.getElementById("askDescription").checked = !!currentSettings.askDescription;
|
|
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.tags) {
|
|
populateTags(payload.tags);
|
|
}
|
|
if (payload.error) {
|
|
setStatus(payload.error, "error");
|
|
} else {
|
|
populateProjects(payload.data);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|