Skip to content

Commit 2fe5e11

Browse files
author
bealqiu
committed
feat(app-expo): 添加书籍元数据提取和导入优化
1 parent d39662c commit 2fe5e11

16 files changed

Lines changed: 704 additions & 87 deletions

File tree

packages/app-expo/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"expo-clipboard": "~55.0.8",
2121
"expo-constants": "~55.0.7",
2222
"expo-crypto": "~55.0.9",
23+
"expo-document-picker": "~55.0.8",
2324
"expo-file-system": "~55.0.10",
2425
"expo-font": "~55.0.4",
2526
"expo-linking": "~55.0.7",
@@ -30,6 +31,7 @@
3031
"expo-sqlite": "~55.0.10",
3132
"expo-status-bar": "~55.0.4",
3233
"i18next": "^25.8.13",
34+
"pako": "^2.1.0",
3335
"react": "^19.0.0",
3436
"react-i18next": "^16.5.4",
3537
"react-native": "~0.83.2",
@@ -44,6 +46,7 @@
4446
},
4547
"devDependencies": {
4648
"@babel/core": "^7.25.0",
49+
"@types/pako": "^2.0.4",
4750
"@types/react": "^19.0.0",
4851
"babel-plugin-module-resolver": "^5.0.2",
4952
"typescript": "^5.8.0"

packages/app-expo/src/components/library/BookCard.tsx

Lines changed: 168 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,57 @@ export const BookCard = memo(function BookCard({
9797
{/* Cover — 28:41 aspect ratio */}
9898
<View style={s.coverWrap}>
9999
{resolvedCoverUrl && !imageError ? (
100-
<Image
101-
source={{ uri: resolvedCoverUrl }}
102-
style={s.coverImage}
103-
resizeMode="cover"
104-
onError={() => setImageError(true)}
105-
/>
100+
<>
101+
<Image
102+
source={{ uri: resolvedCoverUrl }}
103+
style={s.coverImage}
104+
resizeMode="cover"
105+
onError={() => setImageError(true)}
106+
/>
107+
{/* Book spine crease overlay — matches desktop .book-spine */}
108+
<View style={s.spineOverlay} pointerEvents="none">
109+
{/* Left edge dark line */}
110+
<View style={s.spineStrip1} />
111+
{/* Spine shadow dip */}
112+
<View style={s.spineStrip2} />
113+
{/* Highlight reflection */}
114+
<View style={s.spineStrip3} />
115+
{/* Transition bright */}
116+
<View style={s.spineStrip4} />
117+
{/* Crease dark */}
118+
<View style={s.spineStrip5} />
119+
{/* Deep fold */}
120+
<View style={s.spineStrip6} />
121+
{/* Subtle bright transition */}
122+
<View style={s.spineStrip7} />
123+
{/* Right edge subtle shadow */}
124+
<View style={s.spineEdgeRight} />
125+
</View>
126+
{/* Top highlight */}
127+
<View style={s.spineTopHighlight} pointerEvents="none" />
128+
{/* Bottom shadow */}
129+
<View style={s.spineBottomShadow} pointerEvents="none" />
130+
</>
106131
) : (
107132
<View style={s.fallbackCover}>
108-
<View style={s.fallbackTitleWrap}>
109-
<Text style={s.fallbackTitle} numberOfLines={3}>
110-
{book.meta.title}
111-
</Text>
112-
</View>
113-
<View style={s.fallbackDivider} />
114-
{book.meta.author ? (
115-
<View style={s.fallbackAuthorWrap}>
116-
<Text style={s.fallbackAuthor} numberOfLines={1}>
117-
{book.meta.author}
133+
{/* Simulate gradient: stone-100 top half, stone-200 bottom half */}
134+
<View style={s.fallbackGradientTop} />
135+
<View style={s.fallbackGradientBottom} />
136+
<View style={s.fallbackContentOverlay}>
137+
<View style={s.fallbackTitleWrap}>
138+
<Text style={s.fallbackTitle} numberOfLines={3}>
139+
{book.meta.title}
118140
</Text>
119141
</View>
120-
) : null}
142+
<View style={s.fallbackDivider} />
143+
{book.meta.author ? (
144+
<View style={s.fallbackAuthorWrap}>
145+
<Text style={s.fallbackAuthor} numberOfLines={1}>
146+
{book.meta.author}
147+
</Text>
148+
</View>
149+
) : null}
150+
</View>
121151
</View>
122152
)}
123153

@@ -261,19 +291,117 @@ const makeStyles = (colors: ThemeColors) => StyleSheet.create({
261291
borderRadius: radius.sm,
262292
overflow: "hidden",
263293
position: "relative",
294+
// Book cover shadow
295+
shadowColor: "#000",
296+
shadowOffset: { width: 0, height: 2 },
297+
shadowOpacity: 0.15,
298+
shadowRadius: 4,
299+
elevation: 3,
264300
},
265301
coverImage: {
266302
width: "100%",
267303
height: "100%",
268304
borderRadius: radius.sm,
269305
},
306+
// Book spine crease effect — simulates desktop .book-spine linear-gradient overlay
307+
spineOverlay: {
308+
position: "absolute",
309+
top: 0,
310+
left: 0,
311+
bottom: 0,
312+
width: "8%",
313+
flexDirection: "row",
314+
zIndex: 2,
315+
},
316+
spineStrip1: {
317+
width: "6%",
318+
height: "100%",
319+
backgroundColor: "rgba(0,0,0,0.10)",
320+
},
321+
spineStrip2: {
322+
width: "8%",
323+
height: "100%",
324+
backgroundColor: "rgba(20,20,20,0.20)",
325+
},
326+
spineStrip3: {
327+
width: "5%",
328+
height: "100%",
329+
backgroundColor: "rgba(240,240,240,0.40)",
330+
},
331+
spineStrip4: {
332+
width: "18%",
333+
height: "100%",
334+
backgroundColor: "rgba(215,215,215,0.35)",
335+
},
336+
spineStrip5: {
337+
width: "12%",
338+
height: "100%",
339+
backgroundColor: "rgba(150,150,150,0.25)",
340+
},
341+
spineStrip6: {
342+
width: "20%",
343+
height: "100%",
344+
backgroundColor: "rgba(100,100,100,0.18)",
345+
},
346+
spineStrip7: {
347+
width: "31%",
348+
height: "100%",
349+
backgroundColor: "rgba(175,175,175,0.12)",
350+
},
351+
spineEdgeRight: {
352+
position: "absolute",
353+
top: 0,
354+
right: -coverWidth * 0.92,
355+
bottom: 0,
356+
width: coverWidth * 0.02,
357+
backgroundColor: "rgba(30,30,30,0.12)",
358+
},
359+
spineTopHighlight: {
360+
position: "absolute",
361+
top: 0,
362+
left: 0,
363+
right: 0,
364+
height: "3%",
365+
backgroundColor: "rgba(240,240,240,0.15)",
366+
zIndex: 3,
367+
},
368+
spineBottomShadow: {
369+
position: "absolute",
370+
bottom: 0,
371+
left: 0,
372+
right: 0,
373+
height: "8%",
374+
backgroundColor: "rgba(15,15,15,0.15)",
375+
zIndex: 3,
376+
},
270377
fallbackCover: {
271378
flex: 1,
272-
padding: 8,
379+
borderRadius: radius.sm,
380+
overflow: "hidden",
381+
position: "relative",
382+
},
383+
fallbackGradientTop: {
384+
position: "absolute",
385+
top: 0,
386+
left: 0,
387+
right: 0,
388+
height: "50%",
389+
backgroundColor: colors.stone100,
390+
},
391+
fallbackGradientBottom: {
392+
position: "absolute",
393+
bottom: 0,
394+
left: 0,
395+
right: 0,
396+
height: "50%",
397+
backgroundColor: colors.stone200,
398+
},
399+
fallbackContentOverlay: {
400+
flex: 1,
401+
padding: 10,
273402
alignItems: "center",
274403
justifyContent: "center",
275-
backgroundColor: colors.stone200,
276-
borderRadius: radius.sm,
404+
zIndex: 1,
277405
},
278406
fallbackTitleWrap: {
279407
flex: 1,
@@ -284,14 +412,15 @@ const makeStyles = (colors: ThemeColors) => StyleSheet.create({
284412
textAlign: "center",
285413
fontSize: fontSize.sm,
286414
fontWeight: fontWeight.medium,
415+
fontFamily: "serif",
287416
color: colors.stone500,
288-
lineHeight: 16,
417+
lineHeight: 18,
289418
},
290419
fallbackDivider: {
291-
width: 24,
420+
width: 32,
292421
height: 1,
293-
backgroundColor: "rgba(168,162,158,0.4)",
294-
marginVertical: 4,
422+
backgroundColor: `${colors.stone300}99`,
423+
marginVertical: 6,
295424
},
296425
fallbackAuthorWrap: {
297426
height: "25%",
@@ -300,7 +429,8 @@ const makeStyles = (colors: ThemeColors) => StyleSheet.create({
300429
},
301430
fallbackAuthor: {
302431
textAlign: "center",
303-
fontSize: 8,
432+
fontSize: 10,
433+
fontFamily: "serif",
304434
color: colors.stone400,
305435
},
306436
progressBarBg: {
@@ -347,48 +477,48 @@ const makeStyles = (colors: ThemeColors) => StyleSheet.create({
347477
paddingVertical: 2,
348478
},
349479
vecBadgeText: { fontSize: 7, fontWeight: fontWeight.medium, color: "#fff" },
350-
infoWrap: { paddingTop: 6 },
480+
infoWrap: { paddingTop: 6, paddingHorizontal: 1 },
351481
bookTitle: {
352482
fontSize: 11,
353483
fontWeight: fontWeight.semibold,
354484
color: colors.foreground,
355485
lineHeight: 14,
356486
},
357-
tagsRow: { flexDirection: "row", flexWrap: "wrap", gap: 2, marginTop: 2 },
487+
tagsRow: { flexDirection: "row", flexWrap: "wrap", gap: 3, marginTop: 3 },
358488
tagBadge: {
359-
backgroundColor: "rgba(245,245,244,0.1)",
489+
backgroundColor: `${colors.muted}`,
360490
borderRadius: radius.full,
361-
paddingHorizontal: 5,
491+
paddingHorizontal: 6,
362492
paddingVertical: 1,
363493
},
364494
tagText: { fontSize: 8, color: colors.mutedForeground },
365495
tagBadgeUncategorized: {
366-
backgroundColor: "rgba(245,245,244,0.05)",
496+
backgroundColor: `${colors.muted}80`,
367497
borderRadius: radius.full,
368-
paddingHorizontal: 5,
498+
paddingHorizontal: 6,
369499
paddingVertical: 1,
370500
},
371-
tagTextUncategorized: { fontSize: 8, color: "rgba(124,124,130,0.6)" },
372-
tagOverflow: { fontSize: 8, color: "rgba(124,124,130,0.6)" },
501+
tagTextUncategorized: { fontSize: 8, color: `${colors.mutedForeground}99` },
502+
tagOverflow: { fontSize: 8, color: `${colors.mutedForeground}99`, alignSelf: "center" },
373503
statusRow: {
374504
flexDirection: "row",
375505
alignItems: "center",
376506
justifyContent: "space-between",
377-
marginTop: 2,
378-
minHeight: 12,
507+
marginTop: 3,
508+
minHeight: 14,
379509
},
380510
progressText: { fontSize: 9, color: colors.mutedForeground, fontVariant: ["tabular-nums"] },
381511
completeText: { fontSize: 9, fontWeight: fontWeight.medium, color: "#16a34a" },
382512
newBadge: {
383-
backgroundColor: "rgba(224,224,230,0.08)",
513+
backgroundColor: `${colors.primary}14`,
384514
borderRadius: radius.full,
385-
paddingHorizontal: 4,
515+
paddingHorizontal: 5,
386516
paddingVertical: 1,
387517
},
388518
newText: { fontSize: 8, fontWeight: fontWeight.medium, color: colors.primary },
389519
formatText: {
390520
fontSize: 8,
391-
color: "rgba(124,124,130,0.4)",
521+
color: `${colors.mutedForeground}99`,
392522
textTransform: "uppercase",
393523
letterSpacing: 0.5,
394524
},

0 commit comments

Comments
 (0)