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,144 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Alert, FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { HIGHLIGHT_COLORS } from '../themes';
|
|
4
|
+
import type { ChromeColors } from '../themes';
|
|
5
|
+
import type { Bookmark, Highlight, TocItem } from '../types';
|
|
6
|
+
import { Sheet } from './Sheet';
|
|
7
|
+
|
|
8
|
+
export type ContentsTab = 'contents' | 'bookmarks' | 'notes';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
visible: boolean;
|
|
12
|
+
colors: ChromeColors;
|
|
13
|
+
title: string;
|
|
14
|
+
/** e.g. "Page 100 of 219" */
|
|
15
|
+
subtitle: string;
|
|
16
|
+
toc: TocItem[];
|
|
17
|
+
currentSpine: number;
|
|
18
|
+
/** estimated page number in the whole book for a spine item + character offset */
|
|
19
|
+
pageOf: (spine: number, offset: number) => number;
|
|
20
|
+
bookmarks: Bookmark[];
|
|
21
|
+
highlights: Highlight[];
|
|
22
|
+
initialTab?: ContentsTab;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onGoToc: (item: TocItem) => void;
|
|
25
|
+
onGoBookmark: (b: Bookmark) => void;
|
|
26
|
+
onGoHighlight: (h: Highlight) => void;
|
|
27
|
+
onDeleteBookmark: (id: string) => void;
|
|
28
|
+
onDeleteHighlight: (id: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TABS: { id: ContentsTab; label: string }[] = [
|
|
32
|
+
{ id: 'contents', label: 'Chapters' },
|
|
33
|
+
{ id: 'bookmarks', label: 'Bookmarks' },
|
|
34
|
+
{ id: 'notes', label: 'Highlights' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Contents sheet: Chapters | Bookmarks | Highlights, each row with its page number. */
|
|
38
|
+
export function ContentsSheet(p: Props) {
|
|
39
|
+
const { colors } = p;
|
|
40
|
+
const [tab, setTab] = useState<ContentsTab>(p.initialTab || 'contents');
|
|
41
|
+
const confirmDelete = (what: string, fn: () => void) =>
|
|
42
|
+
Alert.alert(`Delete ${what}?`, undefined, [
|
|
43
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
44
|
+
{ text: 'Delete', style: 'destructive', onPress: fn }
|
|
45
|
+
]);
|
|
46
|
+
const empty = (text: string) => <Text style={[styles.empty, { color: colors.subtext }]}>{text}</Text>;
|
|
47
|
+
const rowBorder = { borderColor: colors.separator };
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Sheet visible={p.visible} colors={colors} onClose={p.onClose} closeIcon="check" title={p.title} subtitle={p.subtitle} height="90%">
|
|
51
|
+
<View style={[styles.segment, { backgroundColor: colors.card }]}>
|
|
52
|
+
{TABS.map((t) => (
|
|
53
|
+
<Pressable
|
|
54
|
+
key={t.id}
|
|
55
|
+
onPress={() => setTab(t.id)}
|
|
56
|
+
style={[styles.segItem, tab === t.id && { backgroundColor: colors.dark ? 'rgba(255,255,255,0.22)' : '#ffffff' }]}
|
|
57
|
+
accessibilityRole="tab"
|
|
58
|
+
accessibilityState={{ selected: tab === t.id }}
|
|
59
|
+
>
|
|
60
|
+
<Text style={{ color: colors.text, fontWeight: tab === t.id ? '600' : '400', fontSize: 16 }}>{t.label}</Text>
|
|
61
|
+
</Pressable>
|
|
62
|
+
))}
|
|
63
|
+
</View>
|
|
64
|
+
|
|
65
|
+
{tab === 'contents' && (
|
|
66
|
+
<FlatList
|
|
67
|
+
data={p.toc}
|
|
68
|
+
keyExtractor={(_, i) => String(i)}
|
|
69
|
+
ListEmptyComponent={empty('No table of contents')}
|
|
70
|
+
renderItem={({ item }) => {
|
|
71
|
+
const active = item.spine === p.currentSpine && !item.fragment;
|
|
72
|
+
return (
|
|
73
|
+
<Pressable onPress={() => p.onGoToc(item)} style={[styles.row, rowBorder, { paddingStart: 20 + item.level * 18 }]}>
|
|
74
|
+
<Text style={[styles.rowText, { color: active ? colors.accent : colors.text, flex: 1 }]} numberOfLines={2}>
|
|
75
|
+
{item.label}
|
|
76
|
+
</Text>
|
|
77
|
+
<Text style={[styles.page, { color: colors.subtext }]}>{p.pageOf(item.spine, 0)}</Text>
|
|
78
|
+
</Pressable>
|
|
79
|
+
);
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{tab === 'bookmarks' && (
|
|
85
|
+
<FlatList
|
|
86
|
+
data={p.bookmarks.slice().sort((a, b) => a.progress - b.progress)}
|
|
87
|
+
keyExtractor={(b) => b.id}
|
|
88
|
+
ListEmptyComponent={empty('No bookmarks yet. Tap the bookmark button while reading.')}
|
|
89
|
+
renderItem={({ item }) => (
|
|
90
|
+
<Pressable
|
|
91
|
+
onPress={() => p.onGoBookmark(item)}
|
|
92
|
+
onLongPress={() => confirmDelete('bookmark', () => p.onDeleteBookmark(item.id))}
|
|
93
|
+
style={[styles.row, rowBorder]}
|
|
94
|
+
>
|
|
95
|
+
<View style={{ flex: 1 }}>
|
|
96
|
+
<Text style={[styles.meta, { color: colors.accent }]}>{item.chapter}</Text>
|
|
97
|
+
<Text style={[styles.rowText, { color: colors.text }]} numberOfLines={2}>
|
|
98
|
+
{item.excerpt}
|
|
99
|
+
</Text>
|
|
100
|
+
</View>
|
|
101
|
+
<Text style={[styles.page, { color: colors.subtext }]}>{p.pageOf(item.spine, item.offset)}</Text>
|
|
102
|
+
</Pressable>
|
|
103
|
+
)}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{tab === 'notes' && (
|
|
108
|
+
<FlatList
|
|
109
|
+
data={p.highlights.slice().sort((a, b) => a.spine - b.spine || a.start - b.start)}
|
|
110
|
+
keyExtractor={(h) => h.id}
|
|
111
|
+
ListEmptyComponent={empty('No highlights or notes yet. Select text to add one.')}
|
|
112
|
+
renderItem={({ item }) => (
|
|
113
|
+
<Pressable
|
|
114
|
+
onPress={() => p.onGoHighlight(item)}
|
|
115
|
+
onLongPress={() => confirmDelete('highlight', () => p.onDeleteHighlight(item.id))}
|
|
116
|
+
style={[styles.row, rowBorder]}
|
|
117
|
+
>
|
|
118
|
+
<View style={[styles.bar, { backgroundColor: HIGHLIGHT_COLORS[item.color] }]} />
|
|
119
|
+
<View style={{ flex: 1 }}>
|
|
120
|
+
{!!item.note && <Text style={[styles.rowText, { color: colors.text, fontWeight: '600' }]}>{item.note}</Text>}
|
|
121
|
+
<Text style={[styles.rowText, { color: item.note ? colors.subtext : colors.text }]} numberOfLines={3}>
|
|
122
|
+
{item.text}
|
|
123
|
+
</Text>
|
|
124
|
+
{!!item.chapter && <Text style={[styles.meta, { color: colors.subtext }]}>{item.chapter}</Text>}
|
|
125
|
+
</View>
|
|
126
|
+
<Text style={[styles.page, { color: colors.subtext }]}>{p.pageOf(item.spine, item.start)}</Text>
|
|
127
|
+
</Pressable>
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</Sheet>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const styles = StyleSheet.create({
|
|
136
|
+
segment: { flexDirection: 'row', marginHorizontal: 16, marginTop: 34, marginBottom: 6, borderRadius: 18, padding: 3 },
|
|
137
|
+
segItem: { flex: 1, alignItems: 'center', paddingVertical: 8, borderRadius: 15 },
|
|
138
|
+
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 17, paddingHorizontal: 20, marginHorizontal: 0, borderBottomWidth: StyleSheet.hairlineWidth },
|
|
139
|
+
rowText: { fontSize: 18, lineHeight: 24 },
|
|
140
|
+
page: { fontSize: 18, marginStart: 12, minWidth: 34, textAlign: 'right' },
|
|
141
|
+
meta: { fontSize: 13, marginBottom: 2, fontWeight: '600' },
|
|
142
|
+
bar: { width: 3, alignSelf: 'stretch', borderRadius: 2, marginEnd: 12 },
|
|
143
|
+
empty: { textAlign: 'center', padding: 32, fontSize: 16 }
|
|
144
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
|
|
3
|
+
import { FONTS, getFont, serifFont } from '../themes';
|
|
4
|
+
import type { ChromeColors } from '../themes';
|
|
5
|
+
import type { ColumnsSetting, ReaderSettings, ThemeColors } from '../types';
|
|
6
|
+
import { Icon } from './Icons';
|
|
7
|
+
import type { IconName } from './Icons';
|
|
8
|
+
import { Sheet } from './Sheet';
|
|
9
|
+
import { Slider } from './Slider';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
visible: boolean;
|
|
13
|
+
colors: ChromeColors;
|
|
14
|
+
page: ThemeColors;
|
|
15
|
+
settings: ReaderSettings;
|
|
16
|
+
sample: string;
|
|
17
|
+
onChange: (patch: Partial<ReaderSettings>) => void;
|
|
18
|
+
onReset: () => void;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COLUMN_LABEL: Record<string, string> = { auto: 'Auto Set', '1': 'One', '2': 'Two' };
|
|
23
|
+
const GREEN = { true: '#34c759' };
|
|
24
|
+
|
|
25
|
+
function Toggle(props: React.ComponentProps<typeof Switch>) {
|
|
26
|
+
return (
|
|
27
|
+
<View style={styles.toggleBox}>
|
|
28
|
+
<Switch {...props} />
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** "Customize Theme" sheet: font, bold, spacing, margins, columns, justify, sound. */
|
|
34
|
+
export function CustomizeSheet({ visible, colors, page, settings: s, sample, onChange, onReset, onClose }: Props) {
|
|
35
|
+
const [fonts, setFonts] = useState(false);
|
|
36
|
+
const [columnsMenu, setColumnsMenu] = useState(false);
|
|
37
|
+
const off = !s.customize;
|
|
38
|
+
const font = getFont(s.fontId);
|
|
39
|
+
const previewFont = s.fontId === 'system' ? undefined : serifFont;
|
|
40
|
+
const border = { borderTopWidth: StyleSheet.hairlineWidth, borderColor: colors.separator };
|
|
41
|
+
|
|
42
|
+
const sliderRow = (label: string, icon: IconName, value: number, min: number, max: number, step: number, show: string, key: keyof ReaderSettings) => (
|
|
43
|
+
<View style={[styles.sliderBlock, border, off && styles.dim]} pointerEvents={off ? 'none' : 'auto'}>
|
|
44
|
+
<Text style={[styles.caption, { color: colors.subtext }]}>{label}</Text>
|
|
45
|
+
<View style={styles.sliderRow}>
|
|
46
|
+
<Icon name={icon} color={colors.text} size={24} />
|
|
47
|
+
<View style={{ flex: 1, marginHorizontal: 12 }}>
|
|
48
|
+
<Slider value={value} min={min} max={max} step={step} onChange={(v) => onChange({ [key]: v } as Partial<ReaderSettings>)} color={colors.text} track={colors.separator} thumbColor="#ffffff" />
|
|
49
|
+
</View>
|
|
50
|
+
<Text style={[styles.value, { color: colors.subtext }]}>{show}</Text>
|
|
51
|
+
</View>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const row = (icon: IconName, label: string, right: React.ReactNode, onPress?: () => void, top = true, iconActive = false) => {
|
|
56
|
+
const content = (
|
|
57
|
+
<>
|
|
58
|
+
<View style={styles.rowIcon}>
|
|
59
|
+
<Icon name={icon} color={colors.text} size={22} active={iconActive} />
|
|
60
|
+
</View>
|
|
61
|
+
<Text style={[styles.label, { color: colors.text }]}>{label}</Text>
|
|
62
|
+
{right}
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
return onPress ? (
|
|
66
|
+
<Pressable style={[styles.line, top && border]} onPress={onPress}>
|
|
67
|
+
{content}
|
|
68
|
+
</Pressable>
|
|
69
|
+
) : (
|
|
70
|
+
<View style={[styles.line, top && border]}>{content}</View>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const close = () => {
|
|
75
|
+
setFonts(false);
|
|
76
|
+
setColumnsMenu(false);
|
|
77
|
+
onClose();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Sheet visible={visible} colors={colors} onClose={close} closeIcon="check" title="Customize Theme" height="92%">
|
|
82
|
+
<View style={[styles.preview, { backgroundColor: page.bg }]}>
|
|
83
|
+
<Text style={[styles.aa, { color: page.fg, fontFamily: previewFont }]}>Aa</Text>
|
|
84
|
+
<Text
|
|
85
|
+
numberOfLines={4}
|
|
86
|
+
style={{
|
|
87
|
+
color: page.fg, fontFamily: previewFont, fontSize: Math.min(s.fontSize, 22),
|
|
88
|
+
lineHeight: Math.min(s.fontSize, 22) * (s.customize ? s.lineHeight : 1.5),
|
|
89
|
+
fontWeight: s.bold ? '700' : '400', textAlign: s.justify ? 'justify' : 'auto'
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{sample}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
|
|
96
|
+
<ScrollView contentContainerStyle={styles.pad}>
|
|
97
|
+
<View style={[styles.group, { backgroundColor: colors.card }]}>
|
|
98
|
+
{row(
|
|
99
|
+
'textSize',
|
|
100
|
+
'Font',
|
|
101
|
+
<View style={styles.value2}>
|
|
102
|
+
<Text style={{ color: colors.subtext, fontSize: 17 }}>{font.name}</Text>
|
|
103
|
+
<Icon name={fonts ? 'chevronUp' : 'chevronRight'} color={colors.subtext} size={18} />
|
|
104
|
+
</View>,
|
|
105
|
+
() => setFonts((v) => !v),
|
|
106
|
+
false
|
|
107
|
+
)}
|
|
108
|
+
{fonts &&
|
|
109
|
+
FONTS.map((f) => (
|
|
110
|
+
<Pressable
|
|
111
|
+
key={f.id}
|
|
112
|
+
onPress={() => {
|
|
113
|
+
onChange({ fontId: f.id });
|
|
114
|
+
setFonts(false);
|
|
115
|
+
}}
|
|
116
|
+
style={[styles.line, border]}
|
|
117
|
+
>
|
|
118
|
+
<View style={styles.check}>{s.fontId === f.id && <Icon name="check" color={colors.text} size={20} />}</View>
|
|
119
|
+
<Text style={[styles.label, { color: colors.text }]}>{f.name}</Text>
|
|
120
|
+
</Pressable>
|
|
121
|
+
))}
|
|
122
|
+
{row('bold', 'Bold Text', <Toggle value={s.bold} onValueChange={(v) => onChange({ bold: v })} trackColor={GREEN} />)}
|
|
123
|
+
</View>
|
|
124
|
+
|
|
125
|
+
<Text style={[styles.heading, { color: colors.text }]}>Accessibility & Layout Options</Text>
|
|
126
|
+
<View style={[styles.group, { backgroundColor: colors.card }]}>
|
|
127
|
+
{row('sliders', 'Customize', <Toggle value={s.customize} onValueChange={(v) => onChange({ customize: v })} trackColor={GREEN} />, undefined, false)}
|
|
128
|
+
{sliderRow('LINE SPACING', 'lineSpacing', s.lineHeight, 1.1, 2.2, 0.05, s.lineHeight.toFixed(2), 'lineHeight')}
|
|
129
|
+
{sliderRow('CHARACTER SPACING', 'charSpacing', s.letterSpacing, -50, 100, 5, `${s.letterSpacing}%`, 'letterSpacing')}
|
|
130
|
+
{sliderRow('WORD SPACING', 'wordSpacing', s.wordSpacing, -50, 100, 5, `${s.wordSpacing}%`, 'wordSpacing')}
|
|
131
|
+
{sliderRow('MARGINS', 'margins', s.margins, -50, 100, 5, `${s.margins}%`, 'margins')}
|
|
132
|
+
</View>
|
|
133
|
+
|
|
134
|
+
<View style={[styles.group, { backgroundColor: colors.card, marginTop: 16 }]}>
|
|
135
|
+
{row(
|
|
136
|
+
'columns',
|
|
137
|
+
'Columns',
|
|
138
|
+
<View style={styles.value2}>
|
|
139
|
+
<Text style={{ color: colors.subtext, fontSize: 17 }}>{COLUMN_LABEL[String(s.columns)]}</Text>
|
|
140
|
+
<Icon name={columnsMenu ? 'chevronUp' : 'chevronDown'} color={colors.subtext} size={18} />
|
|
141
|
+
</View>,
|
|
142
|
+
() => setColumnsMenu((v) => !v),
|
|
143
|
+
false
|
|
144
|
+
)}
|
|
145
|
+
{columnsMenu &&
|
|
146
|
+
(['auto', 1, 2] as ColumnsSetting[]).map((c) => (
|
|
147
|
+
<Pressable
|
|
148
|
+
key={String(c)}
|
|
149
|
+
style={[styles.line, border]}
|
|
150
|
+
onPress={() => {
|
|
151
|
+
onChange({ columns: c });
|
|
152
|
+
setColumnsMenu(false);
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<View style={styles.check}>{s.columns === c && <Icon name="check" color={colors.text} size={20} />}</View>
|
|
156
|
+
<Text style={[styles.label, { color: colors.text }]}>{COLUMN_LABEL[String(c)]}</Text>
|
|
157
|
+
</Pressable>
|
|
158
|
+
))}
|
|
159
|
+
{row('justify', 'Justify Text', <Toggle value={s.justify} onValueChange={(v) => onChange({ justify: v })} trackColor={GREEN} />)}
|
|
160
|
+
{row(s.pageSound ? 'sound' : 'soundOff', 'Page Turn Sound', <Toggle value={s.pageSound} onValueChange={(v) => onChange({ pageSound: v })} trackColor={GREEN} />)}
|
|
161
|
+
</View>
|
|
162
|
+
|
|
163
|
+
<Pressable onPress={onReset} style={[styles.reset, { backgroundColor: colors.card }]}>
|
|
164
|
+
<Text style={{ color: '#ff3b30', fontSize: 18, fontWeight: '600' }}>Reset Theme</Text>
|
|
165
|
+
</Pressable>
|
|
166
|
+
</ScrollView>
|
|
167
|
+
</Sheet>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const styles = StyleSheet.create({
|
|
172
|
+
preview: { paddingHorizontal: 22, paddingTop: 12, paddingBottom: 18, marginBottom: 10 },
|
|
173
|
+
aa: { fontSize: 34, textAlign: 'right', marginBottom: 6 },
|
|
174
|
+
pad: { paddingHorizontal: 16, paddingBottom: 40 },
|
|
175
|
+
group: { borderRadius: 24, overflow: 'hidden' },
|
|
176
|
+
line: { flexDirection: 'row', alignItems: 'center', minHeight: 56, paddingHorizontal: 18 },
|
|
177
|
+
rowIcon: { width: 34, height: 28, alignItems: 'flex-start', justifyContent: 'center' },
|
|
178
|
+
toggleBox: { height: 31, minWidth: 51, alignItems: 'center', justifyContent: 'center' },
|
|
179
|
+
label: { flex: 1, fontSize: 18, lineHeight: 24 },
|
|
180
|
+
check: { width: 34, height: 24, justifyContent: 'center' },
|
|
181
|
+
value2: { flexDirection: 'row', alignItems: 'center', gap: 4 },
|
|
182
|
+
heading: { fontSize: 22, fontWeight: '700', marginTop: 26, marginBottom: 10, marginStart: 4 },
|
|
183
|
+
sliderBlock: { paddingHorizontal: 18, paddingTop: 10, paddingBottom: 6 },
|
|
184
|
+
dim: { opacity: 0.45 },
|
|
185
|
+
caption: { fontSize: 14, marginBottom: 2 },
|
|
186
|
+
sliderRow: { flexDirection: 'row', alignItems: 'center' },
|
|
187
|
+
value: { width: 50, textAlign: 'right', fontSize: 17, lineHeight: 22 },
|
|
188
|
+
reset: { marginTop: 18, height: 58, borderRadius: 29, alignItems: 'center', justifyContent: 'center' }
|
|
189
|
+
});
|
package/src/ui/Glass.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
// expo-blur is optional: with it the controls are frosted glass, without it they are plain translucent.
|
|
6
|
+
let BlurView: React.ComponentType<any> | null = null;
|
|
7
|
+
try {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
9
|
+
BlurView = require('expo-blur').BlurView;
|
|
10
|
+
} catch {
|
|
11
|
+
BlurView = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
/** translucent tint drawn over the blur (use ChromeColors.glass or .sheetGlass) */
|
|
17
|
+
background: string;
|
|
18
|
+
border?: string;
|
|
19
|
+
dark: boolean;
|
|
20
|
+
/** blur strength 0..100 */
|
|
21
|
+
intensity?: number;
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Frosted glass surface: blurs the book behind it so reading is not interrupted. */
|
|
26
|
+
export function Glass({ style, background, border, dark, intensity = 38, children }: Props) {
|
|
27
|
+
const base: ViewStyle = { overflow: 'hidden', borderWidth: border ? StyleSheet.hairlineWidth : 0, borderColor: border };
|
|
28
|
+
if (BlurView) {
|
|
29
|
+
return (
|
|
30
|
+
<BlurView intensity={intensity} tint={dark ? 'dark' : 'light'} style={[base, style]}>
|
|
31
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: background }]} pointerEvents="none" />
|
|
32
|
+
{children}
|
|
33
|
+
</BlurView>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return <View style={[base, { backgroundColor: background }, style]}>{children}</View>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { HIGHLIGHT_COLORS } from '../themes';
|
|
4
|
+
import type { ChromeColors } from '../themes';
|
|
5
|
+
import type { Highlight, HighlightColor } from '../types';
|
|
6
|
+
import { Glass } from './Glass';
|
|
7
|
+
import { Icon } from './Icons';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
highlight: Highlight;
|
|
11
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
12
|
+
screen: { width: number; height: number };
|
|
13
|
+
colors: ChromeColors;
|
|
14
|
+
onColor: (c: HighlightColor) => void;
|
|
15
|
+
onToggleUnderline: () => void;
|
|
16
|
+
onNote: () => void;
|
|
17
|
+
onDelete: () => void;
|
|
18
|
+
onDismiss: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const W = 316;
|
|
22
|
+
const H = 50;
|
|
23
|
+
|
|
24
|
+
/** Floating glass palette shown over a highlight, like Apple Books. */
|
|
25
|
+
export function HighlightMenu({ highlight, rect, screen, colors, onColor, onToggleUnderline, onNote, onDelete, onDismiss }: Props) {
|
|
26
|
+
const left = Math.max(8, Math.min(screen.width - W - 8, rect.x + rect.width / 2 - W / 2));
|
|
27
|
+
const above = rect.y - H - 10;
|
|
28
|
+
const top = above > 60 ? above : rect.y + rect.height + 10;
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={onDismiss} accessibilityLabel="Dismiss" />
|
|
32
|
+
<Glass dark={colors.dark} background={colors.sheetGlass} border={colors.glassBorder} intensity={55} style={[styles.menu, { left, top }]}>
|
|
33
|
+
{(Object.keys(HIGHLIGHT_COLORS) as HighlightColor[]).map((c) => (
|
|
34
|
+
<Pressable key={c} onPress={() => onColor(c)} hitSlop={4} accessibilityLabel={`${c} highlight`}>
|
|
35
|
+
<View style={[styles.dot, { backgroundColor: HIGHLIGHT_COLORS[c], borderWidth: highlight.color === c ? 2 : 0, borderColor: colors.text }]} />
|
|
36
|
+
</Pressable>
|
|
37
|
+
))}
|
|
38
|
+
<Pressable onPress={onToggleUnderline} hitSlop={6} accessibilityLabel="Underline">
|
|
39
|
+
<Icon name="underline" color={colors.text} size={22} active={highlight.style === 'underline'} strokeWidth={highlight.style === 'underline' ? 2.4 : 1.8} />
|
|
40
|
+
</Pressable>
|
|
41
|
+
<Pressable onPress={onNote} hitSlop={6} accessibilityLabel="Note">
|
|
42
|
+
<Icon name="pencil" color={colors.text} size={22} />
|
|
43
|
+
</Pressable>
|
|
44
|
+
<Pressable onPress={onDelete} hitSlop={6} accessibilityLabel="Remove highlight">
|
|
45
|
+
<Icon name="trash" color="#ff3b30" size={22} />
|
|
46
|
+
</Pressable>
|
|
47
|
+
</Glass>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
menu: {
|
|
54
|
+
position: 'absolute',
|
|
55
|
+
width: W,
|
|
56
|
+
height: H,
|
|
57
|
+
borderRadius: 25,
|
|
58
|
+
flexDirection: 'row',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
justifyContent: 'space-around',
|
|
61
|
+
paddingHorizontal: 12
|
|
62
|
+
},
|
|
63
|
+
dot: { width: 26, height: 26, borderRadius: 13 }
|
|
64
|
+
});
|
package/src/ui/Icons.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Svg, { Circle, G, Line, Path, Rect } from 'react-native-svg';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vector icon set (24 x 24 grid, rounded strokes). Every icon takes the colour it is drawn in, so it follows the
|
|
6
|
+
* theme (light / dark / any page colour). Some icons carry state: `active` fills or closes them
|
|
7
|
+
* (bookmark, rotation lock, line guide).
|
|
8
|
+
*/
|
|
9
|
+
export type IconName =
|
|
10
|
+
| 'close' | 'check' | 'chevronRight' | 'chevronUp' | 'chevronDown'
|
|
11
|
+
| 'list' | 'search' | 'textSize' | 'share' | 'bookmark' | 'rotationLock' | 'lineGuide' | 'undo'
|
|
12
|
+
| 'sun' | 'sunLow' | 'moon' | 'contrast'
|
|
13
|
+
| 'pageSlide' | 'pageCurl' | 'pageFade' | 'pageScroll'
|
|
14
|
+
| 'sliders' | 'lineSpacing' | 'charSpacing' | 'wordSpacing' | 'margins' | 'columns' | 'bold' | 'justify'
|
|
15
|
+
| 'trash' | 'pencil' | 'underline' | 'highlighter' | 'sound' | 'soundOff';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
name: IconName;
|
|
19
|
+
color: string;
|
|
20
|
+
size?: number;
|
|
21
|
+
/** filled / closed state for bookmark, rotation lock, line guide */
|
|
22
|
+
active?: boolean;
|
|
23
|
+
strokeWidth?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const RAYS = [0, 45, 90, 135, 180, 225, 270, 315].map((deg) => {
|
|
27
|
+
const a = (deg * Math.PI) / 180;
|
|
28
|
+
return { x1: 12 + 6.6 * Math.cos(a), y1: 12 + 6.6 * Math.sin(a), x2: 12 + 9.2 * Math.cos(a), y2: 12 + 9.2 * Math.sin(a) };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function Icon({ name, color, size = 24, active = false, strokeWidth = 1.8 }: Props) {
|
|
32
|
+
const p = { stroke: color, strokeWidth, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };
|
|
33
|
+
let body: React.ReactNode = null;
|
|
34
|
+
|
|
35
|
+
switch (name) {
|
|
36
|
+
case 'close':
|
|
37
|
+
body = <Path d="M6 6 L18 18 M18 6 L6 18" {...p} />;
|
|
38
|
+
break;
|
|
39
|
+
case 'check':
|
|
40
|
+
body = <Path d="M5 12.5 L10 17.5 L19 7" {...p} />;
|
|
41
|
+
break;
|
|
42
|
+
case 'chevronRight':
|
|
43
|
+
body = <Path d="M9 5.5 L15.5 12 L9 18.5" {...p} />;
|
|
44
|
+
break;
|
|
45
|
+
case 'chevronUp':
|
|
46
|
+
body = <Path d="M5.5 15 L12 8.5 L18.5 15" {...p} />;
|
|
47
|
+
break;
|
|
48
|
+
case 'chevronDown':
|
|
49
|
+
body = <Path d="M5.5 9 L12 15.5 L18.5 9" {...p} />;
|
|
50
|
+
break;
|
|
51
|
+
case 'list':
|
|
52
|
+
body = (
|
|
53
|
+
<G>
|
|
54
|
+
{[7, 12, 17].map((y) => (
|
|
55
|
+
<G key={y}>
|
|
56
|
+
<Circle cx={4.8} cy={y} r={1.25} fill={color} />
|
|
57
|
+
<Path d={`M9 ${y} H20`} {...p} />
|
|
58
|
+
</G>
|
|
59
|
+
))}
|
|
60
|
+
</G>
|
|
61
|
+
);
|
|
62
|
+
break;
|
|
63
|
+
case 'search':
|
|
64
|
+
body = (
|
|
65
|
+
<G>
|
|
66
|
+
<Circle cx={10.8} cy={10.8} r={6.3} {...p} />
|
|
67
|
+
<Path d="M15.6 15.6 L20.5 20.5" {...p} />
|
|
68
|
+
</G>
|
|
69
|
+
);
|
|
70
|
+
break;
|
|
71
|
+
case 'textSize':
|
|
72
|
+
body = (
|
|
73
|
+
<G>
|
|
74
|
+
<Path d="M2.8 18 L6.3 9.2 L9.8 18 M4.1 14.9 H8.5" {...p} />
|
|
75
|
+
<Path d="M11.6 18 L16.6 5.6 L21.6 18 M13.5 13.6 H19.7" {...p} />
|
|
76
|
+
</G>
|
|
77
|
+
);
|
|
78
|
+
break;
|
|
79
|
+
case 'share':
|
|
80
|
+
body = (
|
|
81
|
+
<G>
|
|
82
|
+
<Path d="M8.5 10 H7 A1.7 1.7 0 0 0 5.3 11.7 V19 A1.7 1.7 0 0 0 7 20.7 H17 A1.7 1.7 0 0 0 18.7 19 V11.7 A1.7 1.7 0 0 0 17 10 H15.5" {...p} />
|
|
83
|
+
<Path d="M12 14.5 V3.5 M8.2 7.3 L12 3.5 L15.8 7.3" {...p} />
|
|
84
|
+
</G>
|
|
85
|
+
);
|
|
86
|
+
break;
|
|
87
|
+
case 'bookmark':
|
|
88
|
+
body = (
|
|
89
|
+
<Path
|
|
90
|
+
d="M7.2 3.8 H16.8 A1.2 1.2 0 0 1 18 5 V20.2 L12 16.2 L6 20.2 V5 A1.2 1.2 0 0 1 7.2 3.8 Z"
|
|
91
|
+
{...p}
|
|
92
|
+
fill={active ? color : 'none'}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
case 'rotationLock':
|
|
97
|
+
body = (
|
|
98
|
+
<G>
|
|
99
|
+
<Path d="M20.2 13 A8.2 8.2 0 1 1 17 5.9" {...p} />
|
|
100
|
+
<Path d="M17.2 2.8 V6.2 H13.8" {...p} />
|
|
101
|
+
<Rect x={9} y={11.2} width={6} height={5} rx={1.2} {...p} fill={active ? color : 'none'} />
|
|
102
|
+
<Path d={active ? 'M10.4 11.2 V10 A1.6 1.6 0 0 1 13.6 10 V11.2' : 'M10.4 11.2 V9.9 A1.6 1.6 0 0 1 13.6 9.9'} {...p} />
|
|
103
|
+
</G>
|
|
104
|
+
);
|
|
105
|
+
break;
|
|
106
|
+
case 'lineGuide':
|
|
107
|
+
body = (
|
|
108
|
+
<G>
|
|
109
|
+
<Path d="M4 6.2 H20 M4 17.8 H20" {...p} opacity={0.5} />
|
|
110
|
+
<Rect x={2.8} y={9.2} width={18.4} height={5.6} rx={2.8} {...p} fill={active ? color : 'none'} fillOpacity={active ? 0.28 : 0} />
|
|
111
|
+
</G>
|
|
112
|
+
);
|
|
113
|
+
break;
|
|
114
|
+
case 'undo':
|
|
115
|
+
body = <Path d="M8.5 6 L4 10.5 L8.5 15 M4.5 10.5 H14.5 A5 5 0 0 1 14.5 20.5 H11" {...p} />;
|
|
116
|
+
break;
|
|
117
|
+
case 'sun':
|
|
118
|
+
body = (
|
|
119
|
+
<G>
|
|
120
|
+
<Circle cx={12} cy={12} r={3.9} {...p} />
|
|
121
|
+
{RAYS.map((r, i) => (
|
|
122
|
+
<Line key={i} x1={r.x1} y1={r.y1} x2={r.x2} y2={r.y2} {...p} />
|
|
123
|
+
))}
|
|
124
|
+
</G>
|
|
125
|
+
);
|
|
126
|
+
break;
|
|
127
|
+
case 'sunLow':
|
|
128
|
+
body = (
|
|
129
|
+
<G>
|
|
130
|
+
<Circle cx={12} cy={12} r={2.4} {...p} />
|
|
131
|
+
{RAYS.map((r, i) => (
|
|
132
|
+
<Line key={i} x1={12 + (r.x1 - 12) * 0.8} y1={12 + (r.y1 - 12) * 0.8} x2={12 + (r.x2 - 12) * 0.62} y2={12 + (r.y2 - 12) * 0.62} {...p} />
|
|
133
|
+
))}
|
|
134
|
+
</G>
|
|
135
|
+
);
|
|
136
|
+
break;
|
|
137
|
+
case 'moon':
|
|
138
|
+
body = <Path d="M20 14.2 A8.4 8.4 0 1 1 9.8 4 A6.6 6.6 0 0 0 20 14.2 Z" {...p} />;
|
|
139
|
+
break;
|
|
140
|
+
case 'contrast':
|
|
141
|
+
body = (
|
|
142
|
+
<G>
|
|
143
|
+
<Circle cx={12} cy={12} r={8.2} {...p} />
|
|
144
|
+
<Path d="M12 3.8 A8.2 8.2 0 0 1 12 20.2 Z" fill={color} stroke={color} strokeWidth={1} />
|
|
145
|
+
</G>
|
|
146
|
+
);
|
|
147
|
+
break;
|
|
148
|
+
case 'pageSlide':
|
|
149
|
+
body = (
|
|
150
|
+
<G>
|
|
151
|
+
<Path d="M9 5.2 H5.2 A1.9 1.9 0 0 0 3.3 7.1 V17.9 A1.9 1.9 0 0 0 5.2 19.8 H9" {...p} opacity={0.55} />
|
|
152
|
+
<Rect x={8} y={3.2} width={12.7} height={16.6} rx={2.4} {...p} />
|
|
153
|
+
</G>
|
|
154
|
+
);
|
|
155
|
+
break;
|
|
156
|
+
case 'pageCurl':
|
|
157
|
+
body = (
|
|
158
|
+
<G>
|
|
159
|
+
<Path d="M6.7 3.5 H14.5 L20 9 V18.6 A1.9 1.9 0 0 1 18.1 20.5 H6.7 A1.9 1.9 0 0 1 4.8 18.6 V5.4 A1.9 1.9 0 0 1 6.7 3.5 Z" {...p} />
|
|
160
|
+
<Path d="M14.5 3.5 V7.4 A1.6 1.6 0 0 0 16.1 9 H20" {...p} />
|
|
161
|
+
</G>
|
|
162
|
+
);
|
|
163
|
+
break;
|
|
164
|
+
case 'pageFade':
|
|
165
|
+
body = <Path d="M13.2 2.8 L5.2 13.2 H11 L10.2 21.2 L18.8 10.4 H13 Z" {...p} />;
|
|
166
|
+
break;
|
|
167
|
+
case 'pageScroll':
|
|
168
|
+
body = (
|
|
169
|
+
<G>
|
|
170
|
+
<Rect x={5} y={3} width={14} height={18} rx={2.4} {...p} />
|
|
171
|
+
<Path d="M9.4 9.6 L12 7 L14.6 9.6 M9.4 14.4 L12 17 L14.6 14.4" {...p} />
|
|
172
|
+
</G>
|
|
173
|
+
);
|
|
174
|
+
break;
|
|
175
|
+
case 'sliders':
|
|
176
|
+
body = (
|
|
177
|
+
<G>
|
|
178
|
+
<Path d="M3.5 8 H9.2 M14.8 8 H20.5 M3.5 16 H14.2 M19.8 16 H20.5" {...p} />
|
|
179
|
+
<Circle cx={12} cy={8} r={2.8} {...p} />
|
|
180
|
+
<Circle cx={17} cy={16} r={2.8} {...p} />
|
|
181
|
+
</G>
|
|
182
|
+
);
|
|
183
|
+
break;
|
|
184
|
+
case 'lineSpacing':
|
|
185
|
+
body = <Path d="M10.5 6.5 H20.5 M10.5 12 H20.5 M10.5 17.5 H20.5 M5 4.5 V19.5 M2.9 6.6 L5 4.5 L7.1 6.6 M2.9 17.4 L5 19.5 L7.1 17.4" {...p} />;
|
|
186
|
+
break;
|
|
187
|
+
case 'charSpacing':
|
|
188
|
+
body = <Path d="M7.6 13.6 L12 3.8 L16.4 13.6 M9.2 10.4 H14.8 M3.8 19.2 H20.2 M6.2 16.9 L3.8 19.2 L6.2 21.5 M17.8 16.9 L20.2 19.2 L17.8 21.5" {...p} />;
|
|
189
|
+
break;
|
|
190
|
+
case 'wordSpacing':
|
|
191
|
+
body = (
|
|
192
|
+
<G>
|
|
193
|
+
<Rect x={2.8} y={4.5} width={7} height={4.2} rx={1.2} {...p} />
|
|
194
|
+
<Rect x={14.2} y={4.5} width={7} height={4.2} rx={1.2} {...p} />
|
|
195
|
+
<Path d="M3.8 15.2 H20.2 M6.2 12.9 L3.8 15.2 L6.2 17.5 M17.8 12.9 L20.2 15.2 L17.8 17.5" {...p} />
|
|
196
|
+
</G>
|
|
197
|
+
);
|
|
198
|
+
break;
|
|
199
|
+
case 'margins':
|
|
200
|
+
body = (
|
|
201
|
+
<G>
|
|
202
|
+
<Rect x={3.8} y={3} width={16.4} height={18} rx={2.4} {...p} />
|
|
203
|
+
<Path d="M8 7 V17 M16 7 V17" {...p} />
|
|
204
|
+
</G>
|
|
205
|
+
);
|
|
206
|
+
break;
|
|
207
|
+
case 'columns':
|
|
208
|
+
body = (
|
|
209
|
+
<G>
|
|
210
|
+
<Rect x={3.5} y={4} width={7.6} height={16} rx={1.8} {...p} />
|
|
211
|
+
<Rect x={12.9} y={4} width={7.6} height={16} rx={1.8} {...p} />
|
|
212
|
+
</G>
|
|
213
|
+
);
|
|
214
|
+
break;
|
|
215
|
+
case 'bold':
|
|
216
|
+
body = <Path d="M8 4.6 V19.4 M8 4.6 H13 A3.6 3.6 0 0 1 13 11.8 H8 M8 11.8 H14 A3.8 3.8 0 0 1 14 19.4 H8" {...p} strokeWidth={strokeWidth + 0.5} />;
|
|
217
|
+
break;
|
|
218
|
+
case 'justify':
|
|
219
|
+
body = <Path d="M4 6 H20 M4 10 H20 M4 14 H20 M4 18 H20" {...p} />;
|
|
220
|
+
break;
|
|
221
|
+
case 'trash':
|
|
222
|
+
body = <Path d="M4.5 7 H19.5 M9.3 7 V4.6 H14.7 V7 M6.6 7 L7.6 19.6 A1.2 1.2 0 0 0 8.8 20.7 H15.2 A1.2 1.2 0 0 0 16.4 19.6 L17.4 7 M10.2 10.8 V16.4 M13.8 10.8 V16.4" {...p} />;
|
|
223
|
+
break;
|
|
224
|
+
case 'pencil':
|
|
225
|
+
body = <Path d="M4 20 L5.2 15.2 L16.2 4.2 A2.2 2.2 0 0 1 19.4 7.4 L8.4 18.4 Z M14 6.4 L17.2 9.6" {...p} />;
|
|
226
|
+
break;
|
|
227
|
+
case 'highlighter':
|
|
228
|
+
body = <Path d="M14.2 4.6 L19.4 9.8 L10.6 18.6 L5.4 13.4 Z M5.4 13.4 L4.2 17.6 L7.6 18.6 M3.5 21 H12.5" {...p} />;
|
|
229
|
+
break;
|
|
230
|
+
case 'underline':
|
|
231
|
+
body = <Path d="M7.4 4.4 V11 A4.6 4.6 0 0 0 16.6 11 V4.4 M5 20 H19" {...p} />;
|
|
232
|
+
break;
|
|
233
|
+
case 'sound':
|
|
234
|
+
body = <Path d="M4 9.5 H7.5 L12.5 5.2 V18.8 L7.5 14.5 H4 Z M16 9 A4.2 4.2 0 0 1 16 15 M18.6 6.6 A7.6 7.6 0 0 1 18.6 17.4" {...p} />;
|
|
235
|
+
break;
|
|
236
|
+
case 'soundOff':
|
|
237
|
+
body = <Path d="M4 9.5 H7.5 L12.5 5.2 V18.8 L7.5 14.5 H4 Z M16.2 9.4 L21 14.6 M21 9.4 L16.2 14.6" {...p} />;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<Svg width={size} height={size} viewBox="0 0 24 24">
|
|
243
|
+
{body}
|
|
244
|
+
</Svg>
|
|
245
|
+
);
|
|
246
|
+
}
|