From 8344a5538e75f9ac652cae040c9c86fdf73d3a29 Mon Sep 17 00:00:00 2001
From: dev-minsoo
Date: Thu, 23 Apr 2026 23:55:55 +0900
Subject: [PATCH 1/5] feat: add synced actions and problem archives
---
src/adapters/leetcode/index.ts | 99 +++++++++--
src/adapters/problemActionsModal.ts | 251 ++++++++++++++++++++++++++++
src/adapters/programmers/index.ts | 133 +++++++++++++--
src/adapters/upload.ts | 23 ++-
src/core/github/client.ts | 29 ++++
src/core/types/messages.ts | 7 +-
src/core/types/upload.ts | 8 +
src/scripts/background/index.ts | 93 +++++++++++
8 files changed, 613 insertions(+), 30 deletions(-)
create mode 100644 src/adapters/problemActionsModal.ts
diff --git a/src/adapters/leetcode/index.ts b/src/adapters/leetcode/index.ts
index 41c192d..a4d400c 100644
--- a/src/adapters/leetcode/index.ts
+++ b/src/adapters/leetcode/index.ts
@@ -7,9 +7,13 @@ import {
import { sendRuntimeMessage } from "../../shared/runtime";
import type { RuntimeMessageResponse } from "../../core/types/messages";
import type { ExtensionSettings } from "../../core/types/domain";
-import type { UploadJob } from "../../core/types/upload";
+import type { ProblemNoteRequest, UploadJob } from "../../core/types/upload";
import type { PlatformAdapter } from "../types";
-import { uploadThroughBackground } from "../upload";
+import { openSyncedActionsModal } from "../problemActionsModal";
+import {
+ appendProblemNoteThroughBackground,
+ uploadThroughBackground,
+} from "../upload";
const SUBMIT_BUTTON_SELECTOR = '[data-e2e-locator="console-submit-button"]';
const SUBMISSION_RESULT_SELECTOR = '[data-e2e-locator="submission-result"]';
@@ -67,6 +71,12 @@ type GraphQLResponse = {
errors?: Array<{ message: string }>;
};
+type SyncedProblemContext = {
+ settings: ExtensionSettings;
+ job: UploadJob;
+ repositoryUrl: string;
+};
+
function isProblemPage(url: URL) {
return url.hostname.includes("leetcode.com") && url.pathname.includes("/problems/");
}
@@ -118,6 +128,28 @@ function escapePipe(value: string) {
return value.replace(/\|/g, "\\|");
}
+function formatArchiveStamp(date = new Date()) {
+ const format = new Intl.DateTimeFormat("sv-SE", {
+ timeZone: "Asia/Seoul",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ });
+
+ return format
+ .format(date)
+ .replace(" ", "_")
+ .replaceAll(":", "-");
+}
+
+function createArchiveFileName(extension: string, uniqueSuffix: string) {
+ return `${formatArchiveStamp()}_${uniqueSuffix}${extension}`;
+}
+
function createProblemReadme(
details: LeetCodeSubmissionDetails,
submissionId: string
@@ -207,26 +239,29 @@ function renderStatusContent(marker: HTMLElement, text: string) {
marker.append(icon, label);
}
-function setStatusLink(marker: HTMLElement, url?: string) {
+function setStatusLink(
+ marker: HTMLElement,
+ action?: () => void,
+ title?: string
+) {
marker.onclick = null;
- if (!url) {
+ if (!action) {
marker.style.cursor = "default";
marker.removeAttribute("title");
return;
}
marker.style.cursor = "pointer";
- marker.title = "Open synced commit";
- marker.onclick = () => {
- window.open(url, "_blank", "noopener,noreferrer");
- };
+ marker.title = title ?? "Open synced actions";
+ marker.onclick = action;
}
function setInlineStatus(
text: string,
tone: "working" | "success" | "error",
- url?: string
+ action?: () => void,
+ actionTitle?: string
) {
const marker = ensureStatusMarker();
if (!marker) {
@@ -234,7 +269,7 @@ function setInlineStatus(
}
renderStatusContent(marker, text);
- setStatusLink(marker, tone === "success" ? url : undefined);
+ setStatusLink(marker, tone === "success" ? action : undefined, actionTitle);
if (tone === "working") {
marker.style.background = "#1f2937";
@@ -428,6 +463,11 @@ function buildUploadJob(
content: details.code,
});
+ job = addUploadFile(job, {
+ path: `archives/${createArchiveFileName(extension, submissionId)}`,
+ content: details.code,
+ });
+
if (settings.platforms.leetcode.createProblemReadme) {
job = addUploadFile(job, {
path: "README.md",
@@ -448,6 +488,7 @@ function buildUploadJob(
function createSubmissionHandler() {
const handledSubmissionIds = new Set();
let lastTriggerAt = 0;
+ let latestSyncContext: SyncedProblemContext | null = null;
return async function handleSubmissionTrigger() {
const settings = await getSettings();
@@ -484,6 +525,7 @@ function createSubmissionHandler() {
return;
}
+ latestSyncContext = null;
setInlineStatus("Syncing...", "working");
const details = await getSubmissionDetails(submissionId);
@@ -495,14 +537,45 @@ function createSubmissionHandler() {
const record = await Promise.all([uploadThroughBackground(job), wait(700)]).then(
([uploadRecord]) => uploadRecord
);
+ latestSyncContext = {
+ settings,
+ job,
+ repositoryUrl: `https://github.com/${record.repository}/tree/${record.branch}/${encodeURI(
+ job.directory
+ )}`,
+ };
setInlineStatus(
"Synced",
"success",
- `https://github.com/${record.repository}/tree/${record.branch}/${encodeURI(
- record.filePaths[0]?.split("/").slice(0, -1).join("/") ?? ""
- )}`
+ () => {
+ const context = latestSyncContext;
+ if (!context) {
+ return;
+ }
+
+ openSyncedActionsModal({
+ locale: context.settings.locale,
+ title: context.job.title,
+ onOpenRepository: () => {
+ window.open(context.repositoryUrl, "_blank", "noopener,noreferrer");
+ },
+ onSaveNote: async (note: string) => {
+ const payload: ProblemNoteRequest = {
+ platform: context.job.platform,
+ problemId: context.job.problemId,
+ title: context.job.title,
+ directory: context.job.directory,
+ note,
+ };
+
+ await appendProblemNoteThroughBackground(payload);
+ },
+ });
+ },
+ "Open synced actions"
);
} catch {
+ latestSyncContext = null;
setInlineStatus("Sync failed", "error");
}
};
diff --git a/src/adapters/problemActionsModal.ts b/src/adapters/problemActionsModal.ts
new file mode 100644
index 0000000..a2f7362
--- /dev/null
+++ b/src/adapters/problemActionsModal.ts
@@ -0,0 +1,251 @@
+import type { Locale } from "../core/types/domain";
+
+type SyncedActionsModalOptions = {
+ locale: Locale;
+ title: string;
+ onOpenRepository: () => void;
+ onSaveNote: (note: string) => Promise;
+};
+
+const MODAL_COPY = {
+ en: {
+ title: "Synced actions",
+ description: "Choose what to do with this solved problem.",
+ openRepository: "Open repository",
+ addNote: "Add note",
+ noteTitle: "Add note",
+ noteDescription: "Each non-empty line will be appended to NOTE.md as a bullet list.",
+ notePlaceholder:
+ "Hash map approach\nEstimated time complexity O(n)\nFaster than my previous attempt",
+ cancel: "Cancel",
+ back: "Back",
+ save: "Save note",
+ saving: "Saving...",
+ emptyNote: "Write at least one line.",
+ },
+ ko: {
+ title: "동기화 액션",
+ description: "이 문제에 대해 이어서 할 작업을 선택하세요.",
+ openRepository: "저장소에서 보기",
+ addNote: "메모 추가",
+ noteTitle: "메모 추가",
+ noteDescription: "입력한 각 줄은 NOTE.md에 불렛 포인트로 누적 저장됩니다.",
+ notePlaceholder:
+ "해시맵으로 풂\n시간복잡도는 O(n) 정도로 예상\n예전 풀이보다 조건 해석이 빨랐음",
+ cancel: "취소",
+ back: "뒤로",
+ save: "메모 저장",
+ saving: "저장 중...",
+ emptyNote: "한 줄 이상 입력해 주세요.",
+ },
+} as const;
+
+export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
+ const copy = MODAL_COPY[options.locale];
+ const overlay = document.createElement("div");
+ overlay.style.position = "fixed";
+ overlay.style.inset = "0";
+ overlay.style.zIndex = "2147483647";
+ overlay.style.display = "flex";
+ overlay.style.alignItems = "center";
+ overlay.style.justifyContent = "center";
+ overlay.style.padding = "24px";
+ overlay.style.background = "rgba(15, 23, 42, 0.56)";
+
+ const panel = document.createElement("div");
+ panel.style.width = "min(100%, 460px)";
+ panel.style.borderRadius = "16px";
+ panel.style.border = "1px solid rgba(148, 163, 184, 0.28)";
+ panel.style.background = "#0f172a";
+ panel.style.boxShadow = "0 24px 64px rgba(15, 23, 42, 0.36)";
+ panel.style.color = "#e2e8f0";
+ panel.style.padding = "20px";
+ panel.style.fontFamily =
+ "Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
+
+ const title = document.createElement("h2");
+ title.textContent = copy.title;
+ title.style.margin = "0";
+ title.style.fontSize = "18px";
+ title.style.fontWeight = "700";
+
+ const problemTitle = document.createElement("p");
+ problemTitle.textContent = options.title;
+ problemTitle.style.margin = "8px 0 0";
+ problemTitle.style.fontSize = "14px";
+ problemTitle.style.fontWeight = "600";
+ problemTitle.style.color = "#f8fafc";
+
+ const description = document.createElement("p");
+ description.textContent = copy.description;
+ description.style.margin = "8px 0 0";
+ description.style.fontSize = "13px";
+ description.style.lineHeight = "1.5";
+ description.style.color = "#94a3b8";
+
+ const content = document.createElement("div");
+ content.style.marginTop = "20px";
+
+ const close = () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ overlay.remove();
+ };
+
+ const buttonClass = (button: HTMLButtonElement) => {
+ button.type = "button";
+ button.style.width = "100%";
+ button.style.border = "1px solid transparent";
+ button.style.borderRadius = "10px";
+ button.style.padding = "11px 14px";
+ button.style.fontSize = "14px";
+ button.style.fontWeight = "600";
+ button.style.cursor = "pointer";
+ button.style.transition = "background-color 120ms ease, border-color 120ms ease";
+ };
+
+ const renderActions = () => {
+ content.replaceChildren();
+
+ const stack = document.createElement("div");
+ stack.style.display = "grid";
+ stack.style.gap = "10px";
+
+ const openButton = document.createElement("button");
+ buttonClass(openButton);
+ openButton.textContent = copy.openRepository;
+ openButton.style.background = "#2563eb";
+ openButton.style.color = "#eff6ff";
+ openButton.onclick = () => {
+ options.onOpenRepository();
+ close();
+ };
+
+ const noteButton = document.createElement("button");
+ buttonClass(noteButton);
+ noteButton.textContent = copy.addNote;
+ noteButton.style.background = "#111827";
+ noteButton.style.borderColor = "#334155";
+ noteButton.style.color = "#e2e8f0";
+ noteButton.onclick = renderNoteEditor;
+
+ const cancelButton = document.createElement("button");
+ buttonClass(cancelButton);
+ cancelButton.textContent = copy.cancel;
+ cancelButton.style.background = "transparent";
+ cancelButton.style.borderColor = "#334155";
+ cancelButton.style.color = "#94a3b8";
+ cancelButton.onclick = close;
+
+ stack.append(openButton, noteButton, cancelButton);
+ content.appendChild(stack);
+ };
+
+ const renderNoteEditor = () => {
+ content.replaceChildren();
+
+ const heading = document.createElement("p");
+ heading.textContent = copy.noteTitle;
+ heading.style.margin = "0";
+ heading.style.fontSize = "14px";
+ heading.style.fontWeight = "600";
+ heading.style.color = "#f8fafc";
+
+ const hint = document.createElement("p");
+ hint.textContent = copy.noteDescription;
+ hint.style.margin = "8px 0 0";
+ hint.style.fontSize = "13px";
+ hint.style.lineHeight = "1.5";
+ hint.style.color = "#94a3b8";
+
+ const textarea = document.createElement("textarea");
+ textarea.placeholder = copy.notePlaceholder;
+ textarea.rows = 7;
+ textarea.style.width = "100%";
+ textarea.style.marginTop = "14px";
+ textarea.style.border = "1px solid #334155";
+ textarea.style.borderRadius = "12px";
+ textarea.style.background = "#020617";
+ textarea.style.color = "#e2e8f0";
+ textarea.style.padding = "12px 14px";
+ textarea.style.fontSize = "14px";
+ textarea.style.lineHeight = "1.6";
+ textarea.style.resize = "vertical";
+
+ const error = document.createElement("p");
+ error.style.margin = "8px 0 0";
+ error.style.fontSize = "12px";
+ error.style.color = "#fca5a5";
+ error.style.minHeight = "18px";
+
+ const actions = document.createElement("div");
+ actions.style.display = "flex";
+ actions.style.justifyContent = "flex-end";
+ actions.style.gap = "10px";
+ actions.style.marginTop = "14px";
+
+ const backButton = document.createElement("button");
+ buttonClass(backButton);
+ backButton.textContent = copy.back;
+ backButton.style.width = "auto";
+ backButton.style.background = "transparent";
+ backButton.style.borderColor = "#334155";
+ backButton.style.color = "#94a3b8";
+ backButton.onclick = renderActions;
+
+ const saveButton = document.createElement("button");
+ buttonClass(saveButton);
+ saveButton.textContent = copy.save;
+ saveButton.style.width = "auto";
+ saveButton.style.background = "#059669";
+ saveButton.style.color = "#ecfdf5";
+ saveButton.onclick = async () => {
+ const note = textarea.value.trim();
+ if (!note) {
+ error.textContent = copy.emptyNote;
+ textarea.focus();
+ return;
+ }
+
+ error.textContent = "";
+ textarea.disabled = true;
+ backButton.disabled = true;
+ saveButton.disabled = true;
+ saveButton.textContent = copy.saving;
+
+ try {
+ await options.onSaveNote(note);
+ close();
+ } catch (reason) {
+ error.textContent =
+ reason instanceof Error ? reason.message : "Failed to save the note.";
+ textarea.disabled = false;
+ backButton.disabled = false;
+ saveButton.disabled = false;
+ saveButton.textContent = copy.save;
+ }
+ };
+
+ actions.append(backButton, saveButton);
+ content.append(heading, hint, textarea, error, actions);
+ window.setTimeout(() => textarea.focus(), 0);
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Escape") {
+ close();
+ }
+ };
+
+ overlay.addEventListener("click", (event) => {
+ if (event.target === overlay) {
+ close();
+ }
+ });
+
+ document.addEventListener("keydown", handleKeyDown);
+
+ panel.append(title, problemTitle, description, content);
+ overlay.appendChild(panel);
+ renderActions();
+ document.body.appendChild(overlay);
+}
diff --git a/src/adapters/programmers/index.ts b/src/adapters/programmers/index.ts
index 0b423f9..86e5b29 100644
--- a/src/adapters/programmers/index.ts
+++ b/src/adapters/programmers/index.ts
@@ -7,9 +7,13 @@ import {
import { sendRuntimeMessage } from "../../shared/runtime";
import type { ExtensionSettings } from "../../core/types/domain";
import type { RuntimeMessageResponse } from "../../core/types/messages";
-import type { UploadJob } from "../../core/types/upload";
+import type { ProblemNoteRequest, UploadJob } from "../../core/types/upload";
import type { PlatformAdapter } from "../types";
-import { uploadThroughBackground } from "../upload";
+import { openSyncedActionsModal } from "../problemActionsModal";
+import {
+ appendProblemNoteThroughBackground,
+ uploadThroughBackground,
+} from "../upload";
const STATUS_MARKER_ID = "algorithmhub-programmers-status-marker";
@@ -28,6 +32,12 @@ type ProgrammersProblemData = {
link: string;
};
+type SyncedProblemContext = {
+ settings: ExtensionSettings;
+ job: UploadJob;
+ repositoryUrl: string;
+};
+
function isProgrammersProblemPage(url: URL) {
return (
url.hostname.includes("programmers.co.kr") &&
@@ -36,6 +46,28 @@ function isProgrammersProblemPage(url: URL) {
);
}
+function formatArchiveStamp(date = new Date()) {
+ const format = new Intl.DateTimeFormat("sv-SE", {
+ timeZone: "Asia/Seoul",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ });
+
+ return format
+ .format(date)
+ .replace(" ", "_")
+ .replaceAll(":", "-");
+}
+
+function createArchiveFileName(extension: string) {
+ return `${formatArchiveStamp()}-${Date.now().toString().slice(-4)}.${extension}`;
+}
+
function isSubmitButtonElement(button: HTMLButtonElement | null) {
if (!button) {
return false;
@@ -159,20 +191,22 @@ function renderStatusContent(marker: HTMLElement, text: string) {
marker.append(icon, label);
}
-function setStatusLink(marker: HTMLElement, url?: string) {
+function setStatusLink(
+ marker: HTMLElement,
+ action?: () => void,
+ title?: string
+) {
marker.onclick = null;
- if (!url) {
+ if (!action) {
marker.style.cursor = "default";
marker.removeAttribute("title");
return;
}
marker.style.cursor = "pointer";
- marker.title = "Open synced commit";
- marker.onclick = () => {
- window.open(url, "_blank", "noopener,noreferrer");
- };
+ marker.title = title ?? "Open synced actions";
+ marker.onclick = action;
}
function positionFooterMarker(marker: HTMLElement) {
@@ -220,7 +254,8 @@ async function prepareFooterStatusSlot() {
function setInlineStatus(
text: string,
tone: "working" | "success" | "error",
- url?: string
+ action?: () => void,
+ actionTitle?: string
) {
const marker = ensureStatusMarker();
if (!marker) {
@@ -228,7 +263,7 @@ function setInlineStatus(
}
renderStatusContent(marker, text);
- setStatusLink(marker, tone === "success" ? url : undefined);
+ setStatusLink(marker, tone === "success" ? action : undefined, actionTitle);
if (marker.style.position === "absolute") {
scheduleFooterMarkerPosition(marker);
@@ -518,6 +553,11 @@ function buildUploadJob(data: ProgrammersProblemData, settings: ExtensionSetting
content: data.code,
});
+ job = addUploadFile(job, {
+ path: `archives/${createArchiveFileName(data.languageExtension)}`,
+ content: data.code,
+ });
+
if (settings.platforms.programmers.createProblemReadme) {
job = addUploadFile(job, {
path: "README.md",
@@ -546,11 +586,13 @@ function createSubmissionHandler() {
let lastUploadedKey = "";
let lastTriggeredAt = 0;
let lastPathname = window.location.pathname;
+ let latestSyncContext: SyncedProblemContext | null = null;
return async function handleSubmission() {
if (window.location.pathname !== lastPathname) {
lastPathname = window.location.pathname;
lastUploadedKey = "";
+ latestSyncContext = null;
clearInlineStatus();
}
@@ -590,23 +632,86 @@ function createSubmissionHandler() {
const data = parseProgrammersProblemData();
const uploadKey = `${data.problemId}:${data.language}:${data.code.length}`;
if (uploadKey === lastUploadedKey) {
- setInlineStatus("Synced", "success");
+ setInlineStatus(
+ "Synced",
+ "success",
+ latestSyncContext
+ ? () => {
+ const context = latestSyncContext;
+ if (!context) {
+ return;
+ }
+
+ openSyncedActionsModal({
+ locale: context.settings.locale,
+ title: context.job.title,
+ onOpenRepository: () => {
+ window.open(context.repositoryUrl, "_blank", "noopener,noreferrer");
+ },
+ onSaveNote: async (note: string) => {
+ const payload: ProblemNoteRequest = {
+ platform: context.job.platform,
+ problemId: context.job.problemId,
+ title: context.job.title,
+ directory: context.job.directory,
+ note,
+ };
+
+ await appendProblemNoteThroughBackground(payload);
+ },
+ });
+ }
+ : undefined,
+ "Open synced actions"
+ );
return;
}
+ latestSyncContext = null;
const job = buildUploadJob(data, settings);
const record = await Promise.all([uploadThroughBackground(job), wait(700)]).then(
([uploadRecord]) => uploadRecord
);
lastUploadedKey = uploadKey;
+ latestSyncContext = {
+ settings,
+ job,
+ repositoryUrl: `https://github.com/${record.repository}/tree/${record.branch}/${encodeURI(
+ job.directory
+ )}`,
+ };
setInlineStatus(
"Synced",
"success",
- `https://github.com/${record.repository}/tree/${record.branch}/${encodeURI(
- record.filePaths[0]?.split("/").slice(0, -1).join("/") ?? ""
- )}`
+ () => {
+ const context = latestSyncContext;
+ if (!context) {
+ return;
+ }
+
+ openSyncedActionsModal({
+ locale: context.settings.locale,
+ title: context.job.title,
+ onOpenRepository: () => {
+ window.open(context.repositoryUrl, "_blank", "noopener,noreferrer");
+ },
+ onSaveNote: async (note: string) => {
+ const payload: ProblemNoteRequest = {
+ platform: context.job.platform,
+ problemId: context.job.problemId,
+ title: context.job.title,
+ directory: context.job.directory,
+ note,
+ };
+
+ await appendProblemNoteThroughBackground(payload);
+ },
+ });
+ },
+ "Open synced actions"
);
} catch {
+ latestSyncContext = null;
setInlineStatus("Sync failed", "error");
}
};
diff --git a/src/adapters/upload.ts b/src/adapters/upload.ts
index 486cb46..17af838 100644
--- a/src/adapters/upload.ts
+++ b/src/adapters/upload.ts
@@ -1,4 +1,4 @@
-import type { UploadJob } from "../core/types/upload";
+import type { ProblemNoteRequest, UploadJob } from "../core/types/upload";
import { sendRuntimeMessage } from "../shared/runtime";
export async function uploadThroughBackground(job: UploadJob) {
@@ -21,3 +21,24 @@ export async function uploadThroughBackground(job: UploadJob) {
return response.record;
}
+
+export async function appendProblemNoteThroughBackground(payload: ProblemNoteRequest) {
+ const response = await sendRuntimeMessage({
+ type: "APPEND_PROBLEM_NOTE",
+ payload,
+ });
+
+ if (!response) {
+ throw new Error("Extension context invalidated.");
+ }
+
+ if (response.type !== "APPEND_PROBLEM_NOTE_RESULT") {
+ throw new Error("Unexpected note response.");
+ }
+
+ if (!response.ok) {
+ throw new Error(response.reason);
+ }
+
+ return response.record;
+}
diff --git a/src/core/github/client.ts b/src/core/github/client.ts
index 5591edf..3a7b5c1 100644
--- a/src/core/github/client.ts
+++ b/src/core/github/client.ts
@@ -40,6 +40,11 @@ type CreateCommitResponse = {
sha: string;
};
+type RepositoryContentResponse = {
+ content?: string;
+ encoding?: string;
+};
+
class GitHubApiError extends Error {
readonly status: number;
@@ -129,6 +134,30 @@ export function createGitHubClient(token: string) {
commit: { sha: string; message: string };
}>(response);
},
+ async getRepositoryFileContent(
+ fullName: string,
+ path: string,
+ branch: string
+ ): Promise {
+ const response = await fetch(
+ `https://api.github.com/repos/${fullName}/contents/${encodeURIComponent(path)}?ref=${encodeURIComponent(branch)}`,
+ {
+ method: "GET",
+ headers,
+ }
+ );
+
+ if (response.status === 404) {
+ return null;
+ }
+
+ const payload = await handleGitHubResponse(response);
+ if (!payload.content) {
+ return null;
+ }
+
+ return decodeURIComponent(escape(atob(payload.content.replace(/\n/g, ""))));
+ },
async updateRepository(
fullName: string,
payload: {
diff --git a/src/core/types/messages.ts b/src/core/types/messages.ts
index eb041f9..da4307e 100644
--- a/src/core/types/messages.ts
+++ b/src/core/types/messages.ts
@@ -3,7 +3,7 @@ import type {
ExtensionSettings,
RepositoryInfo,
} from "./domain";
-import type { UploadJob, UploadRecord } from "./upload";
+import type { ProblemNoteRequest, UploadJob, UploadRecord } from "./upload";
export type RuntimeMessage =
| { type: "PING" }
@@ -20,7 +20,8 @@ export type RuntimeMessage =
| { type: "CREATE_GITHUB_REPOSITORY"; name: string; private: boolean }
| { type: "LINK_GITHUB_REPOSITORY"; repository: string }
| { type: "DISCONNECT_GITHUB_REPOSITORY" }
- | { type: "UPLOAD_JOB"; job: UploadJob };
+ | { type: "UPLOAD_JOB"; job: UploadJob }
+ | { type: "APPEND_PROBLEM_NOTE"; payload: ProblemNoteRequest };
export type RuntimeMessageResponse =
| { type: "PONG"; timestamp: number }
@@ -36,3 +37,5 @@ export type RuntimeMessageResponse =
| { type: "GITHUB_REPOSITORIES"; ok: false; reason: string }
| { type: "GITHUB_REPOSITORY_UPDATE"; ok: true; settings: ExtensionSettings; repository: RepositoryInfo }
| { type: "GITHUB_REPOSITORY_UPDATE"; ok: false; reason: string }
+ | { type: "APPEND_PROBLEM_NOTE_RESULT"; ok: true; record: UploadRecord }
+ | { type: "APPEND_PROBLEM_NOTE_RESULT"; ok: false; reason: string; problemId: string };
diff --git a/src/core/types/upload.ts b/src/core/types/upload.ts
index a4b4197..efe6a90 100644
--- a/src/core/types/upload.ts
+++ b/src/core/types/upload.ts
@@ -27,3 +27,11 @@ export type UploadRecord = {
uploadedAt: number;
filePaths: string[];
};
+
+export type ProblemNoteRequest = {
+ platform: PlatformId;
+ problemId: string;
+ title: string;
+ directory: string;
+ note: string;
+};
diff --git a/src/scripts/background/index.ts b/src/scripts/background/index.ts
index ff099a0..b96a9cc 100644
--- a/src/scripts/background/index.ts
+++ b/src/scripts/background/index.ts
@@ -91,6 +91,37 @@ function withRootSummaryReadme(
};
}
+function formatNoteDate(date = new Date()) {
+ return new Intl.DateTimeFormat("sv-SE", {
+ timeZone: "Asia/Seoul",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(date);
+}
+
+function formatProblemNote(note: string) {
+ const lines = note
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean);
+
+ if (lines.length === 0) {
+ throw new Error("Add at least one line to save a note.");
+ }
+
+ return `## ${formatNoteDate()}\n\n${lines.map((line) => `- ${line}`).join("\n")}`;
+}
+
+function appendProblemNote(existingContent: string | null, note: string) {
+ const nextEntry = formatProblemNote(note);
+ if (!existingContent?.trim()) {
+ return `${nextEntry}\n`;
+ }
+
+ return `${existingContent.trimEnd()}\n\n${nextEntry}\n`;
+}
+
function openWelcomePage() {
const url = chrome.runtime.getURL("welcome.html");
void chrome.tabs.create({ url, active: true });
@@ -465,6 +496,65 @@ async function handleUploadJobMessage(
}
}
+async function handleAppendProblemNoteMessage(
+ message: Extract
+): Promise {
+ const settings = await getSettings();
+ const token = settings.github.token.trim();
+ const repository = settings.github.repository.trim();
+
+ if (!token || !repository) {
+ return {
+ type: "APPEND_PROBLEM_NOTE_RESULT",
+ ok: false,
+ problemId: message.payload.problemId,
+ reason: "GitHub token and repository must be configured.",
+ };
+ }
+
+ try {
+ const github = createGitHubClient(token);
+ const repo = await github.getRepository(repository);
+ const branch = settings.github.branch.trim() || repo.default_branch;
+ const notePath = `${message.payload.directory}/NOTE.md`;
+ const existingContent = await github.getRepositoryFileContent(
+ repository,
+ notePath,
+ branch
+ );
+ const job: UploadJob = {
+ id: `note:${message.payload.platform}:${message.payload.problemId}:${Date.now()}`,
+ platform: message.payload.platform,
+ problemId: message.payload.problemId,
+ title: message.payload.title,
+ directory: message.payload.directory,
+ commitMessage: `[${message.payload.platform}][Note] ${message.payload.title} - AlgorithmHub`,
+ files: [
+ {
+ path: "NOTE.md",
+ content: appendProblemNote(existingContent, message.payload.note),
+ },
+ ],
+ rootFiles: [],
+ };
+ const record = await executeUploadJob(job, settings);
+ await saveUploadRecord(record);
+
+ return {
+ type: "APPEND_PROBLEM_NOTE_RESULT",
+ ok: true,
+ record,
+ };
+ } catch (error) {
+ return {
+ type: "APPEND_PROBLEM_NOTE_RESULT",
+ ok: false,
+ problemId: message.payload.problemId,
+ reason: error instanceof Error ? error.message : "Failed to save note.",
+ };
+ }
+}
+
chrome.runtime.onInstalled.addListener(async () => {
const { settings } = await chrome.storage.local.get("settings");
@@ -513,6 +603,9 @@ chrome.runtime.onMessage.addListener(
case "UPLOAD_JOB":
sendResponse(await handleUploadJobMessage(message));
return;
+ case "APPEND_PROBLEM_NOTE":
+ sendResponse(await handleAppendProblemNoteMessage(message));
+ return;
}
})();
From 82668552d301abd57f1bc14a0c7044faca252e7c Mon Sep 17 00:00:00 2001
From: dev-minsoo
Date: Sat, 25 Apr 2026 00:58:35 +0900
Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EB=85=B8=ED=8A=B8=20=EC=9E=85?=
=?UTF-8?q?=EB=A0=A5=20=EB=AA=A8=EB=8B=AC=20=EB=8F=99=EC=9E=91=20=EC=95=88?=
=?UTF-8?q?=EC=A0=95=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/adapters/leetcode/index.ts | 1 +
src/adapters/problemActionsModal.ts | 326 ++++++++++++++++++++++------
src/adapters/programmers/index.ts | 2 +
3 files changed, 261 insertions(+), 68 deletions(-)
diff --git a/src/adapters/leetcode/index.ts b/src/adapters/leetcode/index.ts
index a4d400c..0182057 100644
--- a/src/adapters/leetcode/index.ts
+++ b/src/adapters/leetcode/index.ts
@@ -555,6 +555,7 @@ function createSubmissionHandler() {
openSyncedActionsModal({
locale: context.settings.locale,
+ themeMode: context.settings.themeMode,
title: context.job.title,
onOpenRepository: () => {
window.open(context.repositoryUrl, "_blank", "noopener,noreferrer");
diff --git a/src/adapters/problemActionsModal.ts b/src/adapters/problemActionsModal.ts
index a2f7362..00cc8c7 100644
--- a/src/adapters/problemActionsModal.ts
+++ b/src/adapters/problemActionsModal.ts
@@ -1,7 +1,8 @@
-import type { Locale } from "../core/types/domain";
+import type { Locale, ThemeMode } from "../core/types/domain";
type SyncedActionsModalOptions = {
locale: Locale;
+ themeMode: ThemeMode;
title: string;
onOpenRepository: () => void;
onSaveNote: (note: string) => Promise;
@@ -9,7 +10,6 @@ type SyncedActionsModalOptions = {
const MODAL_COPY = {
en: {
- title: "Synced actions",
description: "Choose what to do with this solved problem.",
openRepository: "Open repository",
addNote: "Add note",
@@ -17,31 +17,154 @@ const MODAL_COPY = {
noteDescription: "Each non-empty line will be appended to NOTE.md as a bullet list.",
notePlaceholder:
"Hash map approach\nEstimated time complexity O(n)\nFaster than my previous attempt",
- cancel: "Cancel",
back: "Back",
save: "Save note",
saving: "Saving...",
emptyNote: "Write at least one line.",
+ close: "Close",
+ failed: "Failed to save the note.",
},
ko: {
- title: "동기화 액션",
description: "이 문제에 대해 이어서 할 작업을 선택하세요.",
openRepository: "저장소에서 보기",
- addNote: "메모 추가",
- noteTitle: "메모 추가",
- noteDescription: "입력한 각 줄은 NOTE.md에 불렛 포인트로 누적 저장됩니다.",
+ addNote: "노트 추가",
+ noteTitle: "노트 추가",
+ noteDescription: "입력한 각 줄은 NOTE.md에 불릿 포인트로 누적 저장됩니다.",
notePlaceholder:
"해시맵으로 풂\n시간복잡도는 O(n) 정도로 예상\n예전 풀이보다 조건 해석이 빨랐음",
- cancel: "취소",
back: "뒤로",
- save: "메모 저장",
+ save: "노트 저장",
saving: "저장 중...",
emptyNote: "한 줄 이상 입력해 주세요.",
+ close: "닫기",
+ failed: "노트 저장에 실패했습니다.",
},
} as const;
+type EditorMessage =
+ | { type: "ready" }
+ | { type: "value"; requestId: string; value: string };
+
+function resolveLocale(locale: Locale): Locale {
+ if (locale === "ko" || locale === "en") {
+ return locale;
+ }
+
+ const language =
+ document.documentElement.lang || window.navigator.language || "";
+ return language.toLowerCase().startsWith("ko") ? "ko" : "en";
+}
+
+function resolveTheme(themeMode: ThemeMode) {
+ if (themeMode === "light") {
+ return "light";
+ }
+
+ if (themeMode === "dark") {
+ return "dark";
+ }
+
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+}
+
+function createEditorDocument({
+ isDark,
+ placeholder,
+}: {
+ isDark: boolean;
+ placeholder: string;
+}) {
+ const panelText = isDark ? "#e2e8f0" : "#0f172a";
+ const border = isDark ? "#334155" : "#cbd5e1";
+ const inputBackground = isDark ? "#020617" : "#ffffff";
+ const placeholderColor = isDark ? "#64748b" : "#94a3b8";
+ const focusRing = isDark
+ ? "rgba(37, 99, 235, 0.28)"
+ : "rgba(37, 99, 235, 0.18)";
+
+ return `
+
+
+
+
+
+
+
+
+
+`;
+}
+
export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
- const copy = MODAL_COPY[options.locale];
+ const locale = resolveLocale(options.locale);
+ const copy = MODAL_COPY[locale];
+ const theme = resolveTheme(options.themeMode);
+ const isDark = theme === "dark";
+
const overlay = document.createElement("div");
overlay.style.position = "fixed";
overlay.style.inset = "0";
@@ -50,50 +173,68 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.padding = "24px";
- overlay.style.background = "rgba(15, 23, 42, 0.56)";
+ overlay.style.background = isDark ? "rgba(15, 23, 42, 0.58)" : "rgba(15, 23, 42, 0.28)";
const panel = document.createElement("div");
panel.style.width = "min(100%, 460px)";
panel.style.borderRadius = "16px";
- panel.style.border = "1px solid rgba(148, 163, 184, 0.28)";
- panel.style.background = "#0f172a";
- panel.style.boxShadow = "0 24px 64px rgba(15, 23, 42, 0.36)";
- panel.style.color = "#e2e8f0";
+ panel.style.border = isDark
+ ? "1px solid rgba(148, 163, 184, 0.22)"
+ : "1px solid rgba(148, 163, 184, 0.28)";
+ panel.style.background = isDark ? "#0f172a" : "#ffffff";
+ panel.style.boxShadow = isDark
+ ? "0 24px 64px rgba(15, 23, 42, 0.36)"
+ : "0 24px 64px rgba(15, 23, 42, 0.16)";
+ panel.style.color = isDark ? "#e2e8f0" : "#0f172a";
panel.style.padding = "20px";
panel.style.fontFamily =
"Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
-
- const title = document.createElement("h2");
- title.textContent = copy.title;
- title.style.margin = "0";
- title.style.fontSize = "18px";
- title.style.fontWeight = "700";
+ panel.style.position = "relative";
const problemTitle = document.createElement("p");
problemTitle.textContent = options.title;
- problemTitle.style.margin = "8px 0 0";
+ problemTitle.style.margin = "0";
+ problemTitle.style.paddingRight = "32px";
problemTitle.style.fontSize = "14px";
problemTitle.style.fontWeight = "600";
- problemTitle.style.color = "#f8fafc";
+ problemTitle.style.color = isDark ? "#f8fafc" : "#0f172a";
const description = document.createElement("p");
description.textContent = copy.description;
description.style.margin = "8px 0 0";
description.style.fontSize = "13px";
description.style.lineHeight = "1.5";
- description.style.color = "#94a3b8";
+ description.style.color = isDark ? "#94a3b8" : "#475569";
const content = document.createElement("div");
content.style.marginTop = "20px";
const close = () => {
- document.removeEventListener("keydown", handleKeyDown);
+ window.removeEventListener("message", handleEditorMessage);
+ document.removeEventListener("keydown", handleEscape, true);
overlay.remove();
};
+ const closeButton = document.createElement("button");
+ closeButton.type = "button";
+ closeButton.setAttribute("aria-label", copy.close);
+ closeButton.textContent = "×";
+ closeButton.style.position = "absolute";
+ closeButton.style.top = "14px";
+ closeButton.style.right = "14px";
+ closeButton.style.width = "28px";
+ closeButton.style.height = "28px";
+ closeButton.style.border = "0";
+ closeButton.style.borderRadius = "8px";
+ closeButton.style.background = "transparent";
+ closeButton.style.color = isDark ? "#94a3b8" : "#64748b";
+ closeButton.style.fontSize = "22px";
+ closeButton.style.lineHeight = "1";
+ closeButton.style.cursor = "pointer";
+ closeButton.onclick = close;
+
const buttonClass = (button: HTMLButtonElement) => {
button.type = "button";
- button.style.width = "100%";
button.style.border = "1px solid transparent";
button.style.borderRadius = "10px";
button.style.padding = "11px 14px";
@@ -103,7 +244,59 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
button.style.transition = "background-color 120ms ease, border-color 120ms ease";
};
+ let editorFrame: HTMLIFrameElement | null = null;
+ let pendingValueRequest:
+ | {
+ requestId: string;
+ resolve: (value: string) => void;
+ }
+ | null = null;
+
+ const postEditorMessage = (message: object) => {
+ editorFrame?.contentWindow?.postMessage(
+ {
+ source: "algorithmhub-note-editor-parent",
+ ...message,
+ },
+ "*"
+ );
+ };
+
+ const requestEditorValue = () =>
+ new Promise((resolve) => {
+ const requestId = crypto.randomUUID();
+ pendingValueRequest = { requestId, resolve };
+ postEditorMessage({ type: "requestValue", requestId });
+ });
+
+ const handleEditorMessage = (
+ event: MessageEvent
+ ) => {
+ if (!editorFrame || event.source !== editorFrame.contentWindow) {
+ return;
+ }
+
+ if (event.data?.source !== "algorithmhub-note-editor") {
+ return;
+ }
+
+ if (event.data.type === "ready") {
+ postEditorMessage({ type: "focus" });
+ return;
+ }
+
+ if (
+ event.data.type === "value" &&
+ pendingValueRequest?.requestId === event.data.requestId
+ ) {
+ pendingValueRequest.resolve(event.data.value);
+ pendingValueRequest = null;
+ }
+ };
+
const renderActions = () => {
+ editorFrame = null;
+ pendingValueRequest = null;
content.replaceChildren();
const stack = document.createElement("div");
@@ -113,6 +306,7 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
const openButton = document.createElement("button");
buttonClass(openButton);
openButton.textContent = copy.openRepository;
+ openButton.style.width = "100%";
openButton.style.background = "#2563eb";
openButton.style.color = "#eff6ff";
openButton.onclick = () => {
@@ -123,20 +317,13 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
const noteButton = document.createElement("button");
buttonClass(noteButton);
noteButton.textContent = copy.addNote;
- noteButton.style.background = "#111827";
- noteButton.style.borderColor = "#334155";
- noteButton.style.color = "#e2e8f0";
+ noteButton.style.width = "100%";
+ noteButton.style.background = isDark ? "#111827" : "#f8fafc";
+ noteButton.style.borderColor = isDark ? "#334155" : "#cbd5e1";
+ noteButton.style.color = isDark ? "#e2e8f0" : "#0f172a";
noteButton.onclick = renderNoteEditor;
- const cancelButton = document.createElement("button");
- buttonClass(cancelButton);
- cancelButton.textContent = copy.cancel;
- cancelButton.style.background = "transparent";
- cancelButton.style.borderColor = "#334155";
- cancelButton.style.color = "#94a3b8";
- cancelButton.onclick = close;
-
- stack.append(openButton, noteButton, cancelButton);
+ stack.append(openButton, noteButton);
content.appendChild(stack);
};
@@ -148,33 +335,35 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
heading.style.margin = "0";
heading.style.fontSize = "14px";
heading.style.fontWeight = "600";
- heading.style.color = "#f8fafc";
+ heading.style.color = isDark ? "#f8fafc" : "#0f172a";
const hint = document.createElement("p");
hint.textContent = copy.noteDescription;
hint.style.margin = "8px 0 0";
hint.style.fontSize = "13px";
hint.style.lineHeight = "1.5";
- hint.style.color = "#94a3b8";
-
- const textarea = document.createElement("textarea");
- textarea.placeholder = copy.notePlaceholder;
- textarea.rows = 7;
- textarea.style.width = "100%";
- textarea.style.marginTop = "14px";
- textarea.style.border = "1px solid #334155";
- textarea.style.borderRadius = "12px";
- textarea.style.background = "#020617";
- textarea.style.color = "#e2e8f0";
- textarea.style.padding = "12px 14px";
- textarea.style.fontSize = "14px";
- textarea.style.lineHeight = "1.6";
- textarea.style.resize = "vertical";
+ hint.style.color = isDark ? "#94a3b8" : "#475569";
+
+ editorFrame = document.createElement("iframe");
+ editorFrame.title = copy.noteTitle;
+ editorFrame.style.display = "block";
+ editorFrame.style.width = "100%";
+ editorFrame.style.height = "182px";
+ editorFrame.style.marginTop = "14px";
+ editorFrame.style.border = "0";
+ editorFrame.style.borderRadius = "12px";
+ editorFrame.style.background = isDark ? "#020617" : "#ffffff";
+ editorFrame.style.colorScheme = isDark ? "dark" : "light";
+ editorFrame.setAttribute("sandbox", "allow-scripts allow-same-origin");
+ editorFrame.srcdoc = createEditorDocument({
+ isDark,
+ placeholder: copy.notePlaceholder,
+ });
const error = document.createElement("p");
error.style.margin = "8px 0 0";
error.style.fontSize = "12px";
- error.style.color = "#fca5a5";
+ error.style.color = "#ef4444";
error.style.minHeight = "18px";
const actions = document.createElement("div");
@@ -188,8 +377,8 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
backButton.textContent = copy.back;
backButton.style.width = "auto";
backButton.style.background = "transparent";
- backButton.style.borderColor = "#334155";
- backButton.style.color = "#94a3b8";
+ backButton.style.borderColor = isDark ? "#334155" : "#cbd5e1";
+ backButton.style.color = isDark ? "#94a3b8" : "#475569";
backButton.onclick = renderActions;
const saveButton = document.createElement("button");
@@ -199,15 +388,15 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
saveButton.style.background = "#059669";
saveButton.style.color = "#ecfdf5";
saveButton.onclick = async () => {
- const note = textarea.value.trim();
+ const note = (await requestEditorValue()).trim();
if (!note) {
error.textContent = copy.emptyNote;
- textarea.focus();
+ postEditorMessage({ type: "focus" });
return;
}
error.textContent = "";
- textarea.disabled = true;
+ postEditorMessage({ type: "setDisabled", disabled: true });
backButton.disabled = true;
saveButton.disabled = true;
saveButton.textContent = copy.saving;
@@ -217,8 +406,9 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
close();
} catch (reason) {
error.textContent =
- reason instanceof Error ? reason.message : "Failed to save the note.";
- textarea.disabled = false;
+ reason instanceof Error ? reason.message : copy.failed;
+ postEditorMessage({ type: "setDisabled", disabled: false });
+ postEditorMessage({ type: "focus" });
backButton.disabled = false;
saveButton.disabled = false;
saveButton.textContent = copy.save;
@@ -226,11 +416,10 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
};
actions.append(backButton, saveButton);
- content.append(heading, hint, textarea, error, actions);
- window.setTimeout(() => textarea.focus(), 0);
+ content.append(heading, hint, editorFrame, error, actions);
};
- const handleKeyDown = (event: KeyboardEvent) => {
+ const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
close();
}
@@ -242,10 +431,11 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
}
});
- document.addEventListener("keydown", handleKeyDown);
+ window.addEventListener("message", handleEditorMessage);
+ document.addEventListener("keydown", handleEscape, true);
- panel.append(title, problemTitle, description, content);
+ panel.append(closeButton, problemTitle, description, content);
overlay.appendChild(panel);
- renderActions();
document.body.appendChild(overlay);
+ renderActions();
}
diff --git a/src/adapters/programmers/index.ts b/src/adapters/programmers/index.ts
index 86e5b29..9fea5d3 100644
--- a/src/adapters/programmers/index.ts
+++ b/src/adapters/programmers/index.ts
@@ -644,6 +644,7 @@ function createSubmissionHandler() {
openSyncedActionsModal({
locale: context.settings.locale,
+ themeMode: context.settings.themeMode,
title: context.job.title,
onOpenRepository: () => {
window.open(context.repositoryUrl, "_blank", "noopener,noreferrer");
@@ -691,6 +692,7 @@ function createSubmissionHandler() {
openSyncedActionsModal({
locale: context.settings.locale,
+ themeMode: context.settings.themeMode,
title: context.job.title,
onOpenRepository: () => {
window.open(context.repositoryUrl, "_blank", "noopener,noreferrer");
From aca448adaee308f6ef11d21da3303ac44761d0d3 Mon Sep 17 00:00:00 2001
From: dev-minsoo
Date: Sat, 25 Apr 2026 01:03:27 +0900
Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EB=85=B8=ED=8A=B8=20=EC=9E=85?=
=?UTF-8?q?=EB=A0=A5=20=EB=AA=A8=EB=8B=AC=20=EC=9E=85=EB=A0=A5=20=EC=98=81?=
=?UTF-8?q?=EC=97=AD=20=EC=95=88=EC=A0=95=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/adapters/problemActionsModal.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/adapters/problemActionsModal.ts b/src/adapters/problemActionsModal.ts
index 00cc8c7..59a73e3 100644
--- a/src/adapters/problemActionsModal.ts
+++ b/src/adapters/problemActionsModal.ts
@@ -354,7 +354,7 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
editorFrame.style.borderRadius = "12px";
editorFrame.style.background = isDark ? "#020617" : "#ffffff";
editorFrame.style.colorScheme = isDark ? "dark" : "light";
- editorFrame.setAttribute("sandbox", "allow-scripts allow-same-origin");
+ editorFrame.setAttribute("sandbox", "allow-scripts");
editorFrame.srcdoc = createEditorDocument({
isDark,
placeholder: copy.notePlaceholder,
From e74394ee1753f6a29ad0d021b0c3bf9afa0e7421 Mon Sep 17 00:00:00 2001
From: dev-minsoo
Date: Sat, 25 Apr 2026 01:11:50 +0900
Subject: [PATCH 4/5] =?UTF-8?q?fix:=20UI=20=EB=AC=B8=EA=B5=AC=EC=99=80=20?=
=?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=9E=85=EB=A0=A5=20=EB=AA=A8=EB=8B=AC=20?=
=?UTF-8?q?=EC=95=88=EC=A0=95=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/adapters/problemActionsModal.ts | 6 ++----
src/app/options/Options.tsx | 4 ++--
src/app/popup/Popup.tsx | 2 +-
src/app/welcome/Welcome.tsx | 18 ++++++++++++++++--
4 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/src/adapters/problemActionsModal.ts b/src/adapters/problemActionsModal.ts
index 59a73e3..77d7b73 100644
--- a/src/adapters/problemActionsModal.ts
+++ b/src/adapters/problemActionsModal.ts
@@ -15,8 +15,7 @@ const MODAL_COPY = {
addNote: "Add note",
noteTitle: "Add note",
noteDescription: "Each non-empty line will be appended to NOTE.md as a bullet list.",
- notePlaceholder:
- "Hash map approach\nEstimated time complexity O(n)\nFaster than my previous attempt",
+ notePlaceholder: "Enter your note",
back: "Back",
save: "Save note",
saving: "Saving...",
@@ -30,8 +29,7 @@ const MODAL_COPY = {
addNote: "노트 추가",
noteTitle: "노트 추가",
noteDescription: "입력한 각 줄은 NOTE.md에 불릿 포인트로 누적 저장됩니다.",
- notePlaceholder:
- "해시맵으로 풂\n시간복잡도는 O(n) 정도로 예상\n예전 풀이보다 조건 해석이 빨랐음",
+ notePlaceholder: "노트를 입력하세요",
back: "뒤로",
save: "노트 저장",
saving: "저장 중...",
diff --git a/src/app/options/Options.tsx b/src/app/options/Options.tsx
index 3994e91..2811c64 100644
--- a/src/app/options/Options.tsx
+++ b/src/app/options/Options.tsx
@@ -344,7 +344,7 @@ export default function Options() {
const [draggedSegment, setDraggedSegment] =
useState(null);
const [extensionEnabled, setExtensionEnabled] = useState(true);
- const [templatesOpen, setTemplatesOpen] = useState(false);
+ const [templatesOpen, setTemplatesOpen] = useState(true);
useEffect(() => {
void chrome.runtime
@@ -716,7 +716,7 @@ export default function Options() {
{copy.templatesTitle}
diff --git a/src/app/popup/Popup.tsx b/src/app/popup/Popup.tsx
index e385cc4..b143850 100644
--- a/src/app/popup/Popup.tsx
+++ b/src/app/popup/Popup.tsx
@@ -272,7 +272,7 @@ export default function Popup() {
diff --git a/src/app/welcome/Welcome.tsx b/src/app/welcome/Welcome.tsx
index 8c9bcdc..fbd85be 100644
--- a/src/app/welcome/Welcome.tsx
+++ b/src/app/welcome/Welcome.tsx
@@ -8,6 +8,19 @@ import { useResolvedTheme } from "../../shared/theme";
type RepoMode = "" | "new" | "link";
+const WELCOME_COPY = {
+ en: {
+ subtitle:
+ "Automatically sync your accepted LeetCode and Programmers solutions to GitHub.",
+ connectTitle: "Connect a repository to get started",
+ },
+ ko: {
+ subtitle:
+ "LeetCode와 프로그래머스 정답 제출을 GitHub에 자동으로 동기화하세요.",
+ connectTitle: "시작하려면 저장소를 연결하세요",
+ },
+} as const;
+
const emptySettings: ExtensionSettings = {
locale: "en",
themeMode: "system",
@@ -70,6 +83,7 @@ export default function Welcome() {
const [message, setMessage] = useState("");
const [submitting, setSubmitting] = useState(false);
const resolvedTheme = useResolvedTheme(settings.themeMode);
+ const copy = WELCOME_COPY[settings.locale];
const loadRepositories = useCallback(async (token = settings.github.token) => {
if (!token.trim()) {
@@ -249,7 +263,7 @@ export default function Welcome() {
resolvedTheme === "dark" ? "text-stone-400" : "text-stone-700"
}`}
>
- Automatically sync your accepted LeetCode and Programmers solutions to GitHub.
+ {copy.subtitle}
@@ -259,7 +273,7 @@ export default function Welcome() {
resolvedTheme === "dark" ? "text-stone-50" : "text-stone-900"
}`}
>
- Connect a repository to get started
+ {copy.connectTitle}
From 9af0d052b955ff7ccf585927d1c83c7bc99ae174 Mon Sep 17 00:00:00 2001
From: dev-minsoo
Date: Sat, 25 Apr 2026 01:22:34 +0900
Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EB=9F=B0=ED=83=80=EC=9E=84=20?=
=?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=98=88=EC=99=B8=20=EC=B2=98?=
=?UTF-8?q?=EB=A6=AC=20=EB=B3=B4=EA=B0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/adapters/leetcode/index.ts | 46 ++++++++++++------------
src/adapters/problemActionsModal.ts | 16 ++++++---
src/adapters/programmers/index.ts | 54 ++++++++++++++---------------
src/shared/runtime.ts | 12 +++++--
4 files changed, 72 insertions(+), 56 deletions(-)
diff --git a/src/adapters/leetcode/index.ts b/src/adapters/leetcode/index.ts
index 0182057..915b2bf 100644
--- a/src/adapters/leetcode/index.ts
+++ b/src/adapters/leetcode/index.ts
@@ -491,35 +491,35 @@ function createSubmissionHandler() {
let latestSyncContext: SyncedProblemContext | null = null;
return async function handleSubmissionTrigger() {
- const settings = await getSettings();
+ try {
+ const settings = await getSettings();
- if (!settings.platforms.leetcode.enabled || !settings.platforms.leetcode.autoUpload) {
- return;
- }
+ if (!settings.platforms.leetcode.enabled || !settings.platforms.leetcode.autoUpload) {
+ return;
+ }
- if (!(await isExtensionEnabled())) {
- return;
- }
+ if (!(await isExtensionEnabled())) {
+ return;
+ }
- const now = Date.now();
- if (now - lastTriggerAt < 1500) {
- return;
- }
- lastTriggerAt = now;
+ const now = Date.now();
+ if (now - lastTriggerAt < 1500) {
+ return;
+ }
+ lastTriggerAt = now;
- const submissionId = await waitForSubmissionId();
- if (!submissionId || handledSubmissionIds.has(submissionId)) {
- return;
- }
+ const submissionId = await waitForSubmissionId();
+ if (!submissionId || handledSubmissionIds.has(submissionId)) {
+ return;
+ }
- const accepted = await waitForAcceptedState();
- if (!accepted) {
- clearInlineStatus();
- return;
- }
+ const accepted = await waitForAcceptedState();
+ if (!accepted) {
+ clearInlineStatus();
+ return;
+ }
- handledSubmissionIds.add(submissionId);
- try {
+ handledSubmissionIds.add(submissionId);
if (!(await isExtensionEnabled())) {
clearInlineStatus();
return;
diff --git a/src/adapters/problemActionsModal.ts b/src/adapters/problemActionsModal.ts
index 77d7b73..d59ef67 100644
--- a/src/adapters/problemActionsModal.ts
+++ b/src/adapters/problemActionsModal.ts
@@ -189,13 +189,21 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
"Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
panel.style.position = "relative";
+ const modalTitle = document.createElement("p");
+ modalTitle.textContent = "AlgorithmHub";
+ modalTitle.style.margin = "0";
+ modalTitle.style.paddingRight = "32px";
+ modalTitle.style.fontSize = "16px";
+ modalTitle.style.fontWeight = "700";
+ modalTitle.style.color = isDark ? "#f8fafc" : "#0f172a";
+
const problemTitle = document.createElement("p");
problemTitle.textContent = options.title;
- problemTitle.style.margin = "0";
+ problemTitle.style.margin = "6px 0 0";
problemTitle.style.paddingRight = "32px";
- problemTitle.style.fontSize = "14px";
+ problemTitle.style.fontSize = "13px";
problemTitle.style.fontWeight = "600";
- problemTitle.style.color = isDark ? "#f8fafc" : "#0f172a";
+ problemTitle.style.color = isDark ? "#cbd5e1" : "#334155";
const description = document.createElement("p");
description.textContent = copy.description;
@@ -432,7 +440,7 @@ export function openSyncedActionsModal(options: SyncedActionsModalOptions) {
window.addEventListener("message", handleEditorMessage);
document.addEventListener("keydown", handleEscape, true);
- panel.append(closeButton, problemTitle, description, content);
+ panel.append(closeButton, modalTitle, problemTitle, description, content);
overlay.appendChild(panel);
document.body.appendChild(overlay);
renderActions();
diff --git a/src/adapters/programmers/index.ts b/src/adapters/programmers/index.ts
index 9fea5d3..f15c895 100644
--- a/src/adapters/programmers/index.ts
+++ b/src/adapters/programmers/index.ts
@@ -589,39 +589,39 @@ function createSubmissionHandler() {
let latestSyncContext: SyncedProblemContext | null = null;
return async function handleSubmission() {
- if (window.location.pathname !== lastPathname) {
- lastPathname = window.location.pathname;
- lastUploadedKey = "";
- latestSyncContext = null;
- clearInlineStatus();
- }
+ try {
+ if (window.location.pathname !== lastPathname) {
+ lastPathname = window.location.pathname;
+ lastUploadedKey = "";
+ latestSyncContext = null;
+ clearInlineStatus();
+ }
- const settings = await getSettings();
+ const settings = await getSettings();
- if (
- !settings.platforms.programmers.enabled ||
- !settings.platforms.programmers.autoUpload
- ) {
- return;
- }
+ if (
+ !settings.platforms.programmers.enabled ||
+ !settings.platforms.programmers.autoUpload
+ ) {
+ return;
+ }
- if (!(await isExtensionEnabled())) {
- return;
- }
+ if (!(await isExtensionEnabled())) {
+ return;
+ }
- const now = Date.now();
- if (now - lastTriggeredAt < 1500) {
- return;
- }
- lastTriggeredAt = now;
+ const now = Date.now();
+ if (now - lastTriggeredAt < 1500) {
+ return;
+ }
+ lastTriggeredAt = now;
- const accepted = await waitForAcceptedResult();
- if (!accepted) {
- clearInlineStatus();
- return;
- }
+ const accepted = await waitForAcceptedResult();
+ if (!accepted) {
+ clearInlineStatus();
+ return;
+ }
- try {
if (!(await isExtensionEnabled())) {
clearInlineStatus();
return;
diff --git a/src/shared/runtime.ts b/src/shared/runtime.ts
index 144e8aa..b5ec5bb 100644
--- a/src/shared/runtime.ts
+++ b/src/shared/runtime.ts
@@ -3,12 +3,20 @@ import type { RuntimeMessage, RuntimeMessageResponse } from "../core/types/messa
export async function sendRuntimeMessage(
message: RuntimeMessage
): Promise {
+ const runtime = globalThis.chrome?.runtime;
+ if (!runtime?.sendMessage) {
+ return null;
+ }
+
try {
- return (await chrome.runtime.sendMessage(message)) as T;
+ return (await runtime.sendMessage(message)) as T;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
- if (reason.includes("Extension context invalidated")) {
+ if (
+ reason.includes("Extension context invalidated") ||
+ reason.includes("Cannot read properties of undefined")
+ ) {
return null;
}