5 Commits

Author SHA1 Message Date
anian 756b9f5d1c 修复共享链接无法被连带删除 2026-07-04 01:44:03 +08:00
anian 772a68a87b 优化目录效果 2026-07-03 20:25:19 +08:00
anian b75e4bcdd9 修改归档页面菜单、私有图标显示范围 2026-07-03 16:38:33 +08:00
anian b7ecfa5e35 修改置顶样式 2026-07-03 11:27:48 +08:00
anian fc866e23f9 修改默认配置 2026-07-02 19:18:24 +08:00
40 changed files with 264 additions and 101 deletions
+3
View File
@@ -11,3 +11,6 @@ memos
.dockerignore
docs/
.DS_Store
memos_prod.db
memos_prod.db-shm
memos_prod.db-wal
+5
View File
@@ -31,3 +31,8 @@ dist
# Frontend test coverage output (Vitest + @vitest/coverage-v8).
web/coverage/
# Database
memos_prod.db
memos_prod.db-shm
memos_prod.db-wal
+18
View File
@@ -1,3 +1,21 @@
# anian-memos
> 基于原作者v0.29.1代码修改
#### v0.29.1-2
1. 置顶内容新增自动折叠;置顶效果及图标改为全局显示
2. 归档页面新增(取消)置顶、编辑、复制按钮
3. 权限图标改为全局显示
4. 修复中文目录锚点无效;优化目录显示效果
5. 备忘录被删除时显式删除其分享链接
#### v0.29.1-1
1. 修改默认配置
<div align="center">
<p><b>Featured Sponsors</b></p>
<table>
+10 -14
View File
@@ -117,8 +117,8 @@ func EmailConfigFromInstanceSetting(setting *storepb.InstanceNotificationSetting
func NewTestEmailMessage(recipientEmail, replyTo string) *email.Message {
return &email.Message{
To: []string{recipientEmail},
Subject: "[Memos] Test email",
Body: "This is a test email from your Memos notification settings.",
Subject: "[阿年的备忘录] 测试邮件",
Body: "这是一封测试邮件,说明通知邮件设置已正确配置。",
ReplyTo: replyTo,
}
}
@@ -161,19 +161,17 @@ func (d *EmailDispatcher) buildMemoCommentEmailMessage(message *storepb.InboxMes
}
body := []string{
fmt.Sprintf("Hi %s,", displayNameForEmail(receiver)),
fmt.Sprintf("%s,你好:", displayNameForEmail(receiver)),
"",
fmt.Sprintf("%s commented on your memo.", senderName),
fmt.Sprintf("%s 评论了你的备忘录。", senderName),
"",
"Open in Memos:",
"前往查看:",
url,
"",
"You are receiving this because you own this memo.",
}
return &email.Message{
To: []string{receiver.Email},
Subject: fmt.Sprintf("[Memos] %s commented on your memo", senderName),
Subject: fmt.Sprintf("[阿年的备忘录] %s评论了你的备忘录", senderName),
Body: strings.Join(body, "\n"),
}, nil
}
@@ -193,19 +191,17 @@ func (d *EmailDispatcher) buildMemoMentionEmailMessage(message *storepb.InboxMes
}
body := []string{
fmt.Sprintf("Hi %s,", displayNameForEmail(receiver)),
fmt.Sprintf("%s,你好:", displayNameForEmail(receiver)),
"",
fmt.Sprintf("%s mentioned you in a memo.", senderName),
fmt.Sprintf("%s 在备忘录中提到了你。", senderName),
"",
"Open in Memos:",
"前往查看:",
url,
"",
"You are receiving this because you were mentioned in this memo.",
}
return &email.Message{
To: []string{receiver.Email},
Subject: fmt.Sprintf("[Memos] %s mentioned you in a memo", senderName),
Subject: fmt.Sprintf("[阿年的备忘录] %s在备忘录中提到了你", senderName),
Body: strings.Join(body, "\n"),
}, nil
}
+1 -1
View File
@@ -431,7 +431,7 @@ func getDeleteUserAttachmentStorageSetting(ctx context.Context, stores *store.St
func getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting {
return &v1pb.UserSetting_GeneralSetting{
Locale: "en",
Locale: "zh-Hans",
MemoVisibility: "PRIVATE",
Theme: "",
}
+5 -5
View File
@@ -329,7 +329,7 @@ func (*RSSService) generateItemTitle(content string) string {
// If title is empty, use a default
if title == "" {
title = "Memo"
title = "阿年的备忘录"
}
return title
@@ -416,9 +416,9 @@ func getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error)
}
if settings == nil || settings.CustomProfile == nil {
return RSSHeading{
Title: "Memos",
Description: "An open source, lightweight note-taking service. Easily capture and share your great thoughts.",
Language: "en-us",
Title: "阿年的备忘录",
Description: "一日不见 如隔三秋",
Language: "zh-Hans",
}, nil
}
customProfile := settings.CustomProfile
@@ -426,6 +426,6 @@ func getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error)
return RSSHeading{
Title: customProfile.Title,
Description: customProfile.Description,
Language: "en-us",
Language: "zh-Hans",
}, nil
}
+3
View File
@@ -116,6 +116,9 @@ func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare)
if delete.UID != nil {
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
}
if delete.MemoID != nil {
where, args = append(where, "`memo_id` = ?"), append(args, *delete.MemoID)
}
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
return err
}
+3
View File
@@ -137,6 +137,9 @@ func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare)
if delete.UID != nil {
where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *delete.UID)
}
if delete.MemoID != nil {
where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *delete.MemoID)
}
_, err := d.db.ExecContext(ctx, "DELETE FROM memo_share WHERE "+strings.Join(where, " AND "), args...)
return err
}
+3
View File
@@ -139,6 +139,9 @@ func (d *DB) DeleteMemoShare(ctx context.Context, delete *store.DeleteMemoShare)
if delete.UID != nil {
where, args = append(where, "`uid` = ?"), append(args, *delete.UID)
}
if delete.MemoID != nil {
where, args = append(where, "`memo_id` = ?"), append(args, *delete.MemoID)
}
_, err := d.db.ExecContext(ctx, "DELETE FROM `memo_share` WHERE "+strings.Join(where, " AND "), args...)
return err
}
+5
View File
@@ -145,6 +145,11 @@ func (s *Store) DeleteMemo(ctx context.Context, delete *DeleteMemo) error {
if err := s.driver.DeleteMemoRelation(ctx, &DeleteMemoRelation{RelatedMemoID: &delete.ID}); err != nil {
return err
}
// Clean up share links for this memo. SQLite runs with foreign keys disabled,
// so it cannot rely on ON DELETE CASCADE for memo_share rows.
if err := s.driver.DeleteMemoShare(ctx, &DeleteMemoShare{MemoID: &delete.ID}); err != nil {
return err
}
// Clean up attachments linked to this memo.
attachments, err := s.ListAttachments(ctx, &FindAttachment{MemoID: &delete.ID})
if err != nil {
+1
View File
@@ -24,6 +24,7 @@ type FindMemoShare struct {
type DeleteMemoShare struct {
ID *int32
UID *string
MemoID *int32
}
// CreateMemoShare creates a new share grant.
+25
View File
@@ -112,10 +112,35 @@ func TestDeleteMemoStore(t *testing.T) {
memo, err := ts.CreateMemo(ctx, memoCreate)
require.NoError(t, err)
require.Equal(t, memoCreate.Content, memo.Content)
otherMemo, err := ts.CreateMemo(ctx, &store.Memo{
UID: "test-other-memo",
CreatorID: user.ID,
Content: "other test content",
Visibility: store.Public,
})
require.NoError(t, err)
share, err := ts.CreateMemoShare(ctx, &store.MemoShare{
UID: "test-delete-memo-share",
MemoID: memo.ID,
CreatorID: user.ID,
})
require.NoError(t, err)
keptShare, err := ts.CreateMemoShare(ctx, &store.MemoShare{
UID: "test-keep-memo-share",
MemoID: otherMemo.ID,
CreatorID: user.ID,
})
require.NoError(t, err)
err = ts.DeleteMemo(ctx, &store.DeleteMemo{
ID: memo.ID,
})
require.NoError(t, err)
deletedShare, err := ts.GetMemoShare(ctx, &store.FindMemoShare{ID: &share.ID})
require.NoError(t, err)
require.Nil(t, deletedShare)
keptShareAfterDelete, err := ts.GetMemoShare(ctx, &store.FindMemoShare{ID: &keptShare.ID})
require.NoError(t, err)
require.NotNil(t, keptShareAfterDelete)
ts.Close()
}
+2 -2
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="zh-Hans">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
@@ -14,7 +14,7 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!-- memos.metadata.head -->
<title>Memos</title>
<title>阿年的备忘录</title>
</head>
<body class="text-base w-full min-h-svh">
<div id="root" class="relative w-full min-h-full"></div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 58 KiB

+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "Memos",
"short_name": "Memos",
"description": "An open-source, self-hosted note-taking tool. Capture thoughts instantly. Own them completely.",
"name": "阿年的备忘录",
"short_name": "阿年的备忘录",
"description": "一日不见 如隔三秋",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
@@ -45,7 +45,8 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
baseClasses,
intensityClass,
getCalendarCellStateClass(day),
isInteractive ? "cursor-pointer hover:bg-muted/40 hover:border-border/30" : "cursor-default",
day.isToday && "underline decoration-1 underline-offset-4",
isInteractive ? "cursor-pointer hover:bg-muted/40 hover:border-border/30 hover:text-foreground" : "cursor-default",
);
const button = (
@@ -71,8 +71,8 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2}>
{/* Edit actions (non-readonly, non-archived) */}
{!readonly && !isArchived && (
{/* Edit actions (non-readonly) */}
{!readonly && (
<>
{!isComment && (
<DropdownMenuItem onClick={handleTogglePinMemoBtnClick}>
@@ -87,8 +87,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
</>
)}
{/* Copy submenu (non-archived) */}
{!isArchived && (
{/* Copy submenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<CopyIcon className="w-4 h-auto" />
@@ -105,7 +104,6 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{/* Task submenu (writable task memos) */}
{canMutateTasks && (
+1 -1
View File
@@ -68,7 +68,7 @@ const MemoCommentSection = ({ memo, comments, parentPage }: Props) => {
)}
{comments.map((comment) => (
<div className="w-full" key={`${comment.name}-${comment.updateTime}`} id={extractMemoIdFromName(comment.name)}>
<MemoView memo={comment} parentPage={parentPage} showCreator compact />
<MemoView memo={comment} parentPage={parentPage} showCreator showVisibility compact />
</div>
))}
</div>
@@ -1,3 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import type { HeadingItem } from "@/utils/markdown-manipulation";
@@ -12,8 +13,95 @@ const levelIndent: Record<number, string> = {
4: "ml-8",
};
/** Outline navigation for memo headings (h1h4). */
const areStringArraysEqual = (a: string[], b: string[]) => {
if (a.length !== b.length) return false;
return a.every((item, index) => item === b[index]);
};
/** Outline navigation for memo headings (h1-h4). */
const MemoOutline = ({ headings }: MemoOutlineProps) => {
const outlineRef = useRef<HTMLElement>(null);
const itemRefs = useRef(new Map<string, HTMLAnchorElement>());
const [activeSlugs, setActiveSlugs] = useState<string[]>([]);
const orderedSlugs = useMemo(() => headings.map((heading) => heading.slug).filter(Boolean), [headings]);
const activeSlugSet = useMemo(() => new Set(activeSlugs), [activeSlugs]);
useEffect(() => {
setActiveSlugs([]);
const elements = orderedSlugs
.map((slug) => document.getElementById(slug))
.filter((element): element is HTMLElement => Boolean(element));
if (elements.length === 0) {
return;
}
let animationFrame = 0;
const updateActiveSlugs = () => {
animationFrame = 0;
const viewportTop = window.scrollY;
const viewportBottom = viewportTop + (window.innerHeight || document.documentElement.clientHeight);
const documentBottom = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
const headingSections = elements.map((element, index) => {
const sectionTop = element.getBoundingClientRect().top + window.scrollY;
const nextElement = elements[index + 1];
const sectionBottom = nextElement ? nextElement.getBoundingClientRect().top + window.scrollY : documentBottom;
return { slug: element.id, top: sectionTop, bottom: sectionBottom };
});
const nextActiveSlugs = headingSections
.filter((section) => section.bottom > viewportTop && section.top < viewportBottom)
.map((section) => section.slug);
setActiveSlugs((previousSlugs) => (areStringArraysEqual(previousSlugs, nextActiveSlugs) ? previousSlugs : nextActiveSlugs));
};
const scheduleUpdateActiveSlugs = () => {
if (animationFrame) {
return;
}
animationFrame = window.requestAnimationFrame(updateActiveSlugs);
};
updateActiveSlugs();
window.addEventListener("scroll", scheduleUpdateActiveSlugs, { passive: true });
window.addEventListener("resize", scheduleUpdateActiveSlugs);
return () => {
if (animationFrame) {
window.cancelAnimationFrame(animationFrame);
}
window.removeEventListener("scroll", scheduleUpdateActiveSlugs);
window.removeEventListener("resize", scheduleUpdateActiveSlugs);
};
}, [orderedSlugs]);
useEffect(() => {
const firstActiveSlug = activeSlugs[0];
const outline = outlineRef.current;
const activeItem = firstActiveSlug ? itemRefs.current.get(firstActiveSlug) : undefined;
if (!outline || !activeItem || outline.scrollHeight <= outline.clientHeight) {
return;
}
const itemTop = activeItem.offsetTop;
const itemBottom = itemTop + activeItem.offsetHeight;
const comfortableTop = outline.scrollTop + 24;
const comfortableBottom = outline.scrollTop + outline.clientHeight - 24;
if (itemTop >= comfortableTop && itemBottom <= comfortableBottom) {
return;
}
outline.scrollTo({
top: Math.max(0, itemTop - outline.clientHeight / 2 + activeItem.offsetHeight / 2),
behavior: "smooth",
});
}, [activeSlugs]);
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
e.preventDefault();
const el = document.getElementById(slug);
@@ -24,27 +112,50 @@ const MemoOutline = ({ headings }: MemoOutlineProps) => {
};
return (
<nav className="relative flex flex-col">
{headings.map((heading, index) => (
<nav ref={outlineRef} className="relative flex max-h-[45vh] flex-col overflow-y-auto overscroll-contain pr-1">
{headings.map((heading, index) => {
const isActive = activeSlugSet.has(heading.slug);
return (
<a
key={`${heading.slug}-${index}`}
ref={(element) => {
if (element) {
itemRefs.current.set(heading.slug, element);
} else {
itemRefs.current.delete(heading.slug);
}
}}
href={`#${heading.slug}`}
aria-current={activeSlugs[0] === heading.slug ? "location" : undefined}
onClick={(e) => handleClick(e, heading.slug)}
className={cn(
"group relative block py-[5px] pr-1 text-[13px] leading-snug truncate",
"text-muted-foreground/60 hover:text-foreground/90",
"group relative block shrink-0 rounded-l-sm py-[5px] pr-1 pl-2 text-[13px] leading-snug truncate",
"transition-colors duration-200 ease-out",
isActive ? "bg-primary/10 font-medium text-primary" : "text-muted-foreground/60 hover:text-foreground/90",
levelIndent[heading.level],
heading.level === 1 && "font-medium text-muted-foreground/80",
heading.level === 1 && !isActive && "font-medium text-muted-foreground/80",
)}
title={heading.text}
>
<span
className={cn(
"absolute top-1.5 bottom-1.5 left-0 w-0.5 rounded-full bg-primary transition-opacity duration-200",
isActive ? "opacity-100" : "opacity-0",
)}
/>
<span className="relative">
{heading.text}
<span className="absolute -bottom-px left-0 h-px w-0 bg-foreground/30 transition-all duration-200 group-hover:w-full" />
<span
className={cn(
"absolute -bottom-px left-0 h-px bg-foreground/30 transition-all duration-200",
isActive ? "w-full bg-primary/50" : "w-0 group-hover:w-full",
)}
/>
</span>
</a>
))}
);
})}
</nav>
);
};
@@ -53,7 +53,7 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
content={memo.content}
onClick={handleMemoContentClick}
onDoubleClick={handleMemoContentDoubleClick}
compact={memo.pinned ? false : compact} // Always show full content when pinned
compact={compact}
/>
<AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
@@ -5,7 +5,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import useNavigateTo from "@/hooks/useNavigateTo";
import i18n from "@/i18n";
import { cn } from "@/lib/utils";
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
import { convertVisibilityToString } from "@/utils/memo";
@@ -67,7 +66,7 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
/>
)}
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
{showVisibility && (
<Tooltip>
<TooltipTrigger>
<span className="flex justify-center items-center rounded-md hover:opacity-80">
+1 -1
View File
@@ -10,7 +10,7 @@ interface Props {
function MemosLogo(props: Props) {
const { collapsed } = props;
const { generalSetting: instanceGeneralSetting } = useInstance();
const title = instanceGeneralSetting.customProfile?.title || "Memos";
const title = instanceGeneralSetting.customProfile?.title || "阿年的备忘录";
const avatarUrl = instanceGeneralSetting.customProfile?.logoUrl || "/full-logo.webp";
return (
+1 -1
View File
@@ -32,7 +32,7 @@ const MobileHeader = (props: Props) => {
return (
<div
className={cn(
"sticky top-0 pt-3 pb-2 sm:pt-2 px-4 sm:px-6 sm:mb-1 bg-background bg-opacity-80 backdrop-blur-lg flex flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-1",
"sticky top-0 pt-3 pb-2 sm:pt-2 px-4 sm:px-6 sm:mb-1 bg-background bg-opacity-80 backdrop-blur-lg flex flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-4",
offsetTop > 0 && "shadow-md",
className,
)}
+1 -7
View File
@@ -40,12 +40,6 @@ const Navigation = (props: Props) => {
title: t("common.explore"),
icon: <EarthIcon className="w-6 h-auto shrink-0" />,
};
const aboutNavLink: NavLinkItem = {
id: "header-about",
path: Routes.ABOUT,
title: t("common.about"),
icon: <InfoIcon className="w-6 h-auto shrink-0" />,
};
const attachmentsNavLink: NavLinkItem = {
id: "header-attachments",
path: Routes.ATTACHMENTS,
@@ -77,7 +71,7 @@ const Navigation = (props: Props) => {
const primaryNavLinks: NavLinkItem[] = currentUser
? [homeNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink]
: [exploreNavLink, aboutNavLink, signInNavLink];
: [exploreNavLink, signInNavLink];
const inboxAriaLabel = unreadCount > 0 ? `${t("common.inbox")}, ${unreadCount} unread` : t("common.inbox");
return (
+1 -1
View File
@@ -10,7 +10,7 @@ const NavigationDrawer = () => {
const location = useLocation();
const [open, setOpen] = useState(false);
const { generalSetting } = useInstance();
const title = generalSetting.customProfile?.title || "Memos";
const title = generalSetting.customProfile?.title || "阿年的备忘录";
const avatarUrl = generalSetting.customProfile?.logoUrl || "/full-logo.webp";
useEffect(() => {
@@ -70,7 +70,7 @@ const InstanceSection = () => {
<SettingSection title={t("setting.system.label")}>
<SettingGroup title={t("common.basic")} description={t("setting.system.basic-description")}>
<SettingList>
<SettingListItem label={t("setting.system.server-name")} description={instanceGeneralSetting.customProfile?.title || "Memos"}>
<SettingListItem label={t("setting.system.server-name")} description={instanceGeneralSetting.customProfile?.title || "阿年的备忘录"}>
<Button variant="outline" onClick={customizeDialog.open}>
{t("common.edit")}
</Button>
@@ -62,7 +62,7 @@ const PreferencesSection = () => {
const setting: UserSetting_GeneralSetting =
generalSetting ||
create(UserSetting_GeneralSettingSchema, {
locale: "en",
locale: "zh-Hans",
memoVisibility: "PRIVATE",
theme: "system",
});
@@ -59,7 +59,7 @@ function UpdateCustomizedProfileDialog({ open, onOpenChange, onSuccess }: Props)
const handleRestoreButtonClick = () => {
setPartialState({
title: "Memos",
title: "阿年的备忘录",
logoUrl: "/logo.webp",
description: "",
});
-4
View File
@@ -142,10 +142,6 @@ const UserMenu = (props: Props) => {
<ArchiveIcon className="size-4 text-muted-foreground" />
{t("common.archived")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigateTo(Routes.ABOUT)}>
<InfoIcon className="size-4 text-muted-foreground" />
{t("common.about")}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<GlobeIcon className="size-4 text-muted-foreground" />
+1 -1
View File
@@ -78,7 +78,7 @@ i18n
},
fallbackLng: {
...fallbacks,
...{ default: ["en"] },
...{ default: ["zh-Hans"] },
} as FallbackLng,
});
+1 -1
View File
@@ -10,7 +10,7 @@ const AdminSignIn = () => {
<div className="w-full py-4 grow flex flex-col justify-center items-center">
<div className="w-full flex flex-row justify-center items-center mb-6">
<img className="h-14 w-auto rounded-full shadow" src={instanceGeneralSetting.customProfile?.logoUrl || "/logo.webp"} alt="" />
<p className="ml-2 text-5xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "Memos"}</p>
<p className="ml-2 text-4xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "阿年的备忘录"}</p>
</div>
<p className="w-full text-xl font-medium text-muted-foreground">Sign in with admin accounts</p>
<PasswordSignInForm />
+1 -1
View File
@@ -23,7 +23,7 @@ const Archived = () => {
return (
<PagedMemoList
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility compact />}
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility showPinned compact />}
listSort={listSort}
state={State.ARCHIVED}
orderBy={orderBy}
+2 -2
View File
@@ -23,13 +23,13 @@ const Explore = () => {
// Get sorting logic using unified hook (no pinned sorting)
const { listSort, orderBy } = useMemoSorting({
pinnedFirst: false,
pinnedFirst: true,
state: State.NORMAL,
});
return (
<PagedMemoList
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility showPinned compact />}
listSort={listSort}
orderBy={orderBy}
filter={memoFilter}
+1 -1
View File
@@ -75,7 +75,7 @@ const SignIn = () => {
<div className="w-full py-4 grow flex flex-col justify-center items-center">
<div className="w-full flex flex-row justify-center items-center mb-6">
<img className="h-14 w-auto rounded-full shadow" src={instanceGeneralSetting.customProfile?.logoUrl || "/logo.webp"} alt="" />
<p className="ml-2 text-5xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "Memos"}</p>
<p className="ml-2 text-4xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "阿年的备忘录"}</p>
</div>
{!instanceGeneralSetting.disallowPasswordAuth ? (
<PasswordSignInForm redirectPath={redirectTarget} />
+1 -1
View File
@@ -92,7 +92,7 @@ const SignUp = () => {
<div className="w-full py-4 grow flex flex-col justify-center items-center">
<div className="w-full flex flex-row justify-center items-center mb-6">
<img className="h-14 w-auto rounded-full shadow" src={instanceGeneralSetting.customProfile?.logoUrl || "/logo.webp"} alt="" />
<p className="ml-2 text-5xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "Memos"}</p>
<p className="ml-2 text-4xl text-foreground opacity-80">{instanceGeneralSetting.customProfile?.title || "阿年的备忘录"}</p>
</div>
{canUsePasswordSignUp ? (
<>
+3 -1
View File
@@ -118,7 +118,7 @@ export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.replace(/[\s_]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
@@ -143,6 +143,8 @@ export function extractHeadings(markdown: string): HeadingItem[] {
if (!text) return;
let slug = slugify(text);
if (!slug) return;
const count = slugCounts.get(slug) || 0;
slugCounts.set(slug, count + 1);
if (count > 0) slug = `${slug}-${count}`;