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">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
<span className="icon-text">🤠</span>关于 <b>Memos</b>
|
<span className="icon-text">🤠</span>About <b>Memos</b>
|
||||||
</p>
|
</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<p>一个碎片化知识记录工具。</p>
|
<p>
|
||||||
<br />
|
An open-source alternative to <a href="https://flomoapp.com">flomo</a>.
|
||||||
<i>为何做这个?</i>
|
</p>
|
||||||
<ul>
|
<p>You are in charge of your data and customizations.</p>
|
||||||
<li>
|
<p>Built with React and Go.</p>
|
||||||
实践 <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>
|
|
||||||
<br />
|
<br />
|
||||||
<p>Enjoy it and have fun~ </p>
|
<p>Enjoy it and have fun~ </p>
|
||||||
<hr />
|
<hr />
|
||||||
<p className="normal-text">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -16,26 +16,27 @@ const MemoEditor: React.FC<Props> = (props: Props) => {
|
|||||||
const { className, editMemoId } = props;
|
const { className, editMemoId } = props;
|
||||||
const { globalState } = useContext(appContext);
|
const { globalState } = useContext(appContext);
|
||||||
const editorRef = useRef<EditorRefActions>(null);
|
const editorRef = useRef<EditorRefActions>(null);
|
||||||
const prevGlobalStateRef = useRef(globalState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (globalState.markMemoId) {
|
if (globalState.markMemoId) {
|
||||||
const editorCurrentValue = editorRef.current?.getContent();
|
if (editMemoId === globalState.editMemoId || (!editMemoId && !globalState.editMemoId)) {
|
||||||
const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: [@MEMO](${globalState.markMemoId})`;
|
const editorCurrentValue = editorRef.current?.getContent();
|
||||||
editorRef.current?.insertText(memoLinkText);
|
const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: [@MEMO](${globalState.markMemoId})`;
|
||||||
globalStateService.setMarkMemoId("");
|
editorRef.current?.insertText(memoLinkText);
|
||||||
|
globalStateService.setMarkMemoId("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}, [globalState.markMemoId]);
|
||||||
|
|
||||||
if (editMemoId && globalState.editMemoId) {
|
useEffect(() => {
|
||||||
const editMemo = memoService.getMemoById(globalState.editMemoId);
|
if (editMemoId) {
|
||||||
|
const editMemo = memoService.getMemoById(editMemoId);
|
||||||
if (editMemo) {
|
if (editMemo) {
|
||||||
editorRef.current?.setContent(editMemo.content ?? "");
|
editorRef.current?.setContent(editMemo.content ?? "");
|
||||||
editorRef.current?.focus();
|
editorRef.current?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [editMemoId]);
|
||||||
prevGlobalStateRef.current = globalState;
|
|
||||||
}, [globalState.markMemoId, globalState.editMemoId]);
|
|
||||||
|
|
||||||
const handleSaveBtnClick = useCallback(async (content: string) => {
|
const handleSaveBtnClick = useCallback(async (content: string) => {
|
||||||
if (content === "") {
|
if (content === "") {
|
||||||
|
@ -54,7 +54,7 @@ const parseCodeToPrism = (codeStr: string): string => {
|
|||||||
|
|
||||||
const parseMarkedToHtml = (markedStr: string): string => {
|
const parseMarkedToHtml = (markedStr: string): string => {
|
||||||
const htmlText = parseCodeToPrism(markedStr)
|
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(NUM_LI_REG, "<span class='counter-block'>$1.</span>")
|
||||||
.replace(TODO_BLOCK_REG, "<span class='todo-block' data-type='todo'>⬜</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>")
|
.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> {
|
function convertResourceToDataURL(url: string, useCache = true): Promise<string> {
|
||||||
if (useCache && cachedResource.has(url)) {
|
if (useCache && cachedResourceMap.has(url)) {
|
||||||
return Promise.resolve(cachedResource.get(url) as string);
|
return Promise.resolve(cachedResourceMap.get(url) as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
@ -11,7 +11,7 @@ function convertResourceToDataURL(url: string, useCache = true): Promise<string>
|
|||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
const base64Url = reader.result as string;
|
const base64Url = reader.result as string;
|
||||||
cachedResource.set(url, base64Url);
|
cachedResourceMap.set(url, base64Url);
|
||||||
resolve(base64Url);
|
resolve(base64Url);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(blob);
|
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
|
* 1. html-to-image: https://github.com/bubkoo/html-to-image
|
||||||
* 2. <foreignObject>: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
|
* 2. <foreignObject>: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
|
||||||
*/
|
*/
|
||||||
import convertResourceToDataURL from "./convertResourceToDataURL";
|
|
||||||
import getCloneStyledElement from "./getCloneStyledElement";
|
import getCloneStyledElement from "./getCloneStyledElement";
|
||||||
|
import getFontsStyleElement from "./getFontsStyleElement";
|
||||||
|
|
||||||
type Options = Partial<{
|
type Options = Partial<{
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
@ -29,14 +29,14 @@ function convertSVGToDataURL(svg: SVGElement): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateSVGElement(width: number, height: number, element: HTMLElement): SVGSVGElement {
|
function generateSVGElement(width: number, height: number, element: HTMLElement): SVGSVGElement {
|
||||||
const xmlns = "http://www.w3.org/2000/svg";
|
const xmlNS = "http://www.w3.org/2000/svg";
|
||||||
const svg = document.createElementNS(xmlns, "svg");
|
const svgElement = document.createElementNS(xmlNS, "svg");
|
||||||
|
|
||||||
svg.setAttribute("width", `${width}`);
|
svgElement.setAttribute("width", `${width}`);
|
||||||
svg.setAttribute("height", `${height}`);
|
svgElement.setAttribute("height", `${height}`);
|
||||||
svg.setAttribute("viewBox", `0 0 ${width} ${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("width", "100%");
|
||||||
foreignObject.setAttribute("height", "100%");
|
foreignObject.setAttribute("height", "100%");
|
||||||
@ -45,44 +45,9 @@ function generateSVGElement(width: number, height: number, element: HTMLElement)
|
|||||||
foreignObject.setAttribute("externalResourcesRequired", "true");
|
foreignObject.setAttribute("externalResourcesRequired", "true");
|
||||||
|
|
||||||
foreignObject.appendChild(element);
|
foreignObject.appendChild(element);
|
||||||
svg.appendChild(foreignObject);
|
svgElement.appendChild(foreignObject);
|
||||||
|
|
||||||
return svg;
|
return svgElement;
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toSVG(element: HTMLElement, options?: Options) {
|
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);
|
const svg = generateSVGElement(width, height, clonedElement);
|
||||||
svg.prepend(await getFontsStyleElement());
|
svg.prepend(await getFontsStyleElement(element));
|
||||||
|
|
||||||
const url = convertSVGToDataURL(svg);
|
const url = convertSVGToDataURL(svg);
|
||||||
|
|
||||||
|
@ -7,10 +7,8 @@
|
|||||||
> .dialog-content-container {
|
> .dialog-content-container {
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
|
||||||
> ul {
|
> p {
|
||||||
margin: 4px 0;
|
margin: 2px 0;
|
||||||
padding-left: 4px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> hr {
|
> hr {
|
||||||
@ -26,6 +24,7 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: gray;
|
color: gray;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
.mono-font-family();
|
||||||
}
|
}
|
||||||
|
|
||||||
.pre-text {
|
.pre-text {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
> .memo-edit {
|
> .memo-edit {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .status-text-container {
|
> .status-text-container {
|
||||||
|
Reference in New Issue
Block a user