mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
18
README.md
18
README.md
@ -1,16 +1,18 @@
|
|||||||
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://usememos.com/logo-full.png" alt="✍️ memos" /></a></p>
|
# memos
|
||||||
|
|
||||||
<p align="center">
|
<img height="72px" src="https://usememos.com/logo.webp" alt="✍️ memos" align="right" />
|
||||||
|
|
||||||
|
A lightweight, self-hosted memo hub. Open Source and Free forever.
|
||||||
|
|
||||||
|
<a href="https://demo.usememos.com/">Live Demo</a> •
|
||||||
|
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <a href="https://discord.gg/tfPJa4UmAv">Discord</a>
|
||||||
|
|
||||||
|
<p>
|
||||||
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
|
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
|
||||||
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
|
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
|
||||||
<a href="https://discord.gg/tfPJa4UmAv"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
|
<a href="https://discord.gg/tfPJa4UmAv"><img alt="Discord" src="https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://demo.usememos.com/">Live Demo</a> •
|
|
||||||
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <a href="https://discord.gg/tfPJa4UmAv">Discord</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Key points
|
## Key points
|
||||||
@ -39,7 +41,7 @@ Contributions are what make the open-source community such an amazing place to l
|
|||||||
<img src="https://contrib.rocks/image?repo=usememos/memos" />
|
<img src="https://contrib.rocks/image?repo=usememos/memos" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
Here are some products made by our community:
|
---
|
||||||
|
|
||||||
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
|
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
|
||||||
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
|
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/logo.png" type="image/*" />
|
<link rel="icon" href="/logo.webp" type="image/*" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f4f4f5" />
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f4f4f5" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27272a" />
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27272a" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 110 KiB |
BIN
web/public/logo.webp
Normal file
BIN
web/public/logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
@ -4,8 +4,8 @@
|
|||||||
"description": "usememos/memos",
|
"description": "usememos/memos",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/logo.png",
|
"src": "/logo.webp",
|
||||||
"type": "image/png",
|
"type": "image/webp",
|
||||||
"sizes": "520x520"
|
"sizes": "520x520"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -53,7 +53,7 @@ const App = () => {
|
|||||||
// dynamic update metadata with customized profile.
|
// dynamic update metadata with customized profile.
|
||||||
document.title = systemStatus.customizedProfile.name;
|
document.title = systemStatus.customizedProfile.name;
|
||||||
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
||||||
link.href = systemStatus.customizedProfile.logoUrl || "/logo.png";
|
link.href = systemStatus.customizedProfile.logoUrl || "/logo.webp";
|
||||||
}, [systemStatus]);
|
}, [systemStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -31,8 +31,8 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
<div className="mt-4 w-full flex flex-row text-sm justify-start items-center">
|
<div className="mt-4 w-full flex flex-row text-sm justify-start items-center">
|
||||||
<div className="flex flex-row justify-start items-center mr-2">
|
<div className="flex flex-row justify-start items-center mr-2">
|
||||||
Powered by
|
Powered by
|
||||||
<a href="https://usememos.com" target="_blank" className="flex flex-row justify-start items-center mr-1 hover:underline">
|
<a href="https://usememos.com" target="_blank" className="flex flex-row justify-start items-center mx-1 hover:underline">
|
||||||
<img className="w-6 h-auto" src="/logo.png" alt="" />
|
<img className="w-6 h-auto rounded-full mr-1" src="/logo.webp" alt="" />
|
||||||
memos
|
memos
|
||||||
</a>
|
</a>
|
||||||
<span>v{profile.version}</span>
|
<span>v{profile.version}</span>
|
||||||
|
@ -474,7 +474,7 @@ const MemoEditor = () => {
|
|||||||
disabled={!(allowSave || editorState.resourceList.length > 0) || state.isUploadingResource || state.isRequesting}
|
disabled={!(allowSave || editorState.resourceList.length > 0) || state.isUploadingResource || state.isRequesting}
|
||||||
onClick={handleSaveBtnClick}
|
onClick={handleSaveBtnClick}
|
||||||
>
|
>
|
||||||
<img className="w-5 -ml-0.5 mr-0.5 h-auto" src="/logo.png" />
|
<img className="w-5 -ml-0.5 mr-0.5 h-auto" src="/logo.webp" />
|
||||||
{t("editor.save")}
|
{t("editor.save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -131,7 +131,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="watermark-container">
|
<div className="watermark-container">
|
||||||
<div className="logo-container">
|
<div className="logo-container">
|
||||||
<img className="logo-img" src={`${systemStatus.customizedProfile.logoUrl || "/logo.png"}`} alt="" />
|
<img className="logo-img" src={`${systemStatus.customizedProfile.logoUrl || "/logo.webp"}`} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="userinfo-container">
|
<div className="userinfo-container">
|
||||||
<span className="name-text">{user.nickname || user.username}</span>
|
<span className="name-text">{user.nickname || user.username}</span>
|
||||||
|
@ -67,7 +67,7 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
const handleRestoreButtonClick = () => {
|
const handleRestoreButtonClick = () => {
|
||||||
setState({
|
setState({
|
||||||
name: "memos",
|
name: "memos",
|
||||||
logoUrl: "/logo.png",
|
logoUrl: "/logo.webp",
|
||||||
description: "",
|
description: "",
|
||||||
locale: "en",
|
locale: "en",
|
||||||
appearance: "system",
|
appearance: "system",
|
||||||
|
@ -6,8 +6,8 @@ interface Props {
|
|||||||
const UserAvatar = (props: Props) => {
|
const UserAvatar = (props: Props) => {
|
||||||
const { avatarUrl, className } = props;
|
const { avatarUrl, className } = props;
|
||||||
return (
|
return (
|
||||||
<div className={`${className ?? ""} w-8 h-8 overflow-clip bg-gray-100 dark:bg-zinc-800`}>
|
<div className={`${className ?? ""} w-8 h-8 overflow-clip`}>
|
||||||
<img className="w-full h-auto rounded-full min-w-full min-h-full object-cover" src={avatarUrl || "/logo.png"} alt="" />
|
<img className="w-full h-auto rounded-full min-w-full min-h-full object-cover" src={avatarUrl || "/logo.webp"} alt="" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -113,7 +113,7 @@ const Auth = () => {
|
|||||||
<div className="auth-form-wrapper">
|
<div className="auth-form-wrapper">
|
||||||
<div className="page-header-container">
|
<div className="page-header-container">
|
||||||
<div className="title-container">
|
<div className="title-container">
|
||||||
<img className="logo-img" src={systemStatus.customizedProfile.logoUrl} alt="" />
|
<img className="h-12 w-auto rounded-lg mr-1" src={systemStatus.customizedProfile.logoUrl} alt="" />
|
||||||
<p className="logo-text">{systemStatus.customizedProfile.name}</p>
|
<p className="logo-text">{systemStatus.customizedProfile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="slogan-text">{systemStatus.customizedProfile.description || t("slogan")}</p>
|
<p className="slogan-text">{systemStatus.customizedProfile.description || t("slogan")}</p>
|
||||||
|
@ -3,6 +3,7 @@ import copy from "copy-to-clipboard";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import { useResourceStore } from "@/store/module";
|
import { useResourceStore } from "@/store/module";
|
||||||
import { getResourceUrl } from "@/utils/resource";
|
import { getResourceUrl } from "@/utils/resource";
|
||||||
@ -16,7 +17,6 @@ import { showCommonDialog } from "@/components/Dialog/CommonDialog";
|
|||||||
import showChangeResourceFilenameDialog from "@/components/ChangeResourceFilenameDialog";
|
import showChangeResourceFilenameDialog from "@/components/ChangeResourceFilenameDialog";
|
||||||
import showPreviewImageDialog from "@/components/PreviewImageDialog";
|
import showPreviewImageDialog from "@/components/PreviewImageDialog";
|
||||||
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
|
||||||
|
|
||||||
const ResourcesDashboard = () => {
|
const ResourcesDashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -52,53 +52,6 @@ const ResourcesDashboard = () => {
|
|||||||
setSelectedList(selectedList.filter((resId) => resId !== resourceId));
|
setSelectedList(selectedList.filter((resId) => resId !== resourceId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteUnusedResourcesBtnClick = async () => {
|
|
||||||
let warningText = t("resources.warning-text-unused");
|
|
||||||
await loadAllResources((allResources: Resource[]) => {
|
|
||||||
const unusedResources = allResources.filter((resource) => {
|
|
||||||
if (resource.linkedMemoAmount === 0) {
|
|
||||||
warningText = warningText + `\n- ${resource.filename}`;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (unusedResources.length === 0) {
|
|
||||||
toast.success(t("resources.no-unused-resources"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showCommonDialog({
|
|
||||||
title: t("resources.delete-resource"),
|
|
||||||
content: warningText,
|
|
||||||
style: "warning",
|
|
||||||
dialogName: "delete-unused-resources",
|
|
||||||
onConfirm: async () => {
|
|
||||||
for (const resource of unusedResources) {
|
|
||||||
await resourceStore.deleteResourceById(resource.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSelectedBtnClick = () => {
|
|
||||||
if (selectedList.length == 0) {
|
|
||||||
toast.error(t("resources.no-files-selected"));
|
|
||||||
} else {
|
|
||||||
const warningText = t("resources.warning-text");
|
|
||||||
showCommonDialog({
|
|
||||||
title: t("resources.delete-resource"),
|
|
||||||
content: warningText,
|
|
||||||
style: "warning",
|
|
||||||
dialogName: "delete-resource-dialog",
|
|
||||||
onConfirm: async () => {
|
|
||||||
selectedList.map(async (resourceId: ResourceId) => {
|
|
||||||
await resourceStore.deleteResourceById(resourceId);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStyleChangeBtnClick = (listStyle: "GRID" | "TABLE") => {
|
const handleStyleChangeBtnClick = (listStyle: "GRID" | "TABLE") => {
|
||||||
setListStyle(listStyle);
|
setListStyle(listStyle);
|
||||||
setSelectedList([]);
|
setSelectedList([]);
|
||||||
@ -125,6 +78,53 @@ const ResourcesDashboard = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteUnusedResourcesBtnClick = async () => {
|
||||||
|
let warningText = t("resources.warning-text-unused");
|
||||||
|
const allResources = await fetchAllResources();
|
||||||
|
const unusedResources = allResources.filter((resource) => {
|
||||||
|
if (resource.linkedMemoAmount === 0) {
|
||||||
|
warningText = warningText + `\n- ${resource.filename}`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (unusedResources.length === 0) {
|
||||||
|
toast.success(t("resources.no-unused-resources"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showCommonDialog({
|
||||||
|
title: t("resources.delete-resource"),
|
||||||
|
content: warningText,
|
||||||
|
style: "warning",
|
||||||
|
dialogName: "delete-unused-resources",
|
||||||
|
onConfirm: async () => {
|
||||||
|
for (const resource of unusedResources) {
|
||||||
|
await resourceStore.deleteResourceById(resource.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSelectedBtnClick = () => {
|
||||||
|
if (selectedList.length == 0) {
|
||||||
|
toast.error(t("resources.no-files-selected"));
|
||||||
|
} else {
|
||||||
|
const warningText = t("resources.warning-text");
|
||||||
|
showCommonDialog({
|
||||||
|
title: t("resources.delete-resource"),
|
||||||
|
content: warningText,
|
||||||
|
style: "warning",
|
||||||
|
dialogName: "delete-resource-dialog",
|
||||||
|
onConfirm: async () => {
|
||||||
|
selectedList.map(async (resourceId: ResourceId) => {
|
||||||
|
await resourceStore.deleteResourceById(resourceId);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handlePreviewBtnClick = (resource: Resource) => {
|
const handlePreviewBtnClick = (resource: Resource) => {
|
||||||
const resourceUrl = getResourceUrl(resource);
|
const resourceUrl = getResourceUrl(resource);
|
||||||
if (resource.type.startsWith("image")) {
|
if (resource.type.startsWith("image")) {
|
||||||
@ -157,30 +157,30 @@ const ResourcesDashboard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAllResources = async (resolve: (allResources: Resource[]) => void) => {
|
const fetchAllResources = async () => {
|
||||||
if (!isComplete) {
|
if (isComplete) {
|
||||||
loadingState.setLoading();
|
return resources;
|
||||||
try {
|
}
|
||||||
const allResources = await resourceStore.fetchResourceList();
|
|
||||||
loadingState.setFinish();
|
loadingState.setLoading();
|
||||||
setIsComplete(true);
|
try {
|
||||||
resolve(allResources);
|
const allResources = await resourceStore.fetchResourceList();
|
||||||
} catch (error: any) {
|
loadingState.setFinish();
|
||||||
console.error(error);
|
setIsComplete(true);
|
||||||
toast.error(error.response.data.message);
|
return allResources;
|
||||||
}
|
} catch (error: any) {
|
||||||
} else {
|
console.error(error);
|
||||||
resolve(resources);
|
toast.error(error.response.data.message);
|
||||||
|
return resources;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchResourceInputChange = async (query: string) => {
|
const handleSearchResourceInputChange = async (query: string) => {
|
||||||
// to prevent first tiger when page is loaded
|
// to prevent first tiger when page is loaded
|
||||||
if (query === queryText) return;
|
if (query === queryText) return;
|
||||||
await loadAllResources(() => {
|
await fetchAllResources();
|
||||||
setQueryText(query);
|
setQueryText(query);
|
||||||
setSelectedList([]);
|
setSelectedList([]);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourceList = useMemo(
|
const resourceList = useMemo(
|
||||||
|
@ -16,7 +16,7 @@ export const initialGlobalState = async () => {
|
|||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
name: "memos",
|
name: "memos",
|
||||||
logoUrl: "/logo.png",
|
logoUrl: "/logo.webp",
|
||||||
description: "",
|
description: "",
|
||||||
locale: "en",
|
locale: "en",
|
||||||
appearance: "system",
|
appearance: "system",
|
||||||
@ -41,7 +41,7 @@ export const initialGlobalState = async () => {
|
|||||||
...data,
|
...data,
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
name: customizedProfile.name || "memos",
|
name: customizedProfile.name || "memos",
|
||||||
logoUrl: customizedProfile.logoUrl || "/logo.png",
|
logoUrl: customizedProfile.logoUrl || "/logo.webp",
|
||||||
description: customizedProfile.description,
|
description: customizedProfile.description,
|
||||||
locale: customizedProfile.locale || "en",
|
locale: customizedProfile.locale || "en",
|
||||||
appearance: customizedProfile.appearance || "system",
|
appearance: customizedProfile.appearance || "system",
|
||||||
|
@ -24,7 +24,7 @@ const globalSlice = createSlice({
|
|||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
name: "memos",
|
name: "memos",
|
||||||
logoUrl: "/logo.png",
|
logoUrl: "/logo.webp",
|
||||||
description: "",
|
description: "",
|
||||||
locale: "en",
|
locale: "en",
|
||||||
appearance: "system",
|
appearance: "system",
|
||||||
|
Reference in New Issue
Block a user