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

567 lines
20 KiB
TypeScript
Raw Normal View History

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
};
};
2022-09-10 21:22:11 +08:00
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);
}) || "";
2022-10-08 15:23:56 +08:00
};
/// #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;
}
2025-02-16 19:03:38 +08:00
};
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;
}
2023-11-14 23:18:18 +08:00
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;
2024-11-09 15:42:13 +08:00
};
export const isIPhone = () => {
2023-09-06 17:20:52 +08:00
return navigator.userAgent.indexOf("iPhone") > -1;
};
export const isSafari = () => {
const userAgent = navigator.userAgent;
return userAgent.includes("Safari") && !userAgent.includes("Chrome") && !userAgent.includes("Chromium");
};
2023-09-06 17:20:52 +08:00
export const isIPad = () => {
return navigator.userAgent.indexOf("iPad") > -1;
};
export const isMac = () => {
return navigator.platform.toUpperCase().indexOf("MAC") > -1;
};
export const isWin11 = async () => {
2024-12-18 20:58:31 +08:00
if (!(navigator as any).userAgentData || !(navigator as any).userAgentData.getHighEntropyValues) {
return false;
}
2024-12-18 20:58:31 +08:00
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;
}
}
2024-12-18 20:58:31 +08:00
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;
2025-02-14 10:03:03 +08:00
};
export const isInAndroid = () => {
2023-09-06 17:20:52 +08:00
return window.siyuan.config.system.container === "android" && window.JSAndroid;
};
export const isInIOS = () => {
2023-09-06 17:20:52 +08:00
return window.siyuan.config.system.container === "ios" && window.webkit?.messageHandlers;
};
export const isInHarmony = () => {
return window.siyuan.config.system.container === "harmony" && window.JSHarmony;
2024-11-26 01:00:23 +08:00
};
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");
2022-09-10 21:22:11 +08:00
2022-09-18 09:39:07 +08:00
// 不能去最后一个,需匹配 F2
const lastKey = hotkey.replace(/[⌘⇧⌥⌃]/g, "");
if (lastKey) {
keys.push({
"⇥": "Tab",
"⌫": "Backspace",
"⌦": "Delete",
"↩": "Enter"
}[lastKey] || lastKey);
}
2022-09-10 21:22:11 +08:00
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,
2023-08-18 12:09:55 +08:00
types: {},
sort: 0,
k: "",
};
defaultStorage[Constants.LOCAL_SEARCHUNREF] = {
col: "",
row: "",
layout: 0,
};
2023-08-18 12:09:55 +08:00
Constants.SIYUAN_ASSETS_SEARCH.forEach(type => {
2023-08-18 20:03:04 +08:00
defaultStorage[Constants.LOCAL_SEARCHASSET].types[type] = true;
});
2023-03-22 11:56:58 +08:00
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[]
2023-03-22 11:56:58 +08:00
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,
2023-10-19 00:01:42 +08:00
databaseBlock: window.siyuan.config.search.databaseBlock,
},
2023-12-17 15:58:18 +08:00
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) {
2023-12-17 15:58:18 +08:00
window.siyuan.storage[Constants.LOCAL_SEARCHDATA].replaceTypes = Object.assign({}, Constants.SIYUAN_DEFAULT_REPLACETYPES);
}
cb();
});
2023-01-01 14:09:23 +08:00
};
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();
}
});
2022-10-30 09:52:18 +08:00
};
/// #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