Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41ea7eb7b7 | |||
| e31eb9d457 | |||
| 7842e860cb | |||
| daf7bf2ccb | |||
| a712100cfc | |||
| 92e1210fc8 | |||
| 5b4058c093 | |||
| 77ebdebcca | |||
| ea72de0113 | |||
| a1c0820da8 | |||
| c9eb42ff4d | |||
| 8f0c39ab41 | |||
| 4de059d5bb | |||
| 969572b5a0 | |||
| 4236037b6b | |||
| a800c98d02 | |||
| f1c825f826 | |||
| fba8974699 | |||
| 10b90de171 | |||
| 3785dddebc | |||
| 259dd6acc7 | |||
| 90160f7f40 | |||
| 8bdc86c974 | |||
| 5b441562f8 | |||
| 4e1a67efc8 | |||
| cc217e2b04 | |||
| f8d4f50307 | |||
| d0e14d518e | |||
| c06d6f9326 | |||
| 1200b7f75b | |||
| ee7d96daf9 | |||
| 5a6f324638 | |||
| 4b1ff0efc1 | |||
| 45174af3c2 | |||
| baabc073f0 | |||
| 37969da3e9 | |||
| a81b32ae7f | |||
| 389fee8ecb | |||
| 8fcfd22767 | |||
| b216d3739a | |||
| dcceea064a | |||
| 64c027c695 | |||
| 0dd9f2bd94 | |||
| 80228ada1e | |||
| 00c180927e | |||
| 7f61219bb0 | |||
| 00d3a441e3 | |||
| f6b82939de | |||
| 3f2cfc253b | |||
| b107d3ca54 | |||
| 6bda33efd4 | |||
| 1baa3e914c | |||
| ccbbe36ce6 | |||
| 9e81f805c0 | |||
| 47428ec484 | |||
| 58f9427008 | |||
| 5e0f375f7f | |||
| 21611de3aa | |||
| 70520223cd | |||
| 2c8880c9af | |||
| ee864d3e81 | |||
| 6730c773f3 | |||
| 241cb0a664 | |||
| 3d59b8f774 | |||
| 188700112a | |||
| 03f582da98 | |||
| ed215a1d54 | |||
| c487bd2ca6 | |||
| f90a4d1103 | |||
| 8306296736 | |||
| ae022052d5 | |||
| 7299e53df6 | |||
| c513361a5e | |||
| 429829b80b | |||
| ad1ccd815a | |||
| 036608f7a8 | |||
| 8747e8ad39 | |||
| 537cf48fb4 | |||
| 9f51adb196 | |||
| f2d3de4470 | |||
| 5f4507cfd5 | |||
| ecc7a7a6e7 | |||
| 8d5b92bc9e | |||
| 0f5eee1bdb | |||
| 4e3f817792 | |||
| c61c4d0105 | |||
| 659988330c | |||
| 430a067105 | |||
| c2c8935068 | |||
| 9636aab53f | |||
| 22915914fb | |||
| d39dabef68 | |||
| e01cbefdae | |||
| 73fcba007a | |||
| 54a3feb08c | |||
| ecb3875fe2 | |||
| 2c9df1d93f | |||
| 5db79a09eb | |||
| ff771fdd68 | |||
| 169c4b9de1 | |||
| 4bcd22b9de | |||
| 8a2dc15da6 | |||
| 3f3a407820 | |||
| c72808d72f | |||
| dabe4710bb | |||
| cdc4ddcc97 | |||
| 8f783fd4c2 | |||
| 048119e530 | |||
| b68d44d500 | |||
| 4bc3638b38 | |||
| bc5e2f1fd1 | |||
| e8e7519c40 | |||
| 5460047e41 | |||
| bdd00cd87d | |||
| f83c017e09 | |||
| e38cb9972f | |||
| ac9c789a89 | |||
| 445187cf11 | |||
| 52ec11a33d | |||
| c498d7e9cd | |||
| 0fad9b907f | |||
| 0a170f14bc | |||
| 8245e77672 | |||
| 9c68260592 | |||
| b1778c5802 | |||
| be4ed6de79 | |||
| 3597b542d3 | |||
| fd6d912391 | |||
| d9dc18da43 | |||
| a22d754f95 | |||
| d9d7d3aaf0 | |||
| a399927264 | |||
| b4b00a5c66 | |||
| 9861f81589 | |||
| d06aecb5f8 | |||
| 86c82c1a61 | |||
| 1131446ddc | |||
| f6b2516643 | |||
| 7cf8692e77 | |||
| d6290294f7 | |||
| d5cb0a234f | |||
| 306d8d1d42 | |||
| 7b1eb7dd9d | |||
| 1ad2c98473 | |||
| f0b8df93b4 | |||
| 19ce7da32e | |||
| 9d35f6d303 | |||
| 31d0aa1787 | |||
| ea5fde4156 | |||
| 03e3816ab8 | |||
| 9d6e407c40 | |||
| 6781b5d6a4 | |||
| 352913f00e | |||
| 73f7ebf7b5 | |||
| 045d6419d8 | |||
| acc9d8e7ed | |||
| a29cce9eba | |||
| 1292f520bb | |||
| 8bd6131862 | |||
| ed6c2b7ba2 | |||
| f68a7a4e6a | |||
| c9d2ce382f | |||
| 5262567d61 | |||
| b45a30f0cc | |||
| 867b63af22 | |||
| b3a5e237f4 | |||
| 7aae3a81c3 | |||
| 078796784b | |||
| 15aed61aa6 | |||
| b67d6f8088 | |||
| 9b4bdfea89 | |||
| 70220a5a87 | |||
| aa95743d1c | |||
| eb818ef55b | |||
| 0bda5fd055 | |||
| ea336902f2 | |||
| 58a633e5b9 | |||
| 74ed5a446d | |||
| a5d073a1ec | |||
| f4d3d9d4eb | |||
| 772a9ef3a2 | |||
| 6f3278f6ff | |||
| f11a10ba22 | |||
| 02304fd394 | |||
| 94d11a12ff | |||
| fca37868f1 | |||
| 574f54a9bc | |||
| fb0649f07f | |||
| e3def3821e | |||
| 334c981013 | |||
| b62c8e3aea | |||
| 09a482e4b7 | |||
| b7bbbbe962 | |||
| 30265471b5 | |||
| b4b038dd42 | |||
| 6517028cd4 | |||
| 3f62f6f225 | |||
| 822256229f | |||
| 4f23e1814e | |||
| aec6c34b66 | |||
| e50b6c4a47 | |||
| 1ac6d116a2 | |||
| 6864d07dd9 | |||
| 671e4f4bfd | |||
| e2d22a5b05 | |||
| 3cb2eb6b11 | |||
| 4597a95e9c | |||
| cf4d97e7f9 | |||
| 09f7319f2f | |||
| c77dc52f29 | |||
| 3cd5b40c25 | |||
| 42570d29f3 | |||
| 7e7a8866ca | |||
| ed5e599edb | |||
| addcadeba2 | |||
| 484c2b7e33 | |||
| b6fea9111c |
@@ -1,18 +1,22 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-shadow': ['warn'],
|
||||
'no-shadow': 'off',
|
||||
'no-undef': 'off',
|
||||
'react-native/no-inline-styles': 'warn',
|
||||
'no-console': 'warn',
|
||||
'no-console': 'error',
|
||||
'@typescript-eslint/no-shadow': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'curly': ['error', 'multi-line', 'consistent'],
|
||||
'no-useless-return': 'error',
|
||||
'block-scoped-var': 'error',
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
'no-dupe-else-if': 'error',
|
||||
'no-duplicate-imports': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -3,55 +3,92 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Clone Repository (Latest)
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install Dependencies
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
shell: bash
|
||||
run: |
|
||||
npm install
|
||||
- name: Create .env
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: false
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Create Environment File
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
MYANIMELIST_CLIENT_ID=${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID=${{ vars.ANILIST_CLIENT_ID }}
|
||||
GIT_HASH=$(git rev-parse --short HEAD)
|
||||
RELEASE_DATE=$(date --utc +'%d/%m/%y %I:%M %p %Z')
|
||||
BUILD_TYPE=Github Action
|
||||
EOF
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd android && chmod +x ./gradlew
|
||||
|
||||
- name: Build Android Release
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
|
||||
- name: Set Env
|
||||
- name: Set Environment Variables
|
||||
run: |
|
||||
set -x
|
||||
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> $GITHUB_ENV
|
||||
echo "COMMIT_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
- name: Build Android Release
|
||||
env:
|
||||
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
|
||||
COMMIT_ID: ${{ env.COMMIT_ID }}
|
||||
run: |
|
||||
sed -i 's/LNReader/LNReader-r${{ env.COMMIT_COUNT }}(${{ env.COMMIT_ID }})/g' android/app/src/main/res/values/strings.xml
|
||||
cd android && ./gradlew assembleRelease -PcustomAppId=com.rajarsheechatterjee.LNReader.commit_${{ env.COMMIT_ID }} --build-cache
|
||||
mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}.apk
|
||||
|
||||
- name: Upload Release Artifact
|
||||
env:
|
||||
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
|
||||
COMMIT_ID: ${{ env.COMMIT_ID }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: LNReader-r${{ env.COMMIT_COUNT }}
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
name: LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}
|
||||
path: android/app/build/outputs/apk/release/LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}.apk
|
||||
retention-days: 30
|
||||
|
||||
@@ -7,13 +7,42 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
run: pnpm run lint
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Temporarily Disabled
|
||||
name: Type Check
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Type Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check Types
|
||||
run: pnpm run type-check
|
||||
@@ -38,7 +38,7 @@ DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
**/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
@@ -48,6 +48,10 @@ build/
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
.kotlin/
|
||||
|
||||
# node.js
|
||||
#
|
||||
@@ -66,6 +70,7 @@ buck-out/
|
||||
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
Gemfile.lock
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
@@ -74,4 +79,12 @@ web-build/
|
||||
# @end expo-cli
|
||||
|
||||
.vscode/
|
||||
src/sources/en/generators/
|
||||
src/sources/en/generators/
|
||||
*.lockb
|
||||
*.lock
|
||||
!pnpm-lock.yaml
|
||||
.tool-versions
|
||||
flake.lock
|
||||
|
||||
# pnpm
|
||||
.pnpm-store
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
**/node_modules
|
||||
**/node_modules
|
||||
android/**/js/van.js
|
||||
android/**/js/text-vibe.js
|
||||
|
||||
@@ -7,4 +7,5 @@ module.exports = {
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
quoteProps: 'preserve',
|
||||
endOfLine: 'auto', // stop prettier from getting mad on windows
|
||||
};
|
||||
|
||||
@@ -1,45 +1,104 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { StatusBar, StyleSheet } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
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 BackgroundService from 'react-native-background-actions';
|
||||
|
||||
import { createTables } from '@database/db';
|
||||
import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import { deserializePlugins } from '@plugins/pluginManager';
|
||||
import AppErrorBoundary, {
|
||||
ErrorFallback,
|
||||
} from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import { useDatabaseInitialization } from '@hooks';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { BACKGROUND_ACTION } from '@services/constants';
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => {
|
||||
return {
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
createTables();
|
||||
deserializePlugins().then(() => LottieSplashScreen.hide());
|
||||
if (!BackgroundService.isRunning()) {
|
||||
MMKVStorage.delete(BACKGROUND_ACTION);
|
||||
}
|
||||
Notifications.setNotificationCategoryAsync('TTS_CONTROLS', [
|
||||
{
|
||||
identifier: 'TTS_PLAY_PAUSE',
|
||||
buttonTitle: '▶️ Play',
|
||||
options: {
|
||||
opensAppToForeground: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
identifier: 'TTS_STOP',
|
||||
buttonTitle: '⏹️ Stop',
|
||||
options: {
|
||||
opensAppToForeground: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
identifier: 'TTS_NEXT',
|
||||
buttonTitle: '⏭️ Next',
|
||||
options: {
|
||||
opensAppToForeground: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
Notifications.setNotificationChannelAsync('tts-controls', {
|
||||
name: 'TTS Controls',
|
||||
description: 'Text-to-Speech playback controls',
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
vibrationPattern: [],
|
||||
enableLights: false,
|
||||
enableVibrate: false,
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
const { isDbReady, dbError, retryInitialization } =
|
||||
useDatabaseInitialization();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDbReady || dbError) {
|
||||
LottieSplashScreen.hide();
|
||||
}
|
||||
}, [isDbReady, dbError]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||
response => {
|
||||
const actionIdentifier = response.actionIdentifier;
|
||||
if (actionIdentifier.startsWith('TTS_')) {
|
||||
const { setTTSAction } = require('@utils/ttsNotification');
|
||||
setTTSAction(actionIdentifier);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (dbError) {
|
||||
return <ErrorFallback error={dbError} resetError={retryInitialization} />;
|
||||
}
|
||||
|
||||
if (!isDbReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<GestureHandlerRootView style={styles.flex}>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
@@ -55,3 +114,9 @@ const App = () => {
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# React Native Development Nix Environment Setup
|
||||
|
||||
This document outlines how to set up your React Native development environment using Nix flakes for reproducibility and isolation.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This was tested on an Arch Linux system with AMD CPU and GPU. There are likely problems with NVIDIA GPUs.
|
||||
|
||||
Compatibility with Microsoft WSL2 is not guaranteed, but should work withoud major issues, except for the emulator.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Nix installed**: [https://nixos.org/download.html](https://nixos.org/download.html)
|
||||
- **Git**: Required for cloning the repository.
|
||||
- **KVM (for emulator, if enabled)**: Ensure KVM is enabled and your user is in the `kvm` group for optimal emulator performance:
|
||||
```bash
|
||||
sudo usermod -aG kvm $USER && newgrp kvm # (or log out/in)
|
||||
kvm-ok # Verify KVM acceleration
|
||||
```
|
||||
- **GPU Drivers (for emulator, if enabled)**: For hardware-accelerated emulator (AMD/Intel), ensure host Vulkan drivers are working:
|
||||
```bash
|
||||
vulkaninfo | grep -i "device name" # Should show your GPU
|
||||
```
|
||||
If not, install `vulkan-radeon` (AMD) or `vulkan-intel` (Intel) on your host system and reboot.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Clone the Project:**
|
||||
|
||||
```bash
|
||||
git clone <your-fork-url>
|
||||
cd <your-fork-name>
|
||||
```
|
||||
|
||||
2. **Enter the Development Shell:**
|
||||
|
||||
If you have `pnpm` installed, use the defined scripts:
|
||||
|
||||
- **For Physical Device Only (default):**
|
||||
```bash
|
||||
pnpm run nix:shell
|
||||
```
|
||||
- **For Emulator Development (explicitly enable):**
|
||||
```bash
|
||||
pnpm run nix:emulator
|
||||
```
|
||||
|
||||
If you do not have `pnpm` installed, execute the commands manually:
|
||||
|
||||
- **For Physical Device Only (default):**
|
||||
```bash
|
||||
nix develop --extra-experimental-features nix-command --extra-experimental-features flakes
|
||||
```
|
||||
- **For Emulator Development (explicitly enable):**
|
||||
```
|
||||
nix develop .#emulator --extra-experimental-features nix-command --extra-experimental-features flakes
|
||||
```
|
||||
|
||||
The first time, this will download and set up all required tools, which can take considerable time. Subsequent entries will be much faster. You will also see a message about "Setting up writable Android SDK..." which only happens once.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Once inside the `nix develop` shell, all tools are available.
|
||||
|
||||
### 1. Start Metro Bundler
|
||||
|
||||
Open a new terminal or run in the background:
|
||||
|
||||
```bash
|
||||
pnpm run dev:start
|
||||
```
|
||||
|
||||
### 2. Physical Device Development
|
||||
|
||||
1. **Connect Device**: Enable USB debugging on your Android device and connect it via USB.
|
||||
2. **Verify Device**:
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
3. **Mirror Screen (Optional)**:
|
||||
```bash
|
||||
scrcpy # Mirrors screen, provides control
|
||||
scrcpy -s <device_serial> # If multiple devices connected
|
||||
```
|
||||
4. **Run App**:
|
||||
```bash
|
||||
npx run android
|
||||
```
|
||||
|
||||
### 3. Android Emulator Development (if enabled)
|
||||
|
||||
1. **Launch Emulator**:
|
||||
|
||||
```bash
|
||||
emulator -avd ReactNative_API35 -gpu host -no-metrics -no-audio
|
||||
```
|
||||
|
||||
- Use `-gpu swiftshader_indirect` if hardware acceleration (`-gpu host`) fails.
|
||||
|
||||
2. **Verify Emulator**:
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
3. **Run App**:
|
||||
Launches the the emulater automatically, if it is not already running.
|
||||
```bash
|
||||
npx run android
|
||||
```
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
Contributions are welcome and are greatly appreciated!
|
||||
|
||||
## Setup your environment with nix
|
||||
|
||||
If you are on a Linux system, you can install the nix package manager and use the nix flakes to set up your development environment.
|
||||
See [CONTRIBUTING-NIX.md](CONTRIBUTING-NIX.md)
|
||||
|
||||
## Setting up your environment
|
||||
|
||||
After forking to your own github org or account, do the following steps to get started:
|
||||
|
||||
```bash
|
||||
# prerequisites
|
||||
node --version <= 16.13.1 (for version management, get nvm [recommended])
|
||||
java sdk --version <= 11 (for version management, get jenv [optional])
|
||||
node --version >= 20 (for version management, get nvm [recommended])
|
||||
java sdk --version >= 17 (for version management, get jenv [optional])
|
||||
android sdk (https://developer.android.com/studio)
|
||||
|
||||
# clone your fork to your local machine
|
||||
@@ -18,11 +23,14 @@ git clone https://github.com/<your-account-name>/lnreader.git
|
||||
# step into local repo
|
||||
cd lnreader
|
||||
|
||||
# install pnpm (if not already installed)
|
||||
npm install -g pnpm
|
||||
|
||||
# install dependencies
|
||||
npm install
|
||||
pnpm install
|
||||
|
||||
# build the apk (the built apk will be found in ~/lnreader/android/app/build/outputs/apk/release/)
|
||||
npm run buildRelease
|
||||
pnpm run build:release:android
|
||||
```
|
||||
|
||||
### Developing on Android
|
||||
@@ -38,15 +46,12 @@ IDE
|
||||
adb devices
|
||||
|
||||
# run metro for development
|
||||
npm start
|
||||
pnpm run dev:start
|
||||
|
||||
# then to view on your android device (new terminal)
|
||||
npm run android
|
||||
pnpm run dev:android
|
||||
```
|
||||
|
||||
To view any changes to the app with new code, save your code and press "r" on the metro terminal to
|
||||
reload it. The app on the android device/emulator will reload shortly.
|
||||
|
||||
### Style & Linting
|
||||
|
||||
This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
|
||||
@@ -55,48 +60,5 @@ It is recommended that you install an eslint plugin for your editor of choice wh
|
||||
codebase, however you can always check to see if the source code is compliant by running:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
# Database
|
||||
```mermaid
|
||||
erDiagram
|
||||
Category ||--|{ NovelCategory : contains
|
||||
Category{
|
||||
INTEGER id PK
|
||||
TEXT name
|
||||
INTEGER sort
|
||||
}
|
||||
NovelCategory{
|
||||
INTEGER id PK
|
||||
INTEGER novelId FK
|
||||
INTEGER categoryId FK "default: whose sort = 1"
|
||||
}
|
||||
Novel ||--|{ NovelCategory : contains
|
||||
Novel{
|
||||
INTEGER id PK
|
||||
TEXT url
|
||||
TEXT pluginId
|
||||
TEXT name
|
||||
TEXT cover
|
||||
TEXT summary
|
||||
TEXT author
|
||||
TEXT artist
|
||||
TEXT status
|
||||
TEXT genres
|
||||
INTEGER inLibrary "default: 0"
|
||||
}
|
||||
Novel ||--o| Chapter : has
|
||||
Chapter{
|
||||
INTEGER id PK
|
||||
INTEGER novelId FK
|
||||
text url
|
||||
TEXT name
|
||||
TEXT releaseTime
|
||||
INTEGER bookmark "default: 0"
|
||||
INTEGER unread "default: 1"
|
||||
TEXT readTime
|
||||
INTEGER isDownloaded "default: 0"
|
||||
TEXT updatedTime
|
||||
TEXT chapterNumber
|
||||
}
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Ruby 3.4.0 has removed some libraries from the standard library.
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
@@ -1,47 +1,68 @@
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
|
||||
<p>
|
||||
<h1 align="center">
|
||||
LNReader
|
||||
</h1>
|
||||
<p align="center">
|
||||
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
|
||||
<a href="https://lnreader.app">
|
||||
<img src="./.github/readme-images/icon_new.png" align="center" width="128" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">LNReader</h1>
|
||||
|
||||
<p align="center">
|
||||
LNReader is a free and open source light novel reader for Android, inspired by Tachiyomi.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LNReader/lnreader/build.yml?branch=main">
|
||||
</a>
|
||||
<a href="https://discord.gg/QdcWN4MD63">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=5865F2&color=4752C4&label=discord&style=flat">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader-preview/releases/latest">
|
||||
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader-preview?color=red&label=preview">
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader"><img src="https://badges.crowdin.net/lnreader/localized.svg"></a>
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
|
||||
<a href="https://github.com/LNReader/lnreader/releases">
|
||||
<img alt="GitHub Downloads" src="https://img.shields.io/github/downloads/LNReader/lnreader/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<div align="center">
|
||||
<img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LNReader/lnreader/build.yml?labelColor=27303D&style=flat">
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader?labelColor=27303D&color=1a73e8&style=flat">
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader">
|
||||
<img src="https://badges.crowdin.net/lnreader/localized.svg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 align="center">Download</h2>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader?label=Stable&labelColor=0d7377&color=084c4e&style=flat">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader?include_prereleases&sort=semver&label=Beta&labelColor=3d3d5c&color=2a2a47&style=flat">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Get the app from our <a href="https://github.com/LNReader/lnreader/releases">releases page</a>.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<em>Android 7.0 or higher.</em>
|
||||
</p>
|
||||
|
||||
<h2 align="center">Screenshots</h2>
|
||||
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" />
|
||||
</p>
|
||||
|
||||
## Download
|
||||
|
||||
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
|
||||
|
||||
## Sources
|
||||
## Plugins
|
||||
|
||||
LNReader does not have any affiliation with the content providers available.
|
||||
|
||||
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
|
||||
Plugin requests should be created at [lnreader-plugins](https://github.com/LNReader/lnreader-plugins).
|
||||
|
||||
## Translation
|
||||
|
||||
Help translate LNReader into your language on [Crowdin](https://crowdin.com/project/lnreader).
|
||||
|
||||
## Building & Contributing
|
||||
|
||||
@@ -49,4 +70,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
|
||||
[MIT](https://github.com/LNReader/lnreader/blob/main/LICENSE)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
||||
|
||||
lib_deps = []
|
||||
|
||||
create_aar_targets(glob(["libs/*.aar"]))
|
||||
|
||||
create_jar_targets(glob(["libs/*.jar"]))
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "com.rajarsheechatterjee.LNReader",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "com.rajarsheechatterjee.LNReader",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
@@ -7,14 +8,14 @@ apply plugin: "com.facebook.react"
|
||||
*/
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||
// cliFile = file("../../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
@@ -48,6 +49,9 @@ react {
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,14 +63,14 @@ def enableProguardInReleaseBuilds = false
|
||||
* The preferred build flavor of JavaScriptCore (JSC).
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
ext {
|
||||
versionMajor = 2
|
||||
@@ -76,11 +80,11 @@ ext {
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
namespace "com.rajarsheechatterjee.LNReader"
|
||||
defaultConfig {
|
||||
applicationId 'com.rajarsheechatterjee.LNReader'
|
||||
applicationId project.hasProperty('customAppId') ? project.getProperty('customAppId') : 'com.rajarsheechatterjee.LNReader'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Generated version code. Supports versions up to 1024.1024.2048
|
||||
@@ -98,6 +102,8 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
applicationIdSuffix 'debug'
|
||||
versionNameSuffix '-debug'
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
@@ -107,41 +113,24 @@ android {
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/jni/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation 'com.facebook.soloader:soloader:0.10.5+'
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled || isWebpEnabled) {
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.0.0'
|
||||
}
|
||||
|
||||
if(isGifEnabled){
|
||||
// For animated gif support
|
||||
implementation 'com.facebook.fresco:animated-gif:2.0.0'
|
||||
}
|
||||
|
||||
if(isWebpEnabled){
|
||||
// For webp support
|
||||
implementation 'com.facebook.fresco:webpsupport:2.0.0'
|
||||
if(isWebpAnimatedEnabled){
|
||||
// Animated webp support
|
||||
implementation 'com.facebook.fresco:animated-webp:2.0.0'
|
||||
}
|
||||
}
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
tools:ignore="GoogleAppIndexingWarning"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">LNReader debug</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.rajarsheechatterjee.LNReader">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -29,7 +29,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true">
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.notifications.default_notification_icon"
|
||||
@@ -41,7 +42,6 @@
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
@@ -59,13 +59,12 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="lnreader" />
|
||||
<data android:scheme="com.rajarsheechatterjee.LNReader" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" android:foregroundServiceType="shortService" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,419 +1,183 @@
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: var(--StatusBar-currentHeight);
|
||||
word-wrap: break-word;
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
body {
|
||||
padding-left: var(--readerSettings-padding);
|
||||
padding-right: var(--readerSettings-padding);
|
||||
padding-bottom: 40px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-bottom: 40px;
|
||||
padding-top: var(--StatusBar-currentHeight);
|
||||
|
||||
font-size: var(--readerSettings-textSize);
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
text-align: var(--readerSettings-textAlign);
|
||||
line-height: var(--readerSettings-lineHeight);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: var(--readerSettings-textSize);
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
text-align: var(--readerSettings-textAlign);
|
||||
line-height: var(--readerSettings-lineHeight);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
}
|
||||
|
||||
chapter {
|
||||
display: block;
|
||||
#LNReader-chapter {
|
||||
padding-left: var(--readerSettings-padding);
|
||||
padding-right: var(--readerSettings-padding);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme-primary);
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
sup {
|
||||
line-height: 0.1em;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: var(--theme-onPrimary);
|
||||
border-collapse: collapse;
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--theme-outline);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nextButton,
|
||||
.infoText {
|
||||
width: 100%;
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: var(--theme-onPrimary);
|
||||
background-color: var(--theme-primary);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
user-select: none;
|
||||
.next-button,
|
||||
.info-text {
|
||||
margin-left: var(--readerSettings-padding);
|
||||
margin-right: var(--readerSettings-padding);
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: var(--theme-onPrimary);
|
||||
background-color: var(--theme-primary);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nextButton {
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 16px;
|
||||
.next-button {
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 16px;
|
||||
width: calc(100% - var(--readerSettings-padding) * 2);
|
||||
}
|
||||
|
||||
.infoText {
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
color: inherit;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.chapterCtn {
|
||||
min-height: var(--chapterCtn-height);
|
||||
margin-bottom: auto;
|
||||
.info-text {
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
color: inherit;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ToolWrapper {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 5vw;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal {
|
||||
top: unset;
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 120px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 500px) {
|
||||
#ToolWrapper.horizontal {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden {
|
||||
opacity: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden.horizontal {
|
||||
opacity: 0;
|
||||
bottom: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#TTS-Controller {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #TTS-Controller {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#TTS-Controller button{
|
||||
background: var(--theme-surface-0-9);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper > #ScrollBar {
|
||||
margin-top: 8px;
|
||||
width: 2.4rem;
|
||||
height: 45vh;
|
||||
min-height: 200px;
|
||||
border-radius: 1.2rem;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
touch-action: none;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
height: 12.5%;
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
height: 75%;
|
||||
align-items: unset;
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-track {
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-progress {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
top: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar{
|
||||
display: flex;
|
||||
width: 80vw;
|
||||
height: 2.4rem;
|
||||
min-height: unset;
|
||||
align-items: center;
|
||||
margin-top: unset;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
width: 75%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-track {
|
||||
height: 0.1rem;
|
||||
width: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-progress {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
left: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
display: none;
|
||||
}
|
||||
|
||||
#reader-footer-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
#reader-footer {
|
||||
display: flex;
|
||||
flex: 3;
|
||||
padding: 0.2rem 2rem;
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex: 3;
|
||||
padding: 0.2rem 2rem;
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.reader-footer-item {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reader-footer-item:first-child {
|
||||
text-align: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.reader-footer-item:last-child {
|
||||
text-align: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tts.highlight {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
.highlight {
|
||||
color: var(--readerSettings-textColor) !important;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--readerSettings-textColor) 20%,
|
||||
var(--readerSettings-theme)
|
||||
);
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--theme-surface-0-9);
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
position: fixed;
|
||||
top: var(--top);
|
||||
left: var(--left);
|
||||
-webkit-animation: menuAnimation 0.4s 0s both;
|
||||
animation: menuAnimation 0.4s 0s both;
|
||||
transform-origin: left;
|
||||
list-style: none;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 999999999;
|
||||
}
|
||||
|
||||
.contextMenu-item {
|
||||
padding: 4px,
|
||||
}
|
||||
|
||||
.contextMenu-button {
|
||||
color: var(--theme-onSurface);
|
||||
background: 0;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
padding: 6px 24px 6px 7px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
-webkit-animation: menuItemAnimation 0.2s 0s both;
|
||||
animation: menuItemAnimation 0.2s 0s both;
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: var(--theme-rippleColor);
|
||||
}
|
||||
|
||||
.contextMenu-button svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
.contextMenu-button span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes menuAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height);
|
||||
opacity: 1;
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height);
|
||||
opacity: 1;
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes menuItemAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuItemAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
.highlight * {
|
||||
color: var(--readerSettings-textColor) !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#Image-Modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
user-select: none;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#Image-Modal.show {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#Image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
body.page-reader {
|
||||
overflow: hidden;
|
||||
padding-bottom: unset;
|
||||
}
|
||||
|
||||
body.page-reader > #LNReader-chapter {
|
||||
height: calc(90vh);
|
||||
column-width: calc(100vw - var(--readerSettings-padding) * 2);
|
||||
column-gap: calc(var(--readerSettings-padding) * 2);
|
||||
transition: 200ms;
|
||||
}
|
||||
.transition-chapter {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
|
||||
position: fixed;
|
||||
left: 100vw;
|
||||
top: 0;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: var(--readerSettings-padding);
|
||||
padding-top: 30vh;
|
||||
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
z-index: 888888;
|
||||
background-color: var(--readerSettings-theme);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
#ToolWrapper {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 5vw;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal {
|
||||
top: unset;
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 120px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 500px) {
|
||||
#ToolWrapper.horizontal {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden {
|
||||
opacity: 0;
|
||||
right: 0;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden.horizontal {
|
||||
opacity: 0;
|
||||
bottom: 80px;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#ToolWrapper > #ScrollBar {
|
||||
margin-top: 8px;
|
||||
width: 2.4rem;
|
||||
height: 50vh;
|
||||
min-height: 200px;
|
||||
border-radius: 1.2rem;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
touch-action: none;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
height: 12.5%;
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
height: 75%;
|
||||
align-items: unset;
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-track {
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-progress {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
top: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar {
|
||||
display: flex;
|
||||
width: 95.5vw;
|
||||
height: 2.4rem;
|
||||
min-height: unset;
|
||||
align-items: center;
|
||||
margin-top: unset;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
width: 75%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-track {
|
||||
height: 0.1rem;
|
||||
width: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-progress {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
left: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#TTS-Controller {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#TTS-Controller button {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--readerSettings-theme) 85%,
|
||||
var(--readerSettings-textColor) 15%
|
||||
);
|
||||
outline: none;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--readerSettings-textColor) 30%,
|
||||
var(--readerSettings-theme) 70%
|
||||
);
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border-radius: 100%;
|
||||
transition: 0.5s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#TTS-Controller.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#TTS-Controller.active button {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--readerSettings-textColor);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#TTS-Controller.active svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,643 @@
|
||||
/* eslint-disable no-console */
|
||||
window.reader = new (function () {
|
||||
const {
|
||||
readerSettings,
|
||||
chapterGeneralSettings,
|
||||
novel,
|
||||
chapter,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
batteryLevel,
|
||||
autoSaveInterval,
|
||||
DEBUG,
|
||||
strings,
|
||||
} = initialReaderConfig;
|
||||
|
||||
// state
|
||||
this.hidden = van.state(true);
|
||||
this.batteryLevel = van.state(batteryLevel);
|
||||
this.readerSettings = van.state(readerSettings);
|
||||
this.generalSettings = van.state(chapterGeneralSettings);
|
||||
|
||||
this.chapterElement = document.querySelector('#LNReader-chapter');
|
||||
this.selection = window.getSelection();
|
||||
this.viewport = document.querySelector('meta[name=viewport]');
|
||||
|
||||
this.novel = novel;
|
||||
this.chapter = chapter;
|
||||
this.nextChapter = nextChapter;
|
||||
this.prevChapter = prevChapter;
|
||||
this.strings = strings;
|
||||
this.autoSaveInterval = autoSaveInterval;
|
||||
this.rawHTML = this.chapterElement.innerHTML;
|
||||
|
||||
//layout props
|
||||
this.paddingTop = parseInt(
|
||||
getComputedStyle(document.querySelector('body')).getPropertyValue(
|
||||
'padding-top',
|
||||
),
|
||||
10,
|
||||
);
|
||||
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
|
||||
this.layoutHeight = window.screen.height;
|
||||
this.layoutWidth = window.screen.width;
|
||||
|
||||
this.layoutEvent = undefined;
|
||||
this.chapterEndingVisible = van.state(false);
|
||||
|
||||
this.post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
||||
this.refresh = () => {
|
||||
if (this.generalSettings.val.pageReader) {
|
||||
this.chapterWidth = this.chapterElement.scrollWidth;
|
||||
} else {
|
||||
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
|
||||
}
|
||||
};
|
||||
|
||||
van.derive(() => {
|
||||
const settings = this.readerSettings.val;
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-theme',
|
||||
settings.theme,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-padding',
|
||||
settings.padding + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textSize',
|
||||
settings.textSize + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textColor',
|
||||
settings.textColor,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textAlign',
|
||||
settings.textAlign,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-lineHeight',
|
||||
settings.lineHeight,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-fontFamily',
|
||||
settings.fontFamily,
|
||||
);
|
||||
if (settings.fontFamily) {
|
||||
new FontFace(
|
||||
settings.fontFamily,
|
||||
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
|
||||
)
|
||||
.load()
|
||||
.then(function (loadedFont) {
|
||||
document.fonts.add(loadedFont);
|
||||
});
|
||||
} else {
|
||||
// have no affect with a font declared in head
|
||||
document.fonts.forEach(fontFace => document.fonts.delete(fontFace));
|
||||
}
|
||||
});
|
||||
|
||||
document.onscrollend = () => {
|
||||
if (!this.generalSettings.val.pageReader) {
|
||||
this.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
|
||||
10,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-global-assign, no-new-object
|
||||
console = new Object();
|
||||
console.log = function (...data) {
|
||||
reader.post({ 'type': 'console', 'msg': data?.join(' ') });
|
||||
};
|
||||
console.debug = console.log;
|
||||
console.info = console.log;
|
||||
console.warn = console.log;
|
||||
console.error = console.log;
|
||||
}
|
||||
// end reader
|
||||
})();
|
||||
|
||||
window.tts = new (function () {
|
||||
this.readableNodeNames = [
|
||||
'#text',
|
||||
'B',
|
||||
'I',
|
||||
'SPAN',
|
||||
'EM',
|
||||
'BR',
|
||||
'STRONG',
|
||||
'A',
|
||||
];
|
||||
this.prevElement = null;
|
||||
this.currentElement = reader.chapterElement;
|
||||
this.started = false;
|
||||
this.reading = false;
|
||||
|
||||
this.readable = element => {
|
||||
const ele = element ?? this.currentElement;
|
||||
if (
|
||||
ele.nodeName !== 'SPAN' &&
|
||||
this.readableNodeNames.includes(ele.nodeName)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!ele.hasChildNodes()) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < ele.childNodes.length; i++) {
|
||||
if (!this.readableNodeNames.includes(ele.childNodes.item(i).nodeName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
this.normalizeText = text => {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*([.,!?;:])\s*/g, '$1 ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
// if can find a readable node, else stop tts
|
||||
this.findNextTextNode = () => {
|
||||
if (this.currentElement.isSameNode(reader.chapterElement) && this.started) {
|
||||
return false;
|
||||
} else {
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
// is read, have to go next or go back
|
||||
if (this.currentElement.isSameNode(this.prevElement)) {
|
||||
this.prevElement = this.currentElement;
|
||||
if (this.currentElement.nextElementSibling) {
|
||||
this.currentElement = this.currentElement.nextElementSibling;
|
||||
} else {
|
||||
this.currentElement = this.currentElement.parentElement;
|
||||
}
|
||||
return this.findNextTextNode();
|
||||
} else {
|
||||
// can read? read it
|
||||
if (this.readable()) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!this.prevElement?.parentElement?.isSameNode(this.currentElement) &&
|
||||
this.currentElement.firstElementChild
|
||||
) {
|
||||
// go deep
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.firstElementChild;
|
||||
} else if (this.currentElement.nextElementSibling) {
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.nextElementSibling;
|
||||
} else {
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.parentElement;
|
||||
}
|
||||
return this.findNextTextNode();
|
||||
}
|
||||
};
|
||||
|
||||
this.next = () => {
|
||||
try {
|
||||
this.currentElement?.classList?.remove('highlight');
|
||||
if (this.findNextTextNode()) {
|
||||
const text = this.normalizeText(this.currentElement?.innerText);
|
||||
if (text) {
|
||||
this.reading = true;
|
||||
this.speak();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
} else {
|
||||
this.reading = false;
|
||||
this.stop();
|
||||
const controller = document.getElementById('TTS-Controller');
|
||||
if (controller?.firstElementChild) {
|
||||
controller.firstElementChild.innerHTML = volumnIcon;
|
||||
}
|
||||
if (reader.nextChapter) {
|
||||
reader.post({ type: 'next', autoStartTTS: true });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
};
|
||||
|
||||
this.start = element => {
|
||||
this.stop();
|
||||
this.currentElement = element ?? reader.chapterElement;
|
||||
this.next();
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
if (!this.reading) {
|
||||
if (
|
||||
this.currentElement &&
|
||||
this.currentElement.id !== 'LNReader-chapter'
|
||||
) {
|
||||
this.speak();
|
||||
this.reading = true;
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.reading = false;
|
||||
reader.post({ type: 'stop-speak' });
|
||||
reader.post({ type: 'tts-state', data: { isReading: false } });
|
||||
};
|
||||
|
||||
this.stop = () => {
|
||||
reader.post({ type: 'stop-speak' });
|
||||
this.currentElement?.classList?.remove('highlight');
|
||||
this.prevElement = null;
|
||||
this.currentElement = reader.chapterElement;
|
||||
this.started = false;
|
||||
this.reading = false;
|
||||
reader.post({ type: 'tts-state', data: { isReading: false } });
|
||||
};
|
||||
|
||||
this.isElementInViewport = element => {
|
||||
if (!element) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const windowHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight;
|
||||
const windowWidth =
|
||||
window.innerWidth || document.documentElement.clientWidth;
|
||||
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= windowHeight &&
|
||||
rect.right <= windowWidth
|
||||
);
|
||||
};
|
||||
|
||||
this.scrollToElement = element => {
|
||||
if (!element) return;
|
||||
// Check if element is partially visible (at least some part is in viewport)
|
||||
const rect = element.getBoundingClientRect();
|
||||
const windowHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight;
|
||||
const isPartiallyVisible =
|
||||
rect.top < windowHeight &&
|
||||
rect.bottom > 0 &&
|
||||
rect.left < window.innerWidth &&
|
||||
rect.right > 0;
|
||||
|
||||
// Only scroll if element is not visible or barely visible
|
||||
if (!isPartiallyVisible || rect.top < 0 || rect.bottom > windowHeight) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.speak = () => {
|
||||
if (!this.currentElement) return;
|
||||
this.prevElement = this.currentElement;
|
||||
this.scrollToElement(this.currentElement);
|
||||
this.currentElement.classList.add('highlight');
|
||||
const text = this.normalizeText(this.currentElement.innerText);
|
||||
if (text) {
|
||||
reader.post({ type: 'speak', data: text });
|
||||
reader.post({ type: 'tts-state', data: { isReading: true } });
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.pageReader = new (function () {
|
||||
this.page = van.state(0);
|
||||
this.totalPages = van.state(0);
|
||||
this.chapterEndingVisible = van.state(
|
||||
initialPageReaderConfig.nextChapterScreenVisible,
|
||||
);
|
||||
this.chapterEnding = document.getElementsByClassName('transition-chapter')[0];
|
||||
|
||||
this.showChapterEnding = (bool, instant, left) => {
|
||||
if (!this.chapterEnding) {
|
||||
this.chapterEnding =
|
||||
document.getElementsByClassName('transition-chapter')[0];
|
||||
if (!this.chapterEnding) return;
|
||||
}
|
||||
this.chapterEnding.style.transition = 'unset';
|
||||
if (bool) {
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
requestAnimationFrame(() => {
|
||||
if (!instant) this.chapterEnding.style.transition = '200ms';
|
||||
this.chapterEnding.style.transform = 'translateX(-100vw)';
|
||||
});
|
||||
this.chapterEndingVisible.val = true;
|
||||
} else {
|
||||
if (!instant) this.chapterEnding.style.transition = '200ms';
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
this.chapterEndingVisible.val = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.movePage = destPage => {
|
||||
if (this.chapterEndingVisible.val) {
|
||||
if (destPage < 0) {
|
||||
this.showChapterEnding(false);
|
||||
return;
|
||||
}
|
||||
if (destPage < this.totalPages.val) {
|
||||
this.showChapterEnding(false, false, true);
|
||||
return;
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
return reader.post({ type: 'next' });
|
||||
}
|
||||
}
|
||||
destPage = parseInt(destPage, 10);
|
||||
if (destPage < 0) {
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.prevChapter.name;
|
||||
this.showChapterEnding(true, false, true);
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'prev' });
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.nextChapter.name;
|
||||
this.showChapterEnding(true);
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'next' });
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
this.page.val = destPage;
|
||||
reader.chapterElement.style.transform =
|
||||
'translateX(-' + destPage * 100 + '%)';
|
||||
|
||||
const newProgress = parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
10,
|
||||
);
|
||||
|
||||
if (newProgress > reader.chapter.progress) {
|
||||
reader.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
10,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
van.derive(() => {
|
||||
// ignore if initial or other states change
|
||||
if (
|
||||
reader.generalSettings.val.pageReader ===
|
||||
reader.generalSettings.oldVal.pageReader
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const ratio = Math.min(
|
||||
0.99,
|
||||
(window.scrollY + reader.layoutHeight) / reader.chapterHeight,
|
||||
);
|
||||
document.body.classList.add('page-reader');
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
this.totalPages.val = parseInt(
|
||||
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
|
||||
reader.layoutWidth,
|
||||
10,
|
||||
);
|
||||
this.movePage(this.totalPages.val * ratio);
|
||||
}, 100);
|
||||
} else {
|
||||
reader.chapterElement.style = '';
|
||||
document.body.classList.remove('page-reader');
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
window.scrollTo({
|
||||
top:
|
||||
(reader.chapterHeight * (this.page.val + 1)) / this.totalPages.val -
|
||||
reader.layoutHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (pageReader.chapterEndingVisible.val) {
|
||||
pageReader.showChapterEnding(true, true);
|
||||
}
|
||||
});
|
||||
|
||||
function calculatePages() {
|
||||
reader.refresh();
|
||||
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
pageReader.totalPages.val = parseInt(
|
||||
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
|
||||
reader.layoutWidth,
|
||||
10,
|
||||
);
|
||||
|
||||
if (initialPageReaderConfig.nextChapterScreenVisible) return;
|
||||
|
||||
pageReader.movePage(
|
||||
Math.max(
|
||||
0,
|
||||
Math.round(
|
||||
(pageReader.totalPages.val * reader.chapter.progress) / 100,
|
||||
) - 1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
window.scrollTo({
|
||||
top:
|
||||
(reader.chapterHeight * reader.chapter.progress) / 100 -
|
||||
reader.layoutHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (pageReader.totalPages.val) {
|
||||
calculatePages();
|
||||
}
|
||||
});
|
||||
ro.observe(reader.chapterElement);
|
||||
|
||||
// Also call once on load
|
||||
window.addEventListener('load', () => {
|
||||
document.fonts.ready.then(() => {
|
||||
requestAnimationFrame(() => setTimeout(calculatePages, 0));
|
||||
});
|
||||
});
|
||||
|
||||
// click handler
|
||||
(function () {
|
||||
const detectTapPosition = (x, y, horizontal) => {
|
||||
if (horizontal) {
|
||||
if (x < 0.33) {
|
||||
return 'left';
|
||||
}
|
||||
if (x > 0.66) {
|
||||
return 'right';
|
||||
}
|
||||
} else {
|
||||
if (y < 0.33) {
|
||||
return 'top';
|
||||
}
|
||||
if (y > 0.66) {
|
||||
return 'bottom';
|
||||
}
|
||||
}
|
||||
return 'center';
|
||||
};
|
||||
document.onclick = e => {
|
||||
const { clientX, clientY } = e;
|
||||
const { x, y } = {
|
||||
x: clientX / reader.layoutWidth,
|
||||
y: clientY / reader.layoutHeight,
|
||||
};
|
||||
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const position = detectTapPosition(x, y, true);
|
||||
if (position === 'left') {
|
||||
pageReader.movePage(pageReader.page.val - 1);
|
||||
return;
|
||||
}
|
||||
if (position === 'right') {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (reader.generalSettings.val.tapToScroll) {
|
||||
const position = detectTapPosition(x, y, false);
|
||||
if (position === 'top') {
|
||||
window.scrollBy({
|
||||
top: -reader.layoutHeight * 0.75,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (position === 'bottom') {
|
||||
window.scrollBy({
|
||||
top: reader.layoutHeight * 0.75,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.post({ type: 'hide' });
|
||||
};
|
||||
})();
|
||||
|
||||
// swipe handler
|
||||
(function () {
|
||||
this.initialX = null;
|
||||
this.initialY = null;
|
||||
|
||||
reader.chapterElement.addEventListener('touchstart', e => {
|
||||
this.initialX = e.changedTouches[0].screenX;
|
||||
this.initialY = e.changedTouches[0].screenY;
|
||||
});
|
||||
|
||||
reader.chapterElement.addEventListener('touchmove', e => {
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const diffX =
|
||||
(e.changedTouches[0].screenX - this.initialX) / reader.layoutWidth;
|
||||
reader.chapterElement.style.transition = 'unset';
|
||||
reader.chapterElement.style.transform =
|
||||
'translateX(-' + (pageReader.page.val - diffX) * 100 + '%)';
|
||||
}
|
||||
});
|
||||
|
||||
reader.chapterElement.addEventListener('touchend', e => {
|
||||
const diffX = e.changedTouches[0].screenX - this.initialX;
|
||||
const diffY = e.changedTouches[0].screenY - this.initialY;
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
reader.chapterElement.style.transition = '200ms';
|
||||
const diffXPercentage = diffX / reader.layoutWidth;
|
||||
if (diffXPercentage < -0.3) {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
} else if (diffXPercentage > 0.3) {
|
||||
pageReader.movePage(pageReader.page.val - 1);
|
||||
} else {
|
||||
pageReader.movePage(pageReader.page.val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.target.id?.startsWith('scrollbar') ||
|
||||
e.target.id === 'Image-Modal-img'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
reader.generalSettings.val.swipeGestures &&
|
||||
Math.abs(diffX) > Math.abs(diffY) * 2 &&
|
||||
Math.abs(diffX) > 180
|
||||
) {
|
||||
if (diffX < 0 && this.initialX >= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'next' });
|
||||
} else if (diffX > 0 && this.initialX <= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'prev' });
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// text options
|
||||
(function () {
|
||||
van.derive(() => {
|
||||
let html = reader.rawHTML;
|
||||
if (reader.generalSettings.val.bionicReading) {
|
||||
html = textVide.textVide(reader.rawHTML);
|
||||
}
|
||||
|
||||
if (reader.generalSettings.val.removeExtraParagraphSpacing) {
|
||||
html = html
|
||||
.replace(/(?: \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\b[^>]*><br>\s*<br>))\s*/g,
|
||||
'',
|
||||
)
|
||||
: _
|
||||
}`,
|
||||
) //if p found, delete all double br near p
|
||||
.replace(/<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p>\s*<br>))\s*/g, '');
|
||||
}
|
||||
reader.chapterElement.innerHTML = html;
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
const volumnIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M552-152v-75q86-23 139-93.5T744-480q0-89-53.5-158.5T552-734v-75q116 25 190 117t74 211q0 119-73.5 211.5T552-152ZM144-385v-192h144l192-192v576L288-385H144Zm408 55v-302q45 20 70.5 61t25.5 90q0 49-25.5 89.5T552-330Z"/></svg>';
|
||||
const pauseIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M564-228v-504h168v504H564Zm-336 0v-504h168v504H228Z"/></svg>';
|
||||
const resumeIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M240-240v-480h72v480h-72Zm144 0 384-240-384-240v480Z"/></svg>';
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
import {
|
||||
ChapterGeneralSettings,
|
||||
ChapterReaderSettings,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import { State } from './van';
|
||||
|
||||
export interface Reader {
|
||||
// element
|
||||
chapterElement: HTMLElement;
|
||||
viewport: HTMLMetaElement;
|
||||
selection: Selection;
|
||||
|
||||
// state
|
||||
hidden: State<boolean>;
|
||||
generalSettings: State<ChapterGeneralSettings>;
|
||||
readerSettings: State<ChapterReaderSettings>;
|
||||
batteryLevel: State<number>;
|
||||
|
||||
novel: NovelInfo;
|
||||
chapter: ChapterInfo;
|
||||
nextChapter?: ChapterInfo;
|
||||
autoSaveInterval: number;
|
||||
rawHTML: string;
|
||||
strings: {
|
||||
finished: string;
|
||||
nextChapter: string;
|
||||
noNextChapter: string;
|
||||
};
|
||||
|
||||
//layout props
|
||||
paddingTop: number;
|
||||
layoutHeight: number;
|
||||
layoutWidth: number;
|
||||
chapterHeight: number;
|
||||
chapterWidth: number;
|
||||
|
||||
post: (obj: Record<string, any>) => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
interface PageReader {
|
||||
page: State<number>;
|
||||
totalPages: State<number>;
|
||||
movePage: (page: number) => void;
|
||||
}
|
||||
|
||||
interface TTS {
|
||||
started: boolean;
|
||||
reading: boolean;
|
||||
start: (element?: HTMLElement) => void;
|
||||
resume: () => void;
|
||||
stop: () => void;
|
||||
pause: () => void;
|
||||
readable: (element?: HTMLElement) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const reader: Reader;
|
||||
const tts: TTS;
|
||||
const pageReader: PageReader;
|
||||
}
|
||||
@@ -1,658 +1,339 @@
|
||||
/**
|
||||
* https://fonts.google.com/icons
|
||||
* Filled
|
||||
* other settings are default
|
||||
*/
|
||||
const copyIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Z"/></svg>';
|
||||
const volumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320Z"/></svg>';
|
||||
const selectAllIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M280-280v-400h400v400H280Zm80-80h240v-240H360v240ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm80-160h-80q0-33 23.5-56.5T200-840v80Zm80 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80q0 33-23.5 56.5T760-120Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80q33 0 56.5 23.5T840-760h-80Z"/></svg>';
|
||||
const translateIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>';
|
||||
const resumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M240-240v-480h80v480h-80Zm160 0 400-240-400-240v480Z"/></svg>';
|
||||
const pauseIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/></svg>';
|
||||
const stopIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M320-320h320v-320H320v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>';
|
||||
const selectVolumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131Zm-80-29L280-360H120v-240h160l200-200v640Zm80-160v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM40-680v-240h240v80H120v160H40ZM680-40v-80h160v-160h80v240H680Z"/></svg>';
|
||||
/**
|
||||
* @type {import("./type").Reader}
|
||||
*/
|
||||
class Reader {
|
||||
constructor() {
|
||||
this.selection = window.getSelection();
|
||||
this.viewport = document.querySelector('meta[name=viewport]');
|
||||
this.footerWrapper = document.getElementById('reader-footer-wrapper');
|
||||
this.percentage = document.getElementById('reader-percentage');
|
||||
this.battery = document.getElementById('reader-battery');
|
||||
this.time = document.getElementById('reader-time');
|
||||
this.paddingTop = parseInt(
|
||||
getComputedStyle(document.querySelector('html')).getPropertyValue(
|
||||
'padding-top',
|
||||
),
|
||||
);
|
||||
this.chapter = document.querySelector('chapter');
|
||||
this.rawHTML = this.chapter.innerHTML;
|
||||
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
|
||||
this.layoutHeight = window.innerHeight;
|
||||
this.pluginId = this.chapter.getAttribute('data-plugin-id');
|
||||
this.novelId = this.chapter.getAttribute('data-novel-id');
|
||||
this.chapterId = this.chapter.getAttribute('data-chapter-id');
|
||||
this.saveProgressInterval = setInterval(
|
||||
() =>
|
||||
this.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
|
||||
),
|
||||
}),
|
||||
autoSaveInterval,
|
||||
);
|
||||
this.time.innerText = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
this.timeInterval = setInterval(() => {
|
||||
this.time.innerText = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}, 60000);
|
||||
this.updateBatteryLevel(batteryLevel);
|
||||
this.updateGeneralSettings(initSettings);
|
||||
}
|
||||
const { div, p, img, button } = van.tags;
|
||||
|
||||
refresh = () => {
|
||||
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
|
||||
};
|
||||
post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
||||
updateReaderSettings = settings => {
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-theme',
|
||||
settings.theme,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-padding',
|
||||
settings.padding + '%',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textSize',
|
||||
settings.textSize + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textColor',
|
||||
settings.textColor,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textAlign',
|
||||
settings.textAlign,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-lineHeight',
|
||||
settings.lineHeight,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-fontFamily',
|
||||
settings.fontFamily,
|
||||
);
|
||||
new FontFace(
|
||||
settings.fontFamily,
|
||||
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
|
||||
)
|
||||
.load()
|
||||
.then(function (loadedFont) {
|
||||
document.fonts.add(loadedFont);
|
||||
});
|
||||
};
|
||||
updateGeneralSettings = settings => {
|
||||
this.showScrollPercentage = settings.showScrollPercentage;
|
||||
this.showBatteryAndTime = settings.showBatteryAndTime;
|
||||
this.verticalSeekbar = settings.verticalSeekbar;
|
||||
this.bionicReading = settings.bionicReading;
|
||||
if (this.bionicReading) {
|
||||
this.chapter.innerHTML = textVide.textVide(this.rawHTML);
|
||||
} else {
|
||||
this.chapter.innerHTML = this.rawHTML;
|
||||
}
|
||||
if (settings.swipeGestures) {
|
||||
swipeHandler.enable();
|
||||
} else {
|
||||
swipeHandler.disable();
|
||||
}
|
||||
if (!this.showScrollPercentage && !this.showBatteryAndTime) {
|
||||
this.footerWrapper.classList.add('d-none');
|
||||
} else {
|
||||
this.footerWrapper.classList.remove('d-none');
|
||||
if (this.showScrollPercentage) {
|
||||
this.percentage.classList.remove('hidden');
|
||||
} else {
|
||||
this.percentage.classList.add('hidden');
|
||||
}
|
||||
if (this.showBatteryAndTime) {
|
||||
this.battery.classList.remove('hidden');
|
||||
this.time.classList.remove('hidden');
|
||||
} else {
|
||||
this.battery.classList.add('hidden');
|
||||
this.time.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
if (!this.verticalSeekbar) {
|
||||
toolWrapper.$.classList.add('horizontal');
|
||||
} else {
|
||||
toolWrapper.$.classList.remove('horizontal');
|
||||
}
|
||||
if (scrollHandler) {
|
||||
scrollHandler.refresh();
|
||||
scrollHandler.update();
|
||||
}
|
||||
};
|
||||
updateBatteryLevel = level => {
|
||||
this.battery.innerText = Math.ceil(level * 100) + '%';
|
||||
};
|
||||
}
|
||||
const ChapterEnding = () => {
|
||||
return () =>
|
||||
reader.generalSettings.val.pageReader
|
||||
? div()
|
||||
: div(div({ class: 'info-text' }, reader.strings.finished), () =>
|
||||
reader.nextChapter
|
||||
? button(
|
||||
{
|
||||
class: 'next-button',
|
||||
onclick: e => {
|
||||
e.stopPropagation();
|
||||
reader.post({ type: 'next' });
|
||||
},
|
||||
},
|
||||
reader.strings.nextChapter,
|
||||
)
|
||||
: div({ class: 'info-text' }, reader.strings.noNextChapter),
|
||||
);
|
||||
};
|
||||
|
||||
class ToolWrapper {
|
||||
constructor() {
|
||||
this.$ = document.getElementById('ToolWrapper');
|
||||
this.$.onclick = e => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
this.tools = [];
|
||||
}
|
||||
hide = () => {
|
||||
this.$.classList.add('hidden');
|
||||
this.visible = false;
|
||||
};
|
||||
show = () => {
|
||||
this.$.classList.remove('hidden');
|
||||
this.visible = true;
|
||||
for (const tool of this.tools) {
|
||||
tool.onShow?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ScrollHandler {
|
||||
constructor(reader, toolWrapper) {
|
||||
this.reader = reader;
|
||||
this.toolWrapper = toolWrapper;
|
||||
this.$ = document.getElementById('ScrollBar');
|
||||
this.$.innerHTML = `
|
||||
<div class="scrollbar-item scrollbar-text" id="scrollbar-percentage">
|
||||
0
|
||||
</div>
|
||||
<div class="scrollbar-item" id="scrollbar-slider">
|
||||
<div id="scrollbar-track">
|
||||
<div id="scrollbar-progress">
|
||||
<div id="scrollbar-thumb-wrapper">
|
||||
<div id="scrollbar-thumb"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="scrollbar-item scrollbar-text">
|
||||
100
|
||||
</div>
|
||||
`;
|
||||
this.percentage = this.$.querySelector('#scrollbar-percentage');
|
||||
this.progress = this.$.querySelector('#scrollbar-progress');
|
||||
this.thumb = this.$.querySelector('#scrollbar-thumb-wrapper');
|
||||
this.slider = this.$.querySelector('#scrollbar-slider');
|
||||
this.sliderHeight = this.slider.clientHeight;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
|
||||
this.lock = false;
|
||||
window.onscroll = () => !this.lock && this.update();
|
||||
this.thumb.ontouchstart = () => (this.lock = true);
|
||||
this.thumb.ontouchend = () => (this.lock = false);
|
||||
this.thumb.ontouchmove = e => {
|
||||
const ratio =
|
||||
((this.reader.verticalSeekbar
|
||||
? e.changedTouches[0].clientY
|
||||
: e.changedTouches[0].clientX) -
|
||||
this.sliderOffsetY) /
|
||||
this.sliderHeight;
|
||||
this.update(ratio < 0 ? 0 : ratio);
|
||||
};
|
||||
}
|
||||
update = ratio => {
|
||||
const Scrollbar = () => {
|
||||
const horizontal = van.derive(
|
||||
() => !reader.generalSettings.val.verticalSeekbar,
|
||||
);
|
||||
let lock = false;
|
||||
const percentage = van.state(0);
|
||||
const update = ratio => {
|
||||
if (ratio === undefined) {
|
||||
ratio =
|
||||
(window.scrollY + this.reader.layoutHeight) / this.reader.chapterHeight;
|
||||
ratio = (window.scrollY + reader.layoutHeight) / reader.chapterHeight;
|
||||
}
|
||||
if (ratio > 1) {
|
||||
ratio = 1;
|
||||
}
|
||||
const percentage = parseInt(ratio * 100);
|
||||
if (this.toolWrapper.visible) {
|
||||
if (this.reader.verticalSeekbar) {
|
||||
this.progress.style.height = percentage + '%';
|
||||
this.progress.style.width = '100%';
|
||||
} else {
|
||||
this.progress.style.height = '100%';
|
||||
this.progress.style.width = percentage + '%';
|
||||
}
|
||||
this.percentage.innerText = percentage;
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
pageReader.movePage(
|
||||
parseInt(pageReader.totalPages.val * Math.min(0.99, ratio)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.lock) {
|
||||
percentage.val = parseInt(ratio * 100);
|
||||
if (lock) {
|
||||
window.scrollTo({
|
||||
top: this.reader.chapterHeight * ratio - this.reader.layoutHeight,
|
||||
top: reader.chapterHeight * ratio - reader.layoutHeight,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
if (this.reader.percentage) {
|
||||
this.reader.percentage.innerText = percentage + '%';
|
||||
}
|
||||
};
|
||||
refresh = () => {
|
||||
if (this.reader.verticalSeekbar) {
|
||||
this.sliderHeight = this.slider.clientHeight;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
|
||||
} else {
|
||||
this.sliderHeight = this.slider.clientWidth;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().left;
|
||||
}
|
||||
};
|
||||
onShow = () => {
|
||||
this.reader.refresh();
|
||||
this.refresh();
|
||||
this.update();
|
||||
};
|
||||
}
|
||||
|
||||
class SwipeHandler {
|
||||
constructor() {
|
||||
this.initialX = null;
|
||||
this.initialY = null;
|
||||
}
|
||||
|
||||
touchStartHandler = e => {
|
||||
this.initialX = e.changedTouches[0].screenX;
|
||||
this.initialY = e.changedTouches[0].screenY;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} e
|
||||
*/
|
||||
touchEndHandler = e => {
|
||||
let diffX = e.changedTouches[0].screenX - this.initialX;
|
||||
let diffY = e.changedTouches[0].screenY - this.initialY;
|
||||
if (
|
||||
e.target.id?.startsWith('scrollbar') ||
|
||||
e.target.id === 'Image-Modal-img'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(diffX) > Math.abs(diffY) * 2 && Math.abs(diffX) > 180) {
|
||||
if (diffX < 0 && this.initialX >= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'next' });
|
||||
} else if (diffX > 0 && this.initialX <= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'prev' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
enable = () => {
|
||||
document.addEventListener('touchstart', this.touchStartHandler);
|
||||
document.addEventListener('touchend', this.touchEndHandler);
|
||||
};
|
||||
|
||||
disable = () => {
|
||||
document.removeEventListener('touchstart', this.touchStartHandler);
|
||||
document.removeEventListener('touchend', this.touchEndHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./type').TextToSpeech}
|
||||
*/
|
||||
class TextToSpeech {
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
(this.leaf = this.reader.chapter), (this.TTSWrapper = null); // swap these 2 elements
|
||||
this.TTSEle = null;
|
||||
this.reading = false;
|
||||
this.$ = document.getElementById('TTS-Controller');
|
||||
this.$.classList.add('d-none');
|
||||
this.$.onclick = () => {
|
||||
if (this.reading) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.resume();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
start = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.$.classList.remove('d-none');
|
||||
this.stop();
|
||||
this.next();
|
||||
};
|
||||
|
||||
startHere = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.$.classList.remove('d-none');
|
||||
this.stop();
|
||||
if (this.reader.selection.type === 'Range') {
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
}
|
||||
this.leaf = this.reader.selection.anchorNode;
|
||||
this.makeLeafSpeakable();
|
||||
this.speak();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
|
||||
resume = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.next();
|
||||
};
|
||||
|
||||
findLeaf() {
|
||||
while (this.leaf.firstChild) {
|
||||
this.leaf = this.leaf.firstChild;
|
||||
}
|
||||
}
|
||||
|
||||
findNextLeaf() {
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
this.findLeaf();
|
||||
} else if (this.leaf.nextSibling) {
|
||||
this.leaf = this.leaf.nextSibling;
|
||||
this.findLeaf();
|
||||
} else {
|
||||
this.leaf = this.leaf.parentNode;
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.findNextLeaf();
|
||||
}
|
||||
}
|
||||
|
||||
readable() {
|
||||
return (
|
||||
this.leaf.nodeName === '#text' && /[^\s\n,.?!;":“”]/.test(this.leaf.data)
|
||||
);
|
||||
}
|
||||
|
||||
findNextTextNode() {
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
this.TTSWrapper = null;
|
||||
}
|
||||
do {
|
||||
this.findNextLeaf();
|
||||
} while (!this.readable() && !this.reader.chapter.isSameNode(this.leaf));
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.makeLeafSpeakable();
|
||||
}
|
||||
|
||||
makeLeafSpeakable() {
|
||||
this.TTSWrapper = document.createElement('tts-wrapper');
|
||||
this.TTSWrapper.innerHTML = this.leaf.data.replace(
|
||||
/([^\n,.?!;":“”]+)/g,
|
||||
matched => {
|
||||
if (matched.trim().length) {
|
||||
return `<tts>${matched}</tts>`;
|
||||
}
|
||||
return matched;
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
() => !lock && !reader.generalSettings.val.pageReader && update(),
|
||||
);
|
||||
return div(
|
||||
{ id: 'ScrollBar' },
|
||||
div(
|
||||
{ class: 'scrollbar-item scrollbar-text', id: 'scrollbar-percentage' },
|
||||
() =>
|
||||
reader.generalSettings.val.pageReader
|
||||
? pageReader.page.val + 1
|
||||
: percentage.val,
|
||||
),
|
||||
div(
|
||||
{ class: 'scrollbar-item', id: 'scrollbar-slider' },
|
||||
div(
|
||||
{ id: 'scrollbar-track' },
|
||||
div(
|
||||
{
|
||||
id: 'scrollbar-progress',
|
||||
style: () => {
|
||||
const percentageValue = reader.generalSettings.val.pageReader
|
||||
? ((pageReader.page.val + 1) / pageReader.totalPages.val) * 100
|
||||
: percentage.val;
|
||||
return horizontal.val
|
||||
? `width: ${percentageValue}%; height: 100%;`
|
||||
: `height: ${percentageValue}%; width: 100%;`;
|
||||
},
|
||||
},
|
||||
div(
|
||||
{
|
||||
id: 'scrollbar-thumb-wrapper',
|
||||
ontouchstart: () => {
|
||||
lock = true;
|
||||
},
|
||||
ontouchend: () => {
|
||||
lock = false;
|
||||
},
|
||||
ontouchmove: function (e) {
|
||||
const slider = this.parentElement.parentElement.parentElement;
|
||||
const sliderHeight = horizontal.val
|
||||
? slider.clientWidth
|
||||
: slider.clientHeight;
|
||||
const sliderOffsetY = horizontal.val
|
||||
? slider.getBoundingClientRect().left
|
||||
: slider.getBoundingClientRect().top;
|
||||
const ratio =
|
||||
((horizontal.val
|
||||
? e.changedTouches[0].clientX
|
||||
: e.changedTouches[0].clientY) -
|
||||
sliderOffsetY) /
|
||||
sliderHeight;
|
||||
update(ratio < 0 ? 0 : ratio);
|
||||
},
|
||||
},
|
||||
div({ id: 'scrollbar-thumb' }),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
div(
|
||||
{
|
||||
class: 'scrollbar-item scrollbar-text',
|
||||
id: 'scrollbar-percentage-max',
|
||||
},
|
||||
);
|
||||
this.TTSEle = this.TTSWrapper.firstElementChild;
|
||||
this.leaf.replaceWith(this.TTSWrapper);
|
||||
}
|
||||
() =>
|
||||
reader.generalSettings.val.pageReader ? pageReader.totalPages.val : 100,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
next = () => {
|
||||
try {
|
||||
if (this.TTSEle) {
|
||||
this.TTSEle.classList.remove('highlight');
|
||||
}
|
||||
if (this.reading) {
|
||||
if (this.TTSEle && this.TTSEle.nextElementSibling) {
|
||||
this.TTSEle = this.TTSEle.nextElementSibling;
|
||||
} else {
|
||||
this.findNextTextNode();
|
||||
const ToolWrapper = () => {
|
||||
const horizontal = van.derive(
|
||||
() => !reader.generalSettings.val.verticalSeekbar,
|
||||
);
|
||||
return div(
|
||||
{
|
||||
id: 'ToolWrapper',
|
||||
class: () =>
|
||||
`${reader.hidden.val ? 'hidden' : ''} ${
|
||||
horizontal.val ? 'horizontal' : ''
|
||||
}`,
|
||||
},
|
||||
Scrollbar(),
|
||||
);
|
||||
};
|
||||
|
||||
const ImageModal = ({ src }) => {
|
||||
return div(
|
||||
{
|
||||
id: 'Image-Modal',
|
||||
class: () => (src.val ? 'show' : ''),
|
||||
onclick: e => {
|
||||
if (e.target.id !== 'Image-Modal-img') {
|
||||
e.stopPropagation();
|
||||
src.val = '';
|
||||
}
|
||||
} else {
|
||||
if (!this.TTSEle) {
|
||||
this.findNextTextNode();
|
||||
}
|
||||
}
|
||||
this.speak();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
img({
|
||||
id: 'Image-Modal-img',
|
||||
src: src,
|
||||
alt: () => (src.val ? `Cant not render image from ${src.val}` : ''),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
stop = () => {
|
||||
this.$.classList.add('d-none');
|
||||
this.reading = false;
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
this.TTSWrapper = null;
|
||||
this.TTSEle = null;
|
||||
this.leaf = this.reader.chapter;
|
||||
}
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
this.$.innerHTML = `<button>${resumeIcon}</button>`;
|
||||
this.reading = false;
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
started = () => this.TTSWrapper !== null;
|
||||
|
||||
speak = () => {
|
||||
this.reading = true;
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.TTSEle.classList.add('highlight');
|
||||
this.reader.post({ type: 'speak', data: this.TTSEle?.innerText.trim() });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("./type").ContextMenu}
|
||||
*/
|
||||
class ContextMenu {
|
||||
/**
|
||||
* @param {import("./type").Reader} reader
|
||||
*/
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
this.items = {
|
||||
START_READING: this.renderItem({
|
||||
name: 'Start Reading',
|
||||
icon: volumeIcon,
|
||||
action: () => {
|
||||
tts.start();
|
||||
this.closeMenu();
|
||||
},
|
||||
}),
|
||||
START_HERE: this.renderItem({
|
||||
name: 'Start Here',
|
||||
icon: selectVolumeIcon,
|
||||
action: () => {
|
||||
tts.startHere();
|
||||
this.closeMenu();
|
||||
},
|
||||
}),
|
||||
COPY: this.renderItem({
|
||||
name: 'Copy',
|
||||
icon: copyIcon,
|
||||
action: () => {
|
||||
this.reader.post({
|
||||
type: 'copy',
|
||||
data: this.reader.selection.toString(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
SELECT_ALL: this.renderItem({
|
||||
name: 'Select All',
|
||||
icon: selectAllIcon,
|
||||
action: () => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(this.reader.chapter);
|
||||
this.reader.selection.removeAllRanges();
|
||||
this.reader.selection.addRange(range);
|
||||
},
|
||||
}),
|
||||
};
|
||||
this.commonItems = [this.items.COPY, this.items.SELECT_ALL];
|
||||
this.contextMenu = document.createElement('ul');
|
||||
this.contextMenu.classList.add('contextMenu');
|
||||
this.isOpened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{name: string, icon: string, action: () => void}} data
|
||||
* @returns
|
||||
*/
|
||||
renderItem(data) {
|
||||
const item = document.createElement('li');
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = `${data.icon} <span>${data.name}</span>`;
|
||||
button.classList.add('contextMenu-button');
|
||||
item.classList.add('contextMenu-item');
|
||||
item.appendChild(button);
|
||||
button.addEventListener('click', event => {
|
||||
event.stopPropagation();
|
||||
data.action();
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
renderMenu(items) {
|
||||
this.contextMenu.innerHTML = '';
|
||||
items.concat(this.commonItems).forEach((item, index) => {
|
||||
item.firstChild.setAttribute('style', `animation-delay: ${index * 0.1}s`);
|
||||
this.contextMenu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
if (this.isOpened) {
|
||||
this.isOpened = false;
|
||||
this.contextMenu.remove();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.reader.chapter.addEventListener('click', () => {
|
||||
this.closeMenu(this.contextMenu);
|
||||
});
|
||||
document.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
if (e.target instanceof HTMLImageElement) {
|
||||
if (imageModal.showing) {
|
||||
imageModal.hide();
|
||||
} else {
|
||||
imageModal.show(e.target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.isOpened = true;
|
||||
if (this.reader.selection.type === 'Range') {
|
||||
this.renderMenu([this.items.START_HERE, this.items.START_READING]);
|
||||
} else {
|
||||
this.renderMenu([this.items.START_READING]);
|
||||
}
|
||||
const { clientX, clientY } = e;
|
||||
e.target.appendChild(this.contextMenu);
|
||||
const positionY =
|
||||
clientY + this.contextMenu.scrollHeight >= window.innerHeight
|
||||
? window.innerHeight - this.contextMenu.scrollHeight - 20
|
||||
: clientY;
|
||||
const positionX =
|
||||
clientX + this.contextMenu.scrollWidth >= window.innerWidth
|
||||
? window.innerWidth - this.contextMenu.scrollWidth - 20
|
||||
: clientX;
|
||||
this.contextMenu.setAttribute(
|
||||
'style',
|
||||
`--width: ${this.contextMenu.scrollWidth}px;
|
||||
--height: ${this.contextMenu.scrollHeight}px;
|
||||
--top: ${positionY}px;
|
||||
--left: ${positionX}px;`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("./type").ImageModal}
|
||||
*/
|
||||
class ImageModal {
|
||||
/**
|
||||
* @param {import('./type').Reader} reader
|
||||
*/
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
this.$ = document.getElementById('Image-Modal');
|
||||
this.img = this.$.querySelector('img');
|
||||
this.showing = false;
|
||||
this.$.onclick = e => {
|
||||
if (e.target.id !== 'Image-Modal-img') {
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
this.$.ontouchmove = e => {
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLImageElement} image
|
||||
*/
|
||||
show(image) {
|
||||
this.img.src = image.src;
|
||||
this.img.alt = `Can not render image from ${image.src}`;
|
||||
this.reader.viewport.setAttribute(
|
||||
const ModalWrapper = () => {
|
||||
const imgSrc = van.state('');
|
||||
const showImage = src => {
|
||||
imgSrc.val = src;
|
||||
reader.viewport.setAttribute(
|
||||
'content',
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=10',
|
||||
);
|
||||
this.$.classList.add('show');
|
||||
this.showing = true;
|
||||
}
|
||||
hide() {
|
||||
this.$.classList.remove('show');
|
||||
this.showing = false;
|
||||
this.img.src = '';
|
||||
this.img.alt = '';
|
||||
this.reader.viewport.setAttribute(
|
||||
};
|
||||
const hideImage = () => {
|
||||
imgSrc.val = '';
|
||||
reader.viewport.setAttribute(
|
||||
'content',
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=1.0',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
var swipeHandler = new SwipeHandler();
|
||||
var toolWrapper = new ToolWrapper();
|
||||
var reader = new Reader();
|
||||
var scrollHandler = new ScrollHandler(reader, toolWrapper);
|
||||
var tts = new TextToSpeech(reader);
|
||||
toolWrapper.tools = [scrollHandler, tts];
|
||||
imageModal = new ImageModal(reader);
|
||||
const contextMenu = new ContextMenu(reader);
|
||||
contextMenu.init();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
document.addEventListener('contextmenu', e => {
|
||||
if (e.target instanceof HTMLImageElement) {
|
||||
if (!imgSrc.val) {
|
||||
showImage(e.target.src);
|
||||
} else {
|
||||
hideImage();
|
||||
}
|
||||
}
|
||||
});
|
||||
return div(ImageModal({ src: imgSrc }));
|
||||
};
|
||||
|
||||
const Footer = () => {
|
||||
const percentage = van.state(0);
|
||||
const time = van.state(
|
||||
new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}),
|
||||
);
|
||||
window.addEventListener('scroll', () => {
|
||||
let ratio = (window.scrollY + reader.layoutHeight) / reader.chapterHeight;
|
||||
if (ratio > 1) {
|
||||
ratio = 1;
|
||||
}
|
||||
percentage.val = parseInt(ratio * 100);
|
||||
});
|
||||
setInterval(() => {
|
||||
time.val = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}, 10000);
|
||||
return div(
|
||||
{
|
||||
id: 'reader-footer-wrapper',
|
||||
class: () =>
|
||||
reader.generalSettings.val.showBatteryAndTime ||
|
||||
reader.generalSettings.val.showScrollPercentage
|
||||
? ''
|
||||
: 'd-none',
|
||||
},
|
||||
div(
|
||||
{ id: 'reader-footer' },
|
||||
|
||||
div(
|
||||
{
|
||||
id: 'reader-battery',
|
||||
class: () =>
|
||||
`reader-footer-item ${
|
||||
reader.generalSettings.val.showBatteryAndTime ? '' : 'hidden'
|
||||
}`,
|
||||
},
|
||||
() => Math.ceil(reader.batteryLevel.val * 100) + '%',
|
||||
),
|
||||
div(
|
||||
{
|
||||
id: 'reader-percentage',
|
||||
class: () =>
|
||||
`reader-footer-item ${
|
||||
reader.generalSettings.val.showScrollPercentage ? '' : 'hidden'
|
||||
}`,
|
||||
},
|
||||
() =>
|
||||
reader.generalSettings.val.pageReader
|
||||
? `${pageReader.page.val + 1}/${pageReader.totalPages.val}`
|
||||
: percentage.val + '%',
|
||||
),
|
||||
div(
|
||||
{
|
||||
id: 'reader-time',
|
||||
class: () =>
|
||||
`reader-footer-item ${
|
||||
reader.generalSettings.val.showBatteryAndTime ? '' : 'hidden'
|
||||
}`,
|
||||
},
|
||||
time,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const TTSController = () => {
|
||||
let controllerElement = null;
|
||||
let hoverElement = null;
|
||||
let clientX = null;
|
||||
let clientY = null;
|
||||
return div(
|
||||
{
|
||||
id: 'TTS-Controller',
|
||||
class: () => `${reader.generalSettings.val.TTSEnable ? '' : 'hidden'}`,
|
||||
ontouchstart: () => {
|
||||
if (!controllerElement) {
|
||||
controllerElement = document.getElementById('TTS-Controller');
|
||||
}
|
||||
controllerElement.classList.add('active');
|
||||
controllerElement.style.transition = '';
|
||||
},
|
||||
ontouchmove: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clientX = e.changedTouches[0].clientX;
|
||||
clientY = e.changedTouches[0].clientY;
|
||||
controllerElement.style.left = `${clientX}px`;
|
||||
controllerElement.style.top = `${clientY}px`;
|
||||
const hoverElements = document.elementsFromPoint(clientX, clientY);
|
||||
const newHoverElement = hoverElements.reverse().find(e => {
|
||||
if (e.id.includes('scrollbar')) {
|
||||
return false;
|
||||
}
|
||||
return tts.readable(e);
|
||||
});
|
||||
hoverElement?.classList.remove('highlight');
|
||||
if (newHoverElement) {
|
||||
newHoverElement.classList.add('highlight');
|
||||
hoverElement = newHoverElement;
|
||||
} else {
|
||||
hoverElement = null;
|
||||
}
|
||||
},
|
||||
ontouchend: () => {
|
||||
controllerElement.style.transition = '1s';
|
||||
controllerElement.classList.remove('active');
|
||||
controllerElement.style.left = '20px';
|
||||
if (clientX && clientY) {
|
||||
let top = clientY < 120 ? 120 : clientY;
|
||||
if (top + 120 > reader.layoutHeight) {
|
||||
top = reader.layoutHeight - 120;
|
||||
}
|
||||
controllerElement.style.top = `${top}px`;
|
||||
if (hoverElement) {
|
||||
tts.start(hoverElement);
|
||||
controllerElement.firstElementChild.innerHTML = pauseIcon;
|
||||
}
|
||||
}
|
||||
clientX = null;
|
||||
clientY = null;
|
||||
},
|
||||
onclick: e => {
|
||||
e.stopPropagation();
|
||||
if (tts.reading) {
|
||||
tts.pause();
|
||||
controllerElement.firstElementChild.innerHTML = resumeIcon;
|
||||
} else {
|
||||
tts.resume();
|
||||
controllerElement.firstElementChild.innerHTML = pauseIcon;
|
||||
}
|
||||
},
|
||||
},
|
||||
button({ innerHTML: volumnIcon }),
|
||||
);
|
||||
};
|
||||
|
||||
const ReaderUI = () => {
|
||||
return div(
|
||||
ToolWrapper(),
|
||||
TTSController(),
|
||||
ModalWrapper(),
|
||||
Footer(),
|
||||
ChapterEnding(),
|
||||
);
|
||||
};
|
||||
|
||||
van.add(document.getElementById('reader-ui'), ReaderUI());
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable eslint-comments/no-unlimited-disable */
|
||||
/* eslint-disable */
|
||||
'use strict';
|
||||
(() => {
|
||||
function f(i, o, r, s = !1) {
|
||||
if (!(o in i) || typeof r != 'function') return !1;
|
||||
let t = i[o];
|
||||
return (
|
||||
(i[o] = function (...e) {
|
||||
return (
|
||||
s || t.apply(this, e), (t = t.bind(this)), r.apply(this, [t, ...e])
|
||||
);
|
||||
}),
|
||||
!0
|
||||
);
|
||||
}
|
||||
if (typeof window == 'undefined') throw 'Not in a browser.';
|
||||
var p = 'onscrollend' in window;
|
||||
if (!p) {
|
||||
let s = function (e, u, l) {
|
||||
if (u !== 'scrollend') {
|
||||
e.apply(this, [u, l]);
|
||||
return;
|
||||
}
|
||||
let c = this,
|
||||
n = i.get(c);
|
||||
if (!n) {
|
||||
let d;
|
||||
(n = {
|
||||
scrollFn: () => {
|
||||
clearTimeout(d),
|
||||
(d = setTimeout(() => {
|
||||
l();
|
||||
}, 100));
|
||||
},
|
||||
handlers: [l],
|
||||
}),
|
||||
e.apply(c, ['scroll', n.scrollFn, !1]),
|
||||
i.set(c, n);
|
||||
}
|
||||
},
|
||||
t = function (e, u, l) {
|
||||
if (u !== 'scrollend') {
|
||||
e.apply(this, [u, l]);
|
||||
return;
|
||||
}
|
||||
let c = this,
|
||||
n = i.get(c);
|
||||
if (n) {
|
||||
if (
|
||||
(typeof n.scrollFn == 'function' &&
|
||||
e.apply(this, ['scroll', n.scrollFn]),
|
||||
typeof l == 'undefined')
|
||||
)
|
||||
n.handlers = [];
|
||||
else {
|
||||
let d = n.handlers.indexOf(l);
|
||||
d > -1 && n.handlers.splice(d, 1);
|
||||
}
|
||||
n.handlers.length === 0 && i.delete(c);
|
||||
}
|
||||
};
|
||||
(a = s), (w = t);
|
||||
let i = new WeakMap(),
|
||||
o = null,
|
||||
r = null;
|
||||
f(window, 'addEventListener', s, !0),
|
||||
f(window, 'removeEventListener', t, !0),
|
||||
Object.defineProperty(window, 'onscrollend', {
|
||||
set: e => {
|
||||
(typeof e != 'function' || o) &&
|
||||
t.call(window, window.removeEventListener, 'scrollend', o),
|
||||
(o = e),
|
||||
e && s.call(window, window.addEventListener, 'scrollend', e);
|
||||
},
|
||||
get: () => o,
|
||||
}),
|
||||
f(document, 'addEventListener', s, !0),
|
||||
f(document, 'removeEventListener', t, !0),
|
||||
Object.defineProperty(document, 'onscrollend', {
|
||||
set: e => {
|
||||
(typeof e != 'function' || r) &&
|
||||
t.call(document, document.removeEventListener, 'scrollend', r),
|
||||
(r = e),
|
||||
e && s.call(document, document.addEventListener, 'scrollend', e);
|
||||
},
|
||||
get: () => r,
|
||||
});
|
||||
}
|
||||
var a, w;
|
||||
})();
|
||||
@@ -1,111 +1 @@
|
||||
var textVide = (function (L) {
|
||||
'use strict';
|
||||
const f = t => t == null || t === '',
|
||||
h = (t, n) =>
|
||||
Object.keys(t).reduce((e, s) => (n(e[s]) && delete e[s], e), t),
|
||||
I = (t, n) => ({ ...n, ...h(t, f) }),
|
||||
_ = ['<b>', '</b>'],
|
||||
y = t =>
|
||||
I(t, {
|
||||
sep: _,
|
||||
fixationPoint: 1,
|
||||
ignoreHtmlTag: !0,
|
||||
ignoreHtmlEntity: !0,
|
||||
}),
|
||||
u = [
|
||||
[0, 4, 12, 17, 24, 29, 35, 42, 48],
|
||||
[1, 2, 7, 10, 13, 14, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49],
|
||||
[
|
||||
1, 2, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37,
|
||||
39, 41, 43, 45, 47, 49,
|
||||
],
|
||||
[
|
||||
0, 2, 4, 5, 6, 8, 9, 11, 14, 15, 17, 18, 20, 0, 21, 23, 24, 26, 27, 29,
|
||||
30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48,
|
||||
],
|
||||
[
|
||||
0, 2, 3, 5, 6, 7, 8, 10, 11, 12, 14, 15, 17, 19, 20, 21, 23, 24, 25, 26,
|
||||
28, 29, 30, 32, 33, 34, 35, 37, 38, 39, 41, 42, 43, 44, 46, 47, 48,
|
||||
],
|
||||
],
|
||||
A = (t, n) => {
|
||||
const { length: i } = t,
|
||||
e = u[n - 1] ?? u[0],
|
||||
s = e.findIndex(c => i <= c);
|
||||
let o = i - s;
|
||||
return s === -1 && (o = i - e.length), Math.max(o, 0);
|
||||
},
|
||||
R = (t, n) =>
|
||||
typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,
|
||||
m = t =>
|
||||
Array.from(t).map(n => {
|
||||
const i = n.index,
|
||||
[e] = n,
|
||||
{ length: s } = e;
|
||||
return [i, i + s - 1];
|
||||
}),
|
||||
H = /(<!--[\s\S]*?-->)|(<[^>]*>)/g,
|
||||
N = t => {
|
||||
const n = t.matchAll(H),
|
||||
e = m(n).reverse();
|
||||
return s => {
|
||||
const o = s.index,
|
||||
c = e.find(([r]) => o > r);
|
||||
if (!c) {
|
||||
return !1;
|
||||
}
|
||||
const [, a] = c;
|
||||
return o < a;
|
||||
};
|
||||
},
|
||||
O = /(&[\w#]+;)/g,
|
||||
M = t => {
|
||||
const n = t.matchAll(O),
|
||||
e = m(n).reverse();
|
||||
return s => {
|
||||
const o = s.index,
|
||||
c = e.find(([r]) => o > r);
|
||||
if (!c) {
|
||||
return !1;
|
||||
}
|
||||
const [, a] = c;
|
||||
return o < a;
|
||||
};
|
||||
},
|
||||
x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,
|
||||
F = (t, n = {}) => {
|
||||
if (!(t != null && t.length)) {
|
||||
return '';
|
||||
}
|
||||
const {
|
||||
fixationPoint: i,
|
||||
sep: e,
|
||||
ignoreHtmlTag: s,
|
||||
ignoreHtmlEntity: o,
|
||||
} = y(n),
|
||||
c = Array.from(t.matchAll(x));
|
||||
let a = '',
|
||||
l = 0,
|
||||
r;
|
||||
s && (r = N(t));
|
||||
let d;
|
||||
o && (d = M(t));
|
||||
for (const g of c) {
|
||||
if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {
|
||||
continue;
|
||||
}
|
||||
const [D] = g,
|
||||
T = g.index,
|
||||
E = T + A(D, i),
|
||||
U = t.slice(l, T);
|
||||
(a += U), T !== E && (a += R(t.slice(T, E), e)), (l = E);
|
||||
}
|
||||
const G = t.slice(l);
|
||||
return a + G;
|
||||
};
|
||||
return (
|
||||
(L.textVide = F),
|
||||
Object.defineProperty(L, Symbol.toStringTag, { value: 'Module' }),
|
||||
L
|
||||
);
|
||||
})({});
|
||||
var textVide = function(L){'use strict'; const f = t=>t == null || t === '',h = (t,n)=>Object.keys(t).reduce((e,s)=>(n(e[s]) && delete e[s],e),t),I = (t,n)=>({...n,...h(t,f)}),_ = ['<b>','</b>'],y = t=>I(t,{sep:_,fixationPoint:1,ignoreHtmlTag:!0,ignoreHtmlEntity:!0}),u = [[0,4,12,17,24,29,35,42,48],[1,2,7,10,13,14,19,22,25,28,31,34,37,40,43,46,49],[1,2,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49],[0,2,4,5,6,8,9,11,14,15,17,18,20,0,21,23,24,26,27,29,30,32,33,35,36,38,39,41,42,44,45,47,48],[0,2,3,5,6,7,8,10,11,12,14,15,17,19,20,21,23,24,25,26,28,29,30,32,33,34,35,37,38,39,41,42,43,44,46,47,48]],A = (t,n)=>{const {length:i} = t,e = u[n - 1] ?? u[0],s = e.findIndex(c=>i <= c); let o = i - s; return s === -1 && (o = i - e.length),Math.max(o,0);},R = (t,n)=>typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,m = t=>Array.from(t).map(n=>{const i = n.index,[e] = n,{length:s} = e; return [i,i + s - 1];}),H = /<!--[^]*?-->|(<[^>]+>)/g,N = t=>{const n = t.matchAll(H),e = m(n).reverse(); return s=>{const o = s.index,c = e.find(([r])=>o > r); if (!c) {return !1;} const [,a] = c; return o < a;};},O = /&[\w#]+;/g,M = t=>{const n = t.matchAll(O),e = m(n).reverse(); return s=>{const o = s.index,c = e.find(([r])=>o > r); if (!c) {return !1;} const [,a] = c; return o < a;};},x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,F = (t,n = {})=>{if (!(t != null && t.length)) {return '';} const {fixationPoint:i,sep:e,ignoreHtmlTag:s,ignoreHtmlEntity:o} = y(n),c = Array.from(t.matchAll(x)); let a = '',l = 0,r; s && (r = N(t)); let d; o && (d = M(t)); for (const g of c){if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {continue;} const [D] = g,T = g.index,E = T + A(D,i),U = t.slice(l,T); a += U,T !== E && (a += R(t.slice(T,E),e)),l = E;} const G = t.slice(l); return a + G;}; return L.textVide = F,Object.defineProperty(L,Symbol.toStringTag,{value:'Module'}),L;}({});
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
ChapterGeneralSettings,
|
||||
ChapterReaderSettings,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import 'typescript';
|
||||
import 'typescript/lib/lib.dom';
|
||||
|
||||
interface Reader {
|
||||
selection: Selection;
|
||||
viewport: HTMLMetaElement;
|
||||
footerWrapper: HTMLDivElement;
|
||||
percentage: HTMLDivElement;
|
||||
battery: HTMLDivElement;
|
||||
time: HTMLDivElement;
|
||||
paddingTop: number;
|
||||
chapter: HTMLElement;
|
||||
chapterHeight: number;
|
||||
layoutHeight: number;
|
||||
pluginId: string;
|
||||
novelId: string;
|
||||
chapterId: string;
|
||||
saveProgressInterval: NodeJS.Timeout;
|
||||
timeInterval: NodeJS.Timeout;
|
||||
refresh: () => void;
|
||||
post: (message: { type: string; data?: any }) => void;
|
||||
updateReaderSettings: (settings: ChapterReaderSettings) => void;
|
||||
updateGeneralSettings: (settings: ChapterGeneralSettings) => void;
|
||||
updateBatteryLevel: (level: number) => void;
|
||||
}
|
||||
|
||||
interface TextToSpeech {
|
||||
reader: Reader;
|
||||
leaf: HTMLElement;
|
||||
TTSWrapper: HTMLElement;
|
||||
TTSEle: HTMLElement;
|
||||
reading: boolean;
|
||||
start: () => void;
|
||||
startHere: () => void;
|
||||
resume: () => void;
|
||||
pause: () => void;
|
||||
stop: () => void;
|
||||
started: () => boolean;
|
||||
}
|
||||
|
||||
export enum ContextMenuItem {
|
||||
START_READING = 'START_READING',
|
||||
START_HERE = 'START_HERE',
|
||||
COPY = 'COPY',
|
||||
SELECT_ALL = 'SELECT_ALL',
|
||||
}
|
||||
interface ContextMenu {
|
||||
reader: Reader;
|
||||
contextMenu: HTMLUListElement;
|
||||
items: Record<ContextMenuItem, HTMLLIElement>;
|
||||
renderItem: (data: {
|
||||
name: string;
|
||||
icon: string;
|
||||
action: () => void;
|
||||
}) => HTMLLIElement;
|
||||
renderMenu: (items: HTMLLIElement[]) => void;
|
||||
closeMenu: (menu: HTMLUListElement) => void;
|
||||
init: () => void;
|
||||
}
|
||||
|
||||
interface ImageModal {
|
||||
reader: Reader;
|
||||
showing: boolean;
|
||||
show: (image: HTMLImageElement) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'typescript';
|
||||
import 'typescript/lib/lib.dom';
|
||||
export interface State<T> {
|
||||
val: T;
|
||||
readonly oldVal: T;
|
||||
readonly rawVal: T;
|
||||
}
|
||||
|
||||
// Defining readonly view of State<T> for covariance.
|
||||
// Basically we want StateView<string> to implement StateView<string | number>
|
||||
export type StateView<T> = Readonly<State<T>>;
|
||||
|
||||
export type Val<T> = State<T> | T;
|
||||
|
||||
export type Primitive = string | number | boolean | bigint;
|
||||
|
||||
export type PropValue = Primitive | ((e: any) => void) | null;
|
||||
|
||||
export type PropValueOrDerived =
|
||||
| PropValue
|
||||
| StateView<PropValue>
|
||||
| (() => PropValue);
|
||||
|
||||
export type Props = Record<string, PropValueOrDerived> & {
|
||||
class?: PropValueOrDerived;
|
||||
};
|
||||
|
||||
export type PropsWithKnownKeys<ElementType> = Partial<{
|
||||
[K in keyof ElementType]: PropValueOrDerived;
|
||||
}>;
|
||||
|
||||
export type ValidChildDomValue = Primitive | Node | null | undefined;
|
||||
|
||||
export type BindingFunc =
|
||||
| ((dom?: Node) => ValidChildDomValue)
|
||||
| ((dom?: Element) => Element);
|
||||
|
||||
export type ChildDom =
|
||||
| ValidChildDomValue
|
||||
| StateView<Primitive | null | undefined>
|
||||
| BindingFunc
|
||||
| readonly ChildDom[];
|
||||
|
||||
export type TagFunc<Result> = (
|
||||
first?: (Props & PropsWithKnownKeys<Result>) | ChildDom,
|
||||
...rest: readonly ChildDom[]
|
||||
) => Result;
|
||||
|
||||
type Tags = Readonly<Record<string, TagFunc<Element>>> & {
|
||||
[K in keyof HTMLElementTagNameMap]: TagFunc<HTMLElementTagNameMap[K]>;
|
||||
};
|
||||
|
||||
declare function state<T>(): State<T>;
|
||||
declare function state<T>(initVal: T): State<T>;
|
||||
|
||||
export interface Van {
|
||||
readonly state: typeof state;
|
||||
readonly derive: <T>(f: () => T) => State<T>;
|
||||
readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element;
|
||||
readonly tags: Tags &
|
||||
((namespaceURI: string) => Readonly<Record<string, TagFunc<Element>>>);
|
||||
readonly hydrate: <T extends Node>(
|
||||
dom: T,
|
||||
f: (dom: T) => T | null | undefined,
|
||||
) => T;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const van: Van;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{let e,t,r,o,n,l,s,f,i,h,w,a,d,u,_,c,S,g,y,b,m,v,j,x,O; s = Object.getPrototypeOf,i = {},h = s(f = {isConnected:1}),w = s(s),a = (e,t,r,o)=>(e ?? (setTimeout(r,o),new Set())).add(t),d = (e,t,o)=>{let n = r; r = t; try {return e(o);} catch (e){return console.error(e),o;} finally {r = n;}},u = e=>e.filter(e=>e.t?.isConnected),_ = e=>n = a(n,e,()=>{for (let e of n){e.o = u(e.o),e.l = u(e.l);}n = l;},1e3),c = {get val(){return r?.i?.add(this),this.rawVal;},get oldVal(){return r?.i?.add(this),this.h;},set val(o){r?.u?.add(this),o !== this.rawVal && (this.rawVal = o,this.o.length + this.l.length ? (t?.add(this),e = a(e,this,x)) : this.h = o);}},S = e=>({__proto__:c,rawVal:e,h:e,o:[],l:[]}),g = (e,t)=>{let r = {i:new Set(),u:new Set()},n = {f:e},l = o; o = []; let s = d(e,r,t); s = (s ?? document).nodeType ? s : new Text(s); for (let e of r.i){r.u.has(e) || (_(e),e.o.push(n));} for (let e of o){e.t = s;} return o = l,n.t = s;},y = (e,t = S(),r)=>{let n = {i:new Set(),u:new Set()},l = {f:e,s:t}; l.t = r ?? o?.push(l) ?? f,t.val = d(e,n,t.rawVal); for (let e of n.i){n.u.has(e) || (_(e),e.l.push(l));} return t;},b = (e,...t)=>{for (let r of t.flat(1 / 0)){let t = s(r ?? 0),o = t === c ? g(()=>r.val) : t === w ? g(r) : r; o != l && e.append(o);} return e;},m = (e,t,...r)=>{let [o,...n] = s(r[0] ?? 0) === h ? r : [{},...r],f = e ? document.createElementNS(e,t) : document.createElement(t); for (let [e,r] of Object.entries(o)){let o = t=>t ? Object.getOwnPropertyDescriptor(t,e) ?? o(s(t)) : l,n = t + ',' + e,h = i[n] ?? (i[n] = o(s(f))?.set ?? 0),a = e.startsWith('on') ? (t,r)=>{let o = e.slice(2); f.removeEventListener(o,r),f.addEventListener(o,t);} : h ? h.bind(f) : f.setAttribute.bind(f,e),d = s(r ?? 0); e.startsWith('on') || d === w && (r = y(r),d = c),d === c ? g(()=>(a(r.val,r.h),f)) : a(r);} return b(f,n);},v = e=>({get:(t,r)=>m.bind(l,e,r)}),j = (e,t)=>t ? t !== e && e.replaceWith(t) : e.remove(),x = ()=>{let r = 0,o = [...e].filter(e=>e.rawVal !== e.h); do {t = new Set(); for (let e of new Set(o.flatMap(e=>e.l = u(e.l)))){y(e.f,e.s,e.t),e.t = l;}} while (++r < 100 && (o = [...t]).length); let n = [...e].filter(e=>e.rawVal !== e.h); e = l; for (let e of new Set(n.flatMap(e=>e.o = u(e.o)))){j(e.t,g(e.f,e.t)),e.t = l;} for (let e of n){e.h = e.rawVal;}},O = {tags:new Proxy(e=>new Proxy(m,v(e)),v()),hydrate:(e,t)=>j(e,g(t,e)),add:b,state:S,derive:y},window.van = O;}
|
||||
@@ -1,163 +0,0 @@
|
||||
package com.rajarsheechatterjee.EpubUtil;
|
||||
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
public class EpubUtil extends ReactContextBaseJavaModule {
|
||||
EpubUtil(ReactApplicationContext context){super(context);}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "EpubUtil";
|
||||
}
|
||||
|
||||
private XmlPullParser initParse(File file) throws XmlPullParserException, IOException {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||||
parser.setInput(new FileInputStream(file), null);
|
||||
parser.nextTag();
|
||||
return parser;
|
||||
}
|
||||
|
||||
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String result = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
result = parser.getText();
|
||||
parser.nextTag();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String cleanUrl(String url){
|
||||
if(url != null){
|
||||
return url.replaceFirst("#[^.]+?$", "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void parseNovelAndChapters(String epubDirPath, Promise promise) {
|
||||
try {
|
||||
File containerFile = new File(epubDirPath, "META-INF/container.xml");
|
||||
File contentMetaFile = new File(epubDirPath, getContentMetaFilePath(containerFile));
|
||||
String contentDir = contentMetaFile.getParent();
|
||||
|
||||
ReadableMap novel = getNovelMetadata(contentMetaFile, contentDir);
|
||||
promise.resolve(novel);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentMetaFilePath(File file) throws XmlPullParserException, IOException {
|
||||
XmlPullParser parser = initParse(file);
|
||||
while (parser.next() != XmlPullParser.END_TAG){
|
||||
@Nullable String tag = parser.getName();
|
||||
if(tag != null && tag.equals("rootfile")) {
|
||||
return parser.getAttributeValue(null, "full-path");
|
||||
}
|
||||
}
|
||||
return "OEBPS/content.opf"; // default
|
||||
}
|
||||
private ReadableMap getNovelMetadata(File file, String contentDir) throws XmlPullParserException, IOException {
|
||||
WritableMap novel = new WritableNativeMap();
|
||||
WritableArray chapters = new WritableNativeArray();
|
||||
XmlPullParser parser = initParse(file);
|
||||
HashMap<String, String> refMap = new HashMap<>();
|
||||
HashMap<String, String> tocMap = new HashMap<>();
|
||||
File tocFile = new File(contentDir, "toc.ncx");
|
||||
if(tocFile.exists()){
|
||||
XmlPullParser tocParser = initParse(tocFile);
|
||||
String label = null;
|
||||
while (tocParser.next() != XmlPullParser.END_DOCUMENT){
|
||||
String tag = tocParser.getName();
|
||||
if(tag != null){
|
||||
if(tag.equals("text")){
|
||||
label = readText(tocParser);
|
||||
}else if(tag.equals("content")){
|
||||
String href = cleanUrl(tocParser.getAttributeValue(null, "src"));
|
||||
if(href != null){
|
||||
tocMap.put(href, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String cover = null;
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT){
|
||||
@Nullable String tag = parser.getName();
|
||||
if(tag != null){
|
||||
switch (tag) {
|
||||
case "item": {
|
||||
String id = parser.getAttributeValue(null, "id");
|
||||
String href = parser.getAttributeValue(null, "href");
|
||||
if (id != null) {
|
||||
refMap.put(id, href);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "itemref": {
|
||||
String idRef = parser.getAttributeValue(null, "idref");
|
||||
String href = refMap.get(idRef);
|
||||
if (href != null) {
|
||||
WritableMap chapter = new WritableNativeMap();
|
||||
chapter.putString("path", contentDir + "/" + href);
|
||||
String name = tocMap.get(href);
|
||||
chapter.putString("name", name == null ? href : name);
|
||||
chapters.pushMap(chapter);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "title":
|
||||
novel.putString("name", readText(parser));
|
||||
break;
|
||||
case "creator":
|
||||
novel.putString("author", readText(parser));
|
||||
break;
|
||||
case "contributor":
|
||||
novel.putString("artist", readText(parser));
|
||||
break;
|
||||
case "description":
|
||||
novel.putString("summary", readText(parser));
|
||||
break;
|
||||
case "meta":
|
||||
String metaName = parser.getAttributeValue(null, "name");
|
||||
if(metaName != null && metaName.equals("cover")){
|
||||
cover = parser.getAttributeValue(null, "content");
|
||||
}
|
||||
break;
|
||||
}
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
if(cover != null){
|
||||
String coverPath = contentDir + "/" + refMap.get(cover);
|
||||
novel.putString("cover", coverPath);
|
||||
}
|
||||
novel.putArray("chapters", chapters);
|
||||
return novel;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.rajarsheechatterjee.EpubUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.rajarsheechatterjee.ZipArchive.ZipArchive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class EpubUtilPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new EpubUtil(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper;
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
}
|
||||
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this,R.style.SplashScreenTheme, R.id.lottie);
|
||||
SplashScreen.setAnimationFinished(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (VolumeButtonListener.isActive()) {
|
||||
|
||||
int action = event.getAction();
|
||||
int keyCode = event.getKeyCode();
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
// volup
|
||||
VolumeButtonListener.up();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
// voldown
|
||||
VolumeButtonListener.down();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "main";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.rajarsheechatterjee.LNReader
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
import com.rajarsheechatterjee.NativeVolumeButtonListener.NativeVolumeButtonListener
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
import org.devio.rn.splashscreen.SplashScreen
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val layoutParams = WindowManager.LayoutParams()
|
||||
layoutParams.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
super.onCreate(null)
|
||||
SplashScreen.show(this, R.style.SplashScreenTheme, R.id.lottie)
|
||||
SplashScreen.setAnimationFinished(true)
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
if (NativeVolumeButtonListener.isActive) {
|
||||
val action = event.action
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
NativeVolumeButtonListener.sendEvent(true)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
NativeVolumeButtonListener.sendEvent(false)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
fabricEnabled
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Configuration;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.rajarsheechatterjee.EpubUtil.EpubUtilPackage;
|
||||
import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage;
|
||||
import com.rajarsheechatterjee.TextFile.TextFilePackage;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
import com.rajarsheechatterjee.ZipArchive.ZipArchivePackage;
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
packages.add(new ZipArchivePackage());
|
||||
packages.add(new TextFilePackage());
|
||||
packages.add(new EpubUtilPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.rajarsheechatterjee.LNReader
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.rajarsheechatterjee.NativeFile.NativePackage
|
||||
import com.rajarsheechatterjee.NativeVolumeButtonListener.NativeVolumeButtonListenerPackage
|
||||
import com.rajarsheechatterjee.NativeZipArchive.NativeZipArchivePackage
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
override val reactNativeHost: ReactNativeHost =
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
add(NativePackage())
|
||||
add(NativeVolumeButtonListenerPackage())
|
||||
add(NativeZipArchivePackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
loadReactNative(this)
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.rajarsheechatterjee.NativeFile
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.facebook.react.bridge.WritableNativeMap
|
||||
import com.facebook.react.modules.network.CookieJarContainer
|
||||
import com.facebook.react.modules.network.ForwardingCookieHandler
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider
|
||||
import com.lnreader.spec.NativeFileSpec
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Headers
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.PushbackInputStream
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
|
||||
class NativeFile(context: ReactApplicationContext) :
|
||||
NativeFileSpec(context) {
|
||||
private val BUFFER_SIZE = 4096
|
||||
private val okHttpClient = OkHttpClientProvider.createClient()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
val cookieContainer = okHttpClient.cookieJar as CookieJarContainer
|
||||
val cookieHandler = ForwardingCookieHandler(reactApplicationContext)
|
||||
cookieContainer.setCookieJar(JavaNetCookieJar(cookieHandler))
|
||||
}
|
||||
|
||||
private fun getFileUri(filepath: String): Uri {
|
||||
var uri = Uri.parse(filepath)
|
||||
if (uri.scheme == null) {
|
||||
// No prefix, assuming that provided path is absolute path to file
|
||||
val file = File(filepath)
|
||||
if (file.isDirectory) {
|
||||
throw Exception("Invalid file, folder found!")
|
||||
}
|
||||
uri = Uri.parse("file://$filepath")
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
private fun getInputStream(filepath: String): InputStream {
|
||||
val uri = getFileUri(filepath)
|
||||
return reactApplicationContext.contentResolver.openInputStream(uri)
|
||||
?: throw Exception("ENOENT: could not open an input stream for '$filepath'")
|
||||
}
|
||||
|
||||
private val writeAccessByAPILevel: String
|
||||
get() = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) "w" else "rwt"
|
||||
|
||||
private fun getOutputStream(filepath: String): OutputStream {
|
||||
val uri = getFileUri(filepath)
|
||||
return reactApplicationContext.contentResolver.openOutputStream(uri, writeAccessByAPILevel)
|
||||
?: throw Exception("ENOENT: could not open an output stream for '$filepath'")
|
||||
}
|
||||
|
||||
override fun writeFile(path: String, content: String) {
|
||||
val fw = FileWriter(path)
|
||||
fw.write(content)
|
||||
fw.close()
|
||||
}
|
||||
|
||||
override fun readFile(path: String): String {
|
||||
return File(path).bufferedReader().readText()
|
||||
}
|
||||
|
||||
override fun copyFile(filepath: String, destPath: String) {
|
||||
copyFileContent(filepath, destPath)
|
||||
}
|
||||
|
||||
override fun moveFile(filepath: String, destPath: String) {
|
||||
val inFile = File(filepath)
|
||||
copyFileContent(filepath, destPath, { inFile.delete() })
|
||||
}
|
||||
|
||||
private fun copyFileContent(
|
||||
filepath: String,
|
||||
destPath: String,
|
||||
onDone: (() -> Unit)? = null,
|
||||
) {
|
||||
val inputStream = getInputStream(filepath)
|
||||
val outputStream = getOutputStream(destPath)
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } > 0) {
|
||||
outputStream.write(buffer, 0, length)
|
||||
}
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
override fun exists(filepath: String) = File(filepath).exists()
|
||||
|
||||
override fun mkdir(filepath: String) {
|
||||
val file = File(filepath)
|
||||
if (!file.exists()) {
|
||||
val created = file.mkdirs()
|
||||
if (!created) throw Exception("Directory could not be created")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteRecursive(fileOrDirectory: File) {
|
||||
if (fileOrDirectory.isDirectory) {
|
||||
for (child in fileOrDirectory.listFiles()!!) {
|
||||
deleteRecursive(child)
|
||||
}
|
||||
}
|
||||
fileOrDirectory.delete()
|
||||
}
|
||||
|
||||
override fun unlink(filepath: String) {
|
||||
val file = File(filepath)
|
||||
if (!file.exists()) throw Exception("File does not exist")
|
||||
deleteRecursive(file)
|
||||
}
|
||||
|
||||
override fun readDir(directory: String): WritableArray {
|
||||
val file = File(directory)
|
||||
if (!file.exists()) throw Exception("Folder does not exist")
|
||||
val files = file.listFiles()
|
||||
val fileMaps: WritableArray = WritableNativeArray()
|
||||
for (childFile in files!!) {
|
||||
val fileMap: WritableMap = WritableNativeMap()
|
||||
fileMap.putString("name", childFile.name)
|
||||
fileMap.putString("path", childFile.absolutePath)
|
||||
fileMap.putBoolean("isDirectory", childFile.isDirectory)
|
||||
fileMaps.pushMap(fileMap)
|
||||
}
|
||||
return fileMaps
|
||||
}
|
||||
|
||||
private fun decompressStream(input: InputStream?): InputStream {
|
||||
val pb = PushbackInputStream(input, 2)
|
||||
val signature = ByteArray(2)
|
||||
val len = pb.read(signature)
|
||||
if(len == -1) return pb;
|
||||
pb.unread(signature, 0, len)
|
||||
return if (signature[0] == 0x1f.toByte() && signature[1] == 0x8b.toByte())
|
||||
GZIPInputStream(pb) else pb
|
||||
}
|
||||
|
||||
override fun downloadFile(
|
||||
url: String,
|
||||
destPath: String,
|
||||
method: String,
|
||||
headers: ReadableMap,
|
||||
body: String?,
|
||||
promise: Promise
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val headersBuilder = Headers.Builder()
|
||||
headers.entryIterator.forEach { entry ->
|
||||
headersBuilder.add(entry.key, entry.value.toString())
|
||||
}
|
||||
val requestBuilder = Request.Builder()
|
||||
.url(url)
|
||||
.headers(headersBuilder.build())
|
||||
if (method.lowercase() == "get") {
|
||||
requestBuilder.get()
|
||||
} else if (body != null) {
|
||||
requestBuilder.post(body.toRequestBody())
|
||||
}
|
||||
|
||||
okHttpClient.newCall(requestBuilder.build())
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
promise.reject(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful || response.body == null) {
|
||||
promise.reject(Exception("Failed to download load: ${response.code}"))
|
||||
return
|
||||
}
|
||||
try {
|
||||
val inputStream = decompressStream(response.body!!.byteStream())
|
||||
FileOutputStream(destPath).use { fos ->
|
||||
inputStream.copyTo(fos, BUFFER_SIZE)
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTypedExportedConstants(): MutableMap<String, Any> {
|
||||
val constants: MutableMap<String, Any> = HashMap()
|
||||
val externalDirectory = this.reactApplicationContext.getExternalFilesDir(null)
|
||||
if (externalDirectory != null) {
|
||||
constants["ExternalDirectoryPath"] = externalDirectory.absolutePath
|
||||
}
|
||||
val externalCachesDirectory = this.reactApplicationContext.externalCacheDir
|
||||
if (externalCachesDirectory != null) {
|
||||
constants["ExternalCachesDirectoryPath"] = externalCachesDirectory.absolutePath
|
||||
}
|
||||
return constants
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.NativeFile
|
||||
|
||||
import com.facebook.react.BaseReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.module.model.ReactModuleInfo
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||
import com.lnreader.spec.NativeFileSpec
|
||||
|
||||
class NativePackage : BaseReactPackage() {
|
||||
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
||||
if (name == NativeFileSpec.NAME) {
|
||||
NativeFile(reactContext)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
||||
mapOf(
|
||||
NativeFileSpec.NAME to ReactModuleInfo(
|
||||
NativeFileSpec.NAME,
|
||||
NativeFileSpec.NAME,
|
||||
canOverrideExistingModule = false,
|
||||
needsEagerInit = false,
|
||||
isCxxModule = false,
|
||||
isTurboModule = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.rajarsheechatterjee.NativeVolumeButtonListener
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||
import com.lnreader.spec.NativeVolumeButtonListenerSpec
|
||||
|
||||
class NativeVolumeButtonListener(appContext: ReactApplicationContext) :
|
||||
NativeVolumeButtonListenerSpec(appContext) {
|
||||
init {
|
||||
NativeVolumeButtonListener.appContext = appContext
|
||||
}
|
||||
|
||||
override fun addListener(eventName: String?) {
|
||||
isActive = true
|
||||
}
|
||||
|
||||
override fun removeListeners(count: Double) {
|
||||
isActive = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var appContext: ReactApplicationContext
|
||||
var isActive = false
|
||||
|
||||
fun sendEvent(up: Boolean) {
|
||||
if (isActive) {
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
.emit(if (up) "VolumeUp" else "VolumeDown", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.NativeVolumeButtonListener
|
||||
|
||||
import com.facebook.react.BaseReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.module.model.ReactModuleInfo
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||
import com.lnreader.spec.NativeVolumeButtonListenerSpec
|
||||
|
||||
class NativeVolumeButtonListenerPackage : BaseReactPackage() {
|
||||
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
||||
if (name == NativeVolumeButtonListenerSpec.NAME) {
|
||||
NativeVolumeButtonListener(reactContext)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
||||
mapOf(
|
||||
NativeVolumeButtonListenerSpec.NAME to ReactModuleInfo(
|
||||
NativeVolumeButtonListenerSpec.NAME,
|
||||
NativeVolumeButtonListenerSpec.NAME,
|
||||
canOverrideExistingModule = false,
|
||||
needsEagerInit = false,
|
||||
isCxxModule = false,
|
||||
isTurboModule = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.rajarsheechatterjee.NativeZipArchive
|
||||
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.lnreader.spec.NativeZipArchiveSpec
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class NativeZipArchive(context: ReactApplicationContext) : NativeZipArchiveSpec(context) {
|
||||
@ReactMethod
|
||||
override fun unzip(sourceFilePath: String, distDirPath: String, promise: Promise) {
|
||||
Thread {
|
||||
try {
|
||||
ZipFile(sourceFilePath).use { zis ->
|
||||
zis.entries().asSequence().filterNot { it.isDirectory }.forEach { zipEntry ->
|
||||
val newFile = File(distDirPath, zipEntry.name)
|
||||
newFile.parentFile?.mkdirs()
|
||||
zis.getInputStream(zipEntry).use { inputStream ->
|
||||
FileOutputStream(newFile).use { fos -> inputStream.copyTo(fos, 4096) }
|
||||
}
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun zip(sourceDirPath: String, zipFilePath: String, promise: Promise) {
|
||||
Thread {
|
||||
try {
|
||||
FileOutputStream(zipFilePath).use { fos ->
|
||||
ZipOutputStream(fos).use { zos -> zipProcess(sourceDirPath, zos) }
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun remoteUnzip(
|
||||
distDirPath: String,
|
||||
urlString: String,
|
||||
headers: ReadableMap,
|
||||
promise: Promise
|
||||
) {
|
||||
val connection = URL(urlString).openConnection() as HttpURLConnection
|
||||
Thread {
|
||||
try {
|
||||
connection.requestMethod = "GET"
|
||||
val it = headers.entryIterator
|
||||
while (it.hasNext()) {
|
||||
val (key, value) = it.next()
|
||||
connection.setRequestProperty(key, value.toString())
|
||||
}
|
||||
ZipInputStream(connection.inputStream).use { zis ->
|
||||
generateSequence { zis.nextEntry }
|
||||
.filterNot { it.isDirectory }
|
||||
.forEach { zipEntry ->
|
||||
val newFile = File(distDirPath, zipEntry.name)
|
||||
newFile.parentFile?.mkdirs()
|
||||
FileOutputStream(newFile).use { fos -> zis.copyTo(fos, 4096) }
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
if (connection.responseCode == 200) {
|
||||
promise.resolve(null)
|
||||
} else {
|
||||
throw Exception("Network request failed")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun zipProcess(sourceDirPath: String, zos: ZipOutputStream) {
|
||||
val sourceDir = File(sourceDirPath)
|
||||
sourceDir.walkBottomUp().filter { it.isFile }.forEach { file ->
|
||||
val zipFileName =
|
||||
file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/")
|
||||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
|
||||
zos.putNextEntry(entry)
|
||||
file.inputStream().use { fis ->
|
||||
fis.copyTo(zos, 4096)
|
||||
fis.close()
|
||||
}
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun remoteZip(
|
||||
sourceDirPath: String,
|
||||
urlString: String,
|
||||
headers: ReadableMap,
|
||||
promise: Promise
|
||||
) {
|
||||
Thread {
|
||||
val connection = URL(urlString).openConnection() as HttpURLConnection
|
||||
try {
|
||||
connection.requestMethod = "POST"
|
||||
val it = headers.entryIterator
|
||||
while (it.hasNext()) {
|
||||
val (key, value) = it.next()
|
||||
connection.setRequestProperty(key, value.toString())
|
||||
}
|
||||
ZipOutputStream(connection.outputStream).use { zipProcess(sourceDirPath, it) }
|
||||
if (connection.responseCode == 200) {
|
||||
promise.resolve(
|
||||
connection.inputStream.bufferedReader().use { it.readText() })
|
||||
} else {
|
||||
throw Exception("Network request failed")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.NativeZipArchive
|
||||
|
||||
import com.facebook.react.BaseReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.module.model.ReactModuleInfo
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||
import com.lnreader.spec.NativeZipArchiveSpec
|
||||
|
||||
class NativeZipArchivePackage : BaseReactPackage() {
|
||||
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
||||
if (name == NativeZipArchiveSpec.NAME) {
|
||||
NativeZipArchive(reactContext)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
||||
mapOf(
|
||||
NativeZipArchiveSpec.NAME to ReactModuleInfo(
|
||||
NativeZipArchiveSpec.NAME,
|
||||
NativeZipArchiveSpec.NAME,
|
||||
canOverrideExistingModule = false,
|
||||
needsEagerInit = false,
|
||||
isCxxModule = false,
|
||||
isTurboModule = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package com.rajarsheechatterjee.NavigationBarColor;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
||||
|
||||
public class NavigationBarColorModule extends ReactContextBaseJavaModule {
|
||||
public static final String REACT_CLASS = "NavigationBarColor";
|
||||
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
|
||||
private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity";
|
||||
private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
public NavigationBarColorModule(ReactApplicationContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setNavigationBarTheme(Activity activity, Boolean light) {
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Window window = activity.getWindow();
|
||||
int flags = window.getDecorView().getSystemUiVisibility();
|
||||
if (light) {
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
} else {
|
||||
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
}
|
||||
window.getDecorView().setSystemUiVisibility(flags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> constants = new HashMap<>();
|
||||
constants.put("EXAMPLE_CONSTANT", "example");
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) {
|
||||
final WritableMap map = Arguments.createMap();
|
||||
if (getCurrentActivity() != null) {
|
||||
try {
|
||||
final Window window = getCurrentActivity().getWindow();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (color.equals("transparent") || color.equals("translucent")) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
if (color.equals("transparent")) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
} else {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
return;
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
if (animated) {
|
||||
Integer colorFrom = window.getNavigationBarColor();
|
||||
Integer colorTo = Color.parseColor(color);
|
||||
//window.setNavigationBarColor(colorTo);
|
||||
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
window.setNavigationBarColor((Integer) animator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
colorAnimation.start();
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.parseColor(color));
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE));
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void hideNavigationBar(Promise promise) {
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentActivity() != null) {
|
||||
View decorView = getCurrentActivity().getWindow().getDecorView();
|
||||
decorView.setSystemUiVisibility(UI_FLAG_HIDE_NAV_BAR);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void showNavigationBar(Promise promise) {
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentActivity() != null) {
|
||||
Window w = getCurrentActivity().getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
View decorView = w.getDecorView();
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.rajarsheechatterjee.NavigationBarColor;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class NavigationBarColorPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new NavigationBarColorModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.rajarsheechatterjee.TextFile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
|
||||
public class TextFile extends ReactContextBaseJavaModule {
|
||||
TextFile(ReactApplicationContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "TextFile";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void writeFile(String path, String content, Promise promise) {
|
||||
try {
|
||||
FileWriter fw = new FileWriter(path);
|
||||
fw.write(content);
|
||||
fw.close();
|
||||
promise.resolve(null);
|
||||
} catch (Exception e) {
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod void readFile(String path, Promise promise) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
BufferedReader br = new BufferedReader(new FileReader(path));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line).append('\n');
|
||||
promise.resolve(sb.toString());
|
||||
} catch (Exception e) {
|
||||
promise.reject(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.rajarsheechatterjee.TextFile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class TextFilePackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new TextFile(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
|
||||
public class VolumeButtonListener extends ReactContextBaseJavaModule {
|
||||
|
||||
static ReactApplicationContext appContext;
|
||||
|
||||
public VolumeButtonListener(ReactApplicationContext context) {
|
||||
super(context);
|
||||
appContext = context;
|
||||
}
|
||||
|
||||
private static boolean active = false;
|
||||
|
||||
public static boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public static boolean prevent = false;
|
||||
|
||||
public static void sendEvent(boolean up) {
|
||||
if (active) {
|
||||
WritableMap args = Arguments.createMap();
|
||||
if (up)
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeUp", args);
|
||||
else
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeDown", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void down() {
|
||||
sendEvent(false);
|
||||
}
|
||||
|
||||
public static void up() {
|
||||
sendEvent(true);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void connect() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
void disconnect() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void pause() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unpause() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void preventDefault() {
|
||||
prevent = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void noPreventDefault() {
|
||||
prevent = false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VolumeButtonListener";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addListener(String eventName) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeListeners(Integer count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class VolumeButtonListenerPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<NativeModule>();
|
||||
|
||||
modules.add(new VolumeButtonListener(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package com.rajarsheechatterjee.ZipArchive;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class ZipArchive extends ReactContextBaseJavaModule {
|
||||
ZipArchive(ReactApplicationContext context) throws Exception {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ZipArchive";
|
||||
}
|
||||
|
||||
private String escapeFilePath(String filePath){
|
||||
return filePath.replaceAll(":", "\uA789");
|
||||
}
|
||||
|
||||
private void unzipProcess(ZipInputStream zis, String distDirPath) throws Exception {
|
||||
ZipEntry zipEntry;
|
||||
int len;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||
if(zipEntry.getName().endsWith("/")) continue;
|
||||
String escapedFilePath = this.escapeFilePath(zipEntry.getName());
|
||||
File newFile = new File(distDirPath, escapedFilePath);
|
||||
newFile.getParentFile().mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
while ((len = zis.read(buffer)) > 0) fos.write(buffer, 0, len);
|
||||
fos.close();
|
||||
}
|
||||
zis.closeEntry();
|
||||
}
|
||||
@ReactMethod
|
||||
public void unzip(String sourceFilePath, String distDirPath, Promise promise) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try{
|
||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(sourceFilePath));
|
||||
unzipProcess(zis, distDirPath);
|
||||
zis.close();
|
||||
promise.resolve(null);
|
||||
} catch (Exception e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void remoteUnzip(String distDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
URL url = new URL(_url);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
if(headers != null){
|
||||
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
|
||||
while (it.hasNext()){
|
||||
Map.Entry<String, Object> entry = it.next();
|
||||
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
ZipInputStream zis = new ZipInputStream(connection.getInputStream());
|
||||
unzipProcess(zis, distDirPath);
|
||||
connection.disconnect();
|
||||
promise.resolve(null);
|
||||
}catch (Exception e){
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private ArrayList<String> walkDir(String path) {
|
||||
ArrayList<String> res = new ArrayList<>();
|
||||
File node = new File(path);
|
||||
if(node.isFile()){
|
||||
res.add(path);
|
||||
}else{
|
||||
String[] children = node.list();
|
||||
if(children != null){
|
||||
for(String filename: children){
|
||||
ArrayList<String> childPaths = walkDir(new File(path, filename).toString());
|
||||
res.addAll(childPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
private void zipProcess(String sourceDirPath, ZipOutputStream zos) throws Exception{
|
||||
ArrayList<String> paths = walkDir(sourceDirPath);
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
for(String path: paths){
|
||||
ZipEntry zipEntry = new ZipEntry(path.replace(sourceDirPath + "/", ""));
|
||||
zos.putNextEntry(zipEntry);
|
||||
try (FileInputStream fis = new FileInputStream(path)) {
|
||||
while ((len = fis.read(buffer)) > 0) {
|
||||
zos.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
zos.closeEntry();
|
||||
}
|
||||
zos.close();
|
||||
}
|
||||
@ReactMethod
|
||||
public void remoteZip(String sourceDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
URL url = new URL(_url);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
if(headers != null){
|
||||
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
|
||||
while (it.hasNext()){
|
||||
Map.Entry<String, Object> entry = it.next();
|
||||
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
ZipOutputStream zos = new ZipOutputStream(connection.getOutputStream());
|
||||
zipProcess(sourceDirPath, zos);
|
||||
if(connection.getResponseCode() == 200){
|
||||
InputStream is = connection.getInputStream();
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
for (int length; (length = is.read(buffer)) != -1; ) {
|
||||
result.write(buffer, 0, length);
|
||||
}
|
||||
is.close();
|
||||
connection.disconnect();
|
||||
promise.resolve(result.toString());
|
||||
}else{
|
||||
throw new Exception("Network request failed");
|
||||
}
|
||||
}catch (Exception e){
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.rajarsheechatterjee.ZipArchive;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ZipArchivePackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new ZipArchive(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your React Native application
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
||||
|
||||
# Define where the additional source code lives. We need to crawl back the jni, main, src, app, android folders
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
../../../../../shared/NativeEpub.cpp
|
||||
../../../../../shared/Epub.cpp
|
||||
../../../../../shared/pugixml.cpp
|
||||
)
|
||||
|
||||
# Define where CMake can find the additional header files. We need to crawl back the jni, main, src, app, android folders
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// This C++ file is part of the default configuration used by apps and is placed
|
||||
// inside react-native to encapsulate it from user space (so you won't need to
|
||||
// touch C++/Cmake code at all on Android).
|
||||
//
|
||||
// If you wish to customize it (because you want to manually link a C++ library
|
||||
// or pass a custom compilation flag) you can:
|
||||
//
|
||||
// 1. Copy this CMake file inside the `android/app/src/main/jni` folder of your
|
||||
// project
|
||||
// 2. Copy the OnLoad.cpp (in this same folder) file inside the same folder as
|
||||
// above.
|
||||
// 3. Extend your `android/app/build.gradle` as follows
|
||||
//
|
||||
// android {
|
||||
// // Other config here...
|
||||
// externalNativeBuild {
|
||||
// cmake {
|
||||
// path "src/main/jni/CMakeLists.txt"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#include <DefaultComponentsRegistry.h>
|
||||
#include <DefaultTurboModuleManagerDelegate.h>
|
||||
#include <autolinking.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <FBReactNativeSpec.h>
|
||||
#include <NativeEpub.hpp>
|
||||
|
||||
#ifdef REACT_NATIVE_APP_CODEGEN_HEADER
|
||||
#include REACT_NATIVE_APP_CODEGEN_HEADER
|
||||
#endif
|
||||
#ifdef REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
|
||||
#include REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
|
||||
#endif
|
||||
|
||||
namespace facebook::react
|
||||
{
|
||||
|
||||
void registerComponents(
|
||||
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry)
|
||||
{
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
// providerRegistry->add(concreteComponentDescriptorProvider<
|
||||
// MyComponentDescriptor>());
|
||||
|
||||
// We link app local components if available
|
||||
#ifdef REACT_NATIVE_APP_COMPONENT_REGISTRATION
|
||||
REACT_NATIVE_APP_COMPONENT_REGISTRATION(registry);
|
||||
#endif
|
||||
|
||||
// And we fallback to the components autolinked
|
||||
autolinking_registerProviders(registry);
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule> cxxModuleProvider(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker)
|
||||
{
|
||||
// Here you can provide your CXX Turbo Modules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a module called `NativeCxxModuleExample`):
|
||||
//
|
||||
// if (name == NativeCxxModuleExample::kModuleName) {
|
||||
// return std::make_shared<NativeCxxModuleExample>(jsInvoker);
|
||||
// }
|
||||
|
||||
if (name == NativeEpub::kModuleName)
|
||||
{
|
||||
return std::make_shared<NativeEpub>(jsInvoker);
|
||||
}
|
||||
|
||||
// And we fallback to the CXX module providers autolinked
|
||||
return autolinking_cxxModuleProvider(name, jsInvoker);
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule> javaModuleProvider(
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms)
|
||||
{
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a library called `samplelibrary`):
|
||||
//
|
||||
// auto module = samplelibrary_ModuleProvider(name, params);
|
||||
// if (module != nullptr) {
|
||||
// return module;
|
||||
// }
|
||||
// return FBReactNativeSpec_ModuleProvider(name, params);
|
||||
|
||||
// We link app local modules if available
|
||||
#ifdef REACT_NATIVE_APP_MODULE_PROVIDER
|
||||
auto module = REACT_NATIVE_APP_MODULE_PROVIDER(name, params);
|
||||
if (module != nullptr)
|
||||
{
|
||||
return module;
|
||||
}
|
||||
#endif
|
||||
|
||||
// We first try to look up core modules
|
||||
if (auto module = FBReactNativeSpec_ModuleProvider(name, params))
|
||||
{
|
||||
return module;
|
||||
}
|
||||
|
||||
// And we fallback to the module providers autolinked
|
||||
if (auto module = autolinking_ModuleProvider(name, params))
|
||||
{
|
||||
return module;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace facebook::react
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *)
|
||||
{
|
||||
return facebook::jni::initialize(vm, []
|
||||
{
|
||||
facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
|
||||
&facebook::react::cxxModuleProvider;
|
||||
facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
|
||||
&facebook::react::javaModuleProvider;
|
||||
facebook::react::DefaultComponentsRegistry::
|
||||
registerComponentDescriptorsFromEntryPoint =
|
||||
&facebook::react::registerComponents; });
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<string name="app_name">LNReader</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
||||
* flavor of it so it's empty as we don't want to load Flipper.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
// Do nothing as we don't want to initialize Flipper on Release.
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "33.0.0"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
kotlinVersion = "1.8.10"
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
buildToolsVersion = "36.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
ndkVersion = "27.1.12297006"
|
||||
kotlinVersion = "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@@ -19,5 +14,8 @@ buildscript {
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
@@ -36,7 +36,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
@@ -49,3 +49,8 @@ expo.webp.enabled=false
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=false
|
||||
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,10 +85,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -114,7 +114,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -133,10 +133,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -197,16 +200,20 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
@@ -241,4 +248,4 @@ eval "set -- $(
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -1,3 +1,8 @@
|
||||
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
@REM
|
||||
@REM This source code is licensed under the MIT license found in the
|
||||
@REM LICENSE file in the root directory of this source tree.
|
||||
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@@ -13,6 +18,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +50,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +64,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
@@ -89,4 +96,4 @@ exit /b %EXIT_CODE%
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
:omega
|
||||
@@ -1,6 +1,8 @@
|
||||
rootProject.name = 'LNReader'
|
||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||
rootProject.name = 'LNReader'
|
||||
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
"plugins": [
|
||||
"expo-localization"
|
||||
"expo-localization",
|
||||
"react-native-edge-to-edge",
|
||||
[
|
||||
"expo-sqlite",
|
||||
{
|
||||
"enableFTS": false,
|
||||
"useSQLCipher": false
|
||||
}
|
||||
],
|
||||
"expo-web-browser"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -1,8 +1,14 @@
|
||||
const ReactCompilerConfig = {
|
||||
target: '19',
|
||||
};
|
||||
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo', 'module:metro-react-native-babel-preset'],
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
plugins: [
|
||||
'module:@babel/plugin-transform-export-namespace-from',
|
||||
['babel-plugin-react-compiler', ReactCompilerConfig],
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
@@ -12,18 +18,20 @@ module.exports = function (api) {
|
||||
'@hooks': './src/hooks',
|
||||
'@screens': './src/screens',
|
||||
'@strings': './strings',
|
||||
'@redux': './src/redux',
|
||||
'@services': './src/services',
|
||||
'@plugins': './src/plugins',
|
||||
'@utils': './src/utils',
|
||||
'@theme': './src/theme',
|
||||
'@navigators': './src/navigators',
|
||||
'@native': './src/native',
|
||||
'@api': './src/api',
|
||||
'@type': './src/type',
|
||||
'@specs': './specs',
|
||||
'react-native-vector-icons/MaterialCommunityIcons':
|
||||
'@react-native-vector-icons/material-design-icons',
|
||||
},
|
||||
},
|
||||
],
|
||||
'react-native-reanimated/plugin',
|
||||
'react-native-worklets/plugin',
|
||||
[
|
||||
'module:react-native-dotenv',
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pull_request_title: 'chore: Update translations'
|
||||
pull_request_title: 'chore: Update Translations'
|
||||
commit_message: '[ci skip]'
|
||||
files:
|
||||
- source: /strings/languages/en/strings.json
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
declare module '@env' {
|
||||
export const MYANIMELIST_CLIENT_ID: string;
|
||||
export const ANILIST_CLIENT_ID: string;
|
||||
export const GIT_HASH: string;
|
||||
export const RELEASE_DATE: string;
|
||||
export const BUILD_TYPE: 'Debug' | 'Release' | 'Beta' | 'Github Action';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
description = "React Native development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
android_sdk.accept_license = true;
|
||||
};
|
||||
|
||||
androidEnv = pkgs.androidenv.override { licenseAccepted = true; };
|
||||
# The androidComposition now expects enableEmulator to be passed in
|
||||
# It's no longer defined here but passed from mkShell below.
|
||||
mkAndroidComposition = { enableEmulatorInput }: androidEnv.composeAndroidPackages {
|
||||
cmdLineToolsVersion = "8.0";
|
||||
platformToolsVersion = "35.0.2";
|
||||
buildToolsVersions = [ "35.0.0" ];
|
||||
platformVersions = [ "35" ];
|
||||
abiVersions = [ "x86_64" ];
|
||||
includeNDK = true;
|
||||
ndkVersions = [
|
||||
"27.1.12297006"
|
||||
"27.0.12077973"
|
||||
];
|
||||
includeSystemImages = enableEmulatorInput; # Use the input parameter
|
||||
systemImageTypes = [ "google_apis" ];
|
||||
includeEmulator = enableEmulatorInput; # Use the input parameter
|
||||
useGoogleAPIs = true;
|
||||
extraLicenses = [
|
||||
"android-sdk-arm-dbt-license"
|
||||
"android-sdk-license"
|
||||
"android-sdk-preview-license"
|
||||
"google-gdk-license"
|
||||
"intel-android-extra-license"
|
||||
"intel-android-sysimage-license"
|
||||
"mips-android-sysimage-license"
|
||||
];
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
devShells = {
|
||||
# Define a specific devShell for when emulator is enabled
|
||||
emulator =
|
||||
let
|
||||
enableEmulator = true;
|
||||
androidComposition = mkAndroidComposition { enableEmulatorInput = enableEmulator; };
|
||||
androidSdk = androidComposition.androidsdk;
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_20
|
||||
nodePackages.pnpm
|
||||
openjdk17
|
||||
androidSdk
|
||||
gradle
|
||||
git
|
||||
curl
|
||||
unzip
|
||||
which
|
||||
rsync
|
||||
scrcpy
|
||||
]
|
||||
++ pkgs.lib.optionals enableEmulator [
|
||||
libglvnd
|
||||
mesa
|
||||
vulkan-loader
|
||||
qemu_kvm
|
||||
];
|
||||
shellHook = ''
|
||||
export ANDROID_USER_HOME="$HOME/.android-nix"
|
||||
mkdir -p "$ANDROID_USER_HOME"
|
||||
if [ ! -d "$ANDROID_USER_HOME/sdk" ]; then
|
||||
echo "Setting up writable Android SDK. This may take a moment..."
|
||||
mkdir -p "$ANDROID_USER_HOME/sdk"
|
||||
rsync -a "${androidSdk}/libexec/android-sdk/" "$ANDROID_USER_HOME/sdk/"
|
||||
chmod -R u+w "$ANDROID_USER_HOME/sdk"
|
||||
echo "Writable Android SDK setup complete."
|
||||
fi
|
||||
export ANDROID_HOME="$ANDROID_USER_HOME/sdk"
|
||||
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
|
||||
export JAVA_HOME="${pkgs.openjdk17}"
|
||||
export REACT_NATIVE_PACKAGER_HOSTNAME="127.0.0.1"
|
||||
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk-bundle"
|
||||
export NDK_HOME="$ANDROID_NDK_HOME"
|
||||
echo "Emulator support is ENABLED."
|
||||
export VK_ICD_FILENAMES="${pkgs.mesa}/share/vulkan/icd.d/radeon_icd.x86_64.json"
|
||||
export LD_LIBRARY_PATH="${pkgs.libglvnd}/lib:${pkgs.mesa}/lib''${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH"
|
||||
export EGL_PLATFORM=x11
|
||||
if ! "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" list avd | grep -q "ReactNative_API35"; then
|
||||
echo "Creating default AVD (ReactNative_API35 - Pixel 8 profile)..."
|
||||
echo "no" | "$ANDROID_HOME/cmdline-tools/latest/bin/avdmanager" create avd -n "ReactNative_API35" -k "system-images;android-35;google_apis;x86_64" -d "pixel_8"
|
||||
echo "AVD created. First launch may take longer."
|
||||
fi
|
||||
echo "To launch emulator: emulator -avd ReactNative_API35 -gpu host -no-metrics -no-audio"
|
||||
echo "React Native development environment loaded!"
|
||||
echo "Android SDK: $ANDROID_HOME"
|
||||
echo "Android NDK: $ANDROID_NDK_HOME"
|
||||
echo "Java: $JAVA_HOME"
|
||||
echo "Emulator Enabled: Yes"
|
||||
echo ""
|
||||
echo "--- Available Commands ---"
|
||||
echo " npx react-native run-android"
|
||||
echo " adb devices"
|
||||
echo " scrcpy"
|
||||
echo " npm start"
|
||||
echo "--------------------------"
|
||||
'';
|
||||
};
|
||||
|
||||
# Define a specific devShell for when emulator is disabled
|
||||
default =
|
||||
let
|
||||
enableEmulator = false; # Define enableEmulator locally for this shell instance
|
||||
androidComposition = mkAndroidComposition { enableEmulatorInput = enableEmulator; };
|
||||
androidSdk = androidComposition.androidsdk;
|
||||
in
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_20
|
||||
nodePackages.pnpm
|
||||
openjdk17
|
||||
androidSdk
|
||||
gradle
|
||||
git
|
||||
curl
|
||||
unzip
|
||||
which
|
||||
rsync
|
||||
scrcpy
|
||||
]
|
||||
++ pkgs.lib.optionals enableEmulator [ # This list will be empty as enableEmulator is false
|
||||
libglvnd
|
||||
mesa
|
||||
vulkan-loader
|
||||
qemu_kvm
|
||||
];
|
||||
shellHook = ''
|
||||
export ANDROID_USER_HOME="$HOME/.android-nix"
|
||||
mkdir -p "$ANDROID_USER_HOME"
|
||||
if [ ! -d "$ANDROID_USER_HOME/sdk" ]; then
|
||||
echo "Setting up writable Android SDK. This may take a moment..."
|
||||
mkdir -p "$ANDROID_USER_HOME/sdk"
|
||||
rsync -a "${androidSdk}/libexec/android-sdk/" "$ANDROID_USER_HOME/sdk/"
|
||||
chmod -R u+w "$ANDROID_USER_HOME/sdk"
|
||||
echo "Writable Android SDK setup complete."
|
||||
fi
|
||||
export ANDROID_HOME="$ANDROID_USER_HOME/sdk"
|
||||
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
|
||||
export JAVA_HOME="${pkgs.openjdk17}"
|
||||
export REACT_NATIVE_PACKAGER_HOSTNAME="127.0.0.1"
|
||||
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk-bundle"
|
||||
export NDK_HOME="$ANDROID_NDK_HOME"
|
||||
echo "Emulator support is DISABLED. Focusing on physical device development."
|
||||
echo "React Native development environment loaded!"
|
||||
echo "Android SDK: $ANDROID_HOME"
|
||||
echo "Android NDK: $ANDROID_NDK_HOME"
|
||||
echo "Java: $JAVA_HOME"
|
||||
echo "Emulator Enabled: No"
|
||||
echo ""
|
||||
echo "--- Available Commands ---"
|
||||
echo " npx react-native run-android"
|
||||
echo " adb devices"
|
||||
echo " scrcpy"
|
||||
echo " pnpm run dev:start"
|
||||
echo "--------------------------"
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export NODE_BINARY=$(command -v node)
|
||||
@@ -0,0 +1,22 @@
|
||||
import UIKit
|
||||
import Foundation
|
||||
import Lottie
|
||||
|
||||
@objc class Dynamic: NSObject {
|
||||
|
||||
@objc func createAnimationView(rootView: UIView, lottieName: String) -> AnimationView {
|
||||
let animationView = AnimationView(name: lottieName)
|
||||
animationView.frame = rootView.frame
|
||||
animationView.center = rootView.center
|
||||
animationView.backgroundColor = UIColor.white;
|
||||
return animationView;
|
||||
}
|
||||
|
||||
@objc func play(animationView: AnimationView) {
|
||||
animationView.play(
|
||||
completion: { (success) in
|
||||
RNSplashScreen.setAnimationFinished(true)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C80B921A6F3F58F76C31292 /* libPods-LNReader.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-LNReader.a */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
147488B02DD8E9BD00C6D0A2 /* Dynamic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147488AF2DD8E9BD00C6D0A2 /* Dynamic.swift */; };
|
||||
147488B22DD8EA0700C6D0A2 /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 147488B12DD8EA0700C6D0A2 /* loading.json */; };
|
||||
147488B72DD97E8A00C6D0A2 /* RCTNativeFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147488B62DD97E8A00C6D0A2 /* RCTNativeFile.swift */; };
|
||||
147488BC2DD9882000C6D0A2 /* RCTNativeFile.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488BB2DD9882000C6D0A2 /* RCTNativeFile.mm */; };
|
||||
147488C02DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488BF2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm */; };
|
||||
147488C42DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488C32DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm */; };
|
||||
147488C82DD9A68600C6D0A2 /* RCTNativeZipArchive.mm in Sources */ = {isa = PBXBuildFile; fileRef = 147488C72DD9A68600C6D0A2 /* RCTNativeZipArchive.mm */; };
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
A1FA4E89FDA7CFAD14C43693 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F74819B237E0768BB72DED8 /* ExpoModulesProvider.swift */; };
|
||||
CF1A2676AC104DBC83DBFFF9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* LNReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LNReader.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = LNReader/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = LNReader/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = LNReader/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
147488AA2DD8E7DE00C6D0A2 /* LNReader-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "LNReader-Bridging-Header.h"; path = "LNReader/LNReader-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
147488AF2DD8E9BD00C6D0A2 /* Dynamic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dynamic.swift; sourceTree = "<group>"; };
|
||||
147488B12DD8EA0700C6D0A2 /* loading.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = loading.json; sourceTree = "<group>"; };
|
||||
147488B62DD97E8A00C6D0A2 /* RCTNativeFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RCTNativeFile.swift; sourceTree = "<group>"; };
|
||||
147488BA2DD9882000C6D0A2 /* RCTNativeFile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeFile.h; sourceTree = "<group>"; };
|
||||
147488BB2DD9882000C6D0A2 /* RCTNativeFile.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeFile.mm; sourceTree = "<group>"; };
|
||||
147488BE2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeVolumeButtonListener.h; sourceTree = "<group>"; };
|
||||
147488BF2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeVolumeButtonListener.mm; sourceTree = "<group>"; };
|
||||
147488C22DD9A27400C6D0A2 /* RCTNativeEpubUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeEpubUtil.h; sourceTree = "<group>"; };
|
||||
147488C32DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeEpubUtil.mm; sourceTree = "<group>"; };
|
||||
147488C62DD9A68600C6D0A2 /* RCTNativeZipArchive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTNativeZipArchive.h; sourceTree = "<group>"; };
|
||||
147488C72DD9A68600C6D0A2 /* RCTNativeZipArchive.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTNativeZipArchive.mm; sourceTree = "<group>"; };
|
||||
1F74819B237E0768BB72DED8 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-LNReader/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
3B4392A12AC88292D35C810B /* Pods-LNReader.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LNReader.debug.xcconfig"; path = "Target Support Files/Pods-LNReader/Pods-LNReader.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5709B34CF0A7D63546082F79 /* Pods-LNReader.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LNReader.release.xcconfig"; path = "Target Support Files/Pods-LNReader/Pods-LNReader.release.xcconfig"; sourceTree = "<group>"; };
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-LNReader.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-LNReader.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = LNReader/AppDelegate.swift; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = LNReader/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C80B921A6F3F58F76C31292 /* libPods-LNReader.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* LNReader */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */,
|
||||
147488B12DD8EA0700C6D0A2 /* loading.json */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||
147488AA2DD8E7DE00C6D0A2 /* LNReader-Bridging-Header.h */,
|
||||
147488AF2DD8E9BD00C6D0A2 /* Dynamic.swift */,
|
||||
147488B62DD97E8A00C6D0A2 /* RCTNativeFile.swift */,
|
||||
);
|
||||
name = LNReader;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
147488B92DD987DF00C6D0A2 /* NativeFile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
147488BA2DD9882000C6D0A2 /* RCTNativeFile.h */,
|
||||
147488BB2DD9882000C6D0A2 /* RCTNativeFile.mm */,
|
||||
);
|
||||
path = NativeFile;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
147488BD2DD9A0E900C6D0A2 /* NativeVolumeButtonListener */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
147488BE2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.h */,
|
||||
147488BF2DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm */,
|
||||
);
|
||||
path = NativeVolumeButtonListener;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
147488C12DD9A25E00C6D0A2 /* NativeEpubUtil */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
147488C22DD9A27400C6D0A2 /* RCTNativeEpubUtil.h */,
|
||||
147488C32DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm */,
|
||||
);
|
||||
path = NativeEpubUtil;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
147488C52DD9A66F00C6D0A2 /* NativeZipArchive */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
147488C62DD9A68600C6D0A2 /* RCTNativeZipArchive.h */,
|
||||
147488C72DD9A68600C6D0A2 /* RCTNativeZipArchive.mm */,
|
||||
);
|
||||
path = NativeZipArchive;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
24615CBE8A657680F1260CBB /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
28BBFC4322E6EFCA529574FE /* LNReader */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
28BBFC4322E6EFCA529574FE /* LNReader */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1F74819B237E0768BB72DED8 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = LNReader;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-LNReader.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
147488C52DD9A66F00C6D0A2 /* NativeZipArchive */,
|
||||
147488C12DD9A25E00C6D0A2 /* NativeEpubUtil */,
|
||||
147488BD2DD9A0E900C6D0A2 /* NativeVolumeButtonListener */,
|
||||
147488B92DD987DF00C6D0A2 /* NativeFile */,
|
||||
13B07FAE1A68108700A75B9A /* LNReader */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */,
|
||||
24615CBE8A657680F1260CBB /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* LNReader.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B4392A12AC88292D35C810B /* Pods-LNReader.debug.xcconfig */,
|
||||
5709B34CF0A7D63546082F79 /* Pods-LNReader.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* LNReader */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "LNReader" */;
|
||||
buildPhases = (
|
||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
|
||||
E2CB0E1FD58C9A385B2CE9DA /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
|
||||
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = LNReader;
|
||||
productName = LNReader;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* LNReader.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1210;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "LNReader" */;
|
||||
compatibilityVersion = "Xcode 12.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* LNReader */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
147488B22DD8EA0700C6D0A2 /* loading.json in Resources */,
|
||||
CF1A2676AC104DBC83DBFFF9 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
};
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-LNReader-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LNReader/Pods-LNReader-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E2CB0E1FD58C9A385B2CE9DA /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-LNReader/expo-configure-project.sh\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
147488C02DD9A14100C6D0A2 /* RCTNativeVolumeButtonListener.mm in Sources */,
|
||||
147488B02DD8E9BD00C6D0A2 /* Dynamic.swift in Sources */,
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
|
||||
147488BC2DD9882000C6D0A2 /* RCTNativeFile.mm in Sources */,
|
||||
147488B72DD97E8A00C6D0A2 /* RCTNativeFile.swift in Sources */,
|
||||
147488C82DD9A68600C6D0A2 /* RCTNativeZipArchive.mm in Sources */,
|
||||
A1FA4E89FDA7CFAD14C43693 /* ExpoModulesProvider.swift in Sources */,
|
||||
147488C42DD9A27400C6D0A2 /* RCTNativeEpubUtil.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-LNReader.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = LNReader/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = LNReader;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "LNReader/LNReader-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-LNReader.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = LNReader/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = LNReader;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "LNReader/LNReader-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
USE_HERMES = true;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "LNReader" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "LNReader" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1210"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "LNReader.app"
|
||||
BlueprintName = "LNReader"
|
||||
ReferencedContainer = "container:LNReader.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "LNReaderTests.xctest"
|
||||
BlueprintName = "LNReaderTests"
|
||||
ReferencedContainer = "container:LNReader.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "LNReader.app"
|
||||
BlueprintName = "LNReader"
|
||||
ReferencedContainer = "container:LNReader.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "LNReader.app"
|
||||
BlueprintName = "LNReader"
|
||||
ReferencedContainer = "container:LNReader.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:LNReader.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,71 @@
|
||||
import UIKit
|
||||
import Expo
|
||||
import React
|
||||
import React_RCTAppDelegate
|
||||
import ReactAppDependencyProvider
|
||||
import Lottie
|
||||
|
||||
@main
|
||||
class AppDelegate: ExpoAppDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var reactNativeDelegate: ReactNativeDelegate?
|
||||
var reactNativeFactory: RCTReactNativeFactory?
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = ExpoReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
|
||||
reactNativeDelegate = delegate
|
||||
reactNativeFactory = factory
|
||||
bindReactNativeFactory(factory)
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
factory.startReactNative(
|
||||
withModuleName: "main",
|
||||
in: window,
|
||||
launchOptions: launchOptions
|
||||
)
|
||||
|
||||
if let w = window {
|
||||
let splashScreenBackground = UIColor(red: 31/255, green: 32/255, blue: 36/255, alpha: 1)
|
||||
w.backgroundColor = splashScreenBackground
|
||||
w.rootViewController?.view.backgroundColor = splashScreenBackground
|
||||
let t = Dynamic()
|
||||
let animationSize = CGSize(width: 200, height: 200)
|
||||
let screenBounds = UIScreen.main.bounds
|
||||
let animationX = (screenBounds.width - animationSize.width) / 2
|
||||
let animationY = (screenBounds.height - animationSize.height) / 2
|
||||
let animationUIView: UIView = t.createAnimationView(rootView: UIView(frame:CGRect(x: animationX, y: animationY, width: animationSize.width, height: animationSize.height)), lottieName:"loading")
|
||||
RNSplashScreen.showLottieSplash(animationUIView, inRootView: w)
|
||||
animationUIView.backgroundColor = UIColor(white: 1, alpha:0)
|
||||
t.play(animationView: animationUIView as! AnimationView)
|
||||
RNSplashScreen.setAnimationFinished(true)
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
override func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||
return RCTLinkingManager.application(application, open: url, options: options);
|
||||
}
|
||||
}
|
||||
|
||||
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
|
||||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||
// needed to return the correct URL for expo-dev-client.
|
||||
bridge.bundleURL ?? bundleURL()
|
||||
}
|
||||
|
||||
override func bundleURL() -> URL? {
|
||||
#if DEBUG
|
||||
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
||||
#else
|
||||
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1 @@
|
||||
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>LNReader</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>LNReader</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>lnreader</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// LNReader-Bridging-Header.h
|
||||
// LNReader
|
||||
//
|
||||
// Created by QUAN on 17/5/25.
|
||||
//
|
||||
|
||||
#ifndef LNReader_Bridging_Header_h
|
||||
#define LNReader_Bridging_Header_h
|
||||
|
||||
#import "RNSplashScreen.h"
|
||||
#import <React/RCTLinkingManager.h>
|
||||
|
||||
#endif /* LNReader_Bridging_Header_h */
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
<color key="backgroundColor" red="0.12156862745098039" green="0.12549019607843137" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<color key="tintColor" red="0.12156862745098039" green="0.12549019607843137" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
<string>0A2A.1</string>
|
||||
<string>3B52.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
<string>85F4.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <LNReaderSpec/LNReaderSpec.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RCTNativeEpubUtil : NSObject<NativeEpubUtilSpec>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
|
||||
#import "RCTNativeEpubUtil.h"
|
||||
|
||||
@implementation RCTNativeEpubUtil
|
||||
|
||||
|
||||
+ (NSString *)moduleName {
|
||||
return @"NativeEpubUtil";
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
||||
return std::make_shared<facebook::react::NativeEpubUtilSpecJSI>(params);
|
||||
}
|
||||
|
||||
- (NSDictionary * _Nullable)parseNovelAndChapters:(nonnull NSString *)epubDirPath {
|
||||
NSMutableDictionary * res = [NSMutableDictionary dictionary];
|
||||
// TODO: implement parse epub
|
||||
return res;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,10 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <LNReaderSpec/LNReaderSpec.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RCTNativeFile : NSObject <NativeFileSpec>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,92 @@
|
||||
#import "RCTNativeFile.h"
|
||||
|
||||
@implementation RCTNativeFile
|
||||
|
||||
+ (NSString *)moduleName {
|
||||
return @"NativeFile";
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
||||
return std::make_shared<facebook::react::NativeFileSpecJSI>(params);
|
||||
}
|
||||
|
||||
- (void)copyFile:(nonnull NSString *)sourcePath destPath:(nonnull NSString *)destPath {
|
||||
[[NSFileManager defaultManager] copyItemAtPath:sourcePath toPath:destPath error:nil];
|
||||
}
|
||||
|
||||
- (void)downloadFile:(nonnull NSString *)url destPath:(nonnull NSString *)destPath method:(nonnull NSString *)method headers:(nonnull NSDictionary *)headers body:(nonnull NSString *)body resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
||||
}
|
||||
|
||||
- (nonnull NSNumber *)exists:(nonnull NSString *)filePath {
|
||||
|
||||
BOOL isExisted = [[NSFileManager defaultManager] fileExistsAtPath:filePath];
|
||||
|
||||
return [NSNumber numberWithBool:isExisted];
|
||||
}
|
||||
|
||||
- (NSDictionary *)getConstants {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths firstObject];
|
||||
|
||||
NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *cachesDirectory = [cachePaths firstObject];
|
||||
|
||||
return @{
|
||||
@"ExternalDirectoryPath": documentsDirectory,
|
||||
@"ExternalCachesDirectoryPath": cachesDirectory
|
||||
};
|
||||
}
|
||||
|
||||
- (void)mkdir:(nonnull NSString *)filePath {
|
||||
[
|
||||
[NSFileManager defaultManager] createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil
|
||||
];
|
||||
}
|
||||
|
||||
- (void)moveFile:(nonnull NSString *)sourcePath destPath:(nonnull NSString *)destPath {
|
||||
[
|
||||
[NSFileManager defaultManager] moveItemAtPath:sourcePath toPath:destPath error:nil
|
||||
];
|
||||
}
|
||||
|
||||
- (nonnull NSArray<NSDictionary *> *)readDir:(nonnull NSString *)dirPath {
|
||||
NSMutableArray<NSDictionary *>* res = [NSMutableArray array];
|
||||
NSArray *content = [
|
||||
[NSFileManager defaultManager]
|
||||
contentsOfDirectoryAtPath:dirPath error:nil
|
||||
];
|
||||
for(int i=0; i<content.count; i++){
|
||||
NSString *fileName = [content objectAtIndex:i];
|
||||
NSString *path = [dirPath stringByAppendingFormat:@"%@%@",@"/",fileName];
|
||||
BOOL isDir;
|
||||
[[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
|
||||
[res addObject: @{
|
||||
@"name": fileName,
|
||||
@"path": path,
|
||||
@"isDirectory": @(isDir)
|
||||
}];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
- (nonnull NSString *)readFile:(nonnull NSString *)path {
|
||||
return [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
||||
}
|
||||
|
||||
- (void)unlink:(nonnull NSString *)filePath {
|
||||
[
|
||||
[NSFileManager defaultManager] removeItemAtPath:filePath error:nil
|
||||
];
|
||||
}
|
||||
|
||||
- (void)writeFile:(nonnull NSString *)path content:(nonnull NSString *)content {
|
||||
[content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
||||
}
|
||||
|
||||
- (nonnull facebook::react::ModuleConstants<JS::NativeFile::Constants::Builder>)constantsToExport {
|
||||
return [self getConstants];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,10 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <LNReaderSpec/LNReaderSpec.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RCTNativeVolumeButtonListener : NSObject<NativeVolumeButtonListenerSpec>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,21 @@
|
||||
#import "RCTNativeVolumeButtonListener.h"
|
||||
|
||||
@implementation RCTNativeVolumeButtonListener
|
||||
|
||||
+ (NSString *)moduleName {
|
||||
return @"NativeVolumeButtonListener";
|
||||
}
|
||||
|
||||
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
||||
return std::make_shared<facebook::react::NativeVolumeButtonListenerSpecJSI>(params);
|
||||
}
|
||||
|
||||
- (void)addListener:(nonnull NSString *)eventName {
|
||||
// TODO: implement addlistener
|
||||
}
|
||||
|
||||
- (void)removeListeners:(double)count {
|
||||
// TODO: implement count listeners
|
||||
}
|
||||
|
||||
@end
|
||||