siyuan/app/src/protyle/util/compatibility.ts

566 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {focusByRange} from "./selection";
import {fetchPost, fetchSyncPost} from "../../util/fetch";
import {Constants} from "../../constants";
/// #if !BROWSER
import {clipboard, ipcRenderer} from "electron";
import {processSYLink} from "../../editor/openLink";
/// #endif
export const encodeBase64 = (text: string): string => {
if (typeof Buffer !== "undefined") {
return Buffer.from(text, "utf8").toString("base64");
} else {
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
let binary = "";
const chunkSize = 0x8000; // 避免栈溢出
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
binary += String.fromCharCode(...chunk);
}
return btoa(binary);
}
};
export const getTextSiyuanFromTextHTML = (html: string) => {
const siyuanMatch = html.match(/<!--data-siyuan='([^']+)'-->/);
let textSiyuan = "";
let textHtml = html;
if (siyuanMatch) {
try {
if (typeof Buffer !== "undefined") {
const decodedBytes = Buffer.from(siyuanMatch[1], "base64");
textSiyuan = decodedBytes.toString("utf8");
} else {
const decoder = new TextDecoder();
const bytes = Uint8Array.from(atob(siyuanMatch[1]), char => char.charCodeAt(0));
textSiyuan = decoder.decode(bytes);
}
// 移除注释节点,保持原有的 text/html 内容
textHtml = html.replace(/<!--data-siyuan='[^']+'-->/, "");
} catch (e) {
console.log("Failed to decode siyuan data from HTML comment:", e);
}
}
return {
textSiyuan,
textHtml
};
};
export const openByMobile = (uri: string) => {
if (!uri) {
return;
}
//https://github.com/siyuan-note/siyuan/issues/15892
if (processSYLink(window.siyuan.ws.app, uri)) {
return;
}
if (isInIOS()) {
if (uri.startsWith("assets/")) {
// iOS 16.7 之前的版本uri 需要 encodeURIComponent
window.webkit.messageHandlers.openLink.postMessage(location.origin + "/assets/" + encodeURIComponent(uri.replace("assets/", "")));
} else if (uri.startsWith("/")) {
// 导出 zip 返回的是已经 encode 过的,因此不能再 encode
window.webkit.messageHandlers.openLink.postMessage(location.origin + uri);
} else {
try {
new URL(uri);
window.webkit.messageHandlers.openLink.postMessage(uri);
} catch (e) {
window.webkit.messageHandlers.openLink.postMessage("https://" + uri);
}
}
} else if (isInAndroid()) {
window.JSAndroid.openExternal(uri);
} else if (isInHarmony()) {
window.JSHarmony.openExternal(uri);
} else {
window.open(uri);
}
};
export const exportByMobile = (uri: string) => {
if (!uri) {
return;
}
if (isInIOS()) {
openByMobile(uri);
} else if (isInAndroid()) {
window.JSAndroid.exportByDefault(uri);
} else if (isInHarmony()) {
window.JSHarmony.exportByDefault(uri);
} else {
window.open(uri);
}
};
export const readText = () => {
if (isInAndroid()) {
return window.JSAndroid.readClipboard();
} else if (isInHarmony()) {
return window.JSHarmony.readClipboard();
}
if (typeof navigator.clipboard === "undefined") {
alert(window.siyuan.languages.clipboardPermissionDenied);
return "";
}
return navigator.clipboard.readText().catch(() => {
alert(window.siyuan.languages.clipboardPermissionDenied);
}) || "";
};
/// #if !BROWSER
export const getLocalFiles = async () => {
// 不再支持 PC 浏览器 https://github.com/siyuan-note/siyuan/issues/7206
let localFiles: string[] = [];
if ("darwin" === window.siyuan.config.system.os) {
const xmlString = clipboard.read("NSFilenamesPboardType");
const domParser = new DOMParser();
const xmlDom = domParser.parseFromString(xmlString, "application/xml");
Array.from(xmlDom.getElementsByTagName("string")).forEach(item => {
localFiles.push(item.childNodes[0].nodeValue);
});
} else {
const xmlString = await fetchSyncPost("/api/clipboard/readFilePaths", {});
if (xmlString.data.length > 0) {
localFiles = xmlString.data;
}
}
return localFiles;
};
/// #endif
export const readClipboard = async () => {
const text: IClipboardData = {textPlain: "", textHTML: "", siyuanHTML: ""};
if (isInAndroid()) {
text.textPlain = window.JSAndroid.readClipboard();
text.textHTML = window.JSAndroid.readHTMLClipboard();
const textObj = getTextSiyuanFromTextHTML(text.textHTML);
text.textHTML = textObj.textHtml;
text.siyuanHTML = textObj.textSiyuan;
return text;
}
if (isInHarmony()) {
text.textPlain = window.JSHarmony.readClipboard();
text.textHTML = window.JSHarmony.readHTMLClipboard();
const textObj = getTextSiyuanFromTextHTML(text.textHTML);
text.textHTML = textObj.textHtml;
text.siyuanHTML = textObj.textSiyuan;
return text;
}
if (typeof navigator.clipboard === "undefined") {
alert(window.siyuan.languages.clipboardPermissionDenied);
return text;
}
try {
const clipboardContents = await navigator.clipboard.read().catch(() => {
alert(window.siyuan.languages.clipboardPermissionDenied);
});
if (!clipboardContents) {
return text;
}
for (const item of clipboardContents) {
if (item.types.includes("text/html")) {
const blob = await item.getType("text/html");
text.textHTML = await blob.text();
const textObj = getTextSiyuanFromTextHTML(text.textHTML);
text.textHTML = textObj.textHtml;
text.siyuanHTML = textObj.textSiyuan;
}
if (item.types.includes("text/plain")) {
const blob = await item.getType("text/plain");
text.textPlain = await blob.text();
}
if (item.types.includes("image/png")) {
const blob = await item.getType("image/png");
text.files = [new File([blob], "image.png", {type: "image/png", lastModified: Date.now()})];
}
}
/// #if !BROWSER
if (!text.textHTML && !text.files) {
text.localFiles = await getLocalFiles();
}
/// #endif
return text;
} catch (e) {
return text;
}
};
export const writeText = (text: string) => {
let range: Range;
if (getSelection().rangeCount > 0) {
range = getSelection().getRangeAt(0).cloneRange();
}
try {
// navigator.clipboard.writeText 抛出异常不进入 catch这里需要先处理移动端复制
if (isInAndroid()) {
window.JSAndroid.writeClipboard(text);
return;
}
if (isInHarmony()) {
window.JSHarmony.writeClipboard(text);
return;
}
if (isInIOS()) {
window.webkit.messageHandlers.setClipboard.postMessage(text);
return;
}
navigator.clipboard.writeText(text);
} catch (e) {
if (isInIOS()) {
window.webkit.messageHandlers.setClipboard.postMessage(text);
} else if (isInAndroid()) {
window.JSAndroid.writeClipboard(text);
} else if (isInHarmony()) {
window.JSHarmony.writeClipboard(text);
} else {
const textElement = document.createElement("textarea");
textElement.value = text;
textElement.style.position = "fixed"; //avoid scrolling to bottom
document.body.appendChild(textElement);
textElement.focus();
textElement.select();
document.execCommand("copy");
document.body.removeChild(textElement);
if (range) {
focusByRange(range);
}
}
}
};
export const copyPlainText = async (text: string) => {
text = text.replace(new RegExp(Constants.ZWSP, "g"), ""); // `复制纯文本` 时移除所有零宽空格 https://github.com/siyuan-note/siyuan/issues/6674
await writeText(text);
};
// 用户 iPhone 点击延迟/需要双击的处理
export const getEventName = () => {
if (isIPhone()) {
return "touchstart";
} else {
return "click";
}
};
export const isOnlyMeta = (event: KeyboardEvent | MouseEvent) => {
if (isMac()) {
// mac
if (event.metaKey && !event.ctrlKey) {
return true;
}
return false;
} else {
if (!event.metaKey && event.ctrlKey) {
return true;
}
return false;
}
};
export const isNotCtrl = (event: KeyboardEvent | MouseEvent) => {
if (!event.metaKey && !event.ctrlKey) {
return true;
}
return false;
};
export const isHuawei = () => {
return window.siyuan.config.system.osPlatform.toLowerCase().indexOf("huawei") > -1;
};
export const isDisabledFeature = (feature: string): boolean => {
return window.siyuan.config.system.disabledFeatures?.indexOf(feature) > -1;
};
export const isIPhone = () => {
return navigator.userAgent.indexOf("iPhone") > -1;
};
export const isSafari = () => {
const userAgent = navigator.userAgent;
return userAgent.includes("Safari") && !userAgent.includes("Chrome") && !userAgent.includes("Chromium");
};
export const isIPad = () => {
return navigator.userAgent.indexOf("iPad") > -1;
};
export const isMac = () => {
return navigator.platform.toUpperCase().indexOf("MAC") > -1;
};
export const isWin11 = async () => {
if (!(navigator as any).userAgentData || !(navigator as any).userAgentData.getHighEntropyValues) {
return false;
}
const ua = await (navigator as any).userAgentData.getHighEntropyValues(["platformVersion"]);
if ((navigator as any).userAgentData.platform === "Windows") {
if (parseInt(ua.platformVersion.split(".")[0]) >= 13) {
return true;
}
}
return false;
};
export const getScreenWidth = () => {
if (isInAndroid()) {
return window.JSAndroid.getScreenWidthPx();
} else if (isInHarmony()) {
return window.JSHarmony.getScreenWidthPx();
}
return window.outerWidth;
};
export const isWindows = () => {
return navigator.platform.toUpperCase().indexOf("WIN") > -1;
};
export const isInAndroid = () => {
return window.siyuan.config.system.container === "android" && window.JSAndroid;
};
export const isInIOS = () => {
return window.siyuan.config.system.container === "ios" && window.webkit?.messageHandlers;
};
export const isInHarmony = () => {
return window.siyuan.config.system.container === "harmony" && window.JSHarmony;
};
export const updateHotkeyAfterTip = (hotkey: string, split = " ") => {
if (hotkey) {
return split + updateHotkeyTip(hotkey);
}
return "";
};
// MacWindows 快捷键展示
export const updateHotkeyTip = (hotkey: string) => {
if (!hotkey || isMac()) {
return hotkey;
}
const keys = [];
if ((hotkey.indexOf("⌘") > -1 || hotkey.indexOf("⌃") > -1)) keys.push("Ctrl");
if (hotkey.indexOf("⇧") > -1) keys.push("Shift");
if (hotkey.indexOf("⌥") > -1) keys.push("Alt");
// 不能去最后一个,需匹配 F2
const lastKey = hotkey.replace(/[⌘⇧⌥⌃]/g, "");
if (lastKey) {
keys.push({
"⇥": "Tab",
"⌫": "Backspace",
"⌦": "Delete",
"↩": "Enter"
}[lastKey] || lastKey);
}
return keys.join("+");
};
export const getLocalStorage = (cb: () => void) => {
fetchPost("/api/storage/getLocalStorage", undefined, (response) => {
window.siyuan.storage = response.data;
// 历史数据迁移
const defaultStorage: any = {};
defaultStorage[Constants.LOCAL_SEARCHASSET] = {
keys: [],
col: "",
row: "",
layout: 0,
method: 0,
types: {},
sort: 0,
k: "",
};
defaultStorage[Constants.LOCAL_SEARCHUNREF] = {
col: "",
row: "",
layout: 0,
};
Constants.SIYUAN_ASSETS_SEARCH.forEach(type => {
defaultStorage[Constants.LOCAL_SEARCHASSET].types[type] = true;
});
defaultStorage[Constants.LOCAL_SEARCHKEYS] = {
keys: [],
replaceKeys: [],
col: "",
row: "",
layout: 0,
colTab: "",
rowTab: "",
layoutTab: 0
};
defaultStorage[Constants.LOCAL_PDFTHEME] = {
light: "light",
dark: "dark",
annoColor: "var(--b3-pdf-background1)"
};
defaultStorage[Constants.LOCAL_LAYOUTS] = []; // {name: "", layout:{}, time: number, filespaths: IFilesPath[]}
defaultStorage[Constants.LOCAL_AI] = []; // {name: "", memo: ""}
defaultStorage[Constants.LOCAL_PLUGIN_DOCKS] = {}; // { pluginName: {dockId: IPluginDockTab}}
defaultStorage[Constants.LOCAL_PLUGINTOPUNPIN] = [];
defaultStorage[Constants.LOCAL_OUTLINE] = {keepCurrentExpand: false};
defaultStorage[Constants.LOCAL_FILEPOSITION] = {}; // {id: IScrollAttr}
defaultStorage[Constants.LOCAL_DIALOGPOSITION] = {}; // {id: IPosition}
defaultStorage[Constants.LOCAL_HISTORY] = {
notebookId: "%",
type: 0,
operation: "all",
sideWidth: "256px",
sideDocWidth: "256px",
sideDiffWidth: "256px",
};
defaultStorage[Constants.LOCAL_FLASHCARD] = {
fullscreen: false
};
defaultStorage[Constants.LOCAL_BAZAAR] = {
theme: "0",
template: "0",
icon: "0",
widget: "0",
};
defaultStorage[Constants.LOCAL_EXPORTWORD] = {removeAssets: false, mergeSubdocs: false};
defaultStorage[Constants.LOCAL_EXPORTPDF] = {
landscape: false,
marginType: "0",
scale: 1,
pageSize: "A4",
removeAssets: true,
keepFold: false,
mergeSubdocs: false,
watermark: false
};
defaultStorage[Constants.LOCAL_EXPORTIMG] = {
keepFold: false,
watermark: false
};
defaultStorage[Constants.LOCAL_DOCINFO] = {
id: "",
};
defaultStorage[Constants.LOCAL_IMAGES] = {
file: "1f4c4",
note: "1f5c3",
folder: "1f4d1"
};
defaultStorage[Constants.LOCAL_EMOJIS] = {
currentTab: "emoji"
};
defaultStorage[Constants.LOCAL_FONTSTYLES] = [];
defaultStorage[Constants.LOCAL_FILESPATHS] = []; // IFilesPath[]
defaultStorage[Constants.LOCAL_SEARCHDATA] = {
page: 1,
sort: 0,
group: 0,
hasReplace: false,
method: 0,
hPath: "",
idPath: [],
k: "",
r: "",
types: {
document: window.siyuan.config.search.document,
heading: window.siyuan.config.search.heading,
list: window.siyuan.config.search.list,
listItem: window.siyuan.config.search.listItem,
codeBlock: window.siyuan.config.search.codeBlock,
htmlBlock: window.siyuan.config.search.htmlBlock,
mathBlock: window.siyuan.config.search.mathBlock,
table: window.siyuan.config.search.table,
blockquote: window.siyuan.config.search.blockquote,
superBlock: window.siyuan.config.search.superBlock,
paragraph: window.siyuan.config.search.paragraph,
embedBlock: window.siyuan.config.search.embedBlock,
databaseBlock: window.siyuan.config.search.databaseBlock,
},
replaceTypes: Object.assign({}, Constants.SIYUAN_DEFAULT_REPLACETYPES),
};
defaultStorage[Constants.LOCAL_ZOOM] = 1;
defaultStorage[Constants.LOCAL_MOVE_PATH] = {keys: [], k: ""};
defaultStorage[Constants.LOCAL_RECENT_DOCS] = {type: "viewedAt"}; // TRecentDocsSort
[Constants.LOCAL_EXPORTIMG, Constants.LOCAL_SEARCHKEYS, Constants.LOCAL_PDFTHEME, Constants.LOCAL_BAZAAR,
Constants.LOCAL_EXPORTWORD, Constants.LOCAL_EXPORTPDF, Constants.LOCAL_DOCINFO, Constants.LOCAL_FONTSTYLES,
Constants.LOCAL_SEARCHDATA, Constants.LOCAL_ZOOM, Constants.LOCAL_LAYOUTS, Constants.LOCAL_AI,
Constants.LOCAL_PLUGINTOPUNPIN, Constants.LOCAL_SEARCHASSET, Constants.LOCAL_FLASHCARD,
Constants.LOCAL_DIALOGPOSITION, Constants.LOCAL_SEARCHUNREF, Constants.LOCAL_HISTORY,
Constants.LOCAL_OUTLINE, Constants.LOCAL_FILEPOSITION, Constants.LOCAL_FILESPATHS, Constants.LOCAL_IMAGES,
Constants.LOCAL_PLUGIN_DOCKS, Constants.LOCAL_EMOJIS, Constants.LOCAL_MOVE_PATH, Constants.LOCAL_RECENT_DOCS].forEach((key) => {
if (typeof response.data[key] === "string") {
try {
const parseData = JSON.parse(response.data[key]);
if (typeof parseData === "number") {
// https://github.com/siyuan-note/siyuan/issues/8852 Object.assign 会导致 number to Number
window.siyuan.storage[key] = parseData;
} else {
window.siyuan.storage[key] = Object.assign(defaultStorage[key], parseData);
}
} catch (e) {
window.siyuan.storage[key] = defaultStorage[key];
}
} else if (typeof response.data[key] === "undefined") {
window.siyuan.storage[key] = defaultStorage[key];
}
});
// 搜索数据添加 replaceTypes 兼容
if (!window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes ||
Object.keys(window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes).length === 0) {
window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes = Object.assign({}, Constants.SIYUAN_DEFAULT_REPLACETYPES);
}
cb();
});
};
export const setStorageVal = (key: string, val: any, cb?: () => void) => {
if (window.siyuan.config.readonly || window.siyuan.isPublish) {
return;
}
fetchPost("/api/storage/setLocalStorageVal", {
app: Constants.SIYUAN_APPID,
key,
val,
}, () => {
if (cb) {
cb();
}
});
};
/// #if !BROWSER
export const initFocusFix = () => {
if (!isWindows()) {
return;
}
const originalAlert = window.alert;
const originalConfirm = window.confirm;
const fixFocusAfterDialog = () => {
ipcRenderer.send("siyuan-focus-fix");
};
window.alert = function (message: string) {
try {
const result = originalAlert.call(this, message);
fixFocusAfterDialog();
return result;
} catch (error) {
console.error("alert error:", error);
fixFocusAfterDialog();
return undefined;
}
};
window.confirm = function (message: string) {
try {
const result = originalConfirm.call(this, message);
fixFocusAfterDialog();
return result;
} catch (error) {
console.error("confirm error:", error);
fixFocusAfterDialog();
return false;
}
};
};
/// #endif