diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index af7f900..4937246 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,35 +6,51 @@ If you want to contribute your own translation/language, first fork the reposito The first object in the file is a `Locale` type which defines all of the text variables used in the app. You don't need to change this at all. -You will need to add the locale name of your translation to `SupportedLocales`. For example if you were adding Spanish (Mexico) you would add ` | "es_MX";` to the end of the line: +You will need to add the locale name of your translation to `supportedLocales`. For example if you were adding Spanish (Mexico) you would add `"es_MX"` to the array: + +
BeforeAfter
+ +```ts +export const supportedLocales = ["en_US"] as const; +// ^^^^^^^ +``` + + ```ts -export type SupportedLocales = "en_US"; +export const supportedLocales = ["en_US", "es_MX"] as const; +// ^^^^^^^^^^^^^^^^ ``` -to +
+ +The next line generates a type from the array (you don't have to change this): ```ts -export type SupportedLocales = "en_US" | "es_MX"; +export type SupportedLocales = (typeof supportedLocales)[number]; // would be: "en_US" | "es_MX" ``` Then, the last object in the file is `export const locales`. Select the entire `en_US: { ... }` object and copy/paste it below: +
BeforeAfter
+ ```ts export const locales: Record = { en_US: { ... } }; ``` -to + ```ts export const locales: Record = { en_US: { ... }, - es_MX: { ... } + es_MX: { ... } // New translation }; ``` +
+ Then you can go through the new object and change the strings to your own translation. Finally, create a pull request on the original repository with your new changes. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..d9243e8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: jcv8000 +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/package.json b/package.json index 894e109..d0c2678 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex", "private": true, - "version": "2.0.0-beta.0", + "version": "2.0.0", "description": "Note-taking app for programmers and CS students", "author": { "name": "Josh Vickery", @@ -22,93 +22,94 @@ "typecheck": "tsc && tsc -p packages/renderer/tsconfig.json" }, "dependencies": { - "@electron/remote": "^2.0.10", + "@electron/remote": "^2.0.12", "@emotion/react": "^11.11.1", "@lukeed/uuid": "^2.0.1", - "@mantine/core": "^6.0.17", - "@mantine/dropzone": "^6.0.17", - "@mantine/form": "^6.0.17", - "@mantine/hooks": "^6.0.17", - "@mantine/modals": "^6.0.17", - "@mantine/notifications": "^6.0.17", - "@tabler/icons": "^2.30.0", - "@tabler/icons-webfont": "^2.30.0", - "@tiptap/core": "^2.0.4", - "@tiptap/extension-code-block": "^2.0.4", - "@tiptap/extension-code-block-lowlight": "^2.0.4", - "@tiptap/extension-color": "^2.0.4", - "@tiptap/extension-focus": "^2.0.4", - "@tiptap/extension-font-family": "^2.0.4", - "@tiptap/extension-heading": "^2.0.4", - "@tiptap/extension-highlight": "^2.0.4", - "@tiptap/extension-image": "^2.0.4", - "@tiptap/extension-link": "^2.0.4", - "@tiptap/extension-placeholder": "^2.0.4", - "@tiptap/extension-subscript": "^2.0.4", - "@tiptap/extension-superscript": "^2.0.4", - "@tiptap/extension-table": "^2.0.4", - "@tiptap/extension-table-cell": "^2.0.4", - "@tiptap/extension-table-header": "^2.0.4", - "@tiptap/extension-table-row": "^2.0.4", - "@tiptap/extension-task-item": "^2.0.4", - "@tiptap/extension-task-list": "^2.0.4", - "@tiptap/extension-text-align": "^2.0.4", - "@tiptap/extension-text-style": "^2.0.4", - "@tiptap/extension-typography": "^2.0.4", - "@tiptap/extension-underline": "^2.0.4", - "@tiptap/pm": "^2.0.4", - "@tiptap/react": "^2.0.4", - "@tiptap/starter-kit": "^2.0.4", + "@mantine/core": "^6.0.21", + "@mantine/dropzone": "^6.0.21", + "@mantine/form": "^6.0.21", + "@mantine/hooks": "^6.0.21", + "@mantine/modals": "^6.0.21", + "@mantine/notifications": "^6.0.21", + "@tabler/icons": "^2.39.0", + "@tabler/icons-webfont": "^2.39.0", + "@tiptap/core": "^2.1.12", + "@tiptap/extension-code": "^2.1.12", + "@tiptap/extension-code-block": "^2.1.12", + "@tiptap/extension-code-block-lowlight": "^2.1.12", + "@tiptap/extension-color": "^2.1.12", + "@tiptap/extension-focus": "^2.1.12", + "@tiptap/extension-font-family": "^2.1.12", + "@tiptap/extension-heading": "^2.1.12", + "@tiptap/extension-highlight": "^2.1.12", + "@tiptap/extension-image": "^2.1.12", + "@tiptap/extension-link": "^2.1.12", + "@tiptap/extension-placeholder": "^2.1.12", + "@tiptap/extension-subscript": "^2.1.12", + "@tiptap/extension-superscript": "^2.1.12", + "@tiptap/extension-table": "^2.1.12", + "@tiptap/extension-table-cell": "^2.1.12", + "@tiptap/extension-table-header": "^2.1.12", + "@tiptap/extension-table-row": "^2.1.12", + "@tiptap/extension-task-item": "^2.1.12", + "@tiptap/extension-task-list": "^2.1.12", + "@tiptap/extension-text-align": "^2.1.12", + "@tiptap/extension-text-style": "^2.1.12", + "@tiptap/extension-typography": "^2.1.12", + "@tiptap/extension-underline": "^2.1.12", + "@tiptap/pm": "^2.1.12", + "@tiptap/react": "^2.1.12", + "@tiptap/starter-kit": "^2.1.12", "@treverix/custom-electron-titlebar": "^4.2.0", "d3": "^7.8.5", "electron-context-menu": "^3.6.1", "electron-is-dev": "^2.0.0", "electron-log": "^4.4.8", "fuse.js": "^6.6.2", - "highlight.js": "^11.8.0", - "katex": "^0.16.8", - "lowlight": "^2.9.0", - "mathlive": "^0.95.1", - "palettey": "^1.0.3", + "highlight.js": "^11.9.0", + "katex": "^0.16.9", + "lowlight": "^3.1.0", + "mathlive": "^0.95.5", + "palettey": "^1.0.4", "react-lowlight": "^3.0.0", "sanitize-filename": "^1.6.3", "semver": "^7.5.4", "tiptap-markdown": "^0.8.2", - "validator": "^13.9.0" + "validator": "^13.11.0" }, "devDependencies": { - "@babel/plugin-transform-react-jsx": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.15", "@preact/compat": "^17.1.2", "@preact/preset-vite": "^2.5.0", - "@types/d3": "^7.4.0", - "@types/katex": "^0.16.2", - "@types/node": "^16.18.39", - "@types/node-fetch": "^2.6.4", - "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@types/semver": "^7.5.0", - "@types/validator": "^13.9.0", - "@typescript-eslint/eslint-plugin": "^6.2.1", - "@typescript-eslint/parser": "^6.2.1", - "@vitejs/plugin-react": "^4.0.4", - "electron": "^22.3.18", - "electron-builder": "^24.6.3", - "eslint": "^8.46.0", - "eslint-config-prettier": "^8.10.0", - "eslint-plugin-react": "^7.33.1", + "@types/d3": "^7.4.1", + "@types/katex": "^0.16.3", + "@types/node": "^16.18.58", + "@types/node-fetch": "^2.6.6", + "@types/react": "^18.2.28", + "@types/react-dom": "^18.2.13", + "@types/semver": "^7.5.3", + "@types/validator": "^13.11.2", + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "@vitejs/plugin-react": "^4.1.0", + "electron": "^22.3.27", + "electron-builder": "^24.6.4", + "eslint": "^8.51.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", - "node-fetch": "^2.6.12", - "preact": "^10.16.0", - "prettier": "^3.0.1", + "node-fetch": "^2.7.0", + "preact": "10.17.1", + "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "rimraf": "^5.0.1", - "sass": "^1.64.2", - "typescript": "^5.1.6", - "vite": "^4.4.8", + "rimraf": "^5.0.5", + "sass": "^1.69.3", + "typescript": "^5.2.2", + "vite": "^4.4.11", "vite-plugin-static-copy": "^0.17.0", - "vite-tsconfig-paths": "^4.2.0", + "vite-tsconfig-paths": "^4.2.1", "wait-on": "^7.0.1" } } diff --git a/packages/common/Locales.ts b/packages/common/Locales.ts index 75b3505..24786b2 100644 --- a/packages/common/Locales.ts +++ b/packages/common/Locales.ts @@ -94,6 +94,14 @@ export type Locale = { changelogs: string; about: string; }; + shellDialogs: { + open_external_link: { + title: string; + yes: string; + cancel: string; + trust_domain: string; + }; + }; mutateModals: { edit_modal_title: (itemName: string) => string; new_page_modal_title: (parentName: string) => string; @@ -169,6 +177,7 @@ export type Locale = { inline_code: string; superscript: string; subscript: string; + link: string; align_left: string; align_right: string; align_center: string; @@ -236,10 +245,16 @@ export type Locale = { edit: string; insert: string; }; + linkModal: { + cancel: string; + create_link: string; + url: string; + }; }; }; -export type SupportedLocales = "en_US"; +export const supportedLocales = ["en_US"] as const; +export type SupportedLocales = (typeof supportedLocales)[number]; export const locales: Record = { en_US: { @@ -333,7 +348,8 @@ export const locales: Record = { "You can nest folders inside of folders", "You can make a page top-level by dragging it to an empty part of the sidebar", 'Edit your save.json and try setting a page\'s "color" property to "rainbow"', - "Contribute your own language to Codex on the GitHub repository (see CONTRIBUTING.md)" + "Contribute your own language to Codex on the GitHub repository (see CONTRIBUTING.md)", + "Open links in your notes by holding Control (or Cmd) and clicking on it" ], favorites: "Favorites" }, @@ -351,6 +367,14 @@ export const locales: Record = { changelogs: "Changelogs", about: "About" }, + shellDialogs: { + open_external_link: { + title: "Are you sure you want to open this link?", + yes: "Yes", + cancel: "Cancel", + trust_domain: "Always trust this domain" + } + }, mutateModals: { edit_modal_title: (itemName: string) => `Edit ${itemName}`, new_page_modal_title: (parentName) => `New Page in ${parentName}`, @@ -430,6 +454,7 @@ export const locales: Record = { inline_code: "Inline Code", superscript: "Superscript", subscript: "Subscript", + link: "Link", align_left: "Align Left", align_right: "Align Right", align_center: "Align Center", @@ -497,6 +522,11 @@ export const locales: Record = { cancel: "Cancel", edit: "Edit", insert: "Insert" + }, + linkModal: { + cancel: "Cancel", + create_link: "Create Link", + url: "URL" } } } diff --git a/packages/common/Prefs.ts b/packages/common/Prefs.ts index 7f94bcc..3d63982 100644 --- a/packages/common/Prefs.ts +++ b/packages/common/Prefs.ts @@ -18,6 +18,7 @@ class EditorPrefs { spellcheck = true; zoom = 1.0; openPDFonExport = true; + recentCodeLangs: string[] = []; } class MiscPrefs { diff --git a/packages/common/ipc.ts b/packages/common/ipc.ts index 260304f..461b8b0 100644 --- a/packages/common/ipc.ts +++ b/packages/common/ipc.ts @@ -101,6 +101,10 @@ export class TypedIpcMain { isRunningUnderARM64Translation = (callback: () => boolean) => { ipcMain.handle("IS_RUNNING_UNDER_ARM64_TRANSLATION", callback); }; + + onOpenExternalLink = (callback: (href: string) => void) => { + ipcMain.on("OPEN_EXTERNAL_LINK", (e, args) => callback(args[0])); + }; } export class TypedIpcRenderer { @@ -231,4 +235,8 @@ export class TypedIpcRenderer { isRunningUnderARM64Translation = () => { return ipcRenderer.invoke("IS_RUNNING_UNDER_ARM64_TRANSLATION"); }; + + openExternalLink = (href: string) => { + ipcRenderer.send("OPEN_EXTERNAL_LINK", [href]); + }; } diff --git a/packages/main/createWindow.ts b/packages/main/createWindow.ts index 0020da5..34e5ab0 100644 --- a/packages/main/createWindow.ts +++ b/packages/main/createWindow.ts @@ -8,28 +8,25 @@ import contextMenu from "electron-context-menu"; import { Prefs } from "common/Prefs"; import { locales } from "common/Locales"; import nodeFetch from "node-fetch"; -import { compare } from "semver"; +import { lt } from "semver"; import escape from "validator/lib/escape"; +import log from "electron-log"; async function checkForUpdates(window: BrowserWindow) { app.commandLine.appendSwitch("disable-http-cache"); - const resp = await nodeFetch("https://jcv8000.github.io/codex/latestversion.txt"); - const onlineVersion = await resp.text(); + try { + const resp = await nodeFetch("https://api.github.com/repos/jcv8000/Codex/releases"); + const body = (await resp.json()) as any[]; - if (onlineVersion != undefined && import.meta.env.VITE_APP_VERSION != undefined) - if (compare(onlineVersion, import.meta.env.VITE_APP_VERSION) != 0) { - window.webContents.send("UPDATE_AVAILABLE", [escape(onlineVersion)]); + const latest = body[0].tag_name as string; - // if (process.platform === "win32") { - // window.once("focus", () => window.flashFrame(false)); - // window.flashFrame(true); - // } - - // if (process.platform === "darwin") { - // app.dock.bounce("critical"); - // } - } + if (latest != undefined && import.meta.env.VITE_APP_VERSION != undefined) + if (lt(import.meta.env.VITE_APP_VERSION, latest)) + window.webContents.send("UPDATE_AVAILABLE", [escape(latest)]); + } catch (e) { + log.info("Unable to check for updates"); + } } export function createWindow(prefs: Prefs) { diff --git a/packages/main/data.ts b/packages/main/data.ts index 12ad898..fbe3711 100644 --- a/packages/main/data.ts +++ b/packages/main/data.ts @@ -1,10 +1,11 @@ import { app, dialog } from "electron"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import path from "path"; import { Prefs } from "common/Prefs"; import { exampleSave, Save } from "common/Save"; import { logError } from "./logger"; import { convertOldPrefs, convertOldSave } from "./convertOld"; +import { supportedLocales } from "common/Locales"; export function loadPrefs(): Prefs | null { const prefsPath = path.join(app.getPath("userData"), "/prefs.json"); @@ -17,6 +18,7 @@ export function loadPrefs(): Prefs | null { // 1.X Save const prefs = convertOldPrefs(readPrefs); writePrefs(prefs); + fixPrefs(prefs); return prefs; } else { @@ -26,14 +28,9 @@ export function loadPrefs(): Prefs | null { Object.assign(prefs.editor, readPrefs.editor); Object.assign(prefs.misc, readPrefs.misc); - if (prefs.general.saveFolder == "") { - prefs.general.saveFolder = app.getPath("userData"); - } - - if (process.platform === "darwin") { - prefs.general.titlebarStyle = "native"; - } + fixPrefs(prefs); + writePrefs(prefs); return prefs; } } catch (error) { @@ -61,7 +58,7 @@ export function writePrefs(prefs: Prefs) { try { writeFileSync(prefsPath, JSON.stringify(prefs, null, 4), "utf-8"); } catch (error) { - logError("Couldn't write prefs.json to the disk", "" + (error).stack); + logError("Error: Couldn't write prefs.json to the disk", "" + (error).stack); } } @@ -115,7 +112,7 @@ export function loadSave(saveFolderPath: string): Save | null { return Save.parse(saveText); } } catch (error) { - logError("Couldn't parse save.json", "" + (error).stack); + logError("Error: Couldn't parse save.json", "" + (error).stack); return null; } } else { @@ -131,7 +128,7 @@ export function writeSave(saveFolderPath: string, save: Save) { try { writeFileSync(saveFilePath, Save.stringify(save), "utf-8"); } catch (error) { - logError("Couldn't write save.json to the disk", "" + (error).stack); + logError("Error: Couldn't write save.json to the disk", "" + (error).stack); } } @@ -157,3 +154,51 @@ export function writePage(saveFolderPath: string, pageFileName: string, data: st const filePath = path.join(saveFolderPath, "/notes/", pageFileName); writeFileSync(filePath, data, "utf-8"); } + +function fixPrefs(prefs: Prefs) { + if (prefs.general.saveFolder == "") { + prefs.general.saveFolder = app.getPath("userData"); + logError( + "Error", + 'Save folder location was set to "", reverting to default:\n\n' + + app.getPath("userData") + ); + } + + try { + if ( + existsSync(prefs.general.saveFolder) == false || + lstatSync(prefs.general.saveFolder).isDirectory() == false + ) { + logError( + "Error", + `Save folder location (${prefs.general.saveFolder}) does not exist, or is not a directory. Reverting to default:\n\n` + + app.getPath("userData") + ); + prefs.general.saveFolder = app.getPath("userData"); + } + } catch (err) { + logError( + "Error", + `Save folder location (${prefs.general.saveFolder}) does not exist, or is not a directory. Reverting to default:\n\n` + + app.getPath("userData") + ); + prefs.general.saveFolder = app.getPath("userData"); + } + + if (process.platform === "darwin") { + prefs.general.titlebarStyle = "native"; + } + + if (!supportedLocales.includes(prefs.general.locale)) { + prefs.general.locale = "en_US"; + } + + if (prefs.general.theme != "light" && prefs.general.theme != "dark") { + prefs.general.theme = "light"; + } + + if (prefs.general.titlebarStyle != "custom" && prefs.general.titlebarStyle != "native") { + prefs.general.titlebarStyle = process.platform === "darwin" ? "native" : "custom"; + } +} diff --git a/packages/main/index.ts b/packages/main/index.ts index b0baf1d..7c65183 100644 --- a/packages/main/index.ts +++ b/packages/main/index.ts @@ -2,13 +2,14 @@ import { BrowserWindow, app, dialog, shell } from "electron"; import { loadPrefs, loadSave, writePrefs, writeSave, loadPage, writePage } from "./data"; import { createWindow } from "./createWindow"; import { TypedIpcMain } from "common/ipc"; -import { writeFileSync } from "fs"; +import { existsSync, readFileSync, writeFileSync } from "fs"; import { sanitizeStringForFileName } from "common/Utils"; import { join } from "path"; import isDev from "electron-is-dev"; import { electronSecurity } from "./security"; import { setupLogger } from "./logger"; import { saveWindowState } from "./windowState"; +import { locales } from "common/Locales"; if (app.requestSingleInstanceLock()) { app.whenReady().then(() => { @@ -169,6 +170,44 @@ if (app.requestSingleInstanceLock()) { typedIpcMain.isRunningUnderARM64Translation(() => { return app.runningUnderARM64Translation; }); + + typedIpcMain.onOpenExternalLink((href) => { + const filePath = join(app.getPath("userData"), "trusted-link-domains.json"); + if (!existsSync(filePath)) { + writeFileSync(filePath, JSON.stringify([]), "utf-8"); + } + const trusted: string[] = JSON.parse(readFileSync(filePath).toString()); + + const url = new URL(href); + const origin = url.origin; + + if (trusted.includes(origin)) { + shell.openExternal(href); + return; + } + + const result = dialog.showMessageBoxSync(window, { + type: "question", + noLink: true, + message: locales[prefs.general.locale].shellDialogs.open_external_link.title, + detail: href + "\n\nTrusted domains are stored here:\n" + filePath, + buttons: [ + locales[prefs.general.locale].shellDialogs.open_external_link.yes, + locales[prefs.general.locale].shellDialogs.open_external_link.trust_domain, + locales[prefs.general.locale].shellDialogs.open_external_link.cancel + ] + }); + + if (result == 0) { + // Yes + shell.openExternal(href); + } else if (result == 1) { + // Trust domain + shell.openExternal(href); + trusted.push(origin); + writeFileSync(filePath, JSON.stringify(trusted), "utf-8"); + } + }); }); // Quit when all windows are closed, except on macOS. There, it's common diff --git a/packages/main/logger.ts b/packages/main/logger.ts index 28893be..26783c8 100644 --- a/packages/main/logger.ts +++ b/packages/main/logger.ts @@ -34,7 +34,7 @@ export function setupLogger() { export function logError(message: string, detail: string) { log.error(message + "\n" + detail); dialog.showMessageBoxSync({ - title: "Error", + title: "Codex", message: message, detail: detail, type: "error" diff --git a/packages/renderer/src/App.tsx b/packages/renderer/src/App.tsx index 32574f7..f415458 100644 --- a/packages/renderer/src/App.tsx +++ b/packages/renderer/src/App.tsx @@ -1,4 +1,4 @@ -import { Anchor, AppShell, MantineProvider, Portal, Text } from "@mantine/core"; +import { Anchor, AppShell, Box, MantineProvider, Portal, Text } from "@mantine/core"; import { ModalsProvider as MantineModalsProvider } from "@mantine/modals"; import { Sidebar } from "components/Sidebar"; import { EditorView, HomeView, SettingsView } from "components/Views"; @@ -16,6 +16,7 @@ import { EditorContent } from "@tiptap/react"; import { Notifications, notifications } from "@mantine/notifications"; import { Icon } from "components/Icon"; import { locales } from "common/Locales"; +import { EditorStyles } from "components/Views/Editor/EditorStyles"; export function App() { const forceUpdate = useForceUpdate(); @@ -276,8 +277,8 @@ export function App() { /> }>{renderedView} - - - - - - + + + + ({ + color: theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black + })} + > + + + + + + ); } diff --git a/packages/renderer/src/components/Modals/EditorLinkModal.tsx b/packages/renderer/src/components/Modals/EditorLinkModal.tsx new file mode 100644 index 0000000..6ef4a2b --- /dev/null +++ b/packages/renderer/src/components/Modals/EditorLinkModal.tsx @@ -0,0 +1,64 @@ +import { Button, Group, Modal, TextInput, Title } from "@mantine/core"; +import { useContext, useEffect, useState } from "react"; +import { Editor } from "@tiptap/core"; +import { AppContext } from "types/AppStore"; +import { locales } from "common/Locales"; + +export type EditorLinkModalState = { + opened: boolean; + editor: Editor | null; + initialUrl: string; +}; + +export function EditorLinkModal(props: { state: EditorLinkModalState; onClose: () => void }) { + const { opened, editor, initialUrl } = props.state; + + const [url, setUrl] = useState(initialUrl); + + const { prefs } = useContext(AppContext); + const texts = locales[prefs.general.locale].editor.linkModal; + + useEffect(() => { + setUrl(props.state.initialUrl); + }, [props.state]); + + const onClose = () => { + props.onClose(); + setUrl(""); + }; + + if (editor == null) { + return <>; + } else { + return ( + {texts.create_link}} + > + setUrl(e.currentTarget.value)} + /> + + + + + + + ); + } +} diff --git a/packages/renderer/src/components/Modals/IconSelector.tsx b/packages/renderer/src/components/Modals/IconSelector.tsx index 0821edc..7e337fd 100644 --- a/packages/renderer/src/components/Modals/IconSelector.tsx +++ b/packages/renderer/src/components/Modals/IconSelector.tsx @@ -64,7 +64,13 @@ export function IconSelector({ icon, onChangeIcon, color, onChangeColor }: Props tags: icon.tags })); - list.push({ name: "codex", category: "Brand", tags: [] }); + // Insert custom Codex icon in-order + for (let i = 0; i < list.length; i++) { + if (list[i].name > "codex") { + list.splice(i, 0, { name: "codex", category: "Brand", tags: [] }); + break; + } + } return list; }, []); diff --git a/packages/renderer/src/components/Modals/ModalProvider.tsx b/packages/renderer/src/components/Modals/ModalProvider.tsx index 362d2d1..ef95544 100644 --- a/packages/renderer/src/components/Modals/ModalProvider.tsx +++ b/packages/renderer/src/components/Modals/ModalProvider.tsx @@ -13,6 +13,7 @@ import { } from "components/Modals"; import React, { createContext, useState } from "react"; import { exampleSave, Folder } from "common/Save"; +import { EditorLinkModal, EditorLinkModalState } from "./EditorLinkModal"; class ModalStore { openWhatsNewModal: () => void = () => {}; @@ -25,6 +26,7 @@ class ModalStore { // Editor modals openEditorImageModal: (options: Omit) => void = () => {}; openEditorMathModal: (options: Omit) => void = () => {}; + openEditorLinkModal: (options: Omit) => void = () => {}; } export const ModalContext = createContext(new ModalStore()); @@ -68,6 +70,12 @@ export function ModalProvider(props: { children?: React.ReactNode }) { startLatex: "" }); + const [editorLinkModalState, setEditorLinkModalState] = useState({ + opened: false, + editor: null, + initialUrl: "" + }); + return ( { + setEditorLinkModalState({ + opened: true, + editor: options.editor, + initialUrl: options.initialUrl + }); } }} > @@ -132,6 +147,10 @@ export function ModalProvider(props: { children?: React.ReactNode }) { state={editorMathModalState} onClose={() => setEditorMathModalState({ ...editorMathModalState, opened: false })} /> + setEditorLinkModalState({ ...editorLinkModalState, opened: false })} + /> ); } diff --git a/packages/renderer/src/components/Views/Editor/EditorExtensions.ts b/packages/renderer/src/components/Views/Editor/EditorExtensions.ts index 2a8ab3c..cf7c59d 100644 --- a/packages/renderer/src/components/Views/Editor/EditorExtensions.ts +++ b/packages/renderer/src/components/Views/Editor/EditorExtensions.ts @@ -1,9 +1,7 @@ import { Extensions } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; import Focus from "@tiptap/extension-focus"; -import { lowlight } from "lowlight/lib/all"; -import Image from "@tiptap/extension-image"; -import Table from "@tiptap/extension-table"; +import { createLowlight, all as lowlightAll } from "lowlight"; import TableCell from "@tiptap/extension-table-cell"; import TableHeader from "@tiptap/extension-table-header"; import TableRow from "@tiptap/extension-table-row"; @@ -14,7 +12,6 @@ import FontFamily from "@tiptap/extension-font-family"; import TextStyle from "@tiptap/extension-text-style"; import { Color } from "@tiptap/extension-color"; import Highlight from "@tiptap/extension-highlight"; -import Link from "@tiptap/extension-link"; import Placeholder from "@tiptap/extension-placeholder"; import TextAlign from "@tiptap/extension-text-align"; import Subscript from "@tiptap/extension-subscript"; @@ -25,17 +22,22 @@ import { MathBlock, MathInline } from "./extensions/Math"; import { CustomCodeBlock } from "./extensions/CustomCodeBlock"; import { Markdown } from "tiptap-markdown"; import { FontSize } from "./extensions/FontSize"; +import { CustomLink } from "./extensions/CustomLink"; +import { CustomCode } from "./extensions/CustomCode"; +import { CustomTable } from "./extensions/CustomTable"; +import { ResizableImage } from "./extensions/ResizableImage/ResizableImage"; export function extensions(options: { useTypography: boolean }) { const e = [ StarterKit.configure({ codeBlock: false, heading: false, - code: { - HTMLAttributes: { - class: "hljs", - spellCheck: false - } + code: false + }), + CustomCode.configure({ + HTMLAttributes: { + class: "hljs", + spellCheck: false } }), Focus.configure({ @@ -46,12 +48,12 @@ export function extensions(options: { useTypography: boolean }) { placeholder: "Start typing..." }), CustomCodeBlock.configure({ - lowlight: lowlight + lowlight: createLowlight(lowlightAll) }), - Image.configure({ + ResizableImage.configure({ allowBase64: true }), - Table.configure({ + CustomTable.configure({ // resizable: true, // lastColumnResizable: false, // allowTableNodeSelection: true @@ -69,9 +71,7 @@ export function extensions(options: { useTypography: boolean }) { Highlight.configure({ multicolor: true }), - Link.configure({ - openOnClick: false - }), + CustomLink.configure({ openOnClick: true }), TextAlign.configure({ types: ["heading", "paragraph"] }), diff --git a/packages/renderer/src/components/Views/Editor/EditorStyles.tsx b/packages/renderer/src/components/Views/Editor/EditorStyles.tsx new file mode 100644 index 0000000..0ff7ae6 --- /dev/null +++ b/packages/renderer/src/components/Views/Editor/EditorStyles.tsx @@ -0,0 +1,25 @@ +import { Box } from "@mantine/core"; + +export function EditorStyles(props: { children: JSX.Element | JSX.Element[] }) { + return ( + ({ + ".node-mathBlock.has-focus, .node-mathInline.has-focus, .node-canvas.has-focus": { + outline: `2px solid ${theme.fn.primaryColor()}` + }, + ".node-image.has-focus img": { + outline: `2px solid ${theme.fn.primaryColor()}` + }, + "th, td": { + border: `1px solid ${theme.colorScheme == "light" ? "#d0d7de" : "#373A40"}` + }, + "tr:nth-child(even)": { + backgroundColor: theme.colorScheme == "light" ? "#f6f8fa" : "#25262b" + } + })} + mx="md" + > + {props.children} + + ); +} diff --git a/packages/renderer/src/components/Views/Editor/EditorView.tsx b/packages/renderer/src/components/Views/Editor/EditorView.tsx index 904be5f..2073d4b 100644 --- a/packages/renderer/src/components/Views/Editor/EditorView.tsx +++ b/packages/renderer/src/components/Views/Editor/EditorView.tsx @@ -1,5 +1,5 @@ import "./styles.scss"; -import { Box, Container, Paper } from "@mantine/core"; +import { Container, Paper } from "@mantine/core"; import { Page } from "common/Save"; import { useContext, useEffect } from "react"; import { AppContext } from "types/AppStore"; @@ -7,6 +7,7 @@ import { Editor, EditorContent, useEditor } from "@tiptap/react"; import Toolbar from "./Toolbar/Toolbar"; import { extensions } from "./EditorExtensions"; import { TableOfContents } from "./TableOfContents"; +import { EditorStyles } from "./EditorStyles"; type Props = { page: Page; @@ -64,15 +65,7 @@ export function EditorView({ page, setEditorRef }: Props) { if (editor != null) { return ( - ({ - ".node-mathBlock.has-focus, .node-mathInline.has-focus, img.has-focus, .node-canvas.has-focus": - { - outline: `2px solid ${theme.fn.primaryColor()}` - } - })} - mx="md" - > + @@ -98,7 +91,7 @@ export function EditorView({ page, setEditorRef }: Props) { /> - + ); } else return <>; } diff --git a/packages/renderer/src/components/Views/Editor/Toolbar/Constants.ts b/packages/renderer/src/components/Views/Editor/Toolbar/Constants.ts new file mode 100644 index 0000000..0fe4008 --- /dev/null +++ b/packages/renderer/src/components/Views/Editor/Toolbar/Constants.ts @@ -0,0 +1,3 @@ +export const BUTTON_WIDTH = 28; +export const BUTTON_HEIGHT = 30; +export const SPACING_W = 8; diff --git a/packages/renderer/src/components/Views/Editor/Toolbar/Toolbar.tsx b/packages/renderer/src/components/Views/Editor/Toolbar/Toolbar.tsx index eebc614..66f5552 100644 --- a/packages/renderer/src/components/Views/Editor/Toolbar/Toolbar.tsx +++ b/packages/renderer/src/components/Views/Editor/Toolbar/Toolbar.tsx @@ -16,10 +16,11 @@ import { ToolbarButton } from "./ToolbarButton"; import { ToolbarDropdown } from "./ToolbarDropdown"; import { ToolbarSplit } from "./ToolbarSplit"; import { useContext, useEffect, useMemo, useState } from "react"; -import { lowlight } from "lowlight"; +import { createLowlight, all as lowlightAll } from "lowlight"; import { AppContext } from "types/AppStore"; import { locales } from "common/Locales"; import { ToolbarToggler } from "./ToolbarToggler"; +import { BUTTON_WIDTH, BUTTON_HEIGHT, SPACING_W } from "./Constants"; type Props = { editor: Editor; @@ -31,10 +32,10 @@ export default function Toolbar({ editor }: Props) { const texts = locales[appContext.prefs.general.locale].editor.toolbar; const languages = useMemo(() => { - return lowlight.listLanguages().sort(); + return createLowlight(lowlightAll).listLanguages().sort(); }, []); - const [recentLangs, setRecentLangs] = useState([]); + const [recentLangs, setRecentLangs] = useState(appContext.prefs.editor.recentCodeLangs); const [show, setShow] = useState(true); useEffect(() => { @@ -56,6 +57,10 @@ export default function Toolbar({ editor }: Props) { const newRecentLangs = [...recentLangs]; newRecentLangs.splice(newRecentLangs.indexOf(lang), 1); setRecentLangs(newRecentLangs); + + appContext.modifyPrefs( + (p) => (p.editor.recentCodeLangs = newRecentLangs) + ); }} /> } @@ -89,7 +94,7 @@ export default function Toolbar({ editor }: Props) { marginRight: "auto" }} > - + !editor.can().redo()} /> - + editor.chain().focus().toggleSubscript().run()} isActive={() => editor.isActive("subscript")} /> + editor.isActive("link")} + disabled={() => editor.view.state.selection.empty} + onClick={() => { + if (editor.isActive("link")) + modalContext.openEditorLinkModal({ + editor: editor, + initialUrl: editor.getAttributes("link").href + }); + else + modalContext.openEditorLinkModal({ + editor: editor, + initialUrl: "" + }); + }} + /> - + editor.chain().focus().setTextAlign("justify").run()} /> - + editor.chain().focus().setHorizontalRule().run()} /> - + editor.isActive("taskList")} /> - + (p.editor.recentCodeLangs = newRecentLangs) + ); } } }} @@ -420,17 +447,11 @@ export default function Toolbar({ editor }: Props) { }} /> - - - editor.chain().focus().unsetAllMarks().run()} - /> + - } + data={[ + { value: "default", label: texts.default_font }, + "Arial", + "Verdana", + "Tahoma", + "Trebuchet MS", + "Impact", + "Times New Roman", + "Georgia", + "Courier", + "Comic Sans MS" + ]} + value={ + editor.getAttributes("textStyle").fontFamily + ? editor.getAttributes("textStyle").fontFamily + : "default" + } + onChange={(value) => { + if (value == "default" || value == null) + editor.chain().focus().unsetFontFamily().run(); + else editor.chain().focus().setFontFamily(value).run(); + }} + disabled={!editor.can().setFontFamily("Arial")} + />
+ + editor.chain().focus().unsetAllMarks().run()} + /> + {/*