mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
web: update memo editor and about me
This commit is contained in:
@ -12,45 +12,23 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">
|
||||
<span className="icon-text">🤠</span>关于 <b>Memos</b>
|
||||
<span className="icon-text">🤠</span>About <b>Memos</b>
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<p>一个碎片化知识记录工具。</p>
|
||||
<br />
|
||||
<i>为何做这个?</i>
|
||||
<ul>
|
||||
<li>
|
||||
实践 <strong>卢曼卡片盒笔记法</strong>;
|
||||
</li>
|
||||
<li>用于记录:📅 每日/周计划、💡 突发奇想、📕 读后感...</li>
|
||||
<li>代替了我在微信上经常使用的“文件传输助手”;</li>
|
||||
<li>打造一个属于自己的轻量化“卡片”笔记簿;</li>
|
||||
</ul>
|
||||
<br />
|
||||
<i>有何特点呢?</i>
|
||||
<ul>
|
||||
<li>
|
||||
✨{" "}
|
||||
<a target="_blank" href="https://github.com/boojack/insmemo-web" rel="noreferrer">
|
||||
开源项目
|
||||
</a>
|
||||
</li>
|
||||
<li>😋 精美且细节的视觉样式;</li>
|
||||
<li>📑 体验优良的交互逻辑;</li>
|
||||
</ul>
|
||||
<br />
|
||||
<a target="_blank" href="https://github.com/boojack/insmemo-web/discussions" rel="noreferrer">
|
||||
🤔 问题反馈
|
||||
</a>
|
||||
<p>
|
||||
An open-source alternative to <a href="https://flomoapp.com">flomo</a>.
|
||||
</p>
|
||||
<p>You are in charge of your data and customizations.</p>
|
||||
<p>Built with React and Go.</p>
|
||||
<br />
|
||||
<p>Enjoy it and have fun~ </p>
|
||||
<hr />
|
||||
<p className="normal-text">
|
||||
Last updated on <span className="pre-text">2021/11/26 16:17:44</span> 🎉
|
||||
Last updated on <span className="pre-text">2021/12/09 10:14:32</span> 🎉
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
@ -16,26 +16,27 @@ const MemoEditor: React.FC<Props> = (props: Props) => {
|
||||
const { className, editMemoId } = props;
|
||||
const { globalState } = useContext(appContext);
|
||||
const editorRef = useRef<EditorRefActions>(null);
|
||||
const prevGlobalStateRef = useRef(globalState);
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState.markMemoId) {
|
||||
if (editMemoId === globalState.editMemoId || (!editMemoId && !globalState.editMemoId)) {
|
||||
const editorCurrentValue = editorRef.current?.getContent();
|
||||
const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: [@MEMO](${globalState.markMemoId})`;
|
||||
editorRef.current?.insertText(memoLinkText);
|
||||
globalStateService.setMarkMemoId("");
|
||||
}
|
||||
}
|
||||
}, [globalState.markMemoId]);
|
||||
|
||||
if (editMemoId && globalState.editMemoId) {
|
||||
const editMemo = memoService.getMemoById(globalState.editMemoId);
|
||||
useEffect(() => {
|
||||
if (editMemoId) {
|
||||
const editMemo = memoService.getMemoById(editMemoId);
|
||||
if (editMemo) {
|
||||
editorRef.current?.setContent(editMemo.content ?? "");
|
||||
editorRef.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
prevGlobalStateRef.current = globalState;
|
||||
}, [globalState.markMemoId, globalState.editMemoId]);
|
||||
}, [editMemoId]);
|
||||
|
||||
const handleSaveBtnClick = useCallback(async (content: string) => {
|
||||
if (content === "") {
|
||||
|
@ -54,7 +54,7 @@ const parseCodeToPrism = (codeStr: string): string => {
|
||||
|
||||
const parseMarkedToHtml = (markedStr: string): string => {
|
||||
const htmlText = parseCodeToPrism(markedStr)
|
||||
.replace(DOT_LI_REG, "<span class='counter-block'>•</span>")
|
||||
.replace(DOT_LI_REG, "<span class='counter-block'>◦</span>")
|
||||
.replace(NUM_LI_REG, "<span class='counter-block'>$1.</span>")
|
||||
.replace(TODO_BLOCK_REG, "<span class='todo-block' data-type='todo'>⬜</span>")
|
||||
.replace(DONE_BLOCK_REG, "<span class='todo-block' data-type='done'>✅</span>")
|
||||
|
@ -1,8 +1,8 @@
|
||||
const cachedResource = new Map<string, string>();
|
||||
const cachedResourceMap = new Map<string, string>();
|
||||
|
||||
function convertResourceToDataURL(url: string, useCache = true): Promise<string> {
|
||||
if (useCache && cachedResource.has(url)) {
|
||||
return Promise.resolve(cachedResource.get(url) as string);
|
||||
if (useCache && cachedResourceMap.has(url)) {
|
||||
return Promise.resolve(cachedResourceMap.get(url) as string);
|
||||
}
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
@ -11,7 +11,7 @@ function convertResourceToDataURL(url: string, useCache = true): Promise<string>
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64Url = reader.result as string;
|
||||
cachedResource.set(url, base64Url);
|
||||
cachedResourceMap.set(url, base64Url);
|
||||
resolve(base64Url);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
|
46
web/src/labs/html2image/getFontsStyleElement.ts
Normal file
46
web/src/labs/html2image/getFontsStyleElement.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import convertResourceToDataURL from "./convertResourceToDataURL";
|
||||
|
||||
async function getFontsStyleElement(element: HTMLElement) {
|
||||
const styleSheets = element.ownerDocument.styleSheets;
|
||||
const fontFamilyStyles: CSSStyleDeclaration[] = [];
|
||||
|
||||
for (const sheet of styleSheets) {
|
||||
for (const rule of sheet.cssRules) {
|
||||
if (rule.constructor.name === "CSSFontFaceRule") {
|
||||
fontFamilyStyles.push((rule as CSSFontFaceRule).style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
|
||||
for (const f of fontFamilyStyles) {
|
||||
const fontFamily = f.getPropertyValue("font-family");
|
||||
const fontWeight = f.getPropertyValue("font-weight");
|
||||
const src = f.getPropertyValue("src");
|
||||
const resourceUrls = src.split(",").map((s) => {
|
||||
return s.replace(/url\("?(.+?)"?\)/, "$1");
|
||||
});
|
||||
let base64Urls = [];
|
||||
|
||||
for (const url of resourceUrls) {
|
||||
try {
|
||||
const base64Url = await convertResourceToDataURL(url);
|
||||
base64Urls.push(`url("${base64Url}")`);
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
}
|
||||
|
||||
styleElement.innerHTML += `
|
||||
@font-face {
|
||||
font-family: "${fontFamily}";
|
||||
src: ${base64Urls.join(",")};
|
||||
font-weight: ${fontWeight};
|
||||
}`;
|
||||
}
|
||||
|
||||
return styleElement;
|
||||
}
|
||||
|
||||
export default getFontsStyleElement;
|
@ -5,8 +5,8 @@
|
||||
* 1. html-to-image: https://github.com/bubkoo/html-to-image
|
||||
* 2. <foreignObject>: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
|
||||
*/
|
||||
import convertResourceToDataURL from "./convertResourceToDataURL";
|
||||
import getCloneStyledElement from "./getCloneStyledElement";
|
||||
import getFontsStyleElement from "./getFontsStyleElement";
|
||||
|
||||
type Options = Partial<{
|
||||
backgroundColor: string;
|
||||
@ -29,14 +29,14 @@ function convertSVGToDataURL(svg: SVGElement): string {
|
||||
}
|
||||
|
||||
function generateSVGElement(width: number, height: number, element: HTMLElement): SVGSVGElement {
|
||||
const xmlns = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(xmlns, "svg");
|
||||
const xmlNS = "http://www.w3.org/2000/svg";
|
||||
const svgElement = document.createElementNS(xmlNS, "svg");
|
||||
|
||||
svg.setAttribute("width", `${width}`);
|
||||
svg.setAttribute("height", `${height}`);
|
||||
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
svgElement.setAttribute("width", `${width}`);
|
||||
svgElement.setAttribute("height", `${height}`);
|
||||
svgElement.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
|
||||
const foreignObject = document.createElementNS(xmlns, "foreignObject");
|
||||
const foreignObject = document.createElementNS(xmlNS, "foreignObject");
|
||||
|
||||
foreignObject.setAttribute("width", "100%");
|
||||
foreignObject.setAttribute("height", "100%");
|
||||
@ -45,44 +45,9 @@ function generateSVGElement(width: number, height: number, element: HTMLElement)
|
||||
foreignObject.setAttribute("externalResourcesRequired", "true");
|
||||
|
||||
foreignObject.appendChild(element);
|
||||
svg.appendChild(foreignObject);
|
||||
svgElement.appendChild(foreignObject);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
// TODO need rethink how to get the needed font-family
|
||||
async function getFontsStyleElement() {
|
||||
const styleElement = document.createElement("style");
|
||||
|
||||
const fonts = [
|
||||
{
|
||||
name: "DINPro",
|
||||
url: "/fonts/DINPro-Regular.otf",
|
||||
weight: "normal",
|
||||
},
|
||||
{
|
||||
name: "DINPro",
|
||||
url: "/fonts/DINPro-Bold.otf",
|
||||
weight: "bold",
|
||||
},
|
||||
{
|
||||
name: "ubuntu-mono",
|
||||
url: "/fonts/UbuntuMono.ttf",
|
||||
weight: "normal",
|
||||
},
|
||||
];
|
||||
|
||||
for (const f of fonts) {
|
||||
const base64Url = await convertResourceToDataURL(f.url);
|
||||
styleElement.innerHTML += `
|
||||
@font-face {
|
||||
font-family: "${f.name}";
|
||||
src: url("${base64Url}");
|
||||
font-weight: ${f.weight};
|
||||
}`;
|
||||
}
|
||||
|
||||
return styleElement;
|
||||
return svgElement;
|
||||
}
|
||||
|
||||
export async function toSVG(element: HTMLElement, options?: Options) {
|
||||
@ -95,7 +60,7 @@ export async function toSVG(element: HTMLElement, options?: Options) {
|
||||
}
|
||||
|
||||
const svg = generateSVGElement(width, height, clonedElement);
|
||||
svg.prepend(await getFontsStyleElement());
|
||||
svg.prepend(await getFontsStyleElement(element));
|
||||
|
||||
const url = convertSVGToDataURL(svg);
|
||||
|
||||
|
@ -7,10 +7,8 @@
|
||||
> .dialog-content-container {
|
||||
line-height: 1.8;
|
||||
|
||||
> ul {
|
||||
margin: 4px 0;
|
||||
padding-left: 4px;
|
||||
width: 100%;
|
||||
> p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
> hr {
|
||||
@ -26,6 +24,7 @@
|
||||
font-size: 13px;
|
||||
color: gray;
|
||||
white-space: pre-wrap;
|
||||
.mono-font-family();
|
||||
}
|
||||
|
||||
.pre-text {
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
> .memo-edit {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .status-text-container {
|
||||
|
Reference in New Issue
Block a user