aksos-react-native-epub-reader 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +338 -2
- package/lib/EpubReader.d.ts +63 -0
- package/lib/EpubReader.js +199 -0
- package/lib/EpubReaderView.d.ts +24 -0
- package/lib/EpubReaderView.js +504 -0
- package/lib/assets/page-turn-1.wav +0 -0
- package/lib/assets/page-turn-2.wav +0 -0
- package/lib/assets/page-turn-3.wav +0 -0
- package/lib/assets/page-turn-4.wav +0 -0
- package/lib/engine/html.d.ts +1 -0
- package/lib/engine/html.js +5 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +30 -0
- package/lib/sound.d.ts +1 -0
- package/lib/sound.js +71 -0
- package/lib/themes.d.ts +56 -0
- package/lib/themes.js +166 -0
- package/lib/types.d.ts +133 -0
- package/lib/types.js +2 -0
- package/lib/ui/AppearanceSheet.d.ts +15 -0
- package/lib/ui/AppearanceSheet.js +127 -0
- package/lib/ui/ContentsSheet.d.ts +27 -0
- package/lib/ui/ContentsSheet.js +87 -0
- package/lib/ui/CustomizeSheet.d.ts +16 -0
- package/lib/ui/CustomizeSheet.js +139 -0
- package/lib/ui/Glass.d.ts +15 -0
- package/lib/ui/Glass.js +27 -0
- package/lib/ui/HighlightMenu.d.ts +25 -0
- package/lib/ui/HighlightMenu.js +43 -0
- package/lib/ui/Icons.d.ts +17 -0
- package/lib/ui/Icons.js +193 -0
- package/lib/ui/LineGuide.d.ts +21 -0
- package/lib/ui/LineGuide.js +92 -0
- package/lib/ui/NoteEditor.d.ts +12 -0
- package/lib/ui/NoteEditor.js +61 -0
- package/lib/ui/Scrubber.d.ts +15 -0
- package/lib/ui/Scrubber.js +86 -0
- package/lib/ui/SearchSheet.d.ts +14 -0
- package/lib/ui/SearchSheet.js +67 -0
- package/lib/ui/Sheet.d.ts +19 -0
- package/lib/ui/Sheet.js +38 -0
- package/lib/ui/Slider.d.ts +14 -0
- package/lib/ui/Slider.js +53 -0
- package/package.json +75 -5
- package/src/EpubReader.tsx +256 -0
- package/src/EpubReaderView.tsx +721 -0
- package/src/assets/page-turn-1.wav +0 -0
- package/src/assets/page-turn-2.wav +0 -0
- package/src/assets/page-turn-3.wav +0 -0
- package/src/assets/page-turn-4.wav +0 -0
- package/src/engine/html.ts +2 -0
- package/src/engine/reader.js +1696 -0
- package/src/index.ts +6 -0
- package/src/sound.ts +66 -0
- package/src/themes.ts +188 -0
- package/src/types.ts +138 -0
- package/src/ui/AppearanceSheet.tsx +167 -0
- package/src/ui/ContentsSheet.tsx +144 -0
- package/src/ui/CustomizeSheet.tsx +189 -0
- package/src/ui/Glass.tsx +37 -0
- package/src/ui/HighlightMenu.tsx +64 -0
- package/src/ui/Icons.tsx +246 -0
- package/src/ui/LineGuide.tsx +87 -0
- package/src/ui/NoteEditor.tsx +55 -0
- package/src/ui/Scrubber.tsx +78 -0
- package/src/ui/SearchSheet.tsx +79 -0
- package/src/ui/Sheet.tsx +69 -0
- package/src/ui/Slider.tsx +30 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import { PanResponder, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type DimLevel = 'high' | 'medium' | 'low' | 'none';
|
|
5
|
+
|
|
6
|
+
/** Opacity of the page-coloured veil over everything except the guide line. */
|
|
7
|
+
export const DIM_ALPHA: Record<DimLevel, number> = { high: 0.86, medium: 0.62, low: 0.36, none: 0 };
|
|
8
|
+
|
|
9
|
+
function rgba(hex: string, a: number): string {
|
|
10
|
+
const v = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
11
|
+
return `rgba(${v[0]},${v[1]},${v[2]},${a})`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** page background colour (#rrggbb) */
|
|
16
|
+
bg: string;
|
|
17
|
+
dark: boolean;
|
|
18
|
+
dim: DimLevel;
|
|
19
|
+
/** height of one line of text */
|
|
20
|
+
lineHeight: number;
|
|
21
|
+
/** vertical range the guide may move in */
|
|
22
|
+
minY: number;
|
|
23
|
+
maxY: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Line Guide: dims the page except for one line that can be dragged up and down.
|
|
28
|
+
* The veil ignores touches, so page turns and taps still reach the book.
|
|
29
|
+
*/
|
|
30
|
+
export function LineGuide({ bg, dark, dim, lineHeight, minY, maxY }: Props) {
|
|
31
|
+
const h = Math.round(lineHeight) + 14;
|
|
32
|
+
const [y, setY] = useState(minY + 40);
|
|
33
|
+
const state = useRef({ start: 0, minY, maxY, h });
|
|
34
|
+
state.current.minY = minY;
|
|
35
|
+
state.current.maxY = maxY;
|
|
36
|
+
state.current.h = h;
|
|
37
|
+
const yRef = useRef(y);
|
|
38
|
+
yRef.current = y;
|
|
39
|
+
|
|
40
|
+
const pan = useRef(
|
|
41
|
+
PanResponder.create({
|
|
42
|
+
onStartShouldSetPanResponder: () => true,
|
|
43
|
+
onMoveShouldSetPanResponder: () => true,
|
|
44
|
+
onPanResponderGrant: () => {
|
|
45
|
+
state.current.start = yRef.current;
|
|
46
|
+
},
|
|
47
|
+
onPanResponderMove: (_e, g) => {
|
|
48
|
+
const s = state.current;
|
|
49
|
+
setY(Math.max(s.minY, Math.min(s.maxY - s.h, s.start + g.dy)));
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
).current;
|
|
53
|
+
|
|
54
|
+
const veil = rgba(bg, DIM_ALPHA[dim]);
|
|
55
|
+
return (
|
|
56
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
57
|
+
{dim !== 'none' && (
|
|
58
|
+
<>
|
|
59
|
+
<View pointerEvents="none" style={[styles.veil, { top: 0, height: y, backgroundColor: veil }]} />
|
|
60
|
+
<View pointerEvents="none" style={[styles.veil, { top: y + h, bottom: 0, backgroundColor: veil }]} />
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
<View
|
|
64
|
+
{...pan.panHandlers}
|
|
65
|
+
style={[
|
|
66
|
+
styles.band,
|
|
67
|
+
{
|
|
68
|
+
top: y,
|
|
69
|
+
height: h,
|
|
70
|
+
borderRadius: h / 2,
|
|
71
|
+
borderColor: dark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.22)',
|
|
72
|
+
backgroundColor: dark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)'
|
|
73
|
+
}
|
|
74
|
+
]}
|
|
75
|
+
accessibilityLabel="Line guide"
|
|
76
|
+
/>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const styles = StyleSheet.create({
|
|
82
|
+
veil: { position: 'absolute', left: 0, right: 0 },
|
|
83
|
+
band: {
|
|
84
|
+
position: 'absolute', left: 10, right: 10, borderWidth: StyleSheet.hairlineWidth * 2,
|
|
85
|
+
shadowColor: '#000', shadowOpacity: 0.35, shadowRadius: 8, shadowOffset: { width: 0, height: 3 }, elevation: 6
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { KeyboardAvoidingView, Modal, Platform, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
3
|
+
import type { ChromeColors } from '../themes';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
visible: boolean;
|
|
7
|
+
quote: string;
|
|
8
|
+
initial: string;
|
|
9
|
+
colors: ChromeColors;
|
|
10
|
+
onSave: (note: string) => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NoteEditor({ visible, quote, initial, colors, onSave, onCancel }: Props) {
|
|
15
|
+
const [text, setText] = useState(initial);
|
|
16
|
+
useEffect(() => setText(initial), [initial, visible]);
|
|
17
|
+
return (
|
|
18
|
+
<Modal visible={visible} animationType="slide" transparent onRequestClose={onCancel}>
|
|
19
|
+
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.wrap}>
|
|
20
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={onCancel} />
|
|
21
|
+
<View style={[styles.card, { backgroundColor: colors.sheet }]}>
|
|
22
|
+
<View style={styles.head}>
|
|
23
|
+
<Pressable onPress={onCancel} hitSlop={10}>
|
|
24
|
+
<Text style={{ color: colors.accent, fontSize: 17 }}>Cancel</Text>
|
|
25
|
+
</Pressable>
|
|
26
|
+
<Text style={{ color: colors.text, fontSize: 17, fontWeight: '600' }}>Note</Text>
|
|
27
|
+
<Pressable onPress={() => onSave(text.trim())} hitSlop={10}>
|
|
28
|
+
<Text style={{ color: colors.accent, fontSize: 17, fontWeight: '600' }}>Save</Text>
|
|
29
|
+
</Pressable>
|
|
30
|
+
</View>
|
|
31
|
+
<Text numberOfLines={3} style={[styles.quote, { color: colors.subtext, borderColor: colors.accent }]}>
|
|
32
|
+
{quote}
|
|
33
|
+
</Text>
|
|
34
|
+
<TextInput
|
|
35
|
+
autoFocus
|
|
36
|
+
multiline
|
|
37
|
+
value={text}
|
|
38
|
+
onChangeText={setText}
|
|
39
|
+
placeholder="Add a note"
|
|
40
|
+
placeholderTextColor={colors.subtext}
|
|
41
|
+
style={[styles.input, { color: colors.text, backgroundColor: colors.card }]}
|
|
42
|
+
/>
|
|
43
|
+
</View>
|
|
44
|
+
</KeyboardAvoidingView>
|
|
45
|
+
</Modal>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
wrap: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.35)' },
|
|
51
|
+
card: { borderTopLeftRadius: 14, borderTopRightRadius: 14, padding: 16 },
|
|
52
|
+
head: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 12 },
|
|
53
|
+
quote: { fontSize: 14, borderStartWidth: 3, paddingStart: 10, marginBottom: 12, fontStyle: 'italic' },
|
|
54
|
+
input: { minHeight: 110, maxHeight: 220, borderRadius: 10, padding: 12, fontSize: 16, textAlignVertical: 'top' }
|
|
55
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import { PanResponder, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value: number; // 0..1
|
|
6
|
+
onScrub?: (v: number) => void;
|
|
7
|
+
onComplete: (v: number) => void;
|
|
8
|
+
color: string;
|
|
9
|
+
track: string;
|
|
10
|
+
thumbColor?: string;
|
|
11
|
+
/** hide the thumb for a flat progress bar */
|
|
12
|
+
noThumb?: boolean;
|
|
13
|
+
rtl?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Minimal horizontal slider (no native dependency). */
|
|
17
|
+
export function Scrubber({ value, onScrub, onComplete, color, track, thumbColor, noThumb, rtl }: Props) {
|
|
18
|
+
const [width, setWidth] = useState(1);
|
|
19
|
+
const [drag, setDrag] = useState<number | null>(null);
|
|
20
|
+
const latest = useRef({ width: 1, onScrub, onComplete, rtl });
|
|
21
|
+
latest.current = { width, onScrub, onComplete, rtl };
|
|
22
|
+
const startX = useRef(0);
|
|
23
|
+
|
|
24
|
+
const toValue = (x: number) => {
|
|
25
|
+
const { width: w, rtl: r } = latest.current;
|
|
26
|
+
const v = Math.max(0, Math.min(1, x / w));
|
|
27
|
+
return r ? 1 - v : v;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const pan = useRef(
|
|
31
|
+
PanResponder.create({
|
|
32
|
+
onStartShouldSetPanResponder: () => true,
|
|
33
|
+
onMoveShouldSetPanResponder: () => true,
|
|
34
|
+
onPanResponderTerminationRequest: () => false,
|
|
35
|
+
onPanResponderGrant: (e) => {
|
|
36
|
+
startX.current = e.nativeEvent.locationX;
|
|
37
|
+
const v = toValue(startX.current);
|
|
38
|
+
setDrag(v);
|
|
39
|
+
latest.current.onScrub?.(v);
|
|
40
|
+
},
|
|
41
|
+
onPanResponderMove: (_e, g) => {
|
|
42
|
+
const v = toValue(startX.current + g.dx);
|
|
43
|
+
setDrag(v);
|
|
44
|
+
latest.current.onScrub?.(v);
|
|
45
|
+
},
|
|
46
|
+
onPanResponderRelease: (_e, g) => {
|
|
47
|
+
const v = toValue(startX.current + g.dx);
|
|
48
|
+
setDrag(null);
|
|
49
|
+
latest.current.onComplete(v);
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
).current;
|
|
53
|
+
|
|
54
|
+
const shown = drag ?? value;
|
|
55
|
+
const frac = rtl ? 1 - shown : shown;
|
|
56
|
+
return (
|
|
57
|
+
<View style={styles.hit} onLayout={(e) => setWidth(e.nativeEvent.layout.width)} {...pan.panHandlers}>
|
|
58
|
+
<View style={[styles.track, { backgroundColor: track }]} pointerEvents="none">
|
|
59
|
+
<View style={{ height: 4, backgroundColor: color, width: `${frac * 100}%` }} />
|
|
60
|
+
</View>
|
|
61
|
+
{!noThumb && (
|
|
62
|
+
<View
|
|
63
|
+
pointerEvents="none"
|
|
64
|
+
style={[styles.thumb, { backgroundColor: thumbColor || color, left: Math.max(0, Math.min(width - 28, frac * (width - 28))) }]}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
hit: { height: 40, justifyContent: 'center' },
|
|
73
|
+
track: { height: 4, borderRadius: 2, overflow: 'hidden', flexDirection: 'row' },
|
|
74
|
+
thumb: {
|
|
75
|
+
position: 'absolute', width: 28, height: 28, borderRadius: 14, top: 6,
|
|
76
|
+
shadowColor: '#000', shadowOpacity: 0.3, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 3
|
|
77
|
+
}
|
|
78
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { FlatList, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
3
|
+
import type { ChromeColors } from '../themes';
|
|
4
|
+
import type { SearchResult } from '../types';
|
|
5
|
+
import { Icon } from './Icons';
|
|
6
|
+
import { Sheet } from './Sheet';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
colors: ChromeColors;
|
|
11
|
+
results: SearchResult[];
|
|
12
|
+
searching: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onSearch: (query: string) => void;
|
|
15
|
+
onPick: (r: SearchResult) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SearchSheet({ visible, colors, results, searching, onClose, onSearch, onPick }: Props) {
|
|
19
|
+
const [q, setQ] = useState('');
|
|
20
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
clearTimeout(timer.current);
|
|
24
|
+
timer.current = setTimeout(() => onSearch(q), 350);
|
|
25
|
+
return () => clearTimeout(timer.current);
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
}, [q]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Sheet visible={visible} colors={colors} onClose={onClose} height="85%" title="Search">
|
|
31
|
+
<View style={[styles.field, { backgroundColor: colors.card }]}>
|
|
32
|
+
<Icon name="search" color={colors.subtext} size={20} />
|
|
33
|
+
<TextInput
|
|
34
|
+
value={q}
|
|
35
|
+
onChangeText={setQ}
|
|
36
|
+
placeholder="Search in book"
|
|
37
|
+
placeholderTextColor={colors.subtext}
|
|
38
|
+
style={[styles.input, { color: colors.text }]}
|
|
39
|
+
autoCorrect={false}
|
|
40
|
+
returnKeyType="search"
|
|
41
|
+
onSubmitEditing={() => onSearch(q)}
|
|
42
|
+
clearButtonMode="while-editing"
|
|
43
|
+
/>
|
|
44
|
+
</View>
|
|
45
|
+
<FlatList
|
|
46
|
+
data={results}
|
|
47
|
+
keyboardShouldPersistTaps="handled"
|
|
48
|
+
keyExtractor={(_, i) => String(i)}
|
|
49
|
+
ListHeaderComponent={
|
|
50
|
+
q.trim().length >= 2 ? (
|
|
51
|
+
<Text style={[styles.count, { color: colors.subtext }]}>
|
|
52
|
+
{searching ? 'Searching...' : `${results.length}${results.length >= 500 ? '+' : ''} results`}
|
|
53
|
+
</Text>
|
|
54
|
+
) : null
|
|
55
|
+
}
|
|
56
|
+
renderItem={({ item }) => (
|
|
57
|
+
<Pressable onPress={() => onPick(item)} style={[styles.row, { borderColor: colors.separator }]}>
|
|
58
|
+
<Text style={[styles.chapter, { color: colors.accent }]} numberOfLines={1}>
|
|
59
|
+
{item.chapter}
|
|
60
|
+
</Text>
|
|
61
|
+
<Text style={{ color: colors.text, fontSize: 15, lineHeight: 21 }} numberOfLines={3}>
|
|
62
|
+
{item.before}
|
|
63
|
+
<Text style={{ fontWeight: '700', backgroundColor: 'rgba(255,214,10,0.45)' }}>{item.match}</Text>
|
|
64
|
+
{item.after}
|
|
65
|
+
</Text>
|
|
66
|
+
</Pressable>
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
</Sheet>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
field: { flexDirection: 'row', alignItems: 'center', marginHorizontal: 16, marginBottom: 8, paddingHorizontal: 10, borderRadius: 10, height: 38 },
|
|
75
|
+
input: { flex: 1, marginStart: 6, fontSize: 16, paddingVertical: 0 },
|
|
76
|
+
count: { paddingHorizontal: 20, paddingVertical: 8, fontSize: 13 },
|
|
77
|
+
row: { paddingVertical: 12, paddingHorizontal: 20, borderBottomWidth: StyleSheet.hairlineWidth },
|
|
78
|
+
chapter: { fontSize: 12, fontWeight: '600', marginBottom: 3 }
|
|
79
|
+
});
|
package/src/ui/Sheet.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import type { ViewStyle } from 'react-native';
|
|
4
|
+
import type { ChromeColors } from '../themes';
|
|
5
|
+
import { Glass } from './Glass';
|
|
6
|
+
import { Icon } from './Icons';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
colors: ChromeColors;
|
|
13
|
+
height?: ViewStyle['height'];
|
|
14
|
+
/** 'x' puts a glass close button on the left (Themes, Search); 'check' puts a solid check on the right (Contents, Customize) */
|
|
15
|
+
closeIcon?: 'x' | 'check';
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
/** rendered above the content, e.g. popup menus */
|
|
19
|
+
overlay?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Rounded frosted-glass sheet: the book stays faintly visible behind it. */
|
|
23
|
+
export function Sheet({ visible, title, subtitle, colors, height = '70%', closeIcon = 'x', onClose, children, overlay }: Props) {
|
|
24
|
+
const button =
|
|
25
|
+
closeIcon === 'check' ? (
|
|
26
|
+
<Pressable onPress={onClose} hitSlop={10} style={[styles.circle, { backgroundColor: colors.dark ? '#ffffff' : '#000000' }]} accessibilityRole="button" accessibilityLabel="Done">
|
|
27
|
+
<Icon name="check" color={colors.dark ? '#000000' : '#ffffff'} size={24} strokeWidth={2.2} />
|
|
28
|
+
</Pressable>
|
|
29
|
+
) : (
|
|
30
|
+
<Pressable onPress={onClose} hitSlop={10} accessibilityRole="button" accessibilityLabel="Close">
|
|
31
|
+
<Glass dark={colors.dark} background={colors.glass} border={colors.glassBorder} style={styles.circle}>
|
|
32
|
+
<Icon name="close" color={colors.text} size={22} />
|
|
33
|
+
</Glass>
|
|
34
|
+
</Pressable>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose} statusBarTranslucent>
|
|
39
|
+
<Pressable style={styles.backdrop} onPress={onClose} />
|
|
40
|
+
<Glass dark={colors.dark} background={colors.sheetGlass} border={colors.glassBorder} intensity={70} style={[styles.sheet, { height }]}>
|
|
41
|
+
<View style={styles.header}>
|
|
42
|
+
{closeIcon === 'x' && button}
|
|
43
|
+
<View style={styles.titleBox} pointerEvents="none">
|
|
44
|
+
{!!title && (
|
|
45
|
+
<Text style={[styles.title, { color: colors.text }]} numberOfLines={1}>
|
|
46
|
+
{title}
|
|
47
|
+
</Text>
|
|
48
|
+
)}
|
|
49
|
+
{!!subtitle && <Text style={[styles.subtitle, { color: colors.text }]}>{subtitle}</Text>}
|
|
50
|
+
</View>
|
|
51
|
+
<View style={{ flex: 1 }} />
|
|
52
|
+
{closeIcon === 'check' && button}
|
|
53
|
+
</View>
|
|
54
|
+
{children}
|
|
55
|
+
{overlay}
|
|
56
|
+
</Glass>
|
|
57
|
+
</Modal>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.18)' },
|
|
63
|
+
sheet: { borderTopLeftRadius: 34, borderTopRightRadius: 34, paddingTop: 12 },
|
|
64
|
+
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, minHeight: 52, marginBottom: 6 },
|
|
65
|
+
circle: { width: 44, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center' },
|
|
66
|
+
titleBox: { position: 'absolute', left: 70, right: 70, alignItems: 'center' },
|
|
67
|
+
title: { fontSize: 17, fontWeight: '600' },
|
|
68
|
+
subtitle: { fontSize: 17, marginTop: 14 }
|
|
69
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { Scrubber } from './Scrubber';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value: number;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
step: number;
|
|
9
|
+
onChange: (v: number) => void;
|
|
10
|
+
color: string;
|
|
11
|
+
track: string;
|
|
12
|
+
thumbColor?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const round = (v: number, step: number) => Math.round(v / step) * step;
|
|
16
|
+
|
|
17
|
+
/** Value slider on top of Scrubber; emits only when the stepped value changes. */
|
|
18
|
+
export function Slider({ value, min, max, step, onChange, color, track, thumbColor }: Props) {
|
|
19
|
+
const last = useRef(value);
|
|
20
|
+
const toValue = (f: number) => +(round(min + f * (max - min), step)).toFixed(4);
|
|
21
|
+
const emit = (f: number) => {
|
|
22
|
+
const v = toValue(f);
|
|
23
|
+
if (v !== last.current) {
|
|
24
|
+
last.current = v;
|
|
25
|
+
onChange(v);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
last.current = value;
|
|
29
|
+
return <Scrubber value={(value - min) / (max - min)} color={color} track={track} thumbColor={thumbColor} onScrub={emit} onComplete={emit} />;
|
|
30
|
+
}
|