Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9c7800b
copied old code
CD-Z Feb 27, 2026
725b231
clean up WebVieweReader
CD-Z Feb 28, 2026
409ce50
update pnpm-lock.yaml
CD-Z May 8, 2026
d12d8e6
Update index.tsx
CD-Z May 8, 2026
47c012f
Improve TextInput focus and keyboard behavior
CD-Z May 8, 2026
5a26ed6
optimized app bar animation
CD-Z May 8, 2026
6f7212b
Remove gap between keyboard and content
CD-Z May 8, 2026
2e004b1
Update ToggleButton
CD-Z May 8, 2026
11a4fad
Remove AdvancedTab from Reader settings
CD-Z May 8, 2026
eaa4d4c
added code migration
CD-Z May 10, 2026
28a3d09
Update settings webview
CD-Z May 10, 2026
6930a08
updated text remover modal
CD-Z May 10, 2026
0f068b9
small fixes
CD-Z May 10, 2026
030cf65
add stopPropagation
CD-Z May 10, 2026
3490ce4
dismiss Keyboard on closing KeyboardAvoidingModal
CD-Z May 10, 2026
09bc4b9
add simple regex validation
CD-Z May 10, 2026
087548d
Update KeyboardAvoidingModal.tsx
CD-Z May 10, 2026
a0ba6b0
added localization
CD-Z May 10, 2026
b9966ae
Update useTextModifications.ts
CD-Z May 10, 2026
b110e1b
Update useTextModifications.ts
CD-Z May 10, 2026
96d9946
updated KeyboardAvoidingModal
CD-Z May 10, 2026
aa9f8bc
Update SettingsReaderWebView.tsx
CD-Z May 10, 2026
571b0b3
Update useTextModifications.ts
CD-Z May 10, 2026
3c2e823
Update SettingsReaderWebView.tsx
CD-Z May 10, 2026
9fac388
Update ReaderScreen.tsx
CD-Z May 10, 2026
963fdd2
Update setting migration
CD-Z May 10, 2026
c590439
clone snippets on save
CD-Z May 10, 2026
9ce82fb
fade out disabled ToggleButton
CD-Z May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import LottieSplashScreen from 'react-native-lottie-splash-screen';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider as PaperProvider } from 'react-native-paper';
import * as Notifications from 'expo-notifications';
import { KeyboardProvider } from 'react-native-keyboard-controller';

import AppErrorBoundary, {
ErrorFallback,
Expand All @@ -31,7 +32,6 @@ Notifications.setNotificationHandler({
},
});


const App = () => {
const state = useInitDatabase();

Expand All @@ -51,12 +51,17 @@ const App = () => {
<AppErrorBoundary>
<SafeAreaProvider>
<ThemeProvider>
<PaperProvider>
<BottomSheetModalProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<Main />
</BottomSheetModalProvider>
</PaperProvider>
<KeyboardProvider>
<PaperProvider>
<BottomSheetModalProvider>
<StatusBar
translucent={true}
backgroundColor="transparent"
/>
<Main />
</BottomSheetModalProvider>
</PaperProvider>
</KeyboardProvider>
</ThemeProvider>
</SafeAreaProvider>
</AppErrorBoundary>
Expand Down
6 changes: 3 additions & 3 deletions android/app/src/main/assets/css/toolWrapper.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
right: unset;
left: 50%;
transform: translateX(-50%);
bottom: 120px;
bottom: calc(68px + var(--bottom-inset));
display: flex;
flex-direction: row-reverse;
align-items: center;
}

@media only screen and (min-width: 500px) {
#ToolWrapper.horizontal {
bottom: 80px;
bottom: calc(28px + var(--bottom-inset));
}
}

Expand All @@ -32,7 +32,7 @@

#ToolWrapper.hidden.horizontal {
opacity: 0;
bottom: 80px;
bottom: calc(28px + var(--bottom-inset));
right: unset;
}

Expand Down
195 changes: 195 additions & 0 deletions android/app/src/main/assets/js/textRemover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Text selection functionality
window.textRemover = new (function () {
let selectionUI = null;
let isUIActive = false;

function createSelectionUI() {
if (selectionUI) return selectionUI;

const { div, button } = van.tags;
selectionUI = div(
{
id: 'text-selection-ui',
style:
'position: fixed; background: color-mix(in srgb, var(--theme-surface), transparent 10%); border-radius: 8px; padding: 8px; z-index: 100000; opacity: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.25); transition: opacity 150ms',
},
button(
{
style:
'background: var(--theme-secondary); color: var(--theme-onSecondary); padding: 6px 12px; margin: 2px; border-radius: 4px; font-size: 12px;',
onclick: e => {
if (reader.hidden.val) {
e.stopPropagation();
}
removeSelectedText();
},
},
'Remove',
),
button(
{
style:
'background: var(--theme-primary); color: var(--theme-onPrimary); padding: 6px 12px; margin: 2px; border-radius: 4px; font-size: 12px;',
onclick: e => {
if (reader.hidden.val) {
e.stopPropagation();
}
replaceSelectedText();
},
},
'Replace',
),
);

document.body.appendChild(selectionUI);
return selectionUI;
}

function showSelectionUI() {
const ui = createSelectionUI();

// Get selection bounds
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();

// Get UI element heights from CSS variables (with fallbacks)
const statusBarHeight =
parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'--StatusBar-currentHeight',
),
10,
) || 24;
const navigationBarHeight =
parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'--bottom-inset',
),
10,
) || 24;
const readerPadding =
parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'--readerSettings-padding',
),
10,
) || 16;
const uiHeight = 50; // Approximate height of our UI

// Calculate available space
const viewportHeight = window.innerHeight;
const selectionCenterY = rect.top + rect.height / 2;
const topSafeArea = statusBarHeight + readerPadding + 10;
const bottomSafeArea = readerPadding + uiHeight + navigationBarHeight;

// Position UI based on selection location
let topPosition;
if (selectionCenterY < viewportHeight / 2) {
// Selection is in top half, position UI at bottom
//TODO: make this dynamic
const avoidScrollbar = reader.generalSettings.val.verticalSeekbar
? 0
: 42;
const avoidUI = !reader.hidden.val ? 46 + avoidScrollbar : 0;
topPosition = viewportHeight - bottomSafeArea - avoidUI - 4;
ui.style.top = topPosition + 'px';
ui.style.bottom = 'auto';
} else {
// Selection is in bottom half, position UI at top (accounting for status bar)
topPosition = Math.max(topSafeArea, statusBarHeight + 20);
const avoidUI = !reader.hidden.val ? 34 : 0;
ui.style.top = topPosition + avoidUI + 'px';
ui.style.bottom = 'auto';
}

// Center horizontally
ui.style.left = '50%';
ui.style.transform = 'translateX(-50%)';
} else {
// Fallback: position at top if no selection rect available
ui.style.top = '20px';
ui.style.left = '50%';
ui.style.transform = 'translateX(-50%)';
ui.style.bottom = 'auto';
}

ui.style.opacity = '1';
isUIActive = true;
}

function hideSelectionUI() {
if (selectionUI) {
selectionUI.style.opacity = '0';
}
isUIActive = false;
}

function getSelectedText() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
return selection.toString().trim();
}
return '';
}
Comment thread
CD-Z marked this conversation as resolved.

function removeSelectedText() {
const selectedText = getSelectedText();
if (selectedText) {
reader.post({
type: 'text-action',
data: { remove: selectedText },
});
}
hideSelectionUI();
window.getSelection().removeAllRanges();
}

function replaceSelectedText() {
const selectedText = getSelectedText();
if (selectedText) {
// For replace, we need user input, so send a different message
reader.post({
type: 'text-action',
data: { replace: selectedText },
});
}
hideSelectionUI();
window.getSelection().removeAllRanges();
}

// Handle text selection
document.addEventListener('selectionchange', function () {
const selectedText = getSelectedText();
if (selectedText) {
showSelectionUI();
} else if (!isUIActive) {
hideSelectionUI();
}
});

// Hide UI when clicking/tapping elsewhere
document.addEventListener('touchstart', function (e) {
if (isUIActive && selectionUI && !selectionUI.contains(e.target)) {
const selectedText = getSelectedText();
if (!selectedText) {
hideSelectionUI();
}
}
});

document.addEventListener('click', function (e) {
if (isUIActive && selectionUI && !selectionUI.contains(e.target)) {
const selectedText = getSelectedText();
if (!selectedText) {
hideSelectionUI();
}
}
});

// Hide UI on scroll
window.addEventListener('scroll', function () {
hideSelectionUI();
});
})();
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"react-native-error-boundary": "^3.1.0",
"react-native-file-access": "^4.0.2",
"react-native-gesture-handler": "^2.30.1",
"react-native-keyboard-controller": "^1.20.7",
"react-native-lottie-splash-screen": "^1.1.2",
"react-native-mmkv": "^4.3.0",
"react-native-nitro-modules": "^0.35.2",
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion src/components/Common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,27 @@ import { View, StyleSheet } from 'react-native';
const Row = ({
children,
style = {},
horizontalSpacing,
verticalSpacing,
}: {
children?: React.ReactNode;
style?: any;
}) => <View style={[styles.row, style]}>{children}</View>;
horizontalSpacing?: number | `${number}%`;
verticalSpacing?: number | `${number}%`;
}) => (
<View
style={[
styles.row,
style,
{
paddingHorizontal: horizontalSpacing,
paddingVertical: verticalSpacing,
},
]}
>
{children}
</View>
);

export { Row };

Expand Down
10 changes: 8 additions & 2 deletions src/components/Common/ToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from 'react';
import { Pressable, View, StyleSheet } from 'react-native';
import { View, StyleSheet } from 'react-native';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
import { ThemeColors } from '../../theme/types';
import Color from 'color';
import { MaterialDesignIconName } from '@type/icon';
import { Pressable } from 'react-native-gesture-handler';

// --- Dynamic style helpers ---

const getToggleButtonPressableStyle = (
selected: boolean,
theme: ThemeColors,
disabled?: boolean,
) => ({
opacity: disabled ? 0.6 : 1,
backgroundColor: selected
? Color(theme.primary).alpha(0.12).string()
: 'transparent',
Expand All @@ -28,6 +31,7 @@ interface ToggleButtonProps {
theme: ThemeColors;
color?: string;
onPress: () => void;
disabled?: boolean;
}

export const ToggleButton: React.FC<ToggleButtonProps> = ({
Expand All @@ -36,15 +40,17 @@ export const ToggleButton: React.FC<ToggleButtonProps> = ({
theme,
color,
onPress,
disabled,
}) => (
<View style={styles.toggleButtonContainer}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[
styles.toggleButtonPressable,
getToggleButtonPressableStyle(selected, theme),
getToggleButtonPressableStyle(selected, theme, disabled),
]}
onPress={onPress}
disabled={disabled}
>
<MaterialCommunityIcons
name={icon}
Expand Down
Loading
Loading