fix: polish mobile transitions and Mermaid rendering

This commit is contained in:
tianma-if committed 2026-07-24 10:05:32 +08:00
1 parent 2cc23d419e
commit 3ef15a2b8d
6 files changed
+52 -15

No files matched your search

+2 -2
View File
@@ -3,7 +3,7 @@
"name": "EdgeEver",
"slug": "edgeever",
"scheme": "edgeever",
"version": "1.5.14",
"version": "1.5.16",
"runtimeVersion": {
"policy": "appVersion"
},
@@ -28,7 +28,7 @@
},
"android": {
"package": "org.edgeever.mobile",
"versionCode": 35,
"versionCode": 36,
"adaptiveIcon": {
"backgroundColor": "#d1fae5",
"backgroundImage": "./assets/icon.png",
+3 -2
View File
@@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
import { StyleSheet, Text, useWindowDimensions, View } from "react-native";
import { SvgXml } from "react-native-svg";
import LocalTiptapEditor from "./LocalTiptapEditor";
import { getMermaidSvgAspectRatio } from "../lib/mobile-mermaid";
import { getMermaidSvgAspectRatio, sanitizeMobileMermaidSvg } from "../lib/mobile-mermaid";
type MermaidResult = { source: string; svg: string | null };
type MermaidContextValue = {
@@ -109,9 +109,10 @@ export const MobileMermaidDiagram = ({
const aspectRatio = getMermaidSvgAspectRatio(svg);
const availableWidth = Math.max(1, width - 32);
const height = Math.min(440, Math.max(120, availableWidth / aspectRatio));
const nativeSvg = sanitizeMobileMermaidSvg(svg);
return (
<View accessibilityLabel={locale === "en-US" ? "Mermaid diagram" : "Mermaid 图表"} accessible style={[styles.diagram, theme === "dark" && styles.diagramDark, { height }]}>
<SvgXml height="100%" width="100%" xml={svg} />
<SvgXml height="100%" width="100%" xml={nativeSvg} />
</View>
);
};
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
getMermaidSvgAspectRatio,
getMobileMarkdownFenceLanguage,
sanitizeMobileMermaidSvg,
trimMobileMarkdownFenceContent,
} from "./mobile-mermaid";
@@ -20,4 +21,10 @@ describe("mobile Mermaid rendering", () => {
expect(getMermaidSvgAspectRatio('<svg viewBox="0 0 800 400"></svg>')).toBe(2);
expect(getMermaidSvgAspectRatio("<svg></svg>")).toBe(1.6);
});
test("normalizes SVG 2 marker orientation for react-native-svg", () => {
expect(sanitizeMobileMermaidSvg('<marker orient="auto-start-reverse" />')).toBe('<marker orient="auto" />');
expect(sanitizeMobileMermaidSvg("<marker orient='auto-start-reverse' />")).toBe("<marker orient='auto' />");
expect(sanitizeMobileMermaidSvg('<marker orient="45" />')).toBe('<marker orient="45" />');
});
});
+9
View File
@@ -4,6 +4,15 @@ export const getMobileMarkdownFenceLanguage = (sourceInfo: unknown) =>
export const trimMobileMarkdownFenceContent = (content: string) =>
content.endsWith("\n") ? content.slice(0, -1) : content;
/**
* Mermaid uses the SVG 2 `auto-start-reverse` marker orientation for arrows.
* Browsers support it, but react-native-svg parses marker `orient` as a
* numeric angle and crashes while drawing the SVG. `auto` keeps the arrow
* direction compatible on native without changing the Mermaid source.
*/
export const sanitizeMobileMermaidSvg = (svg: string) =>
svg.replace(/(\borient\s*=\s*)(["'])auto-start-reverse\2/gi, "$1$2auto$2");
export const getMermaidSvgAspectRatio = (svg: string) => {
const viewBox = /viewBox=["']\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)\s*["']/i.exec(svg);
const width = Number(viewBox?.[1]);
+30 -10
View File
@@ -77,7 +77,7 @@ import {
type ViewStyle,
} from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
import Animated, { FadeInDown, FadeOutUp, LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Alert, Pressable, Text, TextInput } from "../components/LocalizedText";
import Markdown, { type ASTNode, type RenderRules } from "react-native-markdown-display";
@@ -1115,7 +1115,7 @@ export const WorkspaceScreen = () => {
/>
) : null}
{selectedMemoId ? <MemoDetailModal
<MemoDetailModal
isDeleting={deleteMemoMutation.isPending}
isLoading={memoDetailQuery.isLoading}
isRestoring={restoreMemoMutation.isPending}
@@ -1127,8 +1127,8 @@ export const WorkspaceScreen = () => {
onRichEdit={(memo) => void openRichEditor(memo)}
onOpenRevisions={setRevisionMemo}
onRestore={(memo) => restoreMemoMutation.mutate(memo)}
visible
/> : null}
visible={Boolean(selectedMemoId)}
/>
{editorRuntimeWarm ? (
<EditorRuntimePrewarm
@@ -1163,7 +1163,7 @@ export const WorkspaceScreen = () => {
}}
/> : null}
{createOpen ? <CreateMemoModal
<CreateMemoModal
baseUrl={session?.baseUrl ?? ""}
dataScope={dataScope}
defaultNotebookId={defaultMemoNotebookId}
@@ -1177,8 +1177,8 @@ export const WorkspaceScreen = () => {
}}
onQueued={runAutomaticSync}
syncQueueScope={syncQueueScope}
visible
/> : null}
visible={createOpen}
/>
{selectionMoveOpen ? <MoveSelectionModal
bottomOffset={58 + safeAreaInsets.bottom}
@@ -2383,7 +2383,7 @@ const CreateMemoModal = ({
) : null, [baseUrl, draftLoaded, loadEditorResource, resolvedLocale, resolvedTheme]);
return (
<Modal animationType="none" onRequestClose={() => void requestClose()} presentationStyle="fullScreen" visible={visible}>
<Modal animationType="slide" onRequestClose={() => void requestClose()} presentationStyle="fullScreen" visible={visible}>
<SafeAreaView style={styles.createMemoSafeArea}>
<View style={styles.createMemoHeader}>
<Pressable accessibilityLabel="返回" accessibilityRole="button" disabled={createMutation.isPending || imageOperation !== "idle"} onPress={() => void requestClose()} style={styles.createMemoBackButton}>
@@ -5299,9 +5299,23 @@ const MemoCard = memo(function MemoCard({
}) {
const localePreference = useMobileLocalePreference();
const memoTitle = memo.title?.trim() || DEFAULT_MEMO_TITLE;
const pressScale = useSharedValue(1);
const pressAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: pressScale.value }],
}));
return (
<View style={[styles.memoCard, listDensity === "compact" && styles.memoCardCompact, selected && styles.memoCardSelected]}>
<Animated.View
entering={FadeInDown.duration(260).springify().damping(18)}
exiting={FadeOutUp.duration(220)}
layout={LinearTransition.duration(220)}
style={[
styles.memoCard,
listDensity === "compact" && styles.memoCardCompact,
selected && styles.memoCardSelected,
pressAnimatedStyle,
]}
>
{selectionMode ? (
<Pressable
accessibilityLabel={`${selected ? "取消选择" : "选择"} ${memoTitle}`}
@@ -5321,6 +5335,12 @@ const MemoCard = memo(function MemoCard({
delayLongPress={520}
onLongPress={onLongPress}
onPress={onPress}
onPressIn={() => {
pressScale.value = withTiming(0.985, { duration: 100 });
}}
onPressOut={() => {
pressScale.value = withTiming(1, { duration: 160 });
}}
style={[styles.memoCardContent, listDensity === "compact" && styles.memoCardContentCompact, selectionMode && styles.memoCardContentWithSelection]}
>
<View style={styles.memoCardTop}>
@@ -5345,7 +5365,7 @@ const MemoCard = memo(function MemoCard({
))}
</View>
</Pressable>
</View>
</Animated.View>
);
}, (previous, next) =>
previous.memo === next.memo &&
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "edgeever",
"version": "1.5.15",
"version": "1.5.16",
"private": true,
"type": "module",
"workspaces": [