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() {

-

+

{copy.subtitle}

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; }