mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
feat: add font size and table of contents settings to RichEditor (#10034)
* feat: add font size and table of contents settings to RichEditor - Introduced font size customization in the RichEditor component, allowing users to adjust the font size for better readability. - Added a toggle for displaying a table of contents in the editor settings. - Updated localization files to include new settings descriptions. - Enhanced the NotesSettings component with a slider for font size adjustment and a switch for the table of contents feature. - Migrated state management to include new settings in the Redux store. * feat: enhance CodeEditor with customizable font size and responsive layout * feat: enhance markdown conversion to preserve square brackets - Improved the htmlToMarkdown function to correctly handle and preserve wiki-style double brackets [[foo]] and single brackets [foo] while maintaining proper Markdown link syntax. - Added unit tests to verify the preservation of these bracket formats during conversion. * feat: enhance YamlFrontMatterNodeView with editor content check * fix * chore * chore: bump store persistence version to 153 --------- Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
parent
86f9e93e97
commit
56c1851848
@ -5,6 +5,7 @@
|
||||
min-height: 120px;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-size: var(--editor-font-size, 16px);
|
||||
}
|
||||
|
||||
.tiptap:focus {
|
||||
|
||||
@ -15,7 +15,7 @@ interface ParsedProperty {
|
||||
type: 'string' | 'array' | 'date' | 'number' | 'boolean'
|
||||
}
|
||||
|
||||
const YamlFrontMatterNodeView: React.FC<NodeViewProps> = ({ node, updateAttributes }) => {
|
||||
const YamlFrontMatterNodeView: React.FC<NodeViewProps> = ({ node, updateAttributes, editor }) => {
|
||||
const { t } = useTranslation()
|
||||
const [editingProperty, setEditingProperty] = useState<string | null>(null)
|
||||
const [newPropertyName, setNewPropertyName] = useState('')
|
||||
@ -408,6 +408,11 @@ const YamlFrontMatterNodeView: React.FC<NodeViewProps> = ({ node, updateAttribut
|
||||
)
|
||||
}
|
||||
|
||||
// Check if there's content in the entire editor (excluding YAML front matter)
|
||||
const hasContent = useMemo(() => {
|
||||
return editor.getText().trim().length > 0
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
className="yaml-front-matter-wrapper"
|
||||
@ -418,6 +423,7 @@ const YamlFrontMatterNodeView: React.FC<NodeViewProps> = ({ node, updateAttribut
|
||||
}
|
||||
}}>
|
||||
<PropertiesContainer
|
||||
hasContent={hasContent}
|
||||
onClick={(e) => {
|
||||
// Prevent node selection when clicking inside properties
|
||||
e.stopPropagation()
|
||||
@ -485,7 +491,7 @@ const YamlFrontMatterNodeView: React.FC<NodeViewProps> = ({ node, updateAttribut
|
||||
/>
|
||||
</PropertyRow>
|
||||
) : (
|
||||
<AddPropertyRow onClick={() => setShowAddProperty(true)}>
|
||||
<AddPropertyRow hasContent={hasContent} onClick={() => setShowAddProperty(true)}>
|
||||
<PropertyIcon>
|
||||
<Plus size={16} />
|
||||
</PropertyIcon>
|
||||
@ -497,7 +503,7 @@ const YamlFrontMatterNodeView: React.FC<NodeViewProps> = ({ node, updateAttribut
|
||||
)
|
||||
}
|
||||
|
||||
const PropertiesContainer = styled.div`
|
||||
const PropertiesContainer = styled.div<{ hasContent?: boolean }>`
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
@ -705,7 +711,7 @@ const ArrayInput = styled(Input)`
|
||||
}
|
||||
`
|
||||
|
||||
const AddPropertyRow = styled.button`
|
||||
const AddPropertyRow = styled.button<{ hasContent?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
@ -716,16 +722,22 @@ const AddPropertyRow = styled.button`
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
opacity: ${({ hasContent }) => (hasContent ? 0 : 1)};
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-hover);
|
||||
}
|
||||
|
||||
${PropertiesContainer}:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const AddPropertyText = styled.div`
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
color: var(--color-text);
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
const PropertyActions = styled.div`
|
||||
|
||||
@ -47,7 +47,8 @@ const RichEditor = ({
|
||||
showTableOfContents = false,
|
||||
enableContentSearch = false,
|
||||
isFullWidth = false,
|
||||
fontFamily = 'default'
|
||||
fontFamily = 'default',
|
||||
fontSize = 16
|
||||
// toolbarItems: _toolbarItems // TODO: Implement custom toolbar items
|
||||
}: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => {
|
||||
// Use the rich editor hook for complete editor management
|
||||
@ -388,6 +389,7 @@ const RichEditor = ({
|
||||
$maxHeight={maxHeight}
|
||||
$isFullWidth={isFullWidth}
|
||||
$fontFamily={fontFamily}
|
||||
$fontSize={fontSize}
|
||||
onKeyDown={onKeyDownEditor}>
|
||||
{showToolbar && (
|
||||
<Toolbar
|
||||
|
||||
@ -5,6 +5,7 @@ export const RichEditorWrapper = styled.div<{
|
||||
$maxHeight?: number
|
||||
$isFullWidth?: boolean
|
||||
$fontFamily?: 'default' | 'serif'
|
||||
$fontSize?: number
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -16,6 +17,7 @@ export const RichEditorWrapper = styled.div<{
|
||||
width: ${({ $isFullWidth }) => ($isFullWidth ? '100%' : '60%')};
|
||||
margin: ${({ $isFullWidth }) => ($isFullWidth ? '0' : '0 auto')};
|
||||
font-family: ${({ $fontFamily }) => ($fontFamily === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)')};
|
||||
${({ $fontSize }) => $fontSize && `--editor-font-size: ${$fontSize}px;`}
|
||||
|
||||
${({ $minHeight }) => $minHeight && `min-height: ${$minHeight}px;`}
|
||||
${({ $maxHeight }) => $maxHeight && `max-height: ${$maxHeight}px;`}
|
||||
|
||||
@ -48,6 +48,8 @@ export interface RichEditorProps {
|
||||
isFullWidth?: boolean
|
||||
/** Font family setting */
|
||||
fontFamily?: 'default' | 'serif'
|
||||
/** Font size in pixels */
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
export interface ToolbarItem {
|
||||
|
||||
@ -1743,8 +1743,15 @@
|
||||
"compress_content": "Content Compression",
|
||||
"compress_content_description": "When enabled, it will limit the number of characters per line, reducing the content displayed on the screen, but making longer paragraphs more readable.",
|
||||
"default_font": "Default font",
|
||||
"font_size": "Font Size",
|
||||
"font_size_description": "Adjust the font size for better reading experience (10-30px)",
|
||||
"font_size_large": "Large",
|
||||
"font_size_medium": "Medium",
|
||||
"font_size_small": "Small",
|
||||
"font_title": "Font settings",
|
||||
"serif_font": "Serif font",
|
||||
"show_table_of_contents": "Show Table of Contents",
|
||||
"show_table_of_contents_description": "Display a table of contents sidebar for easy navigation within documents",
|
||||
"title": "Display Settings"
|
||||
},
|
||||
"editor": {
|
||||
|
||||
@ -1743,8 +1743,15 @@
|
||||
"compress_content": "缩减栏宽",
|
||||
"compress_content_description": "开启后将限制每行字数,使屏幕显示的内容减少",
|
||||
"default_font": "默认字体",
|
||||
"font_size": "字体大小",
|
||||
"font_size_description": "调整字体大小以获得更好的阅读体验 (10-30px)",
|
||||
"font_size_large": "大",
|
||||
"font_size_medium": "中",
|
||||
"font_size_small": "小",
|
||||
"font_title": "字体设置",
|
||||
"serif_font": "衬线字体",
|
||||
"show_table_of_contents": "显示目录大纲",
|
||||
"show_table_of_contents_description": "显示目录大纲侧边栏,方便文档内导航",
|
||||
"title": "显示设置"
|
||||
},
|
||||
"editor": {
|
||||
|
||||
@ -1743,8 +1743,15 @@
|
||||
"compress_content": "縮減欄寬",
|
||||
"compress_content_description": "開啟後將限制每行字數,使屏幕顯示的內容減少",
|
||||
"default_font": "默認字體",
|
||||
"font_size": "字體大小",
|
||||
"font_size_description": "調整字體大小以獲得更好的閱讀體驗 (10-30px)",
|
||||
"font_size_large": "大",
|
||||
"font_size_medium": "中",
|
||||
"font_size_small": "小",
|
||||
"font_title": "字體設置",
|
||||
"serif_font": "襯線字體",
|
||||
"show_table_of_contents": "顯示目錄大綱",
|
||||
"show_table_of_contents_description": "顯示目錄大綱側邊欄,方便文檔內導航",
|
||||
"title": "顯示"
|
||||
},
|
||||
"editor": {
|
||||
|
||||
@ -7,7 +7,7 @@ import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
|
||||
import { findNodeInTree } from '@renderer/services/NotesTreeService'
|
||||
import { Breadcrumb, BreadcrumbProps, Dropdown, Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { MoreHorizontal, PanelLeftClose, PanelRightClose } from 'lucide-react'
|
||||
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -15,16 +15,23 @@ import { menuItems } from './MenuConfig'
|
||||
|
||||
const logger = loggerService.withContext('HeaderNavbar')
|
||||
|
||||
const HeaderNavbar = ({ notesTree, getCurrentNoteContent }) => {
|
||||
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => {
|
||||
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
||||
const { activeNode } = useActiveNode(notesTree)
|
||||
const [breadcrumbItems, setBreadcrumbItems] = useState<Required<BreadcrumbProps>['items']>([])
|
||||
const { settings, updateSettings } = useNotesSettings()
|
||||
const canShowStarButton = activeNode?.type === 'file' && onToggleStar
|
||||
|
||||
const handleToggleShowWorkspace = useCallback(() => {
|
||||
toggleShowWorkspace()
|
||||
}, [toggleShowWorkspace])
|
||||
|
||||
const handleToggleStarred = useCallback(() => {
|
||||
if (activeNode) {
|
||||
onToggleStar(activeNode.id)
|
||||
}
|
||||
}, [activeNode, onToggleStar])
|
||||
|
||||
const handleCopyContent = useCallback(async () => {
|
||||
try {
|
||||
const content = getCurrentNoteContent?.()
|
||||
@ -132,6 +139,17 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent }) => {
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</NavbarCenter>
|
||||
<NavbarRight style={{ paddingRight: 0 }}>
|
||||
{canShowStarButton && (
|
||||
<Tooltip title={activeNode.isStarred ? t('notes.unstar') : t('notes.star')} mouseEnterDelay={0.8}>
|
||||
<StarButton onClick={handleToggleStarred}>
|
||||
{activeNode.isStarred ? (
|
||||
<Star size={18} fill="var(--color-status-warning)" stroke="var(--color-status-warning)" />
|
||||
) : (
|
||||
<Star size={18} />
|
||||
)}
|
||||
</StarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('notes.settings.title')} mouseEnterDelay={0.8}>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems.map(buildMenuItem) }}
|
||||
@ -187,4 +205,24 @@ export const NavbarIcon = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const StarButton = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
svg {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@ -33,6 +33,13 @@ export const menuItems: MenuItem[] = [
|
||||
action: (settings, updateSettings) => updateSettings({ isFullWidth: !settings.isFullWidth }),
|
||||
isActive: (settings) => !settings.isFullWidth
|
||||
},
|
||||
{
|
||||
key: 'table-of-contents',
|
||||
labelKey: 'notes.settings.display.show_table_of_contents',
|
||||
icon: Type,
|
||||
action: (settings, updateSettings) => updateSettings({ showTableOfContents: !settings.showTableOfContents }),
|
||||
isActive: (settings) => settings.showTableOfContents
|
||||
},
|
||||
{
|
||||
key: 'divider1',
|
||||
type: 'divider',
|
||||
@ -54,6 +61,29 @@ export const menuItems: MenuItem[] = [
|
||||
labelKey: 'notes.settings.display.serif_font',
|
||||
action: (_, updateSettings) => updateSettings({ fontFamily: 'serif' }),
|
||||
isActive: (settings) => settings.fontFamily === 'serif'
|
||||
},
|
||||
{
|
||||
key: 'divider2',
|
||||
type: 'divider',
|
||||
labelKey: ''
|
||||
},
|
||||
{
|
||||
key: 'font-size-small',
|
||||
labelKey: 'notes.settings.display.font_size_small',
|
||||
action: (_, updateSettings) => updateSettings({ fontSize: 14 }),
|
||||
isActive: (settings) => settings.fontSize === 14
|
||||
},
|
||||
{
|
||||
key: 'font-size-medium',
|
||||
labelKey: 'notes.settings.display.font_size_medium',
|
||||
action: (_, updateSettings) => updateSettings({ fontSize: 16 }),
|
||||
isActive: (settings) => settings.fontSize === 16
|
||||
},
|
||||
{
|
||||
key: 'font-size-large',
|
||||
labelKey: 'notes.settings.display.font_size_large',
|
||||
action: (_, updateSettings) => updateSettings({ fontSize: 20 }),
|
||||
isActive: (settings) => settings.fontSize === 20
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -59,16 +59,19 @@ const NotesEditor: FC<NotesEditorProps> = memo(
|
||||
<>
|
||||
<RichEditorContainer>
|
||||
{tmpViewMode === 'source' ? (
|
||||
<CodeEditor
|
||||
value={currentContent}
|
||||
language="markdown"
|
||||
onChange={onMarkdownChange}
|
||||
height="100%"
|
||||
expanded={false}
|
||||
style={{
|
||||
height: '100%'
|
||||
}}
|
||||
/>
|
||||
<SourceEditorWrapper isFullWidth={settings.isFullWidth} fontSize={settings.fontSize}>
|
||||
<CodeEditor
|
||||
value={currentContent}
|
||||
language="markdown"
|
||||
onChange={onMarkdownChange}
|
||||
height="100%"
|
||||
expanded={false}
|
||||
style={{
|
||||
height: '100%',
|
||||
fontSize: `${settings.fontSize}px`
|
||||
}}
|
||||
/>
|
||||
</SourceEditorWrapper>
|
||||
) : (
|
||||
<RichEditor
|
||||
key={`${activeNodeId}-${tmpViewMode === 'preview' ? 'preview' : 'read'}`}
|
||||
@ -78,11 +81,12 @@ const NotesEditor: FC<NotesEditorProps> = memo(
|
||||
onCommandsReady={handleCommandsReady}
|
||||
showToolbar={tmpViewMode === 'preview'}
|
||||
editable={tmpViewMode === 'preview'}
|
||||
showTableOfContents
|
||||
showTableOfContents={settings.showTableOfContents}
|
||||
enableContentSearch
|
||||
className="notes-rich-editor"
|
||||
isFullWidth={settings.isFullWidth}
|
||||
fontFamily={settings.fontFamily}
|
||||
fontSize={settings.fontSize}
|
||||
/>
|
||||
)}
|
||||
</RichEditorContainer>
|
||||
@ -172,6 +176,28 @@ const RichEditorContainer = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const SourceEditorWrapper = styled.div<{ isFullWidth: boolean; fontSize: number }>`
|
||||
height: 100%;
|
||||
width: ${({ isFullWidth }) => (isFullWidth ? '100%' : '60%')};
|
||||
margin: ${({ isFullWidth }) => (isFullWidth ? '0' : '0 auto')};
|
||||
|
||||
/* 应用字体大小到CodeEditor */
|
||||
.monaco-editor {
|
||||
font-size: ${({ fontSize }) => fontSize}px !important;
|
||||
}
|
||||
|
||||
/* 确保CodeEditor内部元素也应用字体大小 */
|
||||
.monaco-editor .monaco-editor-background,
|
||||
.monaco-editor .inputarea.ime-input,
|
||||
.monaco-editor .monaco-editor-container,
|
||||
.monaco-editor .overflow-guard,
|
||||
.monaco-editor .monaco-scrollable-element,
|
||||
.monaco-editor .lines-content.monaco-editor-background,
|
||||
.monaco-editor .view-line {
|
||||
font-size: ${({ fontSize }) => fontSize}px !important;
|
||||
}
|
||||
`
|
||||
|
||||
const BottomPanel = styled.div`
|
||||
padding: 8px 16px;
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
|
||||
import { Tooltip } from 'antd'
|
||||
import { PanelLeftClose, PanelRightClose } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const NotesNavbar = () => {
|
||||
const { t } = useTranslation()
|
||||
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
||||
const isFullscreen = useFullscreen()
|
||||
|
||||
const handleToggleShowWorkspace = useCallback(() => {
|
||||
toggleShowWorkspace()
|
||||
}, [toggleShowWorkspace])
|
||||
|
||||
return (
|
||||
<Navbar className="notes-navbar">
|
||||
{showWorkspace && (
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
|
||||
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={handleToggleShowWorkspace} style={{ marginLeft: isMac && !isFullscreen ? 16 : 0 }}>
|
||||
<PanelLeftClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="notes-navbar-right">
|
||||
<HStack alignItems="center">
|
||||
{!showWorkspace && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon
|
||||
onClick={handleToggleShowWorkspace}
|
||||
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-appstore {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
export default NotesNavbar
|
||||
@ -20,8 +20,8 @@ import { selectActiveFilePath, selectSortType, setActiveFilePath, setSortType }
|
||||
import { NotesSortType, NotesTreeNode } from '@renderer/types/note'
|
||||
import { FileChangeEvent } from '@shared/config/types'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { debounce } from 'lodash'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -629,7 +629,11 @@ const NotesPage: FC = () => {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<EditorWrapper>
|
||||
<HeaderNavbar notesTree={notesTree} getCurrentNoteContent={getCurrentNoteContent} />
|
||||
<HeaderNavbar
|
||||
notesTree={notesTree}
|
||||
getCurrentNoteContent={getCurrentNoteContent}
|
||||
onToggleStar={handleToggleStar}
|
||||
/>
|
||||
<NotesEditor
|
||||
activeNodeId={activeNode?.id}
|
||||
currentContent={currentContent}
|
||||
|
||||
@ -4,7 +4,7 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
||||
import { initWorkSpace } from '@renderer/services/NotesService'
|
||||
import { EditorView } from '@renderer/types'
|
||||
import { Button, Input, message, Switch } from 'antd'
|
||||
import { Button, Input, message, Slider, Switch } from 'antd'
|
||||
import { FolderOpen } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -167,9 +167,33 @@ const NotesSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('notes.settings.display.compress_content')}</SettingRowTitle>
|
||||
<Switch checked={!settings.isFullWidth} onChange={(checked) => updateSettings({ isFullWidth: checked })} />
|
||||
<Switch checked={!settings.isFullWidth} onChange={(checked) => updateSettings({ isFullWidth: !checked })} />
|
||||
</SettingRow>
|
||||
<SettingHelpText>{t('notes.settings.display.compress_content_description')}</SettingHelpText>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('notes.settings.display.font_size')}</SettingRowTitle>
|
||||
<FontSizeContainer>
|
||||
<Slider
|
||||
min={10}
|
||||
max={30}
|
||||
value={settings.fontSize}
|
||||
onChange={(value) => updateSettings({ fontSize: value })}
|
||||
style={{ width: 200, marginRight: 16 }}
|
||||
/>
|
||||
<FontSizeValue>{settings.fontSize}px</FontSizeValue>
|
||||
</FontSizeContainer>
|
||||
</SettingRow>
|
||||
<SettingHelpText>{t('notes.settings.display.font_size_description')}</SettingHelpText>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('notes.settings.display.show_table_of_contents')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={settings.showTableOfContents}
|
||||
onChange={(checked) => updateSettings({ showTableOfContents: checked })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingHelpText>{t('notes.settings.display.show_table_of_contents_description')}</SettingHelpText>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
@ -194,4 +218,15 @@ const ActionButtons = styled.div`
|
||||
align-self: flex-start;
|
||||
`
|
||||
|
||||
const FontSizeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const FontSizeValue = styled.span`
|
||||
min-width: 40px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
export default NotesSettings
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 152,
|
||||
version: 153,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -2439,6 +2439,18 @@ const migrateConfig = {
|
||||
logger.error('migrate 152 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'153': (state: RootState) => {
|
||||
try {
|
||||
if (state.note.settings) {
|
||||
state.note.settings.fontSize = notesInitialState.settings.fontSize
|
||||
state.note.settings.showTableOfContents = notesInitialState.settings.showTableOfContents
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 153 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ import { NotesSortType } from '@renderer/types/note'
|
||||
export interface NotesSettings {
|
||||
isFullWidth: boolean
|
||||
fontFamily: 'default' | 'serif'
|
||||
fontSize: number
|
||||
showTableOfContents: boolean
|
||||
defaultViewMode: 'edit' | 'read'
|
||||
defaultEditMode: Omit<EditorView, 'read'>
|
||||
showTabStatus: boolean
|
||||
@ -26,6 +28,8 @@ export const initialState: NoteState = {
|
||||
settings: {
|
||||
isFullWidth: true,
|
||||
fontFamily: 'default',
|
||||
fontSize: 16,
|
||||
showTableOfContents: true,
|
||||
defaultViewMode: 'edit',
|
||||
defaultEditMode: 'preview',
|
||||
showTabStatus: true,
|
||||
|
||||
@ -485,7 +485,7 @@ describe('markdownConverter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('shoud keep YAML front matter', () => {
|
||||
describe('should keep YAML front matter', () => {
|
||||
it('should keep YAML front matter', () => {
|
||||
const markdown = `---
|
||||
tags:
|
||||
@ -505,4 +505,19 @@ cssclasses:
|
||||
expect(backToMarkdown).toBe(markdown)
|
||||
})
|
||||
})
|
||||
|
||||
describe('should keep []', () => {
|
||||
it('should keep [[foo]]', () => {
|
||||
const markdown = `[[foo]]`
|
||||
const result = markdownToHtml(markdown)
|
||||
const backToMarkdown = htmlToMarkdown(result)
|
||||
expect(backToMarkdown).toBe(markdown)
|
||||
})
|
||||
it('should keep []', () => {
|
||||
const markdown = `[foo]`
|
||||
const result = markdownToHtml(markdown)
|
||||
const backToMarkdown = htmlToMarkdown(result)
|
||||
expect(backToMarkdown).toBe(markdown)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -750,7 +750,35 @@ export const htmlToMarkdown = (html: string | null | undefined): string => {
|
||||
try {
|
||||
const encodedHtml = escapeCustomTags(html)
|
||||
const turndownResult = turndownService.turndown(encodedHtml)
|
||||
const finalResult = he.decode(turndownResult)
|
||||
let finalResult = he.decode(turndownResult)
|
||||
|
||||
// Post-process to unescape square brackets that are not part of Markdown link syntax
|
||||
// This preserves wiki-style double brackets [[foo]] and single brackets [foo]
|
||||
// but keeps proper Markdown links [text](url) intact
|
||||
|
||||
// Use a more sophisticated approach: check for the link pattern first,
|
||||
// then unescape standalone brackets
|
||||
|
||||
// First, protect actual Markdown links by temporarily replacing them
|
||||
const linkPlaceholders: string[] = []
|
||||
let linkCounter = 0
|
||||
|
||||
// Find and replace all Markdown links with placeholders
|
||||
finalResult = finalResult.replace(/\\\[([^\]]*)\\\]\([^)]*\)/g, (match) => {
|
||||
const placeholder = `__MDLINK_${linkCounter++}__`
|
||||
linkPlaceholders[linkCounter - 1] = match
|
||||
return placeholder
|
||||
})
|
||||
|
||||
// Now unescape all remaining square brackets
|
||||
finalResult = finalResult.replace(/\\\[/g, '[').replace(/\\\]/g, ']')
|
||||
|
||||
// Restore the Markdown links
|
||||
for (let i = 0; i < linkPlaceholders.length; i++) {
|
||||
const placeholder = `__MDLINK_${i}__`
|
||||
finalResult = finalResult.replace(placeholder, linkPlaceholders[i])
|
||||
}
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
logger.error('Error converting HTML to Markdown:', error as Error)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user