Compare commits
235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb1a5d9e82 | |||
| 7e20f7dccb | |||
| 0fab0143b1 | |||
| 5a0717d2c7 | |||
| 09652f9962 | |||
| eef58f212f | |||
| 523aa90c19 | |||
| eb310f7821 | |||
| af2a224781 | |||
| 990cd4f2eb | |||
| 3ac611f633 | |||
| 909504a721 | |||
| 63349de1bb | |||
| c6679b7f41 | |||
| 15560b67b1 | |||
| 7883b28cd9 | |||
| 64707409bb | |||
| 7f1f76408b | |||
| 51560195ba | |||
| 084dcccabc | |||
| 3ad6e372fd | |||
| 179feb56ee | |||
| 67e01bc2df | |||
| 3ece098b94 | |||
| 586e085141 | |||
| e0c89cdd9f | |||
| 3bf025108a | |||
| f69e5d6a7e | |||
| a727c229ca | |||
| 57eca11a9e | |||
| 197d8670f8 | |||
| 675f19ef90 | |||
| 8a12529ba8 | |||
| c3482a8510 | |||
| e4246dee59 | |||
| 23f9b183be | |||
| 1eb8c587c7 | |||
| c3b75ebeb2 | |||
| 91358ad3dd | |||
| 13885320ae | |||
| b9d1abcf27 | |||
| c3260e8e01 | |||
| c0877a9b06 | |||
| 96fb52df17 | |||
| 76106317b6 | |||
| e75f4fba5d | |||
| 4dc933ee5a | |||
| bfb12b04aa | |||
| b7639ab55a | |||
| ecc5b84a64 | |||
| eb12bdfd62 | |||
| 9612bf6f89 | |||
| 0ed7d878b4 | |||
| 5f45e4c7f2 | |||
| ca24671a2f | |||
| ee3a4f20bc | |||
| 0c3542836c | |||
| f71c9c8594 | |||
| f53546cc01 | |||
| 594b078fad | |||
| ac1b2f5ce4 | |||
| 648d21062f | |||
| 89d715ce6b | |||
| bd527a02ef | |||
| 11117c1b54 | |||
| ba25d5b8fe | |||
| 31cb4b994a | |||
| 72ef618af1 | |||
| 936fd223bb | |||
| 6d26438d2d | |||
| 5eb5a55c4c | |||
| 6cd86d1422 | |||
| 9783c4d5e3 | |||
| 87f602a1fd | |||
| 3d32ddd6e9 | |||
| 7ddd934c2b | |||
| b53c91f748 | |||
| 9d106a3716 | |||
| 8b0a8dc4c0 | |||
| 72bfcad034 | |||
| 345d084eba | |||
| 10b6042a9c | |||
| bd71611725 | |||
| 8f53550d2c | |||
| 52914b978d | |||
| ff12759a66 | |||
| e62868b82c | |||
| 97be48dfe1 | |||
| cbe7d70e2d | |||
| 6548e9a6f8 | |||
| 138fdff2e5 | |||
| f1dd4b77a8 | |||
| 9b356ffd17 | |||
| 5cf7e15d76 | |||
| 6a7e90c179 | |||
| 99c31d56bb | |||
| 4d98247980 | |||
| 164f5d2592 | |||
| 89cddf9e3d | |||
| eac6ad584e | |||
| 93fe04c28c | |||
| c9358b3a9f | |||
| 67d2c7c01b | |||
| c30441b949 | |||
| 6d2a9f8e15 | |||
| 799845426c | |||
| 8ac628b3de | |||
| d02291c22c | |||
| 7af0911f5b | |||
| 098782d6aa | |||
| 732628c40b | |||
| 5e62c36a9b | |||
| 3d102a52a7 | |||
| 4dcf213037 | |||
| 4de82472ee | |||
| 58db0352e6 | |||
| 74808f086e | |||
| b96351ea81 | |||
| eb8dc6df36 | |||
| 0c8546188e | |||
| e9f6bdaa20 | |||
| 3d34658d0f | |||
| d44ce7412a | |||
| 17c891e133 | |||
| 9805da63cf | |||
| afc20277e0 | |||
| 14061d207b | |||
| 916374b5df | |||
| 0a2a6c96ca | |||
| 65e8610030 | |||
| 1322065290 | |||
| 22a1efd1e5 | |||
| 269606ebb8 | |||
| 4cb1890b62 | |||
| a421177f0c | |||
| a16ec87631 | |||
| dfd620aa29 | |||
| a5a6e8326f | |||
| 7f48a8ba82 | |||
| ae34d7b4ec | |||
| 44c8e54ed8 | |||
| 4a4208e336 | |||
| d2385b8dc2 | |||
| 9babe1676d | |||
| 8c38f499e4 | |||
| 89c43c1eef | |||
| c3c891cea0 | |||
| e4cf64a112 | |||
| 3e1e211098 | |||
| 02b6b9848f | |||
| 71da09ed44 | |||
| 4404494ec9 | |||
| 98a2a1d725 | |||
| 788f6cede7 | |||
| 7abee71ac3 | |||
| 52d5c99c46 | |||
| 93bc5e5e4b | |||
| 5d996f1388 | |||
| bbb2645a6f | |||
| 54fedc2f53 | |||
| e8c5550256 | |||
| 57b9d41b5c | |||
| 34be7a1234 | |||
| 60f42ed624 | |||
| 25c93afabe | |||
| 2cd5a97ee8 | |||
| 8f47e8fd1e | |||
| eafa545b2a | |||
| b680a07750 | |||
| 2a919ec0f9 | |||
| 01596c045a | |||
| 3de93c5c09 | |||
| 306f5b27e2 | |||
| 06f0f66e3e | |||
| 64921a054e | |||
| 44e035ad38 | |||
| 1803cd071d | |||
| e4d7ba306b | |||
| cc0428752b | |||
| b8c177bc55 | |||
| 7083c51dea | |||
| f0693204e1 | |||
| 3f8071e48e | |||
| a1c5350e8e | |||
| 8f237909d5 | |||
| 11ff69d4f5 | |||
| d15cf98742 | |||
| 29bd6612e9 | |||
| ab1e36d280 | |||
| f1fdafd37f | |||
| 4c7684a3a7 | |||
| 2f45db659b | |||
| 9e38ad114d | |||
| 45c4ea8ca0 | |||
| d2b95ae316 | |||
| 02121152b3 | |||
| caed965b6b | |||
| 4088d5966b | |||
| a062beee87 | |||
| 3095882516 | |||
| 4ad0639796 | |||
| b53ec3352f | |||
| c40edd5b2c | |||
| bdd42caaaf | |||
| ab4eee31ce | |||
| 3f9886a2e5 | |||
| 198b7443ae | |||
| f9c5ca303d | |||
| 9922d43940 | |||
| ae99b077d4 | |||
| 0cb9da9027 | |||
| 467a97dcf7 | |||
| 8d15e418e7 | |||
| 3849b797c9 | |||
| d43c95f98c | |||
| 52d9d9ea1d | |||
| 2cabc9849c | |||
| 17049aca1c | |||
| 9608d59efe | |||
| a7942212a0 | |||
| a22aa53c51 | |||
| 1ce16dc643 | |||
| 0ed6e9a463 | |||
| b7ecd1d749 | |||
| 44e177a5ca | |||
| b09cd3d587 | |||
| b70ebcf5f9 | |||
| 9d58161729 | |||
| e07eed4a95 | |||
| e09704c711 | |||
| 3af24f0815 | |||
| 4c0e7f6013 | |||
| ea298273af | |||
| f60a5af3f2 | |||
| 6ae5c1cdac |
@@ -1,23 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'no-shadow': 'off',
|
||||
'no-undef': 'off',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -45,7 +45,7 @@ body:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "2.0.0"
|
||||
Example: "2.1.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -86,9 +86,9 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
- label: If this is an issue with a plugin, I should be opening an issue in the [plugins repository](https://github.com/lnreader/lnreader-plugins/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.0.0](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.1.3](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -32,9 +32,9 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.0.0](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.1.3](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -1,94 +1,208 @@
|
||||
name: Build
|
||||
name: Build Preview
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '30 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
EXPO_NO_TELEMETRY: '1'
|
||||
NODE_ENV: 'production'
|
||||
|
||||
jobs:
|
||||
check-preview:
|
||||
name: Check Preview
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
should-build: ${{ steps.preview.outputs.should-build }}
|
||||
|
||||
steps:
|
||||
- name: Check For Existing Preview
|
||||
id: preview
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
core.notice('Manual dispatch requested; building a new preview.')
|
||||
core.setOutput('should-build', 'true')
|
||||
return
|
||||
}
|
||||
|
||||
const shortSha = context.sha.slice(0, 8)
|
||||
const artifactName = `LNReader-${shortSha}`
|
||||
const { data: { artifacts } } = await github.rest.actions.listArtifactsForRepo(
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: artifactName,
|
||||
per_page: 100,
|
||||
},
|
||||
)
|
||||
const previewExists = artifacts.some(
|
||||
artifact =>
|
||||
!artifact.expired &&
|
||||
artifact.workflow_run?.head_sha === context.sha,
|
||||
)
|
||||
|
||||
core.notice(
|
||||
previewExists
|
||||
? `A preview artifact already exists for ${shortSha}.`
|
||||
: `No preview artifact exists for ${shortSha}; starting a build.`,
|
||||
)
|
||||
core.setOutput('should-build', String(!previewExists))
|
||||
|
||||
build-android:
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
name: APK
|
||||
needs: check-preview
|
||||
if: needs.check-preview.outputs.should-build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Set Up pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- 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
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v5
|
||||
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'
|
||||
distribution: temurin
|
||||
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
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- 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: Make Gradlew Executable
|
||||
run: cd android && chmod +x ./gradlew
|
||||
|
||||
- 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: Build Android Release
|
||||
- name: Configure Release Environment
|
||||
env:
|
||||
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
|
||||
COMMIT_ID: ${{ env.COMMIT_ID }}
|
||||
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_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
|
||||
pnpm generate:env:release \
|
||||
--build-type "GitHub Action" \
|
||||
--node-env "production"
|
||||
|
||||
- name: Upload Release Artifact
|
||||
env:
|
||||
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
|
||||
COMMIT_ID: ${{ env.COMMIT_ID }}
|
||||
- name: Generate Native Android Project
|
||||
run: |
|
||||
pnpm exec expo prebuild \
|
||||
--platform android \
|
||||
--clean \
|
||||
--no-install \
|
||||
--non-interactive
|
||||
|
||||
- name: Configure Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-provider: enhanced
|
||||
cache-read-only: >-
|
||||
${{ github.ref_name != github.event.repository.default_branch }}
|
||||
cache-cleanup: on-success
|
||||
add-job-summary: always
|
||||
|
||||
- name: Generate Preview Metadata
|
||||
run: |
|
||||
short_sha="${GITHUB_SHA::8}"
|
||||
base_version_name="$(node --print "require('./app.json').expo.version")"
|
||||
base_version_code="$(node --print "require('./app.json').expo.android.versionCode")"
|
||||
|
||||
echo "PRERELEASE_VERSION_CODE=$((base_version_code + GITHUB_RUN_NUMBER))" >> "$GITHUB_ENV"
|
||||
echo "PRERELEASE_VERSION_NAME=${base_version_name}-pre.r${GITHUB_RUN_NUMBER}.${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "PRERELEASE_ARTIFACT_NAME=LNReader-${short_sha}" >> "$GITHUB_ENV"
|
||||
echo "PRERELEASE_APK_NAME=LNReader-r${GITHUB_RUN_NUMBER}-${short_sha}.apk" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Preview APK
|
||||
working-directory: android
|
||||
run: |
|
||||
mkdir -p app/build/intermediates/sourcemaps/react/preRelease
|
||||
|
||||
./gradlew :app:assemblePreRelease \
|
||||
--build-cache \
|
||||
--parallel \
|
||||
-PpreReleaseVersionCode="$PRERELEASE_VERSION_CODE" \
|
||||
-PpreReleaseVersionName="$PRERELEASE_VERSION_NAME" \
|
||||
--stacktrace
|
||||
|
||||
mv \
|
||||
app/build/outputs/apk/preRelease/app-preRelease.apk \
|
||||
"app/build/outputs/apk/preRelease/$PRERELEASE_APK_NAME"
|
||||
|
||||
- name: Upload Preview Sourcemap
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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
|
||||
name: ${{ env.PRERELEASE_ARTIFACT_NAME }}-sourcemap
|
||||
path: android/app/build/generated/sourcemaps/react/preRelease/index.android.bundle.map
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
compression-level: 0
|
||||
- name: Upload Preview APK
|
||||
id: upload-nightly
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PRERELEASE_ARTIFACT_NAME }}
|
||||
path: android/app/build/outputs/apk/preRelease/${{ env.PRERELEASE_APK_NAME }}
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
compression-level: 0
|
||||
|
||||
- name: Check Discord Notification
|
||||
id: discord
|
||||
env:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
run: |
|
||||
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
|
||||
echo "configured=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "configured=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Notify Discord
|
||||
if: steps.discord.outputs.configured == 'true'
|
||||
env:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_NIGHTLY_WEBHOOK }}
|
||||
ARTIFACT_URL: ${{ steps.upload-nightly.outputs.artifact-url }}
|
||||
run: |
|
||||
short_sha="${GITHUB_SHA::8}"
|
||||
message="[${GITHUB_REPOSITORY}] New nightly build published: \`${short_sha}\`"
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg title "$message" \
|
||||
--arg url "$ARTIFACT_URL" \
|
||||
'{
|
||||
username: "GitHub",
|
||||
avatar_url: "https://github.com/github.png",
|
||||
embeds: [{
|
||||
author: {
|
||||
name: "github-actions[bot]",
|
||||
icon_url: "https://github.com/github.png"
|
||||
},
|
||||
title: $title,
|
||||
url: $url,
|
||||
color: 3092790
|
||||
}]
|
||||
}')
|
||||
|
||||
curl --fail-with-body \
|
||||
--request POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL"
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -44,5 +44,8 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Environment Variables
|
||||
run: pnpm generate:env:debug
|
||||
|
||||
- name: Run ESLint
|
||||
run: pnpm run lint
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
name: Release Android
|
||||
run-name: Draft ${{ inputs.bump }} Android release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: Version component to increment
|
||||
required: true
|
||||
default: Patch
|
||||
type: choice
|
||||
options:
|
||||
- Patch
|
||||
- Minor
|
||||
- Major
|
||||
|
||||
concurrency:
|
||||
group: release-android
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
EXPO_NO_TELEMETRY: '1'
|
||||
NODE_ENV: 'production'
|
||||
RELEASE_BUMP: ${{ inputs.bump }}
|
||||
|
||||
jobs:
|
||||
release-android:
|
||||
name: Build Draft Release
|
||||
if: >-
|
||||
github.ref == 'refs/heads/master' &&
|
||||
github.actor_id == vars.RELEASE_ACTOR_ID &&
|
||||
github.triggering_actor == vars.RELEASE_ACTOR_LOGIN
|
||||
environment: release
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set Up pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Resolve Release Version
|
||||
id: version
|
||||
run: |
|
||||
RELEASE_VERSION="$(
|
||||
node scripts/resolve-release-version.cjs "$RELEASE_BUMP"
|
||||
)"
|
||||
|
||||
echo "Releasing $RELEASE_BUMP version: $RELEASE_VERSION"
|
||||
echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV"
|
||||
echo "version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Version must use stable semver, for example 2.0.4"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git rev-parse --verify --quiet "refs/tags/v$RELEASE_VERSION"; then
|
||||
echo "Tag v$RELEASE_VERSION already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if gh release view "v$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||
echo "Release v$RELEASE_VERSION already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Prepare Release Commit
|
||||
run: |
|
||||
pnpm release:prepare "$RELEASE_VERSION"
|
||||
|
||||
if git diff --quiet -- \
|
||||
package.json \
|
||||
app.json \
|
||||
.github/ISSUE_TEMPLATE/report_issue.yml \
|
||||
.github/ISSUE_TEMPLATE/request_feature.yml; then
|
||||
CURRENT_VERSION="$(node -p "require('./package.json').version")"
|
||||
if [ "$CURRENT_VERSION" != "$RELEASE_VERSION" ]; then
|
||||
echo "No release changes were generated for $RELEASE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release version $RELEASE_VERSION is already committed; resuming"
|
||||
else
|
||||
git diff --check
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add \
|
||||
package.json \
|
||||
app.json \
|
||||
.github/ISSUE_TEMPLATE/report_issue.yml \
|
||||
.github/ISSUE_TEMPLATE/request_feature.yml
|
||||
git commit -m "chore: Release v$RELEASE_VERSION"
|
||||
fi
|
||||
|
||||
git fetch origin master
|
||||
REMOTE_VERSION="$(
|
||||
node -e "
|
||||
const { execFileSync } = require('child_process');
|
||||
const contents = execFileSync(
|
||||
'git',
|
||||
['show', 'origin/master:package.json'],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
process.stdout.write(JSON.parse(contents).version);
|
||||
"
|
||||
)"
|
||||
|
||||
if [ "$REMOTE_VERSION" = "$RELEASE_VERSION" ]; then
|
||||
echo "Release commit is already on master; resuming from it"
|
||||
git checkout --detach origin/master
|
||||
echo "RELEASE_ALREADY_PUSHED=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "RELEASE_ALREADY_PUSHED=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
echo "RELEASE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure Release Environment
|
||||
env:
|
||||
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_ID }}
|
||||
run: |
|
||||
pnpm generate:env:release \
|
||||
--build-type "Release" \
|
||||
--node-env "production" \
|
||||
--myanimelist-client-id "$MYANIMELIST_CLIENT_ID" \
|
||||
--anilist-client-id "$ANILIST_CLIENT_ID"
|
||||
|
||||
- name: Generate Native Android Project
|
||||
run: |
|
||||
pnpm exec expo prebuild \
|
||||
--platform android \
|
||||
--clean \
|
||||
--no-install
|
||||
|
||||
- name: Configure Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-provider: enhanced
|
||||
cache-cleanup: on-success
|
||||
add-job-summary: always
|
||||
|
||||
- name: Build Release APKs
|
||||
working-directory: android
|
||||
run: |
|
||||
mkdir -p app/build/intermediates/sourcemaps/react/release
|
||||
|
||||
./gradlew :app:assembleRelease \
|
||||
--build-cache \
|
||||
--parallel \
|
||||
-PreleaseAbiSplits=true \
|
||||
--stacktrace
|
||||
|
||||
- name: Package Release APKs
|
||||
run: pnpm release:package "$RELEASE_VERSION"
|
||||
|
||||
- name: Upload Sourcemaps
|
||||
run: |
|
||||
cp android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
|
||||
"release-artifacts/LNReader-v${RELEASE_VERSION}-sourcemap.map"
|
||||
|
||||
- name: Verify APK Signatures
|
||||
run: |
|
||||
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner |
|
||||
sort -V |
|
||||
tail -1)"
|
||||
|
||||
if [ -z "$APKSIGNER" ]; then
|
||||
echo "Android apksigner was not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for APK in release-artifacts/*.apk; do
|
||||
"$APKSIGNER" verify "$APK"
|
||||
done
|
||||
|
||||
- name: Upload Release APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: LNReader-v${{ steps.version.outputs.version }}
|
||||
path: release-artifacts/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
- name: Push Release Commit
|
||||
run: |
|
||||
if [ "$RELEASE_ALREADY_PUSHED" = "true" ]; then
|
||||
echo "Release commit is already on master"
|
||||
else
|
||||
git push origin HEAD:master
|
||||
fi
|
||||
|
||||
- name: Create Draft GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create "v$RELEASE_VERSION" \
|
||||
release-artifacts/* \
|
||||
--target "$RELEASE_SHA" \
|
||||
--title "LNReader v$RELEASE_VERSION" \
|
||||
--generate-notes \
|
||||
--draft
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Path
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm
|
||||
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: Generate Environment
|
||||
run: pnpm generate:env:debug
|
||||
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
@@ -1,36 +1,39 @@
|
||||
# Temporarily Disabled
|
||||
name: Type Check
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Type Checking
|
||||
type-check:
|
||||
name: TypeScript
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
- name: Get pnpm Store Path
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm Cache
|
||||
- name: Cache pnpm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
@@ -41,5 +44,8 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Environment
|
||||
run: pnpm generate:env:debug
|
||||
|
||||
- name: Check Types
|
||||
run: pnpm run type-check
|
||||
|
||||
@@ -8,7 +8,8 @@ npm-debug.*
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
.env
|
||||
.*env
|
||||
.env.local
|
||||
reader_playground/index.html
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -88,3 +89,28 @@ flake.lock
|
||||
|
||||
# pnpm
|
||||
.pnpm-store
|
||||
|
||||
src/generated/**/*
|
||||
|
||||
.cursor/
|
||||
.agents/
|
||||
.claude/
|
||||
.jj/
|
||||
.sisyphus/
|
||||
|
||||
# Rock
|
||||
.rock/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
*/**/org.eclipse.buildship.core.prefs
|
||||
*/**/org.eclipse.jdt.core.prefs
|
||||
*/**/.classpath
|
||||
*/**/.project
|
||||
android/
|
||||
release-artifacts/
|
||||
!modules/**/android/
|
||||
!plugins/android/
|
||||
ios/
|
||||
!modules/**/ios/
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm run type-check
|
||||
@@ -0,0 +1,132 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI coding agents working in this repository.
|
||||
|
||||
## Project overview
|
||||
|
||||
LNReader is a free, open-source light novel reader for Android, built with
|
||||
React Native (Expo) and TypeScript. Content is fetched at runtime by
|
||||
JavaScript "plugins" (per-source scrapers) rather than bundled with the app.
|
||||
Plugin _requests_ live in a separate repo, [lnreader-plugins](https://github.com/lnreader/lnreader-plugins);
|
||||
this repo is the reader app itself.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Node >= 22.11.0, pnpm (see `packageManager` in `package.json`, currently pnpm@11.20.0).
|
||||
- This is a React Native/Expo app targeting Android; there is no web or iOS
|
||||
build target maintained here. Building/running the app requires an Android
|
||||
SDK and a device/emulator, which is generally not available in an agent
|
||||
sandbox — prefer running lint, type-check, and tests to verify changes.
|
||||
- See `CONTRIBUTING.md` (and `CONTRIBUTING-NIX.md` for a Nix-based dev shell)
|
||||
for full local Android setup instructions.
|
||||
|
||||
## Commands
|
||||
|
||||
Run these from the repo root:
|
||||
|
||||
```bash
|
||||
pnpm run lint # eslint ./src
|
||||
pnpm run lint:fix # eslint --fix
|
||||
pnpm run format:check # prettier --check
|
||||
pnpm run format # prettier --write
|
||||
pnpm run type-check # tsc --noEmit
|
||||
pnpm run check # format:check + lint + type-check (run before finishing any change)
|
||||
pnpm test # jest (all projects)
|
||||
pnpm run test:rn # jest --selectProjects rn (React Native/component/hook tests)
|
||||
pnpm run test:db # jest --selectProjects db (database/query tests, node env)
|
||||
pnpm run test:watch
|
||||
pnpm run test:coverage
|
||||
```
|
||||
|
||||
Always run `pnpm run check` (or at minimum lint + type-check + the relevant
|
||||
test project) before considering a change complete. A pre-commit hook
|
||||
(`husky` + `lint-staged`) runs prettier/eslint on staged files, and a
|
||||
pre-push hook runs `type-check`, so failures here will also block commits/pushes.
|
||||
|
||||
## Code style
|
||||
|
||||
- TypeScript, functional React components, hooks-based state.
|
||||
- Formatting is enforced by Prettier (`.prettierrc.js`) — don't hand-format,
|
||||
run `pnpm run format` instead.
|
||||
- ESLint config is `eslint.config.js` (flat config, based on `eslint-config-expo`).
|
||||
Notable rules: `no-console` is an error (use the project's logging/toast
|
||||
utilities, not `console.*`), `prefer-const`, `no-var`, and several
|
||||
`react-hooks/*` correctness rules are enforced.
|
||||
- Use the `@alias/*` path aliases defined in `tsconfig.json` (and mirrored in
|
||||
`jest.config.js`) instead of deep relative imports, e.g. `@components`,
|
||||
`@hooks`, `@database/*`, `@screens/*`, `@services/*`, `@utils/*`,
|
||||
`@api/*`, `@theme/*`, `@plugins/*`, `@navigators/*`, `@i18n/*`, `@type/*`.
|
||||
- Don't add comments that just restate what the code does; only comment
|
||||
non-obvious "why" (workarounds, invariants, native/platform quirks).
|
||||
|
||||
## Localization (i18n)
|
||||
|
||||
- `src/i18n/languages/en/strings.json` is the source-of-truth string file —
|
||||
it's the only locale you should hand-edit. Every other folder under
|
||||
`src/i18n/languages/*` is a translation synced automatically from Crowdin
|
||||
(see `crowdin.yml`); don't edit or "fix" translated strings in those files.
|
||||
- After adding/changing keys in `en/strings.json`, run
|
||||
`pnpm run generate:string-types` to regenerate the associated TypeScript types.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `src/api` – external API clients (drive/cloud backup providers, etc.)
|
||||
- `src/components` – shared/reusable UI components
|
||||
- `src/database` – SQLite (op-sqlite/Drizzle) schema, queries, and migrations
|
||||
helpers; `src/database/queries/*Queries.ts` are the main data-access layer
|
||||
- `src/hooks` – shared and "persisted" (MMKV-backed) React hooks
|
||||
- `src/i18n` – translated strings (see Crowdin integration in `crowdin.yml`)
|
||||
- `src/navigators` – React Navigation stack/tab setup
|
||||
- `src/plugins` – plugin manager and plugin type definitions (not the plugin
|
||||
source scrapers themselves, which live in the separate lnreader-plugins repo)
|
||||
- `src/screens` – feature screens (library, browse, novel, reader, settings, etc.)
|
||||
- `src/services` – background tasks, downloads, backups, trackers, EPUB export, updates
|
||||
- `src/theme` – Material3 theming
|
||||
- `src/utils` – general utilities
|
||||
- `drizzle/` – generated SQL migrations (see `pnpm run generate:db-migration`)
|
||||
- `modules/` – native modules (Nitro modules: epub, tts, file, zip-archive, etc.)
|
||||
- `test/` – Jest global mocks and the shared `test-utils.tsx` render helpers
|
||||
- `scripts/` – Node scripts for env file generation, release packaging, etc.
|
||||
|
||||
## Testing notes
|
||||
|
||||
- Don't add tests for the sake of coverage. Write tests for user-facing
|
||||
interactions (screens, components a user directly touches) and for core
|
||||
complex logic (parsing, database queries, sync/backup, chapter/novel
|
||||
matching, etc.), not for trivial passthroughs, simple type/prop plumbing,
|
||||
or generated/boilerplate code.
|
||||
- Tests live in `__tests__` directories next to the code under test and use
|
||||
Jest + `@testing-library/react-native`.
|
||||
- Jest is split into two projects (see `jest.config.js`):
|
||||
- `db` — pure logic/query tests under `src/database`, run in a Node environment.
|
||||
- `rn` — component/hook/integration tests under `src/`, run with the `jest-expo` preset.
|
||||
- Import the shared render helpers from `@test-utils` (`render`, `renderNovel`,
|
||||
`AllTheProviders`) rather than using `@testing-library/react-native`'s
|
||||
`render` directly, so providers (theme, gesture handler, safe area, novel
|
||||
context) are set up consistently.
|
||||
- Mock at module level with `jest.mock(...)`, not inside `beforeEach` — see
|
||||
`TESTING.md` for the reasoning and common pitfalls (ESM transform errors,
|
||||
`mockReturnValue` failures, test isolation).
|
||||
- Global mocks for native modules and database queries live in `test/mocks/`;
|
||||
hook-specific mocks live in `src/hooks/__mocks__` and `src/hooks/__tests__/mocks.ts`.
|
||||
|
||||
## Database changes
|
||||
|
||||
- Schema lives under `src/database/schema`; the ORM is Drizzle
|
||||
(`drizzle-orm`, config in `drizzle.config.ts`).
|
||||
- After changing the schema, generate a migration rather than hand-writing
|
||||
SQL: `pnpm run generate:db-migration`. Do not edit files under `drizzle/`
|
||||
by hand.
|
||||
|
||||
## PR / commit conventions
|
||||
|
||||
- Keep changes scoped and consistent with existing patterns in the touched
|
||||
directory; prefer editing existing files over introducing new abstractions.
|
||||
- Match existing code style; don't reformat unrelated code.
|
||||
- If you are an AI agent opening a PR or making a commit, attribute it as
|
||||
such (e.g. a "Generated with <agent/tool name>" note in the commit
|
||||
message/PR description, and a co-author trailer if your tool supports one).
|
||||
@@ -1,115 +1,117 @@
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { PropsWithChildren, Suspense, useEffect, useMemo } from 'react';
|
||||
import { StatusBar, StyleSheet } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import LottieSplashScreen from 'react-native-lottie-splash-screen';
|
||||
import * as SplashScreen from 'expo-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 {
|
||||
MD3DarkTheme,
|
||||
MD3LightTheme,
|
||||
Provider as PaperProvider,
|
||||
} from 'react-native-paper';
|
||||
|
||||
import AppErrorBoundary, {
|
||||
ErrorFallback,
|
||||
} from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import { useDatabaseInitialization } from '@hooks';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { useInitDatabase } from '@database/db';
|
||||
import { useInitializeAppServices } from '@hooks/common/useInitializeAppServices';
|
||||
import { opSqliteAdapter } from './src/rozenite/opSqliteAdapter';
|
||||
import { useRozeniteSqlitePlugin } from '@rozenite/sqlite-plugin';
|
||||
import { ThemeProvider, useTheme } from '@hooks/persisted/useTheme';
|
||||
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
||||
|
||||
enableFreeze(true);
|
||||
const sqliteAdapters = __DEV__ && opSqliteAdapter ? [opSqliteAdapter] : [];
|
||||
|
||||
/**
|
||||
* The Android window background is resolved from the system light/dark setting,
|
||||
* so it is bright white whenever the system is light – regardless of the theme
|
||||
* picked inside the app. Nothing between it and the navigators paints a
|
||||
* background of its own, so every frame in which no screen is drawn (mounting a
|
||||
* nested navigator, freezing the screen being left) shows through as a flash.
|
||||
* Painting the root view keeps the window covered at all times.
|
||||
*/
|
||||
const ThemedRootView = ({ children }: PropsWithChildren) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView
|
||||
style={[styles.flex, { backgroundColor: theme.background }]}
|
||||
>
|
||||
{children}
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
const ThemedPaperProvider = ({ children }: PropsWithChildren) => {
|
||||
const theme = useTheme();
|
||||
const paperTheme = useMemo(() => {
|
||||
const baseTheme = theme.isDark ? MD3DarkTheme : MD3LightTheme;
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => {
|
||||
return {
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
...baseTheme,
|
||||
colors: {
|
||||
...baseTheme.colors,
|
||||
...theme,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
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,
|
||||
});
|
||||
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const { isDbReady, dbError, retryInitialization } =
|
||||
useDatabaseInitialization();
|
||||
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
|
||||
const { success: databaseReady, error: databaseError } = useInitDatabase();
|
||||
const { ready: servicesReady, error: servicesError } =
|
||||
useInitializeAppServices(Boolean(databaseReady));
|
||||
|
||||
useEffect(() => {
|
||||
if (isDbReady || dbError) {
|
||||
LottieSplashScreen.hide();
|
||||
if ((databaseReady && servicesReady) || databaseError || servicesError) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [isDbReady, dbError]);
|
||||
}, [databaseReady, databaseError, servicesReady, servicesError]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||
response => {
|
||||
const actionIdentifier = response.actionIdentifier;
|
||||
if (actionIdentifier.startsWith('TTS_')) {
|
||||
const { setTTSAction } = require('@utils/ttsNotification');
|
||||
setTTSAction(actionIdentifier);
|
||||
}
|
||||
},
|
||||
const initializationError = databaseError || servicesError;
|
||||
|
||||
if (initializationError) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<ErrorFallback error={initializationError} resetError={() => null} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (dbError) {
|
||||
return <ErrorFallback error={dbError} resetError={retryInitialization} />;
|
||||
}
|
||||
|
||||
if (!isDbReady) {
|
||||
if (!databaseReady || !servicesReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={styles.flex}>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</GestureHandlerRootView>
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider>
|
||||
<ThemedRootView>
|
||||
<KeyboardProvider>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<ThemedPaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar
|
||||
translucent={true}
|
||||
backgroundColor="transparent"
|
||||
/>
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</ThemedPaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</KeyboardProvider>
|
||||
</ThemedRootView>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This document outlines how to set up your React Native development environment u
|
||||
|
||||
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.
|
||||
Compatibility with Microsoft WSL2 is not guaranteed, but should work without major issues, except for the emulator.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
Contributions are welcome and are greatly appreciated!
|
||||
|
||||
## Setup your environment with nix
|
||||
## 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)
|
||||
See [CONTRIBUTING-NIX.md](CONTRIBUTING-NIX.md).
|
||||
|
||||
## Setting up your environment
|
||||
## Setting Up Your Environment
|
||||
|
||||
After forking to your own github org or account, do the following steps to get started:
|
||||
After forking to your own GitHub organization or account, take the following steps to get started:
|
||||
|
||||
```bash
|
||||
# prerequisites
|
||||
@@ -35,7 +35,7 @@ pnpm run build:release:android
|
||||
|
||||
### Developing on Android
|
||||
|
||||
You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
|
||||
You will need an Android device or emulator connected to your computer as well as an IDE of your choice (e.g., VS Code).
|
||||
|
||||
```bash
|
||||
# prerequisites
|
||||
@@ -57,7 +57,7 @@ pnpm run dev:android
|
||||
This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
|
||||
|
||||
It is recommended that you install an eslint plugin for your editor of choice when working on this
|
||||
codebase, however you can always check to see if the source code is compliant by running:
|
||||
codebase, however, you can always check to see if the source code is compliant by running:
|
||||
|
||||
```bash
|
||||
pnpm run lint
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Debugging Production Builds
|
||||
|
||||
Symbolicate production stack traces from LNReader using uploaded sourcemaps.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** (v18 or later)
|
||||
- The **sourcemap** matching the crashing APK version
|
||||
- A **stack trace** from either `adb logcat` (device attached) or pasted from a Discord / GitHub issue report
|
||||
|
||||
## Getting the Sourcemap
|
||||
|
||||
### Release builds
|
||||
|
||||
Sourcemaps are uploaded as GitHub release assets. Download the one matching the crashing version:
|
||||
|
||||
```
|
||||
LNReader-v{version}-sourcemap.map
|
||||
```
|
||||
|
||||
Example for `v2.5.0`:
|
||||
|
||||
1. Go to the [releases page](https://github.com/lnreader/lnreader/releases)
|
||||
2. Find release `v2.5.0`
|
||||
3. Under **Assets**, download `LNReader-v2.5.0-sourcemap.map`
|
||||
|
||||
### Preview / nightly builds
|
||||
|
||||
Sourcemaps are uploaded as a separate artifact alongside the preview APK, named `LNReader-{sha}-sourcemap`.
|
||||
|
||||
1. Go to the [Actions tab](https://github.com/lnreader/lnreader/actions) → **Build Preview**
|
||||
2. Find the workflow run matching the build hash
|
||||
3. Under **Artifacts**, download the one with the `-sourcemap` suffix
|
||||
|
||||
## Symbolicating a Stack Trace
|
||||
|
||||
Save the raw stack trace to a file (from `adb logcat`, or paste from Discord/GitHub into `trace.txt`), then run:
|
||||
|
||||
```bash
|
||||
npx metro-symbolicate <sourcemap.map> < trace.txt
|
||||
```
|
||||
|
||||
Or pipe it directly:
|
||||
|
||||
```bash
|
||||
adb logcat -d | npx metro-symbolicate <sourcemap.map>
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Before (Hermes-obfuscated):
|
||||
|
||||
```
|
||||
com.lnreader E Error: Unexpected token
|
||||
at 93795 (address at index.js:1:729193)
|
||||
at 28471 (address at index.js:1:219482)
|
||||
at 10384 (address at index.js:1:81729)
|
||||
```
|
||||
|
||||
After symbolication:
|
||||
|
||||
```
|
||||
com.lnreader E Error: Unexpected token
|
||||
at handleResponse (src/api/plugins.ts:120:12)
|
||||
at fetchPlugin (src/hooks/usePlugins.ts:45:8)
|
||||
at getPluginList (src/screens/browse/BrowseScreen.tsx:89:20)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Version mismatch**: the sourcemap must match the exact APK build. A mismatched version produces wrong file names and line numbers.
|
||||
- **No sourcemap artifact**: verify the workflow that built the APK ran successfully and that the "Upload Sourcemaps" / "Upload Preview Sourcemap" step completed without errors.
|
||||
- **Hermes bytecode**: Hermes compiles JS to bytecode. The sourcemap is generated during this step and maps bytecode offsets back to source. `metro-symbolicate` handles this transparently.
|
||||
@@ -13,4 +13,5 @@ gem 'concurrent-ruby', '< 1.3.4'
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
gem 'mutex_m'
|
||||
gem 'nkf'
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
<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=5865F2&color=4752C4&label=discord&style=flat">
|
||||
</a>
|
||||
<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 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>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
@@ -32,16 +32,16 @@
|
||||
<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 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 href="https://github.com/lnreader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="Build Preview" src="https://github.com/lnreader/lnreader/actions/workflows/build.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Get the app from our <a href="https://github.com/LNReader/lnreader/releases">releases page</a>.
|
||||
Get the app from our <a href="https://github.com/lnreader/lnreader/releases">releases page</a>.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
LNReader does not have any affiliation with the content providers available.
|
||||
|
||||
Plugin requests should be created at [lnreader-plugins](https://github.com/LNReader/lnreader-plugins).
|
||||
Plugin requests should be created at [lnreader-plugins](https://github.com/lnreader/lnreader-plugins).
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -70,4 +70,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/LNReader/lnreader/blob/main/LICENSE)
|
||||
[MIT](https://github.com/lnreader/lnreader/blob/main/LICENSE)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Testing Guide for LNReader
|
||||
|
||||
This guide explains how to write tests in this React Native project using Jest and React Testing Library.
|
||||
|
||||
|
||||
## Existing Mocks
|
||||
|
||||
### Global Mocks
|
||||
|
||||
The project has global mocks configured in Jest. These are automatically applied:
|
||||
|
||||
- `test/mocks/` - Global mocks for native modules (react-native-mmkv, react-navigation, all database queries, etc.)
|
||||
- `src/hooks/__mocks__/index.ts` - Hook-specific mocks (showToast, getString, parseChapterNumber, etc.)
|
||||
- `src/hooks/__tests__/mocks.ts` - Extended mocks for persisted hooks
|
||||
|
||||
### Using @test-utils
|
||||
|
||||
There is a custom render wrapper at `test/test-utils.tsx` with:
|
||||
|
||||
- `render` - wraps with GestureHandlerRootView, SafeAreaProvider, PaperProvider, etc.
|
||||
- `renderNovel` - includes NovelContextProvider
|
||||
- `AllTheProviders` - the full provider wrapper
|
||||
|
||||
Usage:
|
||||
|
||||
```typescript
|
||||
import { render, renderNovel } from '@test-utils';
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### 1. ESM Modules Not Transforming
|
||||
|
||||
If you see `Cannot use import statement outside a module`, you need to add mocks for the module:
|
||||
|
||||
```typescript
|
||||
jest.mock('@hooks/persisted/usePlugins');
|
||||
// Add more specific mocks as needed
|
||||
```
|
||||
|
||||
### 2. Mock Functions Not Working
|
||||
|
||||
If `mockReturnValue` throws "not a function", create mock functions at the module level:
|
||||
|
||||
```typescript
|
||||
// CORRECT: Module-level mock functions
|
||||
const mockUseTheme = jest.fn();
|
||||
jest.mock('@hooks/persisted', () => ({
|
||||
useTheme: () => mockUseTheme(),
|
||||
}));
|
||||
|
||||
// INCORRECT: Trying to use jest.Mock type casting
|
||||
// (useTheme as jest.Mock).mockReturnValue(...) // This fails!
|
||||
```
|
||||
|
||||
### 3. Test Isolation
|
||||
|
||||
Tests must mock at module level, not in `beforeEach`:
|
||||
|
||||
```typescript
|
||||
// CORRECT
|
||||
const mockFn = jest.fn();
|
||||
jest.mock('module', () => ({ useHook: () => mockFn() }));
|
||||
|
||||
// INCORRECT - mocks get reset between tests
|
||||
jest.mock('module');
|
||||
beforeEach(() => {
|
||||
// This doesn't work properly
|
||||
});
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
pnpm test # Run all tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With coverage
|
||||
pnpm test:rn # React Native only
|
||||
pnpm test:db # Database only
|
||||
```
|
||||
@@ -1,136 +0,0 @@
|
||||
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.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
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")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/index.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC).
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `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 = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
ext {
|
||||
versionMajor = 2
|
||||
versionMinor = 0
|
||||
versionPatch = 0
|
||||
}
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
namespace "com.rajarsheechatterjee.LNReader"
|
||||
defaultConfig {
|
||||
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
|
||||
versionCode ((((versionMajor << 10) | versionMinor) << 11) | versionPatch)
|
||||
versionName "$versionMajor.$versionMinor.$versionPatch"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
applicationIdSuffix 'debug'
|
||||
versionNameSuffix '-debug'
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
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 'androidx.core:core-ktx:1.15.0'
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "523872485654",
|
||||
"project_id": "lnreader-backup",
|
||||
"storage_bucket": "lnreader-backup.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:523872485654:android:169cd062458db962ce1037",
|
||||
"android_client_info": {
|
||||
"package_name": "com.rajarsheechatterjee.LNReader"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "523872485654-14jtut8orr7dbrk2chea279d7k8889sr.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.rajarsheechatterjee.LNReader",
|
||||
"certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBH_j-0Jyo9aFJ_KV8Nr3te2hs_L_ZYhrE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning"/>
|
||||
</manifest>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">LNReader debug</string>
|
||||
</resources>
|
||||
@@ -1,70 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:largeHeap="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true">
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.notifications.default_notification_icon"
|
||||
android:resource="@drawable/notification_icon" />
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.notifications.default_notification_color"
|
||||
android:resource="@color/notification_icon_color" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.App.SplashScreen"
|
||||
android:screenOrientation="unspecified">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="lnreader" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" android:foregroundServiceType="shortService" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,48 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,643 +0,0 @@
|
||||
/* 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;
|
||||
});
|
||||
})();
|
||||
@@ -1,6 +0,0 @@
|
||||
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>';
|
||||
@@ -1,339 +0,0 @@
|
||||
const { div, p, img, button } = van.tags;
|
||||
|
||||
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),
|
||||
);
|
||||
};
|
||||
|
||||
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 + reader.layoutHeight) / reader.chapterHeight;
|
||||
}
|
||||
if (ratio > 1) {
|
||||
ratio = 1;
|
||||
}
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
pageReader.movePage(
|
||||
parseInt(pageReader.totalPages.val * Math.min(0.99, ratio)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
percentage.val = parseInt(ratio * 100);
|
||||
if (lock) {
|
||||
window.scrollTo({
|
||||
top: reader.chapterHeight * ratio - reader.layoutHeight,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
};
|
||||
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',
|
||||
},
|
||||
() =>
|
||||
reader.generalSettings.val.pageReader ? pageReader.totalPages.val : 100,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
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 = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
img({
|
||||
id: 'Image-Modal-img',
|
||||
src: src,
|
||||
alt: () => (src.val ? `Cant not render image from ${src.val}` : ''),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
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',
|
||||
);
|
||||
};
|
||||
const hideImage = () => {
|
||||
imgSrc.val = '';
|
||||
reader.viewport.setAttribute(
|
||||
'content',
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=1.0',
|
||||
);
|
||||
};
|
||||
|
||||
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());
|
||||
@@ -1,74 +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.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,52 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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,17 +0,0 @@
|
||||
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)
|
||||
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* 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; });
|
||||
}
|
||||
|
Before Width: | Height: | Size: 801 B |
|
Before Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 617 B |
|
Before Width: | Height: | Size: 500 B |
|
Before Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 837 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,93 +0,0 @@
|
||||
<vector android:alpha="0" android:height="1dp"
|
||||
android:viewportHeight="1024" android:viewportWidth="1024"
|
||||
android:width="1dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.994" android:fillColor="#132c33"
|
||||
android:pathData="M479.5,-0.5C500.8,-0.5 522.2,-0.5 543.5,-0.5C704.4,13.5 832.9,84.8 929,213.5C985.1,293.3 1016.5,382 1023.5,479.5C1023.5,500.8 1023.5,522.2 1023.5,543.5C1009.2,709.4 934.5,840.2 799.5,936C722,987.8 636.6,1017 543.5,1023.5C522.2,1023.5 500.8,1023.5 479.5,1023.5C333.8,1011.4 213.3,950.4 118,840.5C47.3,754.3 7.8,655.3 -0.5,543.5C-0.5,522.2 -0.5,500.8 -0.5,479.5C13.5,318.6 84.8,190.1 213.5,94C293.3,37.9 382,6.4 479.5,-0.5Z" android:strokeAlpha="0.994"/>
|
||||
<path android:fillColor="#106476" android:pathData="M870.5,325.5C876.8,340.6 882.7,355.9 888,371.5C889.7,373.2 891.1,375.2 892,377.5C895.1,386.3 897.4,395.3 899,404.5C900,406.7 901.5,408.4 903.5,409.5C903.5,410.2 903.5,410.8 903.5,411.5C907.2,436.2 910.8,460.8 914.5,485.5C915.4,500.5 915.7,515.5 915.5,530.5C911.7,612.8 885.4,686.8 836.5,752.5C836.5,750.7 837,749 838,747.5C894.4,667.2 918.8,577.8 911,479.5C899,365 848.5,271.8 759.5,200C652.9,120.8 534.9,97.5 405.5,130C299,161.6 218.1,225.4 163,321.5C108.7,424.4 99.1,531.4 134,642.5C147,680.2 165,715.2 188,747.5C189,749 189.5,750.7 189.5,752.5C141.4,688 115.1,615.3 110.5,534.5C110.3,518.5 110.5,502.5 111,486.5C112.8,477.3 113.8,467.9 114,458.5C119,435.8 124.4,413.1 130,390.5C140,359.6 153.3,330.6 170,303.5C184.2,283.1 199.4,263.5 215.5,244.5C231.1,229.1 246.7,213.9 262.5,199C273.8,191.7 285.1,184.4 296.5,177C297.3,175.5 298.3,174.2 299.5,173C303.2,172.6 306.5,171.3 309.5,169C313,166 316.3,162.9 319.5,159.5C330.1,155.2 340.1,149.9 349.5,143.5C360.1,142.3 370.1,139.2 379.5,134C385.7,132.1 392.1,130.5 398.5,129C400.5,128 402.5,127 404.5,126C452.1,113.9 500.4,109.7 549.5,113.5C567,115.2 584.3,117.7 601.5,121C620.6,126.7 639.8,132.6 659,138.5C668,141.9 676.8,145.8 685.5,150C695.4,156.1 705.8,161.5 716.5,166C717.2,167 717.8,168 718.5,169C733.7,175.2 747.1,184.2 758.5,196C764.2,199.9 769.5,204.2 774.5,209C776.6,210.8 778.9,212.1 781.5,213C785.3,218.2 789.8,222.7 795,226.5C797.1,228.3 798.4,230.7 799,233.5C807,241.6 815,249.6 823,257.5C823.3,258.8 823.7,260.2 824,261.5C828,266.5 832.4,271.2 837,275.5C841.8,284.4 847.1,293.1 853,301.5C858.3,307.4 862.6,314.1 866,321.5C867,323.4 868.5,324.7 870.5,325.5Z"/>
|
||||
<path android:fillColor="#126e81" android:pathData="M836.5,752.5C791,813.4 733,858.3 662.5,887C545.5,931 431.5,924.3 320.5,867C268.3,838.7 224.7,800.5 189.5,752.5C189.5,750.7 189,749 188,747.5C165,715.2 147,680.2 134,642.5C99.1,531.4 108.7,424.4 163,321.5C218.1,225.4 299,161.6 405.5,130C534.9,97.5 652.9,120.8 759.5,200C848.5,271.8 899,365 911,479.5C918.8,577.8 894.4,667.2 838,747.5C837,749 836.5,750.7 836.5,752.5Z"/>
|
||||
<path android:fillColor="#a1abaf" android:pathData="M660.5,203.5C657.7,203.4 655,202.8 652.5,201.5C641.6,195.4 629.9,191.5 617.5,190C609.7,188 602.4,185 595.5,181C580.9,179.2 566.6,176.6 552.5,173C521.2,169 490.2,169.9 459.5,175.5C447.6,178.3 435.6,180.5 423.5,182C418.2,183.8 412.9,185.3 407.5,186.5C388.5,192.1 370.1,199.6 352.5,209C351.2,209.5 349.9,209.7 348.5,209.5C423.4,170 502.4,159.1 585.5,177C611.8,182.6 636.8,191.4 660.5,203.5Z"/>
|
||||
<path android:fillColor="#d7e2e6" android:pathData="M688.5,223.5C690.9,224.3 692.9,225.7 694.5,227.5C695.4,229.8 696.7,232 698.5,234C734,257.8 763.8,287.3 788,322.5C789.3,323.9 790.8,324.9 792.5,325.5C793.2,325.8 793.8,326.2 794.5,326.5C794.3,327.9 794.5,329.2 795,330.5C851.6,424.2 862.9,523.2 829,627.5C824.9,638.7 820.3,649.7 815,660.5C812.6,667.1 810.4,673.7 808.5,680.5C806.4,682.7 803.9,684.7 801,686.5C790.6,704.3 778.8,720.7 765.5,735.5C765.5,737.2 765.5,738.8 765.5,740.5C721,790.4 666.4,824.1 601.5,841.5C586.8,843.9 572.2,846.4 557.5,849C523.2,853.2 488.9,853.4 454.5,849.5C364.7,834.1 292.3,790.4 237.5,718.5C236.2,715.7 235.4,712.7 235,709.5C188.1,642.5 167.8,567.8 174,485.5C176.7,456.8 182.5,428.8 191.5,401.5C194.1,391.7 197.3,382 201,372.5C201.9,372 202.8,371.3 203.5,370.5C208.1,361.7 212.3,352.7 216,343.5C220.2,338.6 223.9,333.3 227,327.5C242.1,304 260,282.5 280.5,263C290.7,256.1 300.3,248.4 309.5,240C314.3,237.6 318.9,235.1 323.5,232.5C408.4,179.1 499.7,164.2 597.5,188C611.5,191.7 625.2,196.3 638.5,202C641.5,202.3 644.5,202.7 647.5,203C661.2,209.8 674.9,216.7 688.5,223.5Z"/>
|
||||
<path android:fillColor="#cfdade" android:pathData="M556.5,177.5C577.5,179.3 597.9,184.2 617.5,192C621.3,192.3 624.9,192.9 628.5,194C636.6,198 645,201.2 653.5,203.5C663.6,208.4 673.6,213.6 683.5,219C685.6,220 687.3,221.5 688.5,223.5C674.9,216.7 661.2,209.8 647.5,203C644.5,202.7 641.5,202.3 638.5,202C625.2,196.3 611.5,191.7 597.5,188C499.7,164.2 408.4,179.1 323.5,232.5C330.8,226.2 338.8,220.9 347.5,216.5C348.5,215.1 349.5,213.8 350.5,212.5C363.9,207 377.2,201.1 390.5,195C402.2,191.3 413.8,187.7 425.5,184C448.7,180 472.1,177 495.5,175C505.5,174.3 515.5,174.3 525.5,175C535.8,176.1 546.2,176.9 556.5,177.5Z"/>
|
||||
<path android:fillColor="#c4ced2" android:pathData="M556.5,177.5C546.2,176.9 535.8,176.1 525.5,175C515.5,174.3 505.5,174.3 495.5,175C472.1,177 448.7,180 425.5,184C413.8,187.7 402.2,191.3 390.5,195C377.2,201.1 363.9,207 350.5,212.5C368.9,202.5 387.9,193.8 407.5,186.5C412.9,185.3 418.2,183.8 423.5,182C435.6,180.5 447.6,178.3 459.5,175.5C491.2,171.4 522.9,171.5 554.5,176C555.4,176.3 556.1,176.8 556.5,177.5Z"/>
|
||||
<path android:fillColor="#bac3c7" android:pathData="M652.5,201.5C652.8,202.2 653.2,202.8 653.5,203.5C645,201.2 636.6,198 628.5,194C624.9,192.9 621.3,192.3 617.5,192C597.9,184.2 577.5,179.3 556.5,177.5C556.1,176.8 555.4,176.3 554.5,176C522.9,171.5 491.2,171.4 459.5,175.5C490.2,169.9 521.2,169 552.5,173C566.6,176.6 580.9,179.2 595.5,181C602.4,185 609.7,188 617.5,190C629.9,191.5 641.6,195.4 652.5,201.5Z"/>
|
||||
<path android:fillColor="#0e5565" android:pathData="M870.5,325.5C868.5,324.7 867,323.4 866,321.5C862.6,314.1 858.3,307.4 853,301.5C847.1,293.1 841.8,284.4 837,275.5C832.4,271.2 828,266.5 824,261.5C823.7,260.2 823.3,258.8 823,257.5C815,249.6 807,241.6 799,233.5C798.4,230.7 797.1,228.3 795,226.5C789.8,222.7 785.3,218.2 781.5,213C778.9,212.1 776.6,210.8 774.5,209C769.5,204.2 764.2,199.9 758.5,196C747.1,184.2 733.7,175.2 718.5,169C717.8,168 717.2,167 716.5,166C705.8,161.5 695.4,156.1 685.5,150C676.8,145.8 668,141.9 659,138.5C639.8,132.6 620.6,126.7 601.5,121C584.3,117.7 567,115.2 549.5,113.5C500.4,109.7 452.1,113.9 404.5,126C402.5,127 400.5,128 398.5,129C392.1,130.5 385.7,132.1 379.5,134C370.1,139.2 360.1,142.3 349.5,143.5C420.7,111.5 495,101.3 572.5,113C678.8,130.2 765.3,180.7 832,264.5C846.7,283.7 859.5,304 870.5,325.5Z"/>
|
||||
<path android:fillColor="#a9b3b7" android:pathData="M652.5,201.5C655,202.8 657.7,203.4 660.5,203.5C704.9,224.6 742.6,254.2 773.5,292.5C765,283.5 756.4,274.7 747.5,266C744,263.6 741,260.8 738.5,257.5C733.8,254.1 729.1,250.8 724.5,247.5C722.4,245.3 720.1,243.3 717.5,241.5C709.8,236.7 702.1,232 694.5,227.5C692.9,225.7 690.9,224.3 688.5,223.5C687.3,221.5 685.6,220 683.5,219C673.6,213.6 663.6,208.4 653.5,203.5C653.2,202.8 652.8,202.2 652.5,201.5Z"/>
|
||||
<path android:fillColor="#cfdadd" android:pathData="M694.5,227.5C702.1,232 709.8,236.7 717.5,241.5C720.1,243.3 722.4,245.3 724.5,247.5C740.5,264 756.6,280.3 773,296.5C779.5,304.9 785.8,313.3 792,321.5C792.5,322.8 792.7,324.1 792.5,325.5C790.8,324.9 789.3,323.9 788,322.5C763.8,287.3 734,257.8 698.5,234C696.7,232 695.4,229.8 694.5,227.5Z"/>
|
||||
<path android:fillColor="#a8b3b6" android:pathData="M407.5,186.5C387.9,193.8 368.9,202.5 350.5,212.5C348.1,212.5 345.7,213 343.5,214C280.9,251.6 233.3,303.1 200.5,368.5C200,369.3 199.4,369.6 198.5,369.5C231,299.7 281,246.4 348.5,209.5C349.9,209.7 351.2,209.5 352.5,209C370.1,199.6 388.5,192.1 407.5,186.5Z"/>
|
||||
<path android:fillColor="#c7d1d5" android:pathData="M350.5,212.5C349.5,213.8 348.5,215.1 347.5,216.5C338.8,220.9 330.8,226.2 323.5,232.5C318.9,235.1 314.3,237.6 309.5,240C300.3,248.4 290.7,256.1 280.5,263C260,282.5 242.1,304 227,327.5C223.9,333.3 220.2,338.6 216,343.5C212.3,352.7 208.1,361.7 203.5,370.5C202.8,371.3 201.9,372 201,372.5C197.3,382 194.1,391.7 191.5,401.5C187.7,410.3 184.4,419.3 181.5,428.5C183.2,412.5 188.1,397.5 196,383.5C196.7,378.7 197.5,374.1 198.5,369.5C199.4,369.6 200,369.3 200.5,368.5C233.3,303.1 280.9,251.6 343.5,214C345.7,213 348.1,212.5 350.5,212.5Z"/>
|
||||
<path android:fillColor="#0f5969" android:pathData="M349.5,143.5C340.1,149.9 330.1,155.2 319.5,159.5C316.3,162.9 313,166 309.5,169C306.5,171.3 303.2,172.6 299.5,173C298.3,174.2 297.3,175.5 296.5,177C285.1,184.4 273.8,191.7 262.5,199C246.7,213.9 231.1,229.1 215.5,244.5C199.4,263.5 184.2,283.1 170,303.5C153.3,330.6 140,359.6 130,390.5C124.4,413.1 119,435.8 114,458.5C113.8,467.9 112.8,477.3 111,486.5C110.5,502.5 110.3,518.5 110.5,534.5C105.6,408 149,301.1 240.5,214C273,184.7 309.3,161.2 349.5,143.5Z"/>
|
||||
<path android:fillColor="#b5bfc3" android:pathData="M724.5,247.5C729.1,250.8 733.8,254.1 738.5,257.5C741,260.8 744,263.6 747.5,266C756.4,274.7 765,283.5 773.5,292.5C804,327.4 825.6,367.4 838.5,412.5C837.5,412 836.7,411.4 836,410.5C833.5,403.3 831,396.2 828.5,389C820.3,372.7 812.4,356.2 805,339.5C803.9,338.7 802.7,338 801.5,337.5C799.9,333.3 797.6,329.6 794.5,326.5C793.8,326.2 793.2,325.8 792.5,325.5C792.7,324.1 792.5,322.8 792,321.5C785.8,313.3 779.5,304.9 773,296.5C756.6,280.3 740.5,264 724.5,247.5Z"/>
|
||||
<path android:fillColor="#d0dade" android:pathData="M558.5,314.5C570.2,314.3 581.8,314.5 593.5,315C603.6,318.9 605.1,325 598,333.5C597.2,342.4 597.3,351.2 598.5,360C629.8,360.3 661.2,360.7 692.5,361C694.7,361.8 696.2,363.3 697,365.5C697.7,376.5 697.7,387.5 697,398.5C696.2,400 695,401.2 693.5,402C661.6,403.5 629.6,404 597.5,403.5C597.2,411.8 597.5,419.9 598.5,428C623.3,428 647.9,428.7 672.5,430C673.7,430.9 674.5,432.1 675,433.5C675.7,444.2 675.7,454.8 675,465.5C674.2,468.8 672,470.7 668.5,471C638.7,471.2 608.8,471.3 579,471.5C547.8,471.7 516.6,471.2 485.5,470C483.5,468.7 482.3,466.8 482,464.5C481.3,454.5 481.3,444.5 482,434.5C482.3,432.7 483.1,431.2 484.5,430C507.1,428.5 529.7,428 552.5,428.5C552.5,420.2 552.5,411.8 552.5,403.5C525.2,403.7 497.8,403.5 470.5,403C467.2,403.1 465.1,401.6 464,398.5C462,387.2 462,375.8 464,364.5C465.2,363 466.7,361.8 468.5,361C496.5,360.5 524.5,360.3 552.5,360.5C552.1,346.5 552.6,332.5 554,318.5C555.4,316.9 556.9,315.6 558.5,314.5Z"/>
|
||||
<path android:fillColor="#849094" android:pathData="M591.5,317.5C580.5,317.5 569.5,317.5 558.5,317.5C558.8,332.7 558.5,347.7 557.5,362.5C557.5,347.2 557.5,331.8 557.5,316.5C569,316.2 580.3,316.5 591.5,317.5Z"/>
|
||||
<path android:fillColor="#132c33" android:pathData="M591.5,317.5C599,318.8 599.5,321.8 593,326.5C592.5,338.5 592.3,350.5 592.5,362.5C625.8,362.5 659.2,362.5 692.5,362.5C692.5,373.2 692.5,383.8 692.5,394.5C658.8,394.5 625.2,394.5 591.5,394.5C591.2,406.7 591.5,418.7 592.5,430.5C618,431.5 643.7,431.8 669.5,431.5C669.5,441.8 669.5,452.2 669.5,462.5C608.8,462.5 548.2,462.5 487.5,462.5C487.5,451.8 487.5,441.2 487.5,430.5C510.2,430.5 532.8,430.5 555.5,430.5C556.2,430.5 556.8,430.5 557.5,430.5C558.5,418.7 558.8,406.7 558.5,394.5C528.5,394.5 498.5,394.5 468.5,394.5C468.5,383.8 468.5,373.2 468.5,362.5C498.2,362.5 527.8,362.5 557.5,362.5C558.5,347.7 558.8,332.7 558.5,317.5C569.5,317.5 580.5,317.5 591.5,317.5Z"/>
|
||||
<path android:fillColor="#cfdade" android:pathData="M331.5,324.5C366.9,324 402.2,324.5 437.5,326C439.5,327.3 440.7,329.2 441,331.5C441.9,342.2 441.6,352.9 440,363.5C439,365.3 437.5,366.4 435.5,367C399.8,368 364.1,367.6 328.5,366C327.3,365.1 326.5,363.9 326,362.5C325.3,351.5 325.3,340.5 326,329.5C327.4,327.3 329.2,325.6 331.5,324.5Z"/>
|
||||
<path android:fillColor="#b8c1c4" android:pathData="M330.5,331.5C330.2,341 330.5,350.3 331.5,359.5C331.6,360.1 331.9,360.6 332.5,361C349.3,361.2 366.2,361.3 383,361.5C400.2,361.5 417,360.8 433.5,359.5C434.2,359.5 434.8,359.5 435.5,359.5C435.8,359.5 436.2,359.5 436.5,359.5C436.4,361.4 435.4,362.6 433.5,363C399.2,363.7 364.8,363.7 330.5,363C329.9,362.3 329.4,361.4 329,360.5C328.3,351.2 328.3,341.8 329,332.5C329.4,331.9 329.9,331.6 330.5,331.5Z"/>
|
||||
<path android:fillColor="#a3abaf" android:pathData="M331.5,359.5C365.5,359.5 399.5,359.5 433.5,359.5C417,360.8 400.2,361.5 383,361.5C366.2,361.3 349.3,361.2 332.5,361C331.9,360.6 331.6,360.1 331.5,359.5Z"/>
|
||||
<path android:fillColor="#142d34" android:pathData="M330.5,327.5C365.5,327.5 400.5,327.5 435.5,327.5C435.5,338.2 435.5,348.8 435.5,359.5C434.8,359.5 434.2,359.5 433.5,359.5C399.5,359.5 365.5,359.5 331.5,359.5C330.5,350.3 330.2,341 330.5,331.5C330.5,330.2 330.5,328.8 330.5,327.5Z"/>
|
||||
<path android:fillColor="#79868a" android:pathData="M330.5,327.5C365.7,326.5 401,326.2 436.5,326.5C436.5,337.5 436.5,348.5 436.5,359.5C436.2,359.5 435.8,359.5 435.5,359.5C435.5,348.8 435.5,338.2 435.5,327.5C400.5,327.5 365.5,327.5 330.5,327.5Z"/>
|
||||
<path android:fillColor="#0f5a6b" android:pathData="M870.5,325.5C884.8,352.1 895.8,380.1 903.5,409.5C901.5,408.4 900,406.7 899,404.5C897.4,395.3 895.1,386.3 892,377.5C891.1,375.2 889.7,373.2 888,371.5C882.7,355.9 876.8,340.6 870.5,325.5Z"/>
|
||||
<path android:fillColor="#8e979a" android:pathData="M468.5,394.5C498.5,394.5 528.5,394.5 558.5,394.5C558.8,406.7 558.5,418.7 557.5,430.5C558.1,419.1 557.5,407.9 555.5,397C528.2,396.7 500.8,396.3 473.5,396C471.6,395.8 470,395.3 468.5,394.5Z"/>
|
||||
<path android:fillColor="#a6aeb1" android:pathData="M690.5,395.5C690.4,396.1 690.1,396.6 689.5,397C658.2,397.3 626.8,397.7 595.5,398C594.6,399 593.6,399.9 592.5,400.5C592.5,398.8 592.5,397.2 592.5,395.5C625.2,395.5 657.8,395.5 690.5,395.5Z"/>
|
||||
<path android:fillColor="#b4bec1" android:pathData="M468.5,362.5C468.5,373.2 468.5,383.8 468.5,394.5C470,395.3 471.6,395.8 473.5,396C500.8,396.3 528.2,396.7 555.5,397C557.5,407.9 558.1,419.1 557.5,430.5C556.8,430.5 556.2,430.5 555.5,430.5C556.6,420.9 556.8,411.2 556,401.5C555.5,401 555,400.5 554.5,400C526.5,399.7 498.5,399.3 470.5,399C469.7,398.2 468.8,397.3 468,396.5C467.2,385 467.3,373.7 468.5,362.5Z"/>
|
||||
<path android:fillColor="#b9c3c6" android:pathData="M690.5,395.5C691.2,395.5 691.8,395.5 692.5,395.5C691.9,396.5 691.2,397.5 690.5,398.5C658.8,399.3 627.2,399.8 595.5,400C595,400.5 594.5,401 594,401.5C593.2,411.2 593.4,420.9 594.5,430.5C593.8,430.5 593.2,430.5 592.5,430.5C592.5,420.5 592.5,410.5 592.5,400.5C593.6,399.9 594.6,399 595.5,398C626.8,397.7 658.2,397.3 689.5,397C690.1,396.6 690.4,396.1 690.5,395.5Z"/>
|
||||
<path android:fillColor="#515f63" android:pathData="M692.5,394.5C692.5,394.8 692.5,395.2 692.5,395.5C691.8,395.5 691.2,395.5 690.5,395.5C657.8,395.5 625.2,395.5 592.5,395.5C592.5,397.2 592.5,398.8 592.5,400.5C592.5,410.5 592.5,420.5 592.5,430.5C591.5,418.7 591.2,406.7 591.5,394.5C625.2,394.5 658.8,394.5 692.5,394.5Z"/>
|
||||
<path android:fillColor="#ced9dd" android:pathData="M191.5,401.5C182.5,428.8 176.7,456.8 174,485.5C167.8,567.8 188.1,642.5 235,709.5C235.4,712.7 236.2,715.7 237.5,718.5C214.2,688.9 196.9,655.9 185.5,619.5C182.6,606.1 179.5,592.8 176,579.5C174,566 171.8,552.6 169.5,539.5C167.9,501.8 171.9,464.8 181.5,428.5C184.4,419.3 187.7,410.3 191.5,401.5Z"/>
|
||||
<path android:fillColor="#ced8dc" android:pathData="M313.5,384.5C359.2,384.3 404.8,384.5 450.5,385C452,386.2 453.2,387.7 454,389.5C454.7,400.5 454.7,411.5 454,422.5C451.8,425.8 448.6,427.3 444.5,427C400.5,428 356.5,427.6 312.5,426C311,424.8 309.8,423.3 309,421.5C308.3,410.8 308.3,400.2 309,389.5C310,387.3 311.5,385.6 313.5,384.5Z"/>
|
||||
<path android:fillColor="#bbc4c8" android:pathData="M313.5,389.5C313.5,399.2 313.5,408.8 313.5,418.5C314.4,419.3 315.4,420.1 316.5,421C360.2,421.7 403.8,421.7 447.5,421C448.4,420.3 449,419.5 449.5,418.5C449.5,408.8 449.5,399.2 449.5,389.5C450.7,399.7 450.8,410 450,420.5C449.1,421.7 447.9,422.5 446.5,423C402.5,423.7 358.5,423.7 314.5,423C313.8,421.7 313,420.6 312,419.5C311.3,409.8 311.3,400.2 312,390.5C312.4,389.9 312.9,389.6 313.5,389.5Z"/>
|
||||
<path android:fillColor="#9da6a9" android:pathData="M313.5,418.5C358.8,418.5 404.2,418.5 449.5,418.5C449,419.5 448.4,420.3 447.5,421C403.8,421.7 360.2,421.7 316.5,421C315.4,420.1 314.4,419.3 313.5,418.5Z"/>
|
||||
<path android:fillColor="#132c33" android:pathData="M449.5,389.5C449.5,399.2 449.5,408.8 449.5,418.5C404.2,418.5 358.8,418.5 313.5,418.5C313.5,408.8 313.5,399.2 313.5,389.5C313.5,388.5 313.5,387.5 313.5,386.5C358.8,386.5 404.2,386.5 449.5,386.5C449.5,387.5 449.5,388.5 449.5,389.5Z"/>
|
||||
<path android:fillColor="#273b41" android:pathData="M592.5,430.5C593.2,430.5 593.8,430.5 594.5,430.5C620.2,429.8 645.5,430.5 670.5,432.5C670.5,432.8 670.5,433.2 670.5,433.5C670.5,443.2 670.5,452.8 670.5,462.5C670.2,462.5 669.8,462.5 669.5,462.5C669.5,452.2 669.5,441.8 669.5,431.5C643.7,431.8 618,431.5 592.5,430.5Z"/>
|
||||
<path android:fillColor="#b8c4c8" android:pathData="M198.5,369.5C197.5,374.1 196.7,378.7 196,383.5C188.1,397.5 183.2,412.5 181.5,428.5C171.9,464.8 167.9,501.8 169.5,539.5C163.8,480.3 173.5,423.6 198.5,369.5Z"/>
|
||||
<path android:fillColor="#cfdadd" android:pathData="M330.5,445.5C365.8,445.3 401.2,445.5 436.5,446C439.1,447.3 440.6,449.5 441,452.5C441.9,463.2 441.6,473.9 440,484.5C439.2,485.3 438.3,486.2 437.5,487C401.5,487.7 365.5,487.7 329.5,487C327.3,486.3 326.1,484.8 326,482.5C325.3,471.8 325.3,461.2 326,450.5C327,448.3 328.5,446.6 330.5,445.5Z"/>
|
||||
<path android:fillColor="#132c32" android:pathData="M435.5,448.5C435.5,458.8 435.5,469.2 435.5,479.5C400.8,479.5 366.2,479.5 331.5,479.5C331.5,469.2 331.5,458.8 331.5,448.5C366.2,448.5 400.8,448.5 435.5,448.5Z"/>
|
||||
<path android:fillColor="#273b41" android:pathData="M435.5,447.5C435.5,447.8 435.5,448.2 435.5,448.5C400.8,448.5 366.2,448.5 331.5,448.5C331.5,458.8 331.5,469.2 331.5,479.5C330.5,470.7 330.2,461.7 330.5,452.5C330.5,450.8 330.5,449.2 330.5,447.5C365.5,447.5 400.5,447.5 435.5,447.5Z"/>
|
||||
<path android:fillColor="#6b7679" android:pathData="M487.5,430.5C487.5,441.2 487.5,451.8 487.5,462.5C548.2,462.5 608.8,462.5 669.5,462.5C669.8,462.5 670.2,462.5 670.5,462.5C670.2,463.3 669.5,463.7 668.5,463.5C608.5,463.5 548.5,463.5 488.5,463.5C488.2,463.5 487.8,463.5 487.5,463.5C486.5,453 486.2,442.3 486.5,431.5C486.5,430.8 486.8,430.5 487.5,430.5Z"/>
|
||||
<path android:fillColor="#c8d3d6" android:pathData="M486.5,431.5C486.2,442.3 486.5,453 487.5,463.5C487.8,465.8 489.1,466.9 491.5,467C550.8,467.7 610.2,467.7 669.5,467C670.9,465.8 671.7,464.3 672,462.5C672.7,453.2 672.7,443.8 672,434.5C671.6,433.9 671.1,433.6 670.5,433.5C670.5,433.2 670.5,432.8 670.5,432.5C672.4,432.6 673.6,433.6 674,435.5C674.7,445.2 674.7,454.8 674,464.5C672.6,466.1 671.1,467.6 669.5,469C608.8,469.7 548.2,469.7 487.5,469C486.2,467.6 485,466.1 484,464.5C483.3,454.2 483.3,443.8 484,433.5C484.7,432.6 485.5,432 486.5,431.5Z"/>
|
||||
<path android:fillColor="#bac3c7" android:pathData="M670.5,433.5C671.1,433.6 671.6,433.9 672,434.5C672.7,443.8 672.7,453.2 672,462.5C671.7,464.3 670.9,465.8 669.5,467C610.2,467.7 550.8,467.7 491.5,467C489.1,466.9 487.8,465.8 487.5,463.5C487.8,463.5 488.2,463.5 488.5,463.5C488.6,464.1 488.9,464.6 489.5,465C548.8,465.7 608.2,465.7 667.5,465C668.1,464.6 668.4,464.1 668.5,463.5C669.5,463.7 670.2,463.3 670.5,462.5C670.5,452.8 670.5,443.2 670.5,433.5Z"/>
|
||||
<path android:fillColor="#a6aeb1" android:pathData="M488.5,463.5C548.5,463.5 608.5,463.5 668.5,463.5C668.4,464.1 668.1,464.6 667.5,465C608.2,465.7 548.8,465.7 489.5,465C488.9,464.6 488.6,464.1 488.5,463.5Z"/>
|
||||
<path android:fillColor="#0f5b6c" android:pathData="M903.5,411.5C914.1,450.5 918.1,490.2 915.5,530.5C915.7,515.5 915.4,500.5 914.5,485.5C910.8,460.8 907.2,436.2 903.5,411.5Z"/>
|
||||
<path android:fillColor="#9fa8ab" android:pathData="M435.5,447.5C436.4,448.6 436.8,449.9 436.5,451.5C436.8,461.5 436.5,471.5 435.5,481.5C434.7,481.8 434,482.3 433.5,483C399.5,483.7 365.5,483.7 331.5,483C331,482.3 330.3,481.8 329.5,481.5C329.2,471.7 329.5,462 330.5,452.5C330.2,461.7 330.5,470.7 331.5,479.5C366.2,479.5 400.8,479.5 435.5,479.5C435.5,469.2 435.5,458.8 435.5,448.5C435.5,448.2 435.5,447.8 435.5,447.5Z"/>
|
||||
<path android:fillColor="#c2ccd0" android:pathData="M436.5,451.5C437.1,451.6 437.6,451.9 438,452.5C438.7,462.2 438.7,471.8 438,481.5C437.6,482.4 437.1,483.3 436.5,484C418.2,485.1 399.9,485.6 381.5,485.5C364.5,485.2 347.5,484.8 330.5,484.5C329.9,483.6 329.5,482.6 329.5,481.5C330.3,481.8 331,482.3 331.5,483C365.5,483.7 399.5,483.7 433.5,483C434,482.3 434.7,481.8 435.5,481.5C436.5,471.5 436.8,461.5 436.5,451.5Z"/>
|
||||
<path android:fillColor="#142c33" android:pathData="M459.5,485.5C538.8,485.5 618.2,485.5 697.5,485.5C697.5,509.5 697.5,533.5 697.5,557.5C686.2,557.5 674.8,557.5 663.5,557.5C663.5,545.8 663.5,534.2 663.5,522.5C663.5,521.8 663.5,521.2 663.5,520.5C663.5,519.5 663.5,518.5 663.5,517.5C606.8,517.5 550.2,517.5 493.5,517.5C493.5,531.5 493.5,545.5 493.5,559.5C482.2,559.5 470.8,559.5 459.5,559.5C459.5,536.5 459.5,513.5 459.5,490.5C459.5,489.2 459.5,487.8 459.5,486.5C459.5,486.2 459.5,485.8 459.5,485.5Z"/>
|
||||
<path android:fillColor="#8f9a9e" android:pathData="M459.5,485.5C499.2,484.8 539,484.5 579,484.5C619.2,484.5 659,485.2 698.5,486.5C698.8,510.7 698.5,534.7 697.5,558.5C686,558.8 674.7,558.5 663.5,557.5C674.8,557.5 686.2,557.5 697.5,557.5C697.5,533.5 697.5,509.5 697.5,485.5C618.2,485.5 538.8,485.5 459.5,485.5Z"/>
|
||||
<path android:fillColor="#bac6ca" android:pathData="M838.5,412.5C841.3,419.5 843.4,426.9 845,434.5C854.7,478.4 855.8,522.4 848.5,566.5C851.3,530.5 851.1,494.5 848,458.5C846.2,447.9 843.5,437.6 840,427.5C839.5,422.5 839,417.5 838.5,412.5Z"/>
|
||||
<path android:fillColor="#132c32" android:pathData="M331.5,507.5C366.2,507.5 400.8,507.5 435.5,507.5C435.5,517.8 435.5,528.2 435.5,538.5C400.8,538.5 366.2,538.5 331.5,538.5C331.5,528.2 331.5,517.8 331.5,507.5Z"/>
|
||||
<path android:fillColor="#cbd6d9" android:pathData="M459.5,486.5C459.5,487.8 459.5,489.2 459.5,490.5C458.3,514.3 458.2,538.3 459,562.5C459.5,563 460,563.5 460.5,564C471.2,564.7 481.8,564.7 492.5,564C493.3,563.7 494,563.2 494.5,562.5C495.3,549.9 495.8,537.2 496,524.5C496.9,523.3 498.1,522.5 499.5,522C553.2,521.3 606.8,521.3 660.5,522C661,522.5 661.5,523 662,523.5C662.3,535.5 662.7,547.5 663,559.5C663.8,560.3 664.7,561.2 665.5,562C675.2,562.7 684.8,562.7 694.5,562C695.7,561 696.7,559.8 697.5,558.5C698.5,534.7 698.8,510.7 698.5,486.5C699.5,487 700.3,487.6 701,488.5C701.7,511.8 701.7,535.2 701,558.5C700.3,561.3 698.4,562.8 695.5,563C684.5,563.7 673.5,563.7 662.5,563C662,562.5 661.5,562 661,561.5C659.9,549.4 659,537.2 658.5,525C605.2,524.5 551.8,524.3 498.5,524.5C498.7,537.5 498.5,550.5 498,563.5C496.2,566.3 493.7,567.8 490.5,568C479.8,568.9 469.1,568.5 458.5,567C456.3,565.4 454.9,563.2 454.5,560.5C455.3,537.2 455.8,513.9 456,490.5C456.7,488.7 457.8,487.3 459.5,486.5Z"/>
|
||||
<path android:fillColor="#aab3b7" android:pathData="M663.5,520.5C663.5,521.2 663.5,521.8 663.5,522.5C662.7,522.2 662,521.7 661.5,521C605.8,520.2 550.1,520.3 494.5,521.5C494.9,520.1 495.9,519.2 497.5,519C552.2,518.3 606.8,518.3 661.5,519C662,519.7 662.7,520.2 663.5,520.5Z"/>
|
||||
<path android:fillColor="#9ca4a7" android:pathData="M663.5,520.5C662.7,520.2 662,519.7 661.5,519C606.8,518.3 552.2,518.3 497.5,519C495.9,519.2 494.9,520.1 494.5,521.5C494.8,534.3 494.5,547 493.5,559.5C493.5,545.5 493.5,531.5 493.5,517.5C550.2,517.5 606.8,517.5 663.5,517.5C663.5,518.5 663.5,519.5 663.5,520.5Z"/>
|
||||
<path android:fillColor="#132c33" android:pathData="M629.5,538.5C629.9,541.8 628.5,544.1 625.5,545.5C624.2,582 624.2,618.3 625.5,654.5C625.7,658.6 627.7,661.4 631.5,663C641.5,663.7 651.5,663.7 661.5,663C665.1,661.1 667.6,658.3 669,654.5C671.2,646.6 673,638.6 674.5,630.5C675,629.1 675.6,627.8 676.5,626.5C686.4,629.9 696.4,632.9 706.5,635.5C707.1,636.4 707.4,637.4 707.5,638.5C704.9,648.7 702.4,659 700,669.5C698.8,673.6 697.3,677.6 695.5,681.5C691.1,688.5 684.8,692.9 676.5,694.5C655.5,695.5 634.5,695.8 613.5,695.5C600.6,694.3 593.1,687.3 591,674.5C590.5,630.5 590.3,586.5 590.5,542.5C590.5,541.2 590.5,539.8 590.5,538.5C596.1,537.2 602.1,536.5 608.5,536.5C614.5,536.7 620.5,536.8 626.5,537C627.7,537.2 628.7,537.7 629.5,538.5Z"/>
|
||||
<path android:fillColor="#2b3e44" android:pathData="M331.5,507.5C331.5,517.8 331.5,528.2 331.5,538.5C366.2,538.5 400.8,538.5 435.5,538.5C435.2,539.3 434.5,539.7 433.5,539.5C399.8,539.5 366.2,539.5 332.5,539.5C330.5,531.2 329.9,522.6 330.5,513.5C330.5,512.2 330.5,510.8 330.5,509.5C330.3,508.5 330.7,507.8 331.5,507.5Z"/>
|
||||
<path android:fillColor="#b3bcbf" android:pathData="M435.5,507.5C436.2,508.8 437.1,510.1 438,511.5C438.7,521.2 438.7,530.8 438,540.5C437.6,542.4 436.4,543.4 434.5,543.5C400.5,543.7 366.5,543.5 332.5,543C331.7,542.2 330.8,541.3 330,540.5C329.2,531.3 329.3,522.3 330.5,513.5C329.9,522.6 330.5,531.2 332.5,539.5C332.8,540.2 333.1,540.8 333.5,541.5C366.5,542.7 399.5,542.8 432.5,542C433.3,541.3 433.6,540.5 433.5,539.5C434.5,539.7 435.2,539.3 435.5,538.5C435.5,528.2 435.5,517.8 435.5,507.5Z"/>
|
||||
<path android:fillColor="#a5aeb1" android:pathData="M332.5,539.5C366.2,539.5 399.8,539.5 433.5,539.5C433.6,540.5 433.3,541.3 432.5,542C399.5,542.8 366.5,542.7 333.5,541.5C333.1,540.8 332.8,540.2 332.5,539.5Z"/>
|
||||
<path android:fillColor="#c7d1d5" android:pathData="M330.5,509.5C330.5,510.8 330.5,512.2 330.5,513.5C329.3,522.3 329.2,531.3 330,540.5C330.8,541.3 331.7,542.2 332.5,543C366.5,543.5 400.5,543.7 434.5,543.5C417.5,544.8 400.2,545.5 382.5,545.5C365.2,545.3 347.8,545.2 330.5,545C330,544.1 329.3,543.2 328.5,542.5C327.3,532.2 327.2,521.8 328,511.5C328.7,510.6 329.5,510 330.5,509.5Z"/>
|
||||
<path android:fillColor="#cdd8dc" android:pathData="M794.5,326.5C797.6,329.6 799.9,333.3 801.5,337.5C802.7,338 803.9,338.7 805,339.5C812.4,356.2 820.3,372.7 828.5,389C831,396.2 833.5,403.3 836,410.5C836.7,411.4 837.5,412 838.5,412.5C839,417.5 839.5,422.5 840,427.5C843.5,437.6 846.2,447.9 848,458.5C851.1,494.5 851.3,530.5 848.5,566.5C842.5,607.2 829.2,645.2 808.5,680.5C810.4,673.7 812.6,667.1 815,660.5C820.3,649.7 824.9,638.7 829,627.5C862.9,523.2 851.6,424.2 795,330.5C794.5,329.2 794.3,327.9 794.5,326.5Z"/>
|
||||
<path android:fillColor="#b4bdc0" android:pathData="M459.5,490.5C459.5,513.5 459.5,536.5 459.5,559.5C470.8,559.5 482.2,559.5 493.5,559.5C494.5,547 494.8,534.3 494.5,521.5C550.1,520.3 605.8,520.2 661.5,521C662,521.7 662.7,522.2 663.5,522.5C663.5,534.2 663.5,545.8 663.5,557.5C674.7,558.5 686,558.8 697.5,558.5C696.7,559.8 695.7,561 694.5,562C684.8,562.7 675.2,562.7 665.5,562C664.7,561.2 663.8,560.3 663,559.5C662.7,547.5 662.3,535.5 662,523.5C661.5,523 661,522.5 660.5,522C606.8,521.3 553.2,521.3 499.5,522C498.1,522.5 496.9,523.3 496,524.5C495.8,537.2 495.3,549.9 494.5,562.5C494,563.2 493.3,563.7 492.5,564C481.8,564.7 471.2,564.7 460.5,564C460,563.5 459.5,563 459,562.5C458.2,538.3 458.3,514.3 459.5,490.5Z"/>
|
||||
<path android:fillColor="#becacd" android:pathData="M169.5,539.5C171.8,552.6 174,566 176,579.5C179.5,592.8 182.6,606.1 185.5,619.5C176.5,593.7 171.1,567 169.5,539.5Z"/>
|
||||
<path android:fillColor="#ccd7db" android:pathData="M674.5,630.5C673,638.6 671.2,646.6 669,654.5C667.6,658.3 665.1,661.1 661.5,663C651.5,663.7 641.5,663.7 631.5,663C627.7,661.4 625.7,658.6 625.5,654.5C625.3,619.5 625.5,584.5 626,549.5C630.1,546.6 631.3,542.9 629.5,538.5C628.7,537.7 627.7,537.2 626.5,537C620.5,536.8 614.5,536.7 608.5,536.5C602.1,536.5 596.1,537.2 590.5,538.5C589.8,538.8 589.2,539.1 588.5,539.5C587.3,586.2 587.2,632.8 588,679.5C588.6,683.6 590.1,687.2 592.5,690.5C593.3,691.4 594.3,691.7 595.5,691.5C599.4,695.2 604.1,697.7 609.5,699C631.2,699.7 652.8,699.7 674.5,699C678.4,697.5 682.4,696.4 686.5,695.5C691,693.2 694.3,689.7 696.5,685C697.8,684.7 698.9,684.2 700,683.5C702.3,679.1 703.9,674.4 705,669.5C705.5,659.1 706.3,648.7 707.5,638.5C707.4,637.4 707.1,636.4 706.5,635.5C708,636.2 708.9,637.6 709,639.5C710.4,656.8 707.1,673.1 699,688.5C693.4,696.1 685.9,701 676.5,703C653.8,703.7 631.2,703.7 608.5,703C595.4,699.9 587.9,691.8 586,678.5C585.3,631.8 585.3,585.2 586,538.5C587.1,536.2 588.9,534.7 591.5,534C602.5,533.3 613.5,533.3 624.5,534C630.5,534.3 633.8,537.3 634.5,543C633.9,546.9 632.4,550.4 630,553.5C629,588.5 629.4,623.5 631,658.5C632.5,660 634.3,660.8 636.5,661C644.6,661.9 652.6,661.6 660.5,660C667.7,652.6 671.7,643.8 672.5,633.5C672.6,632 673.3,631 674.5,630.5Z"/>
|
||||
<path android:fillColor="#aeb7ba" android:pathData="M590.5,542.5C590.3,586.5 590.5,630.5 591,674.5C593.1,687.3 600.6,694.3 613.5,695.5C633.7,696.7 654,696.8 674.5,696C675.4,695.7 676.1,695.2 676.5,694.5C684.8,692.9 691.1,688.5 695.5,681.5C694,687.1 691,691.8 686.5,695.5C682.4,696.4 678.4,697.5 674.5,699C652.8,699.7 631.2,699.7 609.5,699C604.1,697.7 599.4,695.2 595.5,691.5C594.5,687.4 592.7,683.8 590,680.5C589.2,634.3 589.3,588.3 590.5,542.5Z"/>
|
||||
<path android:fillColor="#838d90" android:pathData="M676.5,694.5C676.1,695.2 675.4,695.7 674.5,696C654,696.8 633.7,696.7 613.5,695.5C634.5,695.8 655.5,695.5 676.5,694.5Z"/>
|
||||
<path android:fillColor="#bdc7ca" android:pathData="M707.5,638.5C706.3,648.7 705.5,659.1 705,669.5C703.9,674.4 702.3,679.1 700,683.5C698.9,684.2 697.8,684.7 696.5,685C694.3,689.7 691,693.2 686.5,695.5C691,691.8 694,687.1 695.5,681.5C697.3,677.6 698.8,673.6 700,669.5C702.4,659 704.9,648.7 707.5,638.5Z"/>
|
||||
<path android:fillColor="#aeb7bb" android:pathData="M629.5,538.5C631.3,542.9 630.1,546.6 626,549.5C625.5,584.5 625.3,619.5 625.5,654.5C624.2,618.3 624.2,582 625.5,545.5C628.5,544.1 629.9,541.8 629.5,538.5Z"/>
|
||||
<path android:fillColor="#c3cdd1" android:pathData="M590.5,538.5C590.5,539.8 590.5,541.2 590.5,542.5C589.3,588.3 589.2,634.3 590,680.5C592.7,683.8 594.5,687.4 595.5,691.5C594.3,691.7 593.3,691.4 592.5,690.5C590.1,687.2 588.6,683.6 588,679.5C587.2,632.8 587.3,586.2 588.5,539.5C589.2,539.1 589.8,538.8 590.5,538.5Z"/>
|
||||
<path android:fillColor="#cad6db" android:pathData="M808.5,680.5C796.6,702.3 782.3,722.3 765.5,740.5C765.5,738.8 765.5,737.2 765.5,735.5C778.8,720.7 790.6,704.3 801,686.5C803.9,684.7 806.4,682.7 808.5,680.5Z"/>
|
||||
<path android:fillColor="#d0dbdf" android:pathData="M474.5,666.5C466.2,669.7 457.6,671.4 448.5,671.5C447.8,671.9 447.3,672.6 447,673.5C446.7,673.2 446.3,672.8 446,672.5C445.7,640.8 445.3,609.2 445,577.5C444.8,576.3 444.3,575.3 443.5,574.5C443.5,572.8 443.5,571.2 443.5,569.5C403.3,569.2 363.3,569.5 323.5,570.5C323.5,571.5 323.5,572.5 323.5,573.5C322.9,573.6 322.4,573.9 322,574.5C321.3,619.5 321.3,664.5 322,709.5C322.8,710.3 323.7,711.2 324.5,712C333.8,714 343.2,714 352.5,712C353.9,710.8 354.7,709.3 355,707.5C355.3,703.8 355.7,700.2 356,696.5C356.4,695.6 356.9,694.8 357.5,694C374.2,693.3 390.8,693.3 407.5,694C408.4,694.4 409.3,694.9 410,695.5C409.7,700.1 411.2,703.9 414.5,707C423.2,707.7 431.8,707.7 440.5,707C442.3,706.2 443.8,705 445,703.5C446,697.9 446.9,692.2 447.5,686.5C448.7,687.3 449.7,688.3 450.5,689.5C452.8,693.9 454.6,698.5 456,703.5C459.1,710.8 464.6,713.3 472.5,711C481.2,708.8 489.2,705.3 496.5,700.5C502.5,698 507.8,694.5 512.5,690C513.8,689.5 515.1,689.3 516.5,689.5C518.3,688.9 519.9,688.1 521.5,687C527.7,679.8 534.2,673 541,666.5C552.5,646.5 558.9,624.8 560,601.5C560.3,584.5 560.7,567.5 561,550.5C563.7,548.3 564.8,545.5 564.5,542C564.4,540 563.8,538.5 562.5,537.5C560.9,537 559.2,536.6 557.5,536.5C546.3,535.5 535,535.2 523.5,535.5C523.5,536.5 523.5,537.5 523.5,538.5C522.2,539.1 521.4,540.1 521,541.5C520.8,565.5 520.2,589.5 519,613.5C517.2,619.1 515.7,624.8 514.5,630.5C508.7,644 499.4,654.3 486.5,661.5C486.6,660.5 486.3,659.7 485.5,659C499.8,650.4 509.3,637.9 514,621.5C515.8,614.9 517.2,608.3 518,601.5C518.3,582.2 518.7,562.8 519,543.5C518.8,538.6 521,535.5 525.5,534C536.5,533.3 547.5,533.3 558.5,534C569.5,537.3 571.3,543.5 564,552.5C563.9,572.5 563.2,592.5 562,612.5C554.2,670.8 521.4,705.1 463.5,715.5C461,715.2 458.8,714.2 457,712.5C454.1,706.2 451.5,699.9 449,693.5C448.2,697.1 447.6,700.8 447,704.5C445.2,707.3 442.7,708.8 439.5,709C430.4,709.8 421.4,709.5 412.5,708C411.7,707.2 410.8,706.3 410,705.5C409.8,702.2 409.3,699.1 408.5,696C391.8,695.3 375.2,695.3 358.5,696C357.7,701.1 357.2,706.3 357,711.5C356.5,713.3 355.3,714.5 353.5,715C343.5,715.7 333.5,715.7 323.5,715C322.1,714.5 320.9,713.7 320,712.5C318.3,666.3 318,619.9 319,573.5C319.9,570.2 322.1,568.4 325.5,568C365.2,567 404.9,567.4 444.5,569C445.7,569.9 446.5,571.1 447,572.5C447.5,605.2 447.7,637.8 447.5,670.5C456.5,669.2 465.5,667.8 474.5,666.5Z"/>
|
||||
<path android:fillColor="#c5d0d3" android:pathData="M323.5,573.5C323.5,574.2 323.5,574.8 323.5,575.5C322.5,619.7 322.2,664 322.5,708.5C331.6,710.5 341,711 350.5,710C351.4,709.3 352,708.5 352.5,707.5C353.5,703 353.8,698.4 353.5,693.5C354.3,693.2 355,692.7 355.5,692C373.5,691.3 391.5,691.3 409.5,692C410,692.7 410.7,693.2 411.5,693.5C412.2,696.3 412.8,699 413.5,701.5C413.8,701.5 414.2,701.5 414.5,701.5C414.4,702.5 414.7,703.3 415.5,704C424.9,704.9 434.2,704.6 443.5,703C444.1,693.9 444.9,684.7 446,675.5C446.9,677.7 448,679.7 449.5,681.5C450.5,684.1 450.8,686.8 450.5,689.5C449.7,688.3 448.7,687.3 447.5,686.5C446.9,692.2 446,697.9 445,703.5C443.8,705 442.3,706.2 440.5,707C431.8,707.7 423.2,707.7 414.5,707C411.2,703.9 409.7,700.1 410,695.5C409.3,694.9 408.4,694.4 407.5,694C390.8,693.3 374.2,693.3 357.5,694C356.9,694.8 356.4,695.6 356,696.5C355.7,700.2 355.3,703.8 355,707.5C354.7,709.3 353.9,710.8 352.5,712C343.2,714 333.8,714 324.5,712C323.7,711.2 322.8,710.3 322,709.5C321.3,664.5 321.3,619.5 322,574.5C322.4,573.9 322.9,573.6 323.5,573.5Z"/>
|
||||
<path android:fillColor="#737e82" android:pathData="M413.5,687.5C414.5,691.6 414.8,696 414.5,700.5C423.8,700.5 433.2,700.5 442.5,700.5C433.3,701.5 424,701.8 414.5,701.5C414.2,701.5 413.8,701.5 413.5,701.5C413.5,698.5 413.5,695.5 413.5,692.5C413.5,690.8 413.5,689.2 413.5,687.5Z"/>
|
||||
<path android:fillColor="#b6bfc2" android:pathData="M413.5,692.5C413.5,695.5 413.5,698.5 413.5,701.5C412.8,699 412.2,696.3 411.5,693.5C410.7,693.2 410,692.7 409.5,692C391.5,691.3 373.5,691.3 355.5,692C355,692.7 354.3,693.2 353.5,693.5C353.5,692.2 353.5,690.8 353.5,689.5C372.2,689.3 390.8,689.5 409.5,690C411,690.7 412.3,691.5 413.5,692.5Z"/>
|
||||
<path android:fillColor="#a2aaae" android:pathData="M413.5,687.5C413.5,689.2 413.5,690.8 413.5,692.5C412.3,691.5 411,690.7 409.5,690C390.8,689.5 372.2,689.3 353.5,689.5C353.5,690.8 353.5,692.2 353.5,693.5C353.8,698.4 353.5,703 352.5,707.5C352.5,700.8 352.5,694.2 352.5,687.5C372.8,687.5 393.2,687.5 413.5,687.5Z"/>
|
||||
<path android:fillColor="#a5aeb1" android:pathData="M547.5,645.5C546.6,651.6 544.5,657.3 541,662.5C533.3,672 525.1,681 516.5,689.5C515.1,689.3 513.8,689.5 512.5,690C507.8,694.5 502.5,698 496.5,700.5C486.3,703.7 475.9,706.7 465.5,709.5C461.2,709.8 459.2,707.8 459.5,703.5C460.6,704.6 461.4,705.9 462,707.5C490.8,703.1 514.8,690.1 534,668.5C539.2,661.2 543.7,653.5 547.5,645.5Z"/>
|
||||
<path android:fillColor="#b8c2c6" android:pathData="M562.5,537.5C563.8,538.5 564.4,540 564.5,542C564.8,545.5 563.7,548.3 561,550.5C560.7,567.5 560.3,584.5 560,601.5C558.9,624.8 552.5,646.5 541,666.5C534.2,673 527.7,679.8 521.5,687C519.9,688.1 518.3,688.9 516.5,689.5C525.1,681 533.3,672 541,662.5C544.5,657.3 546.6,651.6 547.5,645.5C553,630.3 556.5,614.6 558,598.5C558.3,580.8 558.7,563.2 559,545.5C562.5,543.6 563.7,540.9 562.5,537.5Z"/>
|
||||
<path android:fillColor="#b4bdc0" android:pathData="M323.5,575.5C323.5,619.5 323.5,663.5 323.5,707.5C333.2,707.5 342.8,707.5 352.5,707.5C352,708.5 351.4,709.3 350.5,710C341,711 331.6,710.5 322.5,708.5C322.2,664 322.5,619.7 323.5,575.5Z"/>
|
||||
<path android:fillColor="#bcc5c9" android:pathData="M443.5,574.5C444.3,575.3 444.8,576.3 445,577.5C445.3,609.2 445.7,640.8 446,672.5C446.3,672.8 446.7,673.2 447,673.5C447.3,672.6 447.8,671.9 448.5,671.5C452.2,682.2 455.8,692.8 459.5,703.5C459.2,707.8 461.2,709.8 465.5,709.5C475.9,706.7 486.3,703.7 496.5,700.5C489.2,705.3 481.2,708.8 472.5,711C464.6,713.3 459.1,710.8 456,703.5C454.6,698.5 452.8,693.9 450.5,689.5C450.8,686.8 450.5,684.1 449.5,681.5C448,679.7 446.9,677.7 446,675.5C444.9,684.7 444.1,693.9 443.5,703C434.2,704.6 424.9,704.9 415.5,704C414.7,703.3 414.4,702.5 414.5,701.5C424,701.8 433.3,701.5 442.5,700.5C443.5,658.7 443.8,616.7 443.5,574.5Z"/>
|
||||
<path android:fillColor="#8e999d" android:pathData="M323.5,570.5C363.3,569.5 403.3,569.2 443.5,569.5C443.5,571.2 443.5,572.8 443.5,574.5C443.8,616.7 443.5,658.7 442.5,700.5C442.5,657.2 442.5,613.8 442.5,570.5C402.8,570.5 363.2,570.5 323.5,570.5Z"/>
|
||||
<path android:fillColor="#142c33" android:pathData="M323.5,570.5C363.2,570.5 402.8,570.5 442.5,570.5C442.5,613.8 442.5,657.2 442.5,700.5C433.2,700.5 423.8,700.5 414.5,700.5C414.8,696 414.5,691.6 413.5,687.5C393.2,687.5 372.8,687.5 352.5,687.5C352.5,694.2 352.5,700.8 352.5,707.5C342.8,707.5 333.2,707.5 323.5,707.5C323.5,663.5 323.5,619.5 323.5,575.5C323.5,574.8 323.5,574.2 323.5,573.5C323.5,572.5 323.5,571.5 323.5,570.5Z"/>
|
||||
<path android:fillColor="#728084" android:pathData="M413.5,597.5C414.5,618.3 414.8,639.3 414.5,660.5C393.7,660.8 373,660.5 352.5,659.5C352.8,659.5 353.2,659.5 353.5,659.5C354.2,659.5 354.8,659.5 355.5,659.5C374.5,659.5 393.5,659.5 412.5,659.5C413.5,640 413.8,620.3 413.5,600.5C413.5,599.5 413.5,598.5 413.5,597.5Z"/>
|
||||
<path android:fillColor="#bcc6ca" android:pathData="M413.5,600.5C413.8,620.3 413.5,640 412.5,659.5C412.8,640.8 412.5,622.2 411.5,603.5C393.2,602.3 374.9,602.2 356.5,603C356,603.5 355.5,604 355,604.5C354.2,622.9 354.3,641.2 355.5,659.5C354.8,659.5 354.2,659.5 353.5,659.5C353.2,639.9 353.5,620.4 354.5,601C364.4,599.9 374.4,599.4 384.5,599.5C394.2,599.5 403.9,599.9 413.5,600.5Z"/>
|
||||
<path android:fillColor="#cfdade" android:pathData="M412.5,659.5C393.5,659.5 374.5,659.5 355.5,659.5C354.3,641.2 354.2,622.9 355,604.5C355.5,604 356,603.5 356.5,603C374.9,602.2 393.2,602.3 411.5,603.5C412.5,622.2 412.8,640.8 412.5,659.5Z"/>
|
||||
<path android:fillColor="#d7e2e6" android:pathData="M358.5,605.5C375.2,605.5 391.8,605.5 408.5,605.5C408.5,622.8 408.5,640.2 408.5,657.5C391.6,657.8 374.8,657.5 358,656.5C357.2,639.4 357.3,622.4 358.5,605.5Z"/>
|
||||
<path android:fillColor="#a0a8ab" android:pathData="M413.5,597.5C413.5,598.5 413.5,599.5 413.5,600.5C403.9,599.9 394.2,599.5 384.5,599.5C374.4,599.4 364.4,599.9 354.5,601C353.5,620.4 353.2,639.9 353.5,659.5C353.2,659.5 352.8,659.5 352.5,659.5C352.5,638.8 352.5,618.2 352.5,597.5C372.8,597.5 393.2,597.5 413.5,597.5Z"/>
|
||||
<path android:fillColor="#8e999d" android:pathData="M557.5,536.5C546.5,536.5 535.5,536.5 524.5,536.5C524.8,555 524.5,573.3 523.5,591.5C523.5,573.8 523.5,556.2 523.5,538.5C523.5,537.5 523.5,536.5 523.5,535.5C535,535.2 546.3,535.5 557.5,536.5Z"/>
|
||||
<path android:fillColor="#c4ced2" android:pathData="M523.5,538.5C523.5,556.2 523.5,573.8 523.5,591.5C523.6,605.4 520.6,618.4 514.5,630.5C515.7,624.8 517.2,619.1 519,613.5C520.2,589.5 520.8,565.5 521,541.5C521.4,540.1 522.2,539.1 523.5,538.5Z"/>
|
||||
<path android:fillColor="#142c33" android:pathData="M557.5,536.5C559.2,536.6 560.9,537 562.5,537.5C563.7,540.9 562.5,543.6 559,545.5C558.7,563.2 558.3,580.8 558,598.5C556.5,614.6 553,630.3 547.5,645.5C543.7,653.5 539.2,661.2 534,668.5C514.8,690.1 490.8,703.1 462,707.5C461.4,705.9 460.6,704.6 459.5,703.5C455.8,692.8 452.2,682.2 448.5,671.5C457.6,671.4 466.2,669.7 474.5,666.5C478.9,665.6 482.9,663.9 486.5,661.5C499.4,654.3 508.7,644 514.5,630.5C520.6,618.4 523.6,605.4 523.5,591.5C524.5,573.3 524.8,555 524.5,536.5C535.5,536.5 546.5,536.5 557.5,536.5Z"/>
|
||||
<path android:fillColor="#cad8dc" android:pathData="M601.5,841.5C553.1,855.4 504.1,858.1 454.5,849.5C488.9,853.4 523.2,853.2 557.5,849C572.2,846.4 586.8,843.9 601.5,841.5Z"/>
|
||||
</vector>
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
</inset>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
This file was created by '@expo/configure-splash-screen' and some of it's content shouldn't be modified by hand
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
</layer-list>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/splashscreen_background"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:scaleType="centerCrop"
|
||||
android:navigationBarColor="#333333"
|
||||
tools:context=".MainActivity">
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="130dp"
|
||||
app:lottie_rawRes="@raw/loading"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="false"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 874 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<color name="iconBackground">#FFFFFF</color>
|
||||
<color name="splashscreen_background">#1F2024</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#202125</color>
|
||||
<color name="navigationBarColor">#202125</color>
|
||||
<color name="notification_icon_color">#00adb5</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<string name="app_name">LNReader</string>
|
||||
</resources>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml
|
||||
version="1.0"
|
||||
encoding="UTF-8"
|
||||
standalone="yes"
|
||||
?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="colorControlActivated">#2596be</item>
|
||||
</style>
|
||||
|
||||
<style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<!-- Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually -->
|
||||
<!-- Customize your splash screen theme here -->
|
||||
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/colorPrimaryDark</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s" >@drawable/invisible</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="s">1000</item>
|
||||
<!-- <item name="icon">@drawable/no_icon</item> -->
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "36.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
ndkVersion = "27.1.12297006"
|
||||
kotlinVersion = "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
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"
|
||||
@@ -1,56 +0,0 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
|
||||
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
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# 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=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=false
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
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,7 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -1,251 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# 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/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
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.
|
||||
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=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -1,99 +0,0 @@
|
||||
@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
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@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 ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
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
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
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=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%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
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,11 +0,0 @@
|
||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||
rootProject.name = 'LNReader'
|
||||
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
|
||||
useExpoModules()
|
||||
@@ -1,19 +1,87 @@
|
||||
{
|
||||
"expo": {
|
||||
"version": "2.1.3",
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
"icon": "./assets/native/ic_launcher.png",
|
||||
"plugins": [
|
||||
"expo-localization",
|
||||
"react-native-edge-to-edge",
|
||||
"./plugins/withAndroidCustomizations",
|
||||
[
|
||||
"expo-sqlite",
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"enableFTS": false,
|
||||
"useSQLCipher": false
|
||||
"image": "./assets/native/ic_launcher_round.png",
|
||||
"backgroundColor": "#1D1B20",
|
||||
"resizeMode": "contain",
|
||||
"dark": {
|
||||
"image": "./assets/native/ic_launcher_round.png",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"android": {
|
||||
"image": "./assets/native/splash_icon.png",
|
||||
"backgroundColor": "#132C33",
|
||||
"imageWidth": 288,
|
||||
"resizeMode": "native",
|
||||
"dark": {
|
||||
"image": "./assets/native/splash_icon.png",
|
||||
"backgroundColor": "#132C33"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-web-browser"
|
||||
]
|
||||
"expo-localization",
|
||||
[
|
||||
"react-native-edge-to-edge",
|
||||
{
|
||||
"android": {
|
||||
"enforceNavigationBarContrast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-web-browser",
|
||||
"./plugins/withReaderAssets",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"usesCleartextTraffic": true,
|
||||
"manifestApplicationAttributes": {
|
||||
"android:largeHeap": "true",
|
||||
"android:allowBackup": "true"
|
||||
},
|
||||
"enableMinifyInReleaseBuilds": true,
|
||||
"enableShrinkResourcesInReleaseBuilds": true
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"android": {
|
||||
"versionCode": 4196355,
|
||||
"package": "com.rajarsheechatterjee.LNReader",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/native/ic_launcher_foreground.png",
|
||||
"backgroundImage": "./assets/native/ic_launcher_background.png",
|
||||
"monochromeImage": "./assets/native/ic_launcher_monochrome.png"
|
||||
},
|
||||
"permissions": [
|
||||
"android.permission.DOWNLOAD_WITHOUT_NOTIFICATION",
|
||||
"android.permission.WAKE_LOCK",
|
||||
"android.permission.FOREGROUND_SERVICE",
|
||||
"android.permission.FOREGROUND_SERVICE_DATA_SYNC",
|
||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
|
||||
"android.permission.POST_NOTIFICATIONS"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "3a63596f-e80c-4342-96b7-a8ac48910489"
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.rajarsheechatterjee.LNReader",
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": ["audio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 672 B |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 528 B |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 829 B |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 189 KiB |