A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 570 lines 20 kB view raw
1import {focusByRange} from "./selection"; 2import {fetchPost, fetchSyncPost} from "../../util/fetch"; 3import {Constants} from "../../constants"; 4/// #if !BROWSER 5import {clipboard, ipcRenderer} from "electron"; 6/// #endif 7 8export const encodeBase64 = (text: string): string => { 9 if (typeof Buffer !== "undefined") { 10 return Buffer.from(text, "utf8").toString("base64"); 11 } else { 12 const encoder = new TextEncoder(); 13 const bytes = encoder.encode(text); 14 let binary = ""; 15 const chunkSize = 0x8000; // 避免栈溢出 16 17 for (let i = 0; i < bytes.length; i += chunkSize) { 18 const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length)); 19 binary += String.fromCharCode(...chunk); 20 } 21 22 return btoa(binary); 23 } 24}; 25 26export const getTextSiyuanFromTextHTML = (html: string) => { 27 const siyuanMatch = html.match(/<!--data-siyuan='([^']+)'-->/); 28 let textSiyuan = ""; 29 let textHtml = html; 30 if (siyuanMatch) { 31 try { 32 if (typeof Buffer !== "undefined") { 33 const decodedBytes = Buffer.from(siyuanMatch[1], "base64"); 34 textSiyuan = decodedBytes.toString("utf8"); 35 } else { 36 const decoder = new TextDecoder(); 37 const bytes = Uint8Array.from(atob(siyuanMatch[1]), char => char.charCodeAt(0)); 38 textSiyuan = decoder.decode(bytes); 39 } 40 // 移除注释节点,保持原有的 text/html 内容 41 textHtml = html.replace(/<!--data-siyuan='[^']+'-->/, ""); 42 } catch (e) { 43 console.log("Failed to decode siyuan data from HTML comment:", e); 44 } 45 } 46 return { 47 textSiyuan, 48 textHtml 49 }; 50}; 51 52export const openByMobile = (uri: string) => { 53 if (!uri) { 54 return; 55 } 56 if (isInIOS()) { 57 if (uri.startsWith("assets/")) { 58 // iOS 16.7 之前的版本,uri 需要 encodeURIComponent 59 window.webkit.messageHandlers.openLink.postMessage(location.origin + "/assets/" + encodeURIComponent(uri.replace("assets/", ""))); 60 } else if (uri.startsWith("/")) { 61 // 导出 zip 返回的是已经 encode 过的,因此不能再 encode 62 window.webkit.messageHandlers.openLink.postMessage(location.origin + uri); 63 } else { 64 try { 65 new URL(uri); 66 window.webkit.messageHandlers.openLink.postMessage(uri); 67 } catch (e) { 68 window.webkit.messageHandlers.openLink.postMessage("https://" + uri); 69 } 70 } 71 } else if (isInAndroid()) { 72 window.JSAndroid.openExternal(uri); 73 } else if (isInHarmony()) { 74 window.JSHarmony.openExternal(uri); 75 } else { 76 window.open(uri); 77 } 78}; 79 80export const exportByMobile = (uri: string) => { 81 if (!uri) { 82 return; 83 } 84 if (isInIOS()) { 85 openByMobile(uri); 86 } else if (isInAndroid()) { 87 window.JSAndroid.exportByDefault(uri); 88 } else if (isInHarmony()) { 89 window.JSHarmony.exportByDefault(uri); 90 } else { 91 window.open(uri); 92 } 93}; 94 95export const readText = () => { 96 if (isInAndroid()) { 97 return window.JSAndroid.readClipboard(); 98 } else if (isInHarmony()) { 99 return window.JSHarmony.readClipboard(); 100 } 101 if (typeof navigator.clipboard === "undefined") { 102 alert(window.siyuan.languages.clipboardPermissionDenied); 103 return ""; 104 } 105 return navigator.clipboard.readText().catch(() => { 106 alert(window.siyuan.languages.clipboardPermissionDenied); 107 }) || ""; 108}; 109 110/// #if !BROWSER 111export const getLocalFiles = async () => { 112 // 不再支持 PC 浏览器 https://github.com/siyuan-note/siyuan/issues/7206 113 let localFiles: string[] = []; 114 if ("darwin" === window.siyuan.config.system.os) { 115 const xmlString = clipboard.read("NSFilenamesPboardType"); 116 const domParser = new DOMParser(); 117 const xmlDom = domParser.parseFromString(xmlString, "application/xml"); 118 Array.from(xmlDom.getElementsByTagName("string")).forEach(item => { 119 localFiles.push(item.childNodes[0].nodeValue); 120 }); 121 } else { 122 const xmlString = await fetchSyncPost("/api/clipboard/readFilePaths", {}); 123 if (xmlString.data.length > 0) { 124 localFiles = xmlString.data; 125 } 126 } 127 return localFiles; 128}; 129/// #endif 130 131export const readClipboard = async () => { 132 const text: IClipboardData = {textPlain: "", textHTML: "", siyuanHTML: ""}; 133 if (isInAndroid()) { 134 text.textPlain = window.JSAndroid.readClipboard(); 135 text.textHTML = window.JSAndroid.readHTMLClipboard(); 136 const textObj = getTextSiyuanFromTextHTML(text.textHTML); 137 text.textHTML = textObj.textHtml; 138 text.siyuanHTML = textObj.textSiyuan; 139 return text; 140 } 141 if (isInHarmony()) { 142 text.textPlain = window.JSHarmony.readClipboard(); 143 text.textHTML = window.JSHarmony.readHTMLClipboard(); 144 const textObj = getTextSiyuanFromTextHTML(text.textHTML); 145 text.textHTML = textObj.textHtml; 146 text.siyuanHTML = textObj.textSiyuan; 147 return text; 148 } 149 if (typeof navigator.clipboard === "undefined") { 150 alert(window.siyuan.languages.clipboardPermissionDenied); 151 return text; 152 } 153 try { 154 const clipboardContents = await navigator.clipboard.read().catch(() => { 155 alert(window.siyuan.languages.clipboardPermissionDenied); 156 }); 157 if (!clipboardContents) { 158 return text; 159 } 160 for (const item of clipboardContents) { 161 if (item.types.includes("text/html")) { 162 const blob = await item.getType("text/html"); 163 text.textHTML = await blob.text(); 164 const textObj = getTextSiyuanFromTextHTML(text.textHTML); 165 text.textHTML = textObj.textHtml; 166 text.siyuanHTML = textObj.textSiyuan; 167 } 168 if (item.types.includes("text/plain")) { 169 const blob = await item.getType("text/plain"); 170 text.textPlain = await blob.text(); 171 } 172 if (item.types.includes("image/png")) { 173 const blob = await item.getType("image/png"); 174 text.files = [new File([blob], "image.png", {type: "image/png", lastModified: Date.now()})]; 175 } 176 } 177 /// #if !BROWSER 178 if (!text.textHTML && !text.files) { 179 text.localFiles = await getLocalFiles(); 180 } 181 /// #endif 182 return text; 183 } catch (e) { 184 return text; 185 } 186}; 187 188export const writeText = (text: string) => { 189 let range: Range; 190 if (getSelection().rangeCount > 0) { 191 range = getSelection().getRangeAt(0).cloneRange(); 192 } 193 try { 194 // navigator.clipboard.writeText 抛出异常不进入 catch,这里需要先处理移动端复制 195 if (isInAndroid()) { 196 window.JSAndroid.writeClipboard(text); 197 return; 198 } 199 if (isInHarmony()) { 200 window.JSHarmony.writeClipboard(text); 201 return; 202 } 203 if (isInIOS()) { 204 window.webkit.messageHandlers.setClipboard.postMessage(text); 205 return; 206 } 207 navigator.clipboard.writeText(text); 208 } catch (e) { 209 if (isInIOS()) { 210 window.webkit.messageHandlers.setClipboard.postMessage(text); 211 } else if (isInAndroid()) { 212 window.JSAndroid.writeClipboard(text); 213 } else if (isInHarmony()) { 214 window.JSHarmony.writeClipboard(text); 215 } else { 216 const textElement = document.createElement("textarea"); 217 textElement.value = text; 218 textElement.style.position = "fixed"; //avoid scrolling to bottom 219 document.body.appendChild(textElement); 220 textElement.focus(); 221 textElement.select(); 222 document.execCommand("copy"); 223 document.body.removeChild(textElement); 224 if (range) { 225 focusByRange(range); 226 } 227 } 228 } 229}; 230 231export const copyPlainText = async (text: string) => { 232 text = text.replace(new RegExp(Constants.ZWSP, "g"), ""); // `复制纯文本` 时移除所有零宽空格 https://github.com/siyuan-note/siyuan/issues/6674 233 await writeText(text); 234}; 235 236// 用户 iPhone 点击延迟/需要双击的处理 237export const getEventName = () => { 238 if (isIPhone()) { 239 return "touchstart"; 240 } else { 241 return "click"; 242 } 243}; 244 245export const isOnlyMeta = (event: KeyboardEvent | MouseEvent) => { 246 if (isMac()) { 247 // mac 248 if (event.metaKey && !event.ctrlKey) { 249 return true; 250 } 251 return false; 252 } else { 253 if (!event.metaKey && event.ctrlKey) { 254 return true; 255 } 256 return false; 257 } 258}; 259 260export const isNotCtrl = (event: KeyboardEvent | MouseEvent) => { 261 if (!event.metaKey && !event.ctrlKey) { 262 return true; 263 } 264 return false; 265}; 266 267export const isHuawei = () => { 268 return window.siyuan.config.system.osPlatform.toLowerCase().indexOf("huawei") > -1; 269}; 270 271export const isDisabledFeature = (feature: string): boolean => { 272 return window.siyuan.config.system.disabledFeatures?.indexOf(feature) > -1; 273}; 274 275export const isIPhone = () => { 276 return navigator.userAgent.indexOf("iPhone") > -1; 277}; 278 279export const isSafari = () => { 280 const userAgent = navigator.userAgent; 281 return userAgent.includes("Safari") && !userAgent.includes("Chrome") && !userAgent.includes("Chromium"); 282}; 283 284export const isIPad = () => { 285 return navigator.userAgent.indexOf("iPad") > -1; 286}; 287 288export const isMac = () => { 289 return navigator.platform.toUpperCase().indexOf("MAC") > -1; 290}; 291 292export const isWin11 = async () => { 293 if (!(navigator as any).userAgentData || !(navigator as any).userAgentData.getHighEntropyValues) { 294 return false; 295 } 296 const ua = await (navigator as any).userAgentData.getHighEntropyValues(["platformVersion"]); 297 if ((navigator as any).userAgentData.platform === "Windows") { 298 if (parseInt(ua.platformVersion.split(".")[0]) >= 13) { 299 return true; 300 } 301 } 302 return false; 303}; 304 305export const getScreenWidth = () => { 306 if (isInAndroid()) { 307 return window.JSAndroid.getScreenWidthPx(); 308 } else if (isInHarmony()) { 309 return window.JSHarmony.getScreenWidthPx(); 310 } 311 return window.outerWidth; 312}; 313 314export const isWindows = () => { 315 return navigator.platform.toUpperCase().indexOf("WIN") > -1; 316}; 317 318export const isInAndroid = () => { 319 return window.siyuan.config.system.container === "android" && window.JSAndroid; 320}; 321 322export const isInIOS = () => { 323 return window.siyuan.config.system.container === "ios" && window.webkit?.messageHandlers; 324}; 325 326export const isInHarmony = () => { 327 return window.siyuan.config.system.container === "harmony" && window.JSHarmony; 328}; 329 330export const updateHotkeyAfterTip = (hotkey: string) => { 331 if (hotkey) { 332 return " " + updateHotkeyTip(hotkey); 333 } 334 return ""; 335}; 336 337// Mac,Windows 快捷键展示 338export const updateHotkeyTip = (hotkey: string) => { 339 if (isMac()) { 340 return hotkey; 341 } 342 343 const KEY_MAP = new Map(Object.entries({ 344 "⌘": "Ctrl", 345 "⌃": "Ctrl", 346 "⇧": "Shift", 347 "⌥": "Alt", 348 "⇥": "Tab", 349 "⌫": "Backspace", 350 "⌦": "Delete", 351 "↩": "Enter", 352 })); 353 354 const keys = []; 355 356 if ((hotkey.indexOf("⌘") > -1 || hotkey.indexOf("⌃") > -1)) keys.push(KEY_MAP.get("⌘")); 357 if (hotkey.indexOf("⇧") > -1) keys.push(KEY_MAP.get("⇧")); 358 if (hotkey.indexOf("⌥") > -1) keys.push(KEY_MAP.get("⌥")); 359 360 // 不能去最后一个,需匹配 F2 361 const lastKey = hotkey.replace(/⌘|⇧|⌥|⌃/g, ""); 362 if (lastKey) { 363 keys.push(KEY_MAP.get(lastKey) || lastKey); 364 } 365 366 return keys.join("+"); 367}; 368 369export const getLocalStorage = (cb: () => void) => { 370 fetchPost("/api/storage/getLocalStorage", undefined, (response) => { 371 window.siyuan.storage = response.data; 372 // 历史数据迁移 373 const defaultStorage: any = {}; 374 defaultStorage[Constants.LOCAL_SEARCHASSET] = { 375 keys: [], 376 col: "", 377 row: "", 378 layout: 0, 379 method: 0, 380 types: {}, 381 sort: 0, 382 k: "", 383 }; 384 defaultStorage[Constants.LOCAL_SEARCHUNREF] = { 385 col: "", 386 row: "", 387 layout: 0, 388 }; 389 Constants.SIYUAN_ASSETS_SEARCH.forEach(type => { 390 defaultStorage[Constants.LOCAL_SEARCHASSET].types[type] = true; 391 }); 392 defaultStorage[Constants.LOCAL_SEARCHKEYS] = { 393 keys: [], 394 replaceKeys: [], 395 col: "", 396 row: "", 397 layout: 0, 398 colTab: "", 399 rowTab: "", 400 layoutTab: 0 401 }; 402 defaultStorage[Constants.LOCAL_PDFTHEME] = { 403 light: "light", 404 dark: "dark", 405 annoColor: "var(--b3-pdf-background1)" 406 }; 407 defaultStorage[Constants.LOCAL_LAYOUTS] = []; // {name: "", layout:{}, time: number, filespaths: IFilesPath[]} 408 defaultStorage[Constants.LOCAL_AI] = []; // {name: "", memo: ""} 409 defaultStorage[Constants.LOCAL_PLUGIN_DOCKS] = {}; // { pluginName: {dockId: IPluginDockTab}} 410 defaultStorage[Constants.LOCAL_PLUGINTOPUNPIN] = []; 411 defaultStorage[Constants.LOCAL_OUTLINE] = {keepCurrentExpand: false}; 412 defaultStorage[Constants.LOCAL_FILEPOSITION] = {}; // {id: IScrollAttr} 413 defaultStorage[Constants.LOCAL_DIALOGPOSITION] = {}; // {id: IPosition} 414 defaultStorage[Constants.LOCAL_HISTORY] = { 415 notebookId: "%", 416 type: 0, 417 operation: "all", 418 sideWidth: "256px", 419 sideDocWidth: "256px", 420 sideDiffWidth: "256px", 421 }; 422 defaultStorage[Constants.LOCAL_FLASHCARD] = { 423 fullscreen: false 424 }; 425 defaultStorage[Constants.LOCAL_BAZAAR] = { 426 theme: "0", 427 template: "0", 428 icon: "0", 429 widget: "0", 430 }; 431 defaultStorage[Constants.LOCAL_EXPORTWORD] = {removeAssets: false, mergeSubdocs: false}; 432 defaultStorage[Constants.LOCAL_EXPORTPDF] = { 433 landscape: false, 434 marginType: "0", 435 scale: 1, 436 pageSize: "A4", 437 removeAssets: true, 438 keepFold: false, 439 mergeSubdocs: false, 440 watermark: false 441 }; 442 defaultStorage[Constants.LOCAL_EXPORTIMG] = { 443 keepFold: false, 444 watermark: false 445 }; 446 defaultStorage[Constants.LOCAL_DOCINFO] = { 447 id: "", 448 }; 449 defaultStorage[Constants.LOCAL_IMAGES] = { 450 file: "1f4c4", 451 note: "1f5c3", 452 folder: "1f4d1" 453 }; 454 defaultStorage[Constants.LOCAL_EMOJIS] = { 455 currentTab: "emoji" 456 }; 457 defaultStorage[Constants.LOCAL_FONTSTYLES] = []; 458 defaultStorage[Constants.LOCAL_FILESPATHS] = []; // IFilesPath[] 459 defaultStorage[Constants.LOCAL_SEARCHDATA] = { 460 page: 1, 461 sort: 0, 462 group: 0, 463 hasReplace: false, 464 method: 0, 465 hPath: "", 466 idPath: [], 467 k: "", 468 r: "", 469 types: { 470 document: window.siyuan.config.search.document, 471 heading: window.siyuan.config.search.heading, 472 list: window.siyuan.config.search.list, 473 listItem: window.siyuan.config.search.listItem, 474 codeBlock: window.siyuan.config.search.codeBlock, 475 htmlBlock: window.siyuan.config.search.htmlBlock, 476 mathBlock: window.siyuan.config.search.mathBlock, 477 table: window.siyuan.config.search.table, 478 blockquote: window.siyuan.config.search.blockquote, 479 superBlock: window.siyuan.config.search.superBlock, 480 paragraph: window.siyuan.config.search.paragraph, 481 embedBlock: window.siyuan.config.search.embedBlock, 482 databaseBlock: window.siyuan.config.search.databaseBlock, 483 }, 484 replaceTypes: Object.assign({}, Constants.SIYUAN_DEFAULT_REPLACETYPES), 485 }; 486 defaultStorage[Constants.LOCAL_ZOOM] = 1; 487 defaultStorage[Constants.LOCAL_MOVE_PATH] = {keys: [], k: ""}; 488 defaultStorage[Constants.LOCAL_RECENT_DOCS] = {type: "viewedAt"}; // TRecentDocsSort 489 490 [Constants.LOCAL_EXPORTIMG, Constants.LOCAL_SEARCHKEYS, Constants.LOCAL_PDFTHEME, Constants.LOCAL_BAZAAR, 491 Constants.LOCAL_EXPORTWORD, Constants.LOCAL_EXPORTPDF, Constants.LOCAL_DOCINFO, Constants.LOCAL_FONTSTYLES, 492 Constants.LOCAL_SEARCHDATA, Constants.LOCAL_ZOOM, Constants.LOCAL_LAYOUTS, Constants.LOCAL_AI, 493 Constants.LOCAL_PLUGINTOPUNPIN, Constants.LOCAL_SEARCHASSET, Constants.LOCAL_FLASHCARD, 494 Constants.LOCAL_DIALOGPOSITION, Constants.LOCAL_SEARCHUNREF, Constants.LOCAL_HISTORY, 495 Constants.LOCAL_OUTLINE, Constants.LOCAL_FILEPOSITION, Constants.LOCAL_FILESPATHS, Constants.LOCAL_IMAGES, 496 Constants.LOCAL_PLUGIN_DOCKS, Constants.LOCAL_EMOJIS, Constants.LOCAL_MOVE_PATH, Constants.LOCAL_RECENT_DOCS].forEach((key) => { 497 if (typeof response.data[key] === "string") { 498 try { 499 const parseData = JSON.parse(response.data[key]); 500 if (typeof parseData === "number") { 501 // https://github.com/siyuan-note/siyuan/issues/8852 Object.assign 会导致 number to Number 502 window.siyuan.storage[key] = parseData; 503 } else { 504 window.siyuan.storage[key] = Object.assign(defaultStorage[key], parseData); 505 } 506 } catch (e) { 507 window.siyuan.storage[key] = defaultStorage[key]; 508 } 509 } else if (typeof response.data[key] === "undefined") { 510 window.siyuan.storage[key] = defaultStorage[key]; 511 } 512 }); 513 // 搜索数据添加 replaceTypes 兼容 514 if (!window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes || 515 Object.keys(window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes).length === 0) { 516 window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes = Object.assign({}, Constants.SIYUAN_DEFAULT_REPLACETYPES); 517 } 518 cb(); 519 }); 520}; 521 522export const setStorageVal = (key: string, val: any, cb?: () => void) => { 523 if (window.siyuan.config.readonly || window.siyuan.isPublish) { 524 return; 525 } 526 fetchPost("/api/storage/setLocalStorageVal", { 527 app: Constants.SIYUAN_APPID, 528 key, 529 val, 530 }, () => { 531 if (cb) { 532 cb(); 533 } 534 }); 535}; 536 537/// #if !BROWSER 538export const initFocusFix = () => { 539 if (!isWindows()) { 540 return; 541 } 542 const originalAlert = window.alert; 543 const originalConfirm = window.confirm; 544 const fixFocusAfterDialog = () => { 545 ipcRenderer.send("siyuan-focus-fix"); 546 }; 547 window.alert = function (message: string) { 548 try { 549 const result = originalAlert.call(this, message); 550 fixFocusAfterDialog(); 551 return result; 552 } catch (error) { 553 console.error("alert error:", error); 554 fixFocusAfterDialog(); 555 return undefined; 556 } 557 }; 558 window.confirm = function (message: string) { 559 try { 560 const result = originalConfirm.call(this, message); 561 fixFocusAfterDialog(); 562 return result; 563 } catch (error) { 564 console.error("confirm error:", error); 565 fixFocusAfterDialog(); 566 return false; 567 } 568 }; 569}; 570/// #endif