Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions android/app/src/main/assets/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ body:not(.page-reader) .transition-chapter {
padding-right: var(--readerSettings-padding);
}

#LNReader-chapter p {
margin-block-start: var(--readerSettings-paragraphSpacing);
margin-block-end: var(--readerSettings-paragraphSpacing);
text-indent: var(--readerSettings-paragraphIndent);
}

hr {
margin-top: 20px;
margin-bottom: 20px;
Expand Down
13 changes: 7 additions & 6 deletions android/app/src/main/assets/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ function defineSafeProp(globalName, getParent, key) {
});
}

defineSafeProp('novelName', () => window.reader?.novel, 'name');
defineSafeProp('chapterName', () => window.reader?.chapter, 'name');
defineSafeProp('sourceId', () => window.reader?.novel, 'pluginId');
defineSafeProp('chapterId', () => window.reader?.chapter, 'id');
defineSafeProp('novelId', () => window.reader?.novel, 'id');
defineSafeProp('html', () => window.chapterElement, 'innerHTML');
defineSafeProp('novelName', () => window.reader?.novel, 'name');
defineSafeProp('chapterName', () => window.reader?.chapter, 'name');
defineSafeProp('sourceId', () => window.reader?.novel, 'pluginId');
defineSafeProp('chapterId', () => window.reader?.chapter, 'id');
defineSafeProp('novelId', () => window.reader?.novel, 'id');
defineSafeProp('chapterElement', () => window.reader, 'chapterElement');
defineSafeProp('html', () => window.chapterElement, 'innerHTML');
62 changes: 38 additions & 24 deletions android/app/src/main/assets/js/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
'--readerSettings-padding',
settings.padding + 'px',
);
document.documentElement.style.setProperty(
'--readerSettings-paragraphIndent',
(settings.paragraphIndent ?? 0) + 'em',
);
document.documentElement.style.setProperty(
'--readerSettings-paragraphSpacing',
(settings.paragraphSpacing ?? 1) + 'em',
);
document.documentElement.style.setProperty(
'--readerSettings-textSize',
settings.textSize + 'px',
Expand Down Expand Up @@ -50,30 +58,36 @@

// text options
(() => {
van.derive(() => {
let html = reader.rawHTML;
if (reader.generalSettings.val.bionicReading) {
html = textVide.textVide(reader.rawHTML);
}
const { bionicReading, removeExtraParagraphSpacing } =
reader.generalSettings.val;
let html = reader.rawHTML;
if (bionicReading) {
html = textVide.textVide(reader.rawHTML);
}

if (reader.generalSettings.val.removeExtraParagraphSpacing) {
html = html
.replace(/(?:&nbsp;\s*|[\u200b]\s*)+(?=<\/?p[> ])/g, '')
.replace(/<br>\s*<br>\s*(?:<br>\s*)+/g, '<br><br>') //force max 2 consecutive <br>, chaining regex
.replace(
/<br>\s*<br>[^]+/,
_ =>
`${
/\/p>/.test(_)
? _.replace(
/<br>\s*<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p(?:>| [^>]+>)<br>\s*<br>))\s*/g,
'',
)
: _
}`,
) //if p found, delete all double br near p
.replace(/<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p(?:>| [^>]+>)(?:<[^>]+>)*\s*<br>))\s*/g, '');
}
if (removeExtraParagraphSpacing) {
html = html
.replace(/(?:&nbsp;\s*|[\u200b]\s*)+(?=<\/?p[> ])/g, '')
.replace(/<br>\s*<br>\s*(?:<br>\s*)+/g, '<br><br>') //force max 2 consecutive <br>, chaining regex
.replace(
/<br>\s*<br>[^]+/,
_ =>
`${
/\/p>/.test(_)
? _.replace(
/<br>\s*<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p(?:>| [^>]+>)<br>\s*<br>))\s*/g,
'',
)
: _
}`,
) //if p found, delete all double br near p
.replace(
/<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p(?:>| [^>]+>)(?:<[^>]+>)*\s*<br>))\s*/g,
'',
);
}

if (bionicReading || removeExtraParagraphSpacing) {
reader.chapterElement.innerHTML = html;
});
}
})();
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,6 @@ class TikTokTTSModule(private val reactContext: ReactApplicationContext) :
openWebSockets.add(webSocket)
val message = createTikTokMessage(text, voice)
webSocket.send(message)
if (waitingForHash == hash) {
sendEvent("TikTokTTS_onStart", null)
}
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Expand Down Expand Up @@ -341,5 +338,6 @@ class TikTokTTSModule(private val reactContext: ReactApplicationContext) :
})

audioTrack?.play()
sendEvent("TikTokTTS_onStart", null)
}
}
9 changes: 6 additions & 3 deletions src/components/ColorPickerModal/ColorPickerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
<Preview style={styles.previewStyle} />
<HueCircular
accessibilityLabel="Hue"
thumbShape="pill"
rotate={-30}
style={styles.hueCircular}
containerStyle={[
styles.hueContent,
Expand All @@ -84,6 +86,7 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
<Panel1
accessibilityLabel="Saturation and brightness"
style={styles.panel}
thumbShape="circle"
/>
</HueCircular>
<View
Expand Down Expand Up @@ -150,10 +153,10 @@ const styles = StyleSheet.create({
paddingTop: 6,
},
panel: {
alignSelf: 'center',
borderRadius: 16,
height: '68%',
width: '68%',
width: '74%',
height: '74%',
alignSelf: 'center',
},
picker: {
alignSelf: 'center',
Expand Down
19 changes: 13 additions & 6 deletions src/components/Common/ToggleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { ThemeColors } from '../../theme/types';

// --- Dynamic style helpers ---

const getToggleButtonPressableStyle = (
const getToggleButtonPressableStyle = (disabled?: boolean) => ({
opacity: disabled ? 0.6 : 1,
});

const getToggleButtonContainerStyle = (
selected: boolean,
theme: ThemeColors,
disabled?: boolean,
) => ({
opacity: disabled ? 0.6 : 1,
backgroundColor: selected
? Color(theme.primary).alpha(0.12).string()
: 'transparent',
: theme.surface,
});

const getToggleColorButtonPressableStyle = (backgroundColor: string) => ({
Expand All @@ -42,12 +44,17 @@ export const ToggleButton: React.FC<ToggleButtonProps> = ({
onPress,
disabled,
}) => (
<View style={styles.toggleButtonContainer}>
<View
style={[
styles.toggleButtonContainer,
getToggleButtonContainerStyle(selected, theme),
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[
styles.toggleButtonPressable,
getToggleButtonPressableStyle(selected, theme, disabled),
getToggleButtonPressableStyle(disabled),
]}
onPress={onPress}
disabled={disabled}
Expand Down
12 changes: 9 additions & 3 deletions src/components/Modal/__tests__/KeyboardAvoidingModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { act, render, screen } from '@testing-library/react-native';
import { act, render, screen, waitFor } from '@testing-library/react-native';
import React from 'react';

import KeyboardAvoidingModal from '../KeyboardAvoidingModal';

jest.mock('react-native-keyboard-controller', () => ({
KeyboardController: {
dismiss: jest.fn(() => Promise.resolve()),
},
}));

const mockButtonPresses = new Map<string, () => unknown>();

type MockButtonProps = {
Expand Down Expand Up @@ -206,7 +212,7 @@ describe('KeyboardAvoidingModal', () => {
});
});

it('runs reset without dismissing and cancel before one dismiss', () => {
it('runs reset without dismissing and cancel before one dismiss', async () => {
const onReset = jest.fn();
const onCancel = jest.fn();
const onDismiss = jest.fn();
Expand All @@ -228,6 +234,6 @@ describe('KeyboardAvoidingModal', () => {

act(() => pressButton('common.cancel'));
expect(onCancel).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
await waitFor(() => expect(onDismiss).toHaveBeenCalledTimes(1));
});
});
101 changes: 99 additions & 2 deletions src/components/Modal/__tests__/KeyboardAwareModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { fireEvent, render, screen } from '@testing-library/react-native';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react-native';
import React from 'react';
import { Keyboard, StyleProp, StyleSheet, Text, ViewStyle } from 'react-native';
import { KeyboardController } from 'react-native-keyboard-controller';

import KeyboardAwareModal from '../KeyboardAwareModal';

Expand All @@ -19,6 +26,9 @@ jest.mock('@hooks/persisted', () => ({
}));

jest.mock('react-native-keyboard-controller', () => ({
KeyboardController: {
dismiss: jest.fn(() => Promise.resolve()),
},
useAnimatedKeyboard: () => ({
height: { value: 0 },
state: { value: 0 },
Expand Down Expand Up @@ -50,11 +60,19 @@ jest.mock('react-native-paper', () => {
});

describe('KeyboardAwareModal', () => {
const keyboardControllerDismiss =
KeyboardController.dismiss as jest.MockedFunction<
typeof KeyboardController.dismiss
>;

beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Keyboard, 'dismiss').mockImplementation(() => undefined);
keyboardControllerDismiss.mockReset();
keyboardControllerDismiss.mockResolvedValue();
});

it('renders its regions, merges wrapper styles and centralizes dismiss', () => {
it('renders its regions, merges wrapper styles and centralizes dismiss', async () => {
const onDismiss = jest.fn();

render(
Expand Down Expand Up @@ -82,6 +100,85 @@ describe('KeyboardAwareModal', () => {

fireEvent.press(screen.getByTestId('dismiss-modal'));

await waitFor(() => {
expect(Keyboard.dismiss).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});

it('dismisses the controller keyboard before hiding the modal', async () => {
let resolveKeyboardDismiss: (() => void) | undefined;
keyboardControllerDismiss.mockImplementation(
() =>
new Promise<void>(resolve => {
resolveKeyboardDismiss = resolve;
}),
);
const onDismiss = jest.fn();

render(
<KeyboardAwareModal visible onDismiss={onDismiss}>
<Text>Modal body</Text>
</KeyboardAwareModal>,
);

fireEvent.press(screen.getByTestId('dismiss-modal'));

expect(keyboardControllerDismiss).toHaveBeenCalledWith({
animated: false,
});
expect(onDismiss).not.toHaveBeenCalled();

await act(async () => resolveKeyboardDismiss?.());

expect(Keyboard.dismiss).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
});

it('falls back when the controller keyboard never settles', async () => {
jest.useFakeTimers();
keyboardControllerDismiss.mockImplementation(() => new Promise(() => {}));
const onDismiss = jest.fn();

render(
<KeyboardAwareModal visible onDismiss={onDismiss}>
<Text>Modal body</Text>
</KeyboardAwareModal>,
);

fireEvent.press(screen.getByTestId('dismiss-modal'));
expect(onDismiss).not.toHaveBeenCalled();

await act(async () => jest.runAllTimers());

expect(Keyboard.dismiss).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});

it('ignores repeated dismiss requests while one is in flight', async () => {
let resolveKeyboardDismiss: (() => void) | undefined;
keyboardControllerDismiss.mockImplementation(
() =>
new Promise<void>(resolve => {
resolveKeyboardDismiss = resolve;
}),
);
const onDismiss = jest.fn();

render(
<KeyboardAwareModal visible onDismiss={onDismiss}>
<Text>Modal body</Text>
</KeyboardAwareModal>,
);

fireEvent.press(screen.getByTestId('dismiss-modal'));
fireEvent.press(screen.getByTestId('dismiss-modal'));

expect(keyboardControllerDismiss).toHaveBeenCalledTimes(1);

await act(async () => resolveKeyboardDismiss?.());

expect(Keyboard.dismiss).toHaveBeenCalledTimes(1);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
Expand Down
Loading
Loading