Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/lib/calendarUrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export function buildCalendarCreateUrl(event) {
if (isAllDay) {
return addDays(startDate, 1);
}
if (!explicitEnd || explicitEnd <= startDate) {
return new Date(startDate.getTime() + 60 * 60 * 1000);
}
return explicitEnd;
}

Expand Down
36 changes: 27 additions & 9 deletions src/lib/icsGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,35 @@ function escapeICS(text) {
.replace(/\r/g, '');
}

// Helper to fold lines longer than 75 characters (per RFC 5545)
// Helper to fold lines longer than 75 octets (per RFC 5545)
function foldLine(line) {
if (line.length <= 75) return line;
const encoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;

if (!encoder || encoder.encode(line).length <= 75) return line;

const folded = [];
let currentLine = line;
let remaining = line;

while (remaining.length > 0) {
const maxChars = folded.length === 0 ? 75 : 74;
let cutPoint = remaining.length;

for (let i = Math.min(cutPoint, maxChars); i > 0; i--) {
const chunk = remaining.substring(0, i);
if (encoder.encode(chunk).length <= maxChars) {
cutPoint = i;
break;
}
}

while (currentLine.length > 75) {
folded.push(currentLine.substring(0, 75));
currentLine = ' ' + currentLine.substring(75);
if (cutPoint === 0) cutPoint = 1;

folded.push(remaining.substring(0, cutPoint));
remaining = remaining.substring(cutPoint);
if (remaining.length > 0) {
remaining = ' ' + remaining;
}
}
folded.push(currentLine);

return folded.join(CRLF);
}
Expand Down Expand Up @@ -114,9 +131,10 @@ function generateVEVENT(event) {
lines.push(`DTSTART;VALUE=DATE:${formatICSAllDayDate(startDate)}`);
lines.push(`DTEND;VALUE=DATE:${formatICSAllDayDate(endDate)}`);
} else {
const endDate = event.endTime
const parsedEnd = event.endTime
? toDate(event.endDate || event.startDate, event.endTime)
: addHours(startDate, 1);
: null;
const endDate = parsedEnd || addHours(startDate, 1);
lines.push(`DTSTART:${formatICSDateTime(startDate)}`);
lines.push(`DTEND:${formatICSDateTime(endDate)}`);
}
Expand Down
35 changes: 18 additions & 17 deletions src/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,16 +355,12 @@ function showSkeletonCards(count = 3) {
}

// Ensure Settings button works immediately on popup load
settingsBtn?.addEventListener(
"click",
() => {
try {
const url = chrome.runtime.getURL("src/ui/settings.html");
chrome.tabs.create({ url });
} catch (_) { }
},
{ once: true }
);
settingsBtn?.addEventListener("click", () => {
try {
const url = chrome.runtime.getURL("src/ui/settings.html");
chrome.tabs.create({ url });
} catch (_) { }
});

// Quota exceeded panel - link to settings with BYOK section
const quotaSettingsBtn = document.getElementById("quotaSettingsBtn");
Expand Down Expand Up @@ -547,14 +543,19 @@ function transitionFromSkeletonsToResults(events) {

// Wait for fade out, then render event cards
setTimeout(async () => {
upcomingEventsListEl.innerHTML = "";
if (pastEventsListEl) pastEventsListEl.innerHTML = "";
await renderEvents(events);
try {
upcomingEventsListEl.innerHTML = "";
if (pastEventsListEl) pastEventsListEl.innerHTML = "";
await renderEvents(events);

// Transition to results loaded state
// This removes 'scanning' class and adds 'has-results'
// Button appears smoothly via CSS transitions
setState(UI_STATE.RESULTS_LOADED);
// Transition to results loaded state
// This removes 'scanning' class and adds 'has-results'
// Button appears smoothly via CSS transitions
setState(UI_STATE.RESULTS_LOADED);
} catch (e) {
error("Failed to render events:", e);
setState(UI_STATE.IDLE);
}
}, SKELETON_FADE_DURATION_MS + skeletons.length * SKELETON_STAGGER_DELAY_MS);
}

Expand Down
16 changes: 15 additions & 1 deletion src/popup/dateTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,24 @@ export function isEventPast(event, { now = new Date(), warn = () => { }, error =
}
}

function normalizeTimeStr(timeStr) {
if (!timeStr) return null;
const match = timeStr.match(/(\d{1,2})(?::(\d{2}))?(?::(\d{2}))?\s*([AP]M)/i);
if (!match) return timeStr;
let hours = parseInt(match[1], 10);
const minutes = match[2] ? match[2] : "00";
const seconds = match[3] ? match[3] : "00";
const meridiem = match[4].toUpperCase();
if (meridiem === "PM" && hours !== 12) hours += 12;
else if (meridiem === "AM" && hours === 12) hours = 0;
return `${String(hours).padStart(2, "0")}:${minutes}:${seconds}`;
}

export function formatDateTime(dateStr, timeStr, timeFormat = "12") {
try {
if (!dateStr) return "";
const iso = `${dateStr}${timeStr ? `T${timeStr}` : ""}`;
const normalizedTime = normalizeTimeStr(timeStr);
const iso = `${dateStr}${normalizedTime ? `T${normalizedTime}` : ""}`;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) {
return `${dateStr}${timeStr ? ` ${timeStr}` : ""}`;
Expand Down
9 changes: 9 additions & 0 deletions src/popup/scanPolling.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function createScanPoller({
} = {}) {
const pollTimeouts = new Map();

let deferredTimeoutId = null;

async function pollForResults(url, storageKey, onComplete, onError, options = {}) {
let currentInterval = SCAN_POLL_INITIAL_INTERVAL_MS;
let attempts = 0;
Expand All @@ -35,6 +37,7 @@ export function createScanPoller({

const cleanup = () => {
if (url) pollTimeouts.delete(url);
else deferredTimeoutId = null;
};

const poll = async () => {
Expand Down Expand Up @@ -109,6 +112,7 @@ export function createScanPoller({

const timeoutId = setTimeoutFn(poll, currentInterval);
if (url) pollTimeouts.set(url, timeoutId);
else deferredTimeoutId = timeoutId;
} catch (err) {
error(err);
cleanup();
Expand All @@ -118,6 +122,7 @@ export function createScanPoller({

const initialTimeoutId = setTimeoutFn(poll, currentInterval);
if (url) pollTimeouts.set(url, initialTimeoutId);
else deferredTimeoutId = initialTimeoutId;
}

function clearForUrl(url) {
Expand All @@ -131,6 +136,10 @@ export function createScanPoller({
if (timeout) clearTimeoutFn(timeout);
}
pollTimeouts.clear();
if (deferredTimeoutId) {
clearTimeoutFn(deferredTimeoutId);
deferredTimeoutId = null;
}
}

return {
Expand Down
30 changes: 0 additions & 30 deletions src/ui/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,3 @@ export function applyTheme(theme) {
});
});
}

// Self-executing function to load theme immediately
// This replaces the separate theme-loader.js file
(async () => {
if (typeof chrome === 'undefined' || !chrome.storage) return;

try {
const result = await chrome.storage.sync.get("settings");
const settings = result.settings || {};
const theme = settings.darkModeSettings || "auto";

const body = document.body;
if (body) {
body.classList.remove("light-mode", "dark-mode", "auto-mode");
body.classList.add(theme === "light" ? "light-mode" : theme === "dark" ? "dark-mode" : "auto-mode");
}
} catch (error) {
console.error("Error loading theme:", error);
if (document.body) {
document.body.classList.add("auto-mode");
}
} finally {
if (document.body) {
document.body.classList.remove("theme-loading");
requestAnimationFrame(() => {
document.body.classList.add("theme-loaded");
});
}
}
})();