Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e478b2cc8 |
@@ -0,0 +1,23 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
name: Issue report
|
||||
description: Report an issue in LNReader
|
||||
labels: [bug]
|
||||
labels: [Bug]
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
@@ -45,7 +45,7 @@ body:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "2.1.4"
|
||||
Example: "2.0.1"
|
||||
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 plugin, I should be opening an issue in the [plugins repository](https://github.com/lnreader/lnreader-plugins/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.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.0.1](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
name: Feature request
|
||||
description: Suggest a feature to improve LNReader
|
||||
labels: [feature]
|
||||
labels: [Feature Request]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
@@ -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.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.0.1](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,208 +1,94 @@
|
||||
name: Build Preview
|
||||
|
||||
name: Build
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 0 * * *'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
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: APK
|
||||
needs: check-preview
|
||||
if: needs.check-preview.outputs.should-build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set Up pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
node-version: '20'
|
||||
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v5
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: false
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Configure Release Environment
|
||||
- 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
|
||||
env:
|
||||
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_ID }}
|
||||
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
|
||||
COMMIT_ID: ${{ env.COMMIT_ID }}
|
||||
run: |
|
||||
pnpm generate:env:release \
|
||||
--build-type "GitHub Action" \
|
||||
--node-env "production"
|
||||
sed -i 's/LNReader/LNReader-r${{ env.COMMIT_COUNT }}(${{ env.COMMIT_ID }})/g' android/app/src/main/res/values/strings.xml
|
||||
cd android && ./gradlew assembleRelease -PcustomAppId=com.rajarsheechatterjee.LNReader.commit_${{ env.COMMIT_ID }} --build-cache
|
||||
mv app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/LNReader-r${{ env.COMMIT_COUNT }}-${{ env.COMMIT_ID }}.apk
|
||||
|
||||
- name: 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
|
||||
- name: Upload Release Artifact
|
||||
env:
|
||||
COMMIT_COUNT: ${{ env.COMMIT_COUNT }}
|
||||
COMMIT_ID: ${{ env.COMMIT_ID }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -44,8 +44,5 @@ 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
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
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
|
||||
@@ -1,51 +0,0 @@
|
||||
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,39 +1,36 @@
|
||||
# Temporarily Disabled
|
||||
name: Type Check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - master
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
name: TypeScript
|
||||
build:
|
||||
name: Type Checking
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Path
|
||||
- name: Get pnpm Store Directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm
|
||||
- name: Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
@@ -44,8 +41,5 @@ 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,8 +8,7 @@ npm-debug.*
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
.*env
|
||||
.env.local
|
||||
.env
|
||||
reader_playground/index.html
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -89,31 +88,3 @@ 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/
|
||||
|
||||
# iOS: reader assets inlined into the JS bundle (scripts/embed-reader-assets.mjs)
|
||||
src/utils/readerAssets/embedded.generated.ts
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm run type-check
|
||||
@@ -1,132 +0,0 @@
|
||||
# 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,122 +1,118 @@
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
import { PropsWithChildren, Suspense, useEffect, useMemo } from 'react';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { StatusBar, StyleSheet } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import LottieSplashScreen from 'react-native-lottie-splash-screen';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import {
|
||||
MD3DarkTheme,
|
||||
MD3LightTheme,
|
||||
Provider as PaperProvider,
|
||||
} from 'react-native-paper';
|
||||
import { Provider as PaperProvider } from 'react-native-paper';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
|
||||
import AppErrorBoundary, {
|
||||
ErrorFallback,
|
||||
} from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import CrashRecoveryGate from '@components/AppErrorBoundary/CrashRecoveryGate';
|
||||
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 {
|
||||
...baseTheme,
|
||||
colors: {
|
||||
...baseTheme.colors,
|
||||
...theme,
|
||||
},
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
};
|
||||
}, [theme]);
|
||||
},
|
||||
});
|
||||
|
||||
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
|
||||
};
|
||||
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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const Application = () => {
|
||||
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
|
||||
const { success: databaseReady, error: databaseError } = useInitDatabase();
|
||||
const { ready: servicesReady, error: servicesError } =
|
||||
useInitializeAppServices(Boolean(databaseReady));
|
||||
Notifications.setNotificationChannelAsync('tts-controls', {
|
||||
name: 'TTS Controls',
|
||||
description: 'Text-to-Speech playback controls',
|
||||
importance: Notifications.AndroidImportance.HIGH,
|
||||
vibrationPattern: [],
|
||||
enableLights: false,
|
||||
enableVibrate: false,
|
||||
});
|
||||
|
||||
const App = () => {
|
||||
const { isDbReady, dbError, retryInitialization } =
|
||||
useDatabaseInitialization();
|
||||
|
||||
useEffect(() => {
|
||||
if ((databaseReady && servicesReady) || databaseError || servicesError) {
|
||||
SplashScreen.hideAsync();
|
||||
if (isDbReady || dbError) {
|
||||
LottieSplashScreen.hide();
|
||||
}
|
||||
}, [databaseReady, databaseError, servicesReady, servicesError]);
|
||||
}, [isDbReady, dbError]);
|
||||
|
||||
const initializationError = databaseError || servicesError;
|
||||
|
||||
if (initializationError) {
|
||||
return (
|
||||
<ErrorFallback error={initializationError} resetError={() => null} />
|
||||
useEffect(() => {
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||
response => {
|
||||
const actionIdentifier = response.actionIdentifier;
|
||||
if (actionIdentifier.startsWith('TTS_')) {
|
||||
const { setTTSAction } = require('@utils/ttsNotification');
|
||||
setTTSAction(actionIdentifier);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (dbError) {
|
||||
return <ErrorFallback error={dbError} resetError={retryInitialization} />;
|
||||
}
|
||||
|
||||
if (!databaseReady || !servicesReady) {
|
||||
if (!isDbReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedRootView>
|
||||
<KeyboardProvider>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<ThemedPaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</ThemedPaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</KeyboardProvider>
|
||||
</ThemedRootView>
|
||||
<GestureHandlerRootView style={styles.flex}>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider>
|
||||
<CrashRecoveryGate>
|
||||
<Application />
|
||||
</CrashRecoveryGate>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -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 without major issues, except for the emulator.
|
||||
Compatibility with Microsoft WSL2 is not guaranteed, but should work withoud 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 organization or account, take the following steps to get started:
|
||||
After forking to your own github org or account, do 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 (e.g., VS Code).
|
||||
You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
|
||||
|
||||
```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
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# 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,5 +13,4 @@ gem 'concurrent-ruby', '< 1.3.4'
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
gem 'nkf'
|
||||
gem 'mutex_m'
|
||||
@@ -1,5 +1,3 @@
|
||||
> **open-lab fork:** branch `ios` adds an iOS build (sideloaded with SideStore and a free Apple ID) and preconfigures the open-lab plugin repository. See [docs/ios-sidestore.md](docs/ios-sidestore.md).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://lnreader.app">
|
||||
<img src="./.github/readme-images/icon_new.png" align="center" width="128" />
|
||||
@@ -16,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">
|
||||
@@ -34,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/actions/workflows/build.yml">
|
||||
<img alt="Build Preview" src="https://github.com/lnreader/lnreader/actions/workflows/build.yml/badge.svg">
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader?include_prereleases&sort=semver&label=Beta&labelColor=3d3d5c&color=2a2a47&style=flat">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Get the app from our <a href="https://github.com/lnreader/lnreader/releases">releases page</a>.
|
||||
Get the app from our <a href="https://github.com/LNReader/lnreader/releases">releases page</a>.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -60,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
|
||||
|
||||
@@ -72,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)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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.** { *; }
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">LNReader debug</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
@@ -14,41 +14,6 @@ html {
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
#text-selection-ui {
|
||||
position: fixed;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--theme-outlineVariant);
|
||||
border-radius: 24px;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 150ms;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#text-selection-ui button {
|
||||
min-width: 88px;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--theme-onSurface);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
#text-selection-ui button + button {
|
||||
border-inline-start: 1px solid var(--theme-outlineVariant);
|
||||
}
|
||||
|
||||
#text-selection-ui button:active {
|
||||
background-color: var(--theme-surfaceVariant);
|
||||
}
|
||||
|
||||
body {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
@@ -88,10 +53,6 @@ img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
div:has(> table):not(#LNReader-chapter) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: var(--theme-onPrimary);
|
||||
border-collapse: collapse;
|
||||
@@ -122,6 +83,8 @@ td {
|
||||
margin-left: var(--readerSettings-padding);
|
||||
margin-right: var(--readerSettings-padding);
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: var(--theme-onPrimary);
|
||||
background-color: var(--theme-primary);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: 16px;
|
||||
@@ -130,7 +93,6 @@ td {
|
||||
}
|
||||
|
||||
.next-button {
|
||||
color: var(--theme-onPrimary);
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -198,21 +160,6 @@ td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mark.lnreader-search-match {
|
||||
border-radius: 2px;
|
||||
color: inherit;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--theme-tertiary) 55%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
mark.lnreader-search-match-active {
|
||||
color: var(--theme-onPrimary);
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#Image-Modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -1,25 +1,13 @@
|
||||
body.page-reader {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
padding-bottom: unset;
|
||||
}
|
||||
|
||||
body.page-reader > #LNReader-chapter {
|
||||
height: calc(100% - var(--pageReader-footerHeight, 0px));
|
||||
height: calc(90vh);
|
||||
column-width: calc(100vw - var(--readerSettings-padding) * 2);
|
||||
column-gap: calc(var(--readerSettings-padding) * 2);
|
||||
column-fill: auto;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
body.page-reader.no-animation > #LNReader-chapter {
|
||||
transition: none !important;
|
||||
}
|
||||
body.page-reader.no-animation .transition-chapter {
|
||||
transition: none !important;
|
||||
transition: 200ms;
|
||||
}
|
||||
.transition-chapter {
|
||||
height: 100vh;
|
||||
@@ -0,0 +1,48 @@
|
||||
#TTS-Controller {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#TTS-Controller button {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--readerSettings-theme) 85%,
|
||||
var(--readerSettings-textColor) 15%
|
||||
);
|
||||
outline: none;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--readerSettings-textColor) 30%,
|
||||
var(--readerSettings-theme) 70%
|
||||
);
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border-radius: 100%;
|
||||
transition: 0.5s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#TTS-Controller.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#TTS-Controller.active button {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--readerSettings-textColor);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#TTS-Controller.active svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
/* eslint-disable no-console */
|
||||
window.reader = new (function () {
|
||||
const {
|
||||
readerSettings,
|
||||
chapterGeneralSettings,
|
||||
novel,
|
||||
chapter,
|
||||
nextChapter,
|
||||
prevChapter,
|
||||
batteryLevel,
|
||||
autoSaveInterval,
|
||||
DEBUG,
|
||||
strings,
|
||||
} = initialReaderConfig;
|
||||
|
||||
// state
|
||||
this.hidden = van.state(true);
|
||||
this.batteryLevel = van.state(batteryLevel);
|
||||
this.readerSettings = van.state(readerSettings);
|
||||
this.generalSettings = van.state(chapterGeneralSettings);
|
||||
|
||||
this.chapterElement = document.querySelector('#LNReader-chapter');
|
||||
this.selection = window.getSelection();
|
||||
this.viewport = document.querySelector('meta[name=viewport]');
|
||||
|
||||
this.novel = novel;
|
||||
this.chapter = chapter;
|
||||
this.nextChapter = nextChapter;
|
||||
this.prevChapter = prevChapter;
|
||||
this.strings = strings;
|
||||
this.autoSaveInterval = autoSaveInterval;
|
||||
this.rawHTML = this.chapterElement.innerHTML;
|
||||
|
||||
//layout props
|
||||
this.paddingTop = parseInt(
|
||||
getComputedStyle(document.querySelector('body')).getPropertyValue(
|
||||
'padding-top',
|
||||
),
|
||||
10,
|
||||
);
|
||||
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
|
||||
this.layoutHeight = window.screen.height;
|
||||
this.layoutWidth = window.screen.width;
|
||||
|
||||
this.layoutEvent = undefined;
|
||||
this.chapterEndingVisible = van.state(false);
|
||||
|
||||
this.post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
||||
this.refresh = () => {
|
||||
if (this.generalSettings.val.pageReader) {
|
||||
this.chapterWidth = this.chapterElement.scrollWidth;
|
||||
} else {
|
||||
this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop;
|
||||
}
|
||||
};
|
||||
|
||||
van.derive(() => {
|
||||
const settings = this.readerSettings.val;
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-theme',
|
||||
settings.theme,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-padding',
|
||||
settings.padding + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textSize',
|
||||
settings.textSize + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textColor',
|
||||
settings.textColor,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textAlign',
|
||||
settings.textAlign,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-lineHeight',
|
||||
settings.lineHeight,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-fontFamily',
|
||||
settings.fontFamily,
|
||||
);
|
||||
if (settings.fontFamily) {
|
||||
new FontFace(
|
||||
settings.fontFamily,
|
||||
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
|
||||
)
|
||||
.load()
|
||||
.then(function (loadedFont) {
|
||||
document.fonts.add(loadedFont);
|
||||
});
|
||||
} else {
|
||||
// have no affect with a font declared in head
|
||||
document.fonts.forEach(fontFace => document.fonts.delete(fontFace));
|
||||
}
|
||||
});
|
||||
|
||||
document.onscrollend = () => {
|
||||
if (!this.generalSettings.val.pageReader) {
|
||||
this.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
|
||||
10,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-global-assign, no-new-object
|
||||
console = new Object();
|
||||
console.log = function (...data) {
|
||||
reader.post({ 'type': 'console', 'msg': data?.join(' ') });
|
||||
};
|
||||
console.debug = console.log;
|
||||
console.info = console.log;
|
||||
console.warn = console.log;
|
||||
console.error = console.log;
|
||||
}
|
||||
// end reader
|
||||
})();
|
||||
|
||||
window.tts = new (function () {
|
||||
this.readableNodeNames = [
|
||||
'#text',
|
||||
'B',
|
||||
'I',
|
||||
'SPAN',
|
||||
'EM',
|
||||
'BR',
|
||||
'STRONG',
|
||||
'A',
|
||||
];
|
||||
this.prevElement = null;
|
||||
this.currentElement = reader.chapterElement;
|
||||
this.started = false;
|
||||
this.reading = false;
|
||||
|
||||
this.readable = element => {
|
||||
const ele = element ?? this.currentElement;
|
||||
if (
|
||||
ele.nodeName !== 'SPAN' &&
|
||||
this.readableNodeNames.includes(ele.nodeName)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!ele.hasChildNodes()) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < ele.childNodes.length; i++) {
|
||||
if (!this.readableNodeNames.includes(ele.childNodes.item(i).nodeName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
this.normalizeText = text => {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*([.,!?;:])\s*/g, '$1 ')
|
||||
.trim();
|
||||
};
|
||||
|
||||
// if can find a readable node, else stop tts
|
||||
this.findNextTextNode = () => {
|
||||
if (this.currentElement.isSameNode(reader.chapterElement) && this.started) {
|
||||
return false;
|
||||
} else {
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
// is read, have to go next or go back
|
||||
if (this.currentElement.isSameNode(this.prevElement)) {
|
||||
this.prevElement = this.currentElement;
|
||||
if (this.currentElement.nextElementSibling) {
|
||||
this.currentElement = this.currentElement.nextElementSibling;
|
||||
} else {
|
||||
this.currentElement = this.currentElement.parentElement;
|
||||
}
|
||||
return this.findNextTextNode();
|
||||
} else {
|
||||
// can read? read it
|
||||
if (this.readable()) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!this.prevElement?.parentElement?.isSameNode(this.currentElement) &&
|
||||
this.currentElement.firstElementChild
|
||||
) {
|
||||
// go deep
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.firstElementChild;
|
||||
} else if (this.currentElement.nextElementSibling) {
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.nextElementSibling;
|
||||
} else {
|
||||
this.prevElement = this.currentElement;
|
||||
this.currentElement = this.currentElement.parentElement;
|
||||
}
|
||||
return this.findNextTextNode();
|
||||
}
|
||||
};
|
||||
|
||||
this.next = () => {
|
||||
try {
|
||||
this.currentElement?.classList?.remove('highlight');
|
||||
if (this.findNextTextNode()) {
|
||||
const text = this.normalizeText(this.currentElement?.innerText);
|
||||
if (text) {
|
||||
this.reading = true;
|
||||
this.speak();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
} else {
|
||||
this.reading = false;
|
||||
this.stop();
|
||||
const controller = document.getElementById('TTS-Controller');
|
||||
if (controller?.firstElementChild) {
|
||||
controller.firstElementChild.innerHTML = volumnIcon;
|
||||
}
|
||||
if (reader.nextChapter) {
|
||||
reader.post({ type: 'next', autoStartTTS: true });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
};
|
||||
|
||||
this.start = element => {
|
||||
this.stop();
|
||||
this.currentElement = element ?? reader.chapterElement;
|
||||
this.next();
|
||||
};
|
||||
|
||||
this.resume = () => {
|
||||
if (!this.reading) {
|
||||
if (
|
||||
this.currentElement &&
|
||||
this.currentElement.id !== 'LNReader-chapter'
|
||||
) {
|
||||
this.speak();
|
||||
this.reading = true;
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.pause = () => {
|
||||
this.reading = false;
|
||||
reader.post({ type: 'stop-speak' });
|
||||
reader.post({ type: 'tts-state', data: { isReading: false } });
|
||||
};
|
||||
|
||||
this.stop = () => {
|
||||
reader.post({ type: 'stop-speak' });
|
||||
this.currentElement?.classList?.remove('highlight');
|
||||
this.prevElement = null;
|
||||
this.currentElement = reader.chapterElement;
|
||||
this.started = false;
|
||||
this.reading = false;
|
||||
reader.post({ type: 'tts-state', data: { isReading: false } });
|
||||
};
|
||||
|
||||
this.isElementInViewport = element => {
|
||||
if (!element) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const windowHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight;
|
||||
const windowWidth =
|
||||
window.innerWidth || document.documentElement.clientWidth;
|
||||
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= windowHeight &&
|
||||
rect.right <= windowWidth
|
||||
);
|
||||
};
|
||||
|
||||
this.scrollToElement = element => {
|
||||
if (!element) return;
|
||||
// Check if element is partially visible (at least some part is in viewport)
|
||||
const rect = element.getBoundingClientRect();
|
||||
const windowHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight;
|
||||
const isPartiallyVisible =
|
||||
rect.top < windowHeight &&
|
||||
rect.bottom > 0 &&
|
||||
rect.left < window.innerWidth &&
|
||||
rect.right > 0;
|
||||
|
||||
// Only scroll if element is not visible or barely visible
|
||||
if (!isPartiallyVisible || rect.top < 0 || rect.bottom > windowHeight) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.speak = () => {
|
||||
if (!this.currentElement) return;
|
||||
this.prevElement = this.currentElement;
|
||||
this.scrollToElement(this.currentElement);
|
||||
this.currentElement.classList.add('highlight');
|
||||
const text = this.normalizeText(this.currentElement.innerText);
|
||||
if (text) {
|
||||
reader.post({ type: 'speak', data: text });
|
||||
reader.post({ type: 'tts-state', data: { isReading: true } });
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.pageReader = new (function () {
|
||||
this.page = van.state(0);
|
||||
this.totalPages = van.state(0);
|
||||
this.chapterEndingVisible = van.state(
|
||||
initialPageReaderConfig.nextChapterScreenVisible,
|
||||
);
|
||||
this.chapterEnding = document.getElementsByClassName('transition-chapter')[0];
|
||||
|
||||
this.showChapterEnding = (bool, instant, left) => {
|
||||
if (!this.chapterEnding) {
|
||||
this.chapterEnding =
|
||||
document.getElementsByClassName('transition-chapter')[0];
|
||||
if (!this.chapterEnding) return;
|
||||
}
|
||||
this.chapterEnding.style.transition = 'unset';
|
||||
if (bool) {
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
requestAnimationFrame(() => {
|
||||
if (!instant) this.chapterEnding.style.transition = '200ms';
|
||||
this.chapterEnding.style.transform = 'translateX(-100vw)';
|
||||
});
|
||||
this.chapterEndingVisible.val = true;
|
||||
} else {
|
||||
if (!instant) this.chapterEnding.style.transition = '200ms';
|
||||
this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`;
|
||||
this.chapterEndingVisible.val = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.movePage = destPage => {
|
||||
if (this.chapterEndingVisible.val) {
|
||||
if (destPage < 0) {
|
||||
this.showChapterEnding(false);
|
||||
return;
|
||||
}
|
||||
if (destPage < this.totalPages.val) {
|
||||
this.showChapterEnding(false, false, true);
|
||||
return;
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
return reader.post({ type: 'next' });
|
||||
}
|
||||
}
|
||||
destPage = parseInt(destPage, 10);
|
||||
if (destPage < 0) {
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.prevChapter.name;
|
||||
this.showChapterEnding(true, false, true);
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'prev' });
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
if (destPage >= this.totalPages.val) {
|
||||
document.getElementsByClassName('transition-chapter')[0].innerText =
|
||||
reader.nextChapter.name;
|
||||
this.showChapterEnding(true);
|
||||
setTimeout(() => {
|
||||
reader.post({ type: 'next' });
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
this.page.val = destPage;
|
||||
reader.chapterElement.style.transform =
|
||||
'translateX(-' + destPage * 100 + '%)';
|
||||
|
||||
const newProgress = parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
10,
|
||||
);
|
||||
|
||||
if (newProgress > reader.chapter.progress) {
|
||||
reader.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((pageReader.page.val + 1) / pageReader.totalPages.val) * 100,
|
||||
10,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
van.derive(() => {
|
||||
// ignore if initial or other states change
|
||||
if (
|
||||
reader.generalSettings.val.pageReader ===
|
||||
reader.generalSettings.oldVal.pageReader
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const ratio = Math.min(
|
||||
0.99,
|
||||
(window.scrollY + reader.layoutHeight) / reader.chapterHeight,
|
||||
);
|
||||
document.body.classList.add('page-reader');
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
this.totalPages.val = parseInt(
|
||||
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
|
||||
reader.layoutWidth,
|
||||
10,
|
||||
);
|
||||
this.movePage(this.totalPages.val * ratio);
|
||||
}, 100);
|
||||
} else {
|
||||
reader.chapterElement.style = '';
|
||||
document.body.classList.remove('page-reader');
|
||||
setTimeout(() => {
|
||||
reader.refresh();
|
||||
window.scrollTo({
|
||||
top:
|
||||
(reader.chapterHeight * (this.page.val + 1)) / this.totalPages.val -
|
||||
reader.layoutHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (pageReader.chapterEndingVisible.val) {
|
||||
pageReader.showChapterEnding(true, true);
|
||||
}
|
||||
});
|
||||
|
||||
function calculatePages() {
|
||||
reader.refresh();
|
||||
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
pageReader.totalPages.val = parseInt(
|
||||
(reader.chapterWidth + reader.readerSettings.val.padding * 2) /
|
||||
reader.layoutWidth,
|
||||
10,
|
||||
);
|
||||
|
||||
if (initialPageReaderConfig.nextChapterScreenVisible) return;
|
||||
|
||||
pageReader.movePage(
|
||||
Math.max(
|
||||
0,
|
||||
Math.round(
|
||||
(pageReader.totalPages.val * reader.chapter.progress) / 100,
|
||||
) - 1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
window.scrollTo({
|
||||
top:
|
||||
(reader.chapterHeight * reader.chapter.progress) / 100 -
|
||||
reader.layoutHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (pageReader.totalPages.val) {
|
||||
calculatePages();
|
||||
}
|
||||
});
|
||||
ro.observe(reader.chapterElement);
|
||||
|
||||
// Also call once on load
|
||||
window.addEventListener('load', () => {
|
||||
document.fonts.ready.then(() => {
|
||||
requestAnimationFrame(() => setTimeout(calculatePages, 0));
|
||||
});
|
||||
});
|
||||
|
||||
// click handler
|
||||
(function () {
|
||||
const detectTapPosition = (x, y, horizontal) => {
|
||||
if (horizontal) {
|
||||
if (x < 0.33) {
|
||||
return 'left';
|
||||
}
|
||||
if (x > 0.66) {
|
||||
return 'right';
|
||||
}
|
||||
} else {
|
||||
if (y < 0.33) {
|
||||
return 'top';
|
||||
}
|
||||
if (y > 0.66) {
|
||||
return 'bottom';
|
||||
}
|
||||
}
|
||||
return 'center';
|
||||
};
|
||||
document.onclick = e => {
|
||||
const { clientX, clientY } = e;
|
||||
const { x, y } = {
|
||||
x: clientX / reader.layoutWidth,
|
||||
y: clientY / reader.layoutHeight,
|
||||
};
|
||||
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const position = detectTapPosition(x, y, true);
|
||||
if (position === 'left') {
|
||||
pageReader.movePage(pageReader.page.val - 1);
|
||||
return;
|
||||
}
|
||||
if (position === 'right') {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (reader.generalSettings.val.tapToScroll) {
|
||||
const position = detectTapPosition(x, y, false);
|
||||
if (position === 'top') {
|
||||
window.scrollBy({
|
||||
top: -reader.layoutHeight * 0.75,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (position === 'bottom') {
|
||||
window.scrollBy({
|
||||
top: reader.layoutHeight * 0.75,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.post({ type: 'hide' });
|
||||
};
|
||||
})();
|
||||
|
||||
// swipe handler
|
||||
(function () {
|
||||
this.initialX = null;
|
||||
this.initialY = null;
|
||||
|
||||
reader.chapterElement.addEventListener('touchstart', e => {
|
||||
this.initialX = e.changedTouches[0].screenX;
|
||||
this.initialY = e.changedTouches[0].screenY;
|
||||
});
|
||||
|
||||
reader.chapterElement.addEventListener('touchmove', e => {
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
const diffX =
|
||||
(e.changedTouches[0].screenX - this.initialX) / reader.layoutWidth;
|
||||
reader.chapterElement.style.transition = 'unset';
|
||||
reader.chapterElement.style.transform =
|
||||
'translateX(-' + (pageReader.page.val - diffX) * 100 + '%)';
|
||||
}
|
||||
});
|
||||
|
||||
reader.chapterElement.addEventListener('touchend', e => {
|
||||
const diffX = e.changedTouches[0].screenX - this.initialX;
|
||||
const diffY = e.changedTouches[0].screenY - this.initialY;
|
||||
if (reader.generalSettings.val.pageReader) {
|
||||
reader.chapterElement.style.transition = '200ms';
|
||||
const diffXPercentage = diffX / reader.layoutWidth;
|
||||
if (diffXPercentage < -0.3) {
|
||||
pageReader.movePage(pageReader.page.val + 1);
|
||||
} else if (diffXPercentage > 0.3) {
|
||||
pageReader.movePage(pageReader.page.val - 1);
|
||||
} else {
|
||||
pageReader.movePage(pageReader.page.val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.target.id?.startsWith('scrollbar') ||
|
||||
e.target.id === 'Image-Modal-img'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
reader.generalSettings.val.swipeGestures &&
|
||||
Math.abs(diffX) > Math.abs(diffY) * 2 &&
|
||||
Math.abs(diffX) > 180
|
||||
) {
|
||||
if (diffX < 0 && this.initialX >= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'next' });
|
||||
} else if (diffX > 0 && this.initialX <= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'prev' });
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// text options
|
||||
(function () {
|
||||
van.derive(() => {
|
||||
let html = reader.rawHTML;
|
||||
if (reader.generalSettings.val.bionicReading) {
|
||||
html = textVide.textVide(reader.rawHTML);
|
||||
}
|
||||
|
||||
if (reader.generalSettings.val.removeExtraParagraphSpacing) {
|
||||
html = html
|
||||
.replace(/(?: \s*|[\u200b]\s*)+(?=<\/?p[> ])/g, '')
|
||||
.replace(/<br>\s*<br>\s*(?:<br>\s*)+/g, '<br><br>') //force max 2 consecutive <br>, chaining regex
|
||||
.replace(
|
||||
/<br>\s*<br>[^]+/,
|
||||
_ =>
|
||||
`${
|
||||
/\/p>/.test(_)
|
||||
? _.replace(
|
||||
/<br>\s*<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p\b[^>]*><br>\s*<br>))\s*/g,
|
||||
'',
|
||||
)
|
||||
: _
|
||||
}`,
|
||||
) //if p found, delete all double br near p
|
||||
.replace(/<br>(?:(?=\s*<\/?p[> ])|(?<=<\/?p>\s*<br>))\s*/g, '');
|
||||
}
|
||||
reader.chapterElement.innerHTML = html;
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
const volumnIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M552-152v-75q86-23 139-93.5T744-480q0-89-53.5-158.5T552-734v-75q116 25 190 117t74 211q0 119-73.5 211.5T552-152ZM144-385v-192h144l192-192v576L288-385H144Zm408 55v-302q45 20 70.5 61t25.5 90q0 49-25.5 89.5T552-330Z"/></svg>';
|
||||
const pauseIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M564-228v-504h168v504H564Zm-336 0v-504h168v504H228Z"/></svg>';
|
||||
const resumeIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M240-240v-480h72v480h-72Zm144 0 384-240-384-240v480Z"/></svg>';
|
||||
@@ -16,29 +16,18 @@ export interface Reader {
|
||||
generalSettings: State<ChapterGeneralSettings>;
|
||||
readerSettings: State<ChapterReaderSettings>;
|
||||
batteryLevel: State<number>;
|
||||
/** Bumped whenever the app pushes newly resolved adjacent chapters. */
|
||||
adjacentVersion: State<number>;
|
||||
|
||||
novel: NovelInfo;
|
||||
chapter: ChapterInfo;
|
||||
nextChapter?: ChapterInfo;
|
||||
prevChapter?: ChapterInfo;
|
||||
autoSaveInterval: number;
|
||||
rawHTML: string;
|
||||
strings: {
|
||||
finished: string;
|
||||
nextChapter: string;
|
||||
noNextChapter: string;
|
||||
removeText: string;
|
||||
replaceText: string;
|
||||
};
|
||||
|
||||
setAdjacentChapters: (adjacent: {
|
||||
nextChapter?: ChapterInfo;
|
||||
prevChapter?: ChapterInfo;
|
||||
strings?: Partial<Reader['strings']>;
|
||||
}) => void;
|
||||
|
||||
//layout props
|
||||
paddingTop: number;
|
||||
layoutHeight: number;
|
||||
@@ -0,0 +1,339 @@
|
||||
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());
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.rajarsheechatterjee.LNReader
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
import com.rajarsheechatterjee.NativeVolumeButtonListener.NativeVolumeButtonListener
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
import org.devio.rn.splashscreen.SplashScreen
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val layoutParams = WindowManager.LayoutParams()
|
||||
layoutParams.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
super.onCreate(null)
|
||||
SplashScreen.show(this, R.style.SplashScreenTheme, R.id.lottie)
|
||||
SplashScreen.setAnimationFinished(true)
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
if (NativeVolumeButtonListener.isActive) {
|
||||
val action = event.action
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
NativeVolumeButtonListener.sendEvent(true)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
NativeVolumeButtonListener.sendEvent(false)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
fabricEnabled
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.rajarsheechatterjee.LNReader
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.rajarsheechatterjee.NativeFile.NativePackage
|
||||
import com.rajarsheechatterjee.NativeVolumeButtonListener.NativeVolumeButtonListenerPackage
|
||||
import com.rajarsheechatterjee.NativeZipArchive.NativeZipArchivePackage
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
override val reactNativeHost: ReactNativeHost =
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
add(NativePackage())
|
||||
add(NativeVolumeButtonListenerPackage())
|
||||
add(NativeZipArchivePackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
loadReactNative(this)
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.rajarsheechatterjee.NativeFile
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.facebook.react.bridge.WritableNativeMap
|
||||
import com.facebook.react.modules.network.CookieJarContainer
|
||||
import com.facebook.react.modules.network.ForwardingCookieHandler
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider
|
||||
import com.lnreader.spec.NativeFileSpec
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Headers
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.PushbackInputStream
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
|
||||
class NativeFile(context: ReactApplicationContext) :
|
||||
NativeFileSpec(context) {
|
||||
private val BUFFER_SIZE = 4096
|
||||
private val okHttpClient = OkHttpClientProvider.createClient()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
val cookieContainer = okHttpClient.cookieJar as CookieJarContainer
|
||||
val cookieHandler = ForwardingCookieHandler(reactApplicationContext)
|
||||
cookieContainer.setCookieJar(JavaNetCookieJar(cookieHandler))
|
||||
}
|
||||
|
||||
private fun getFileUri(filepath: String): Uri {
|
||||
var uri = Uri.parse(filepath)
|
||||
if (uri.scheme == null) {
|
||||
// No prefix, assuming that provided path is absolute path to file
|
||||
val file = File(filepath)
|
||||
if (file.isDirectory) {
|
||||
throw Exception("Invalid file, folder found!")
|
||||
}
|
||||
uri = Uri.parse("file://$filepath")
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
private fun getInputStream(filepath: String): InputStream {
|
||||
val uri = getFileUri(filepath)
|
||||
return reactApplicationContext.contentResolver.openInputStream(uri)
|
||||
?: throw Exception("ENOENT: could not open an input stream for '$filepath'")
|
||||
}
|
||||
|
||||
private val writeAccessByAPILevel: String
|
||||
get() = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) "w" else "rwt"
|
||||
|
||||
private fun getOutputStream(filepath: String): OutputStream {
|
||||
val uri = getFileUri(filepath)
|
||||
return reactApplicationContext.contentResolver.openOutputStream(uri, writeAccessByAPILevel)
|
||||
?: throw Exception("ENOENT: could not open an output stream for '$filepath'")
|
||||
}
|
||||
|
||||
override fun writeFile(path: String, content: String) {
|
||||
val fw = FileWriter(path)
|
||||
fw.write(content)
|
||||
fw.close()
|
||||
}
|
||||
|
||||
override fun readFile(path: String): String {
|
||||
return File(path).bufferedReader().readText()
|
||||
}
|
||||
|
||||
override fun copyFile(filepath: String, destPath: String) {
|
||||
copyFileContent(filepath, destPath)
|
||||
}
|
||||
|
||||
override fun moveFile(filepath: String, destPath: String) {
|
||||
val inFile = File(filepath)
|
||||
copyFileContent(filepath, destPath, { inFile.delete() })
|
||||
}
|
||||
|
||||
private fun copyFileContent(
|
||||
filepath: String,
|
||||
destPath: String,
|
||||
onDone: (() -> Unit)? = null,
|
||||
) {
|
||||
val inputStream = getInputStream(filepath)
|
||||
val outputStream = getOutputStream(destPath)
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } > 0) {
|
||||
outputStream.write(buffer, 0, length)
|
||||
}
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
override fun exists(filepath: String) = File(filepath).exists()
|
||||
|
||||
override fun mkdir(filepath: String) {
|
||||
val file = File(filepath)
|
||||
if (!file.exists()) {
|
||||
val created = file.mkdirs()
|
||||
if (!created) throw Exception("Directory could not be created")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteRecursive(fileOrDirectory: File) {
|
||||
if (fileOrDirectory.isDirectory) {
|
||||
for (child in fileOrDirectory.listFiles()!!) {
|
||||
deleteRecursive(child)
|
||||
}
|
||||
}
|
||||
fileOrDirectory.delete()
|
||||
}
|
||||
|
||||
override fun unlink(filepath: String) {
|
||||
val file = File(filepath)
|
||||
if (!file.exists()) throw Exception("File does not exist")
|
||||
deleteRecursive(file)
|
||||
}
|
||||
|
||||
override fun readDir(directory: String): WritableArray {
|
||||
val file = File(directory)
|
||||
if (!file.exists()) throw Exception("Folder does not exist")
|
||||
val files = file.listFiles()
|
||||
val fileMaps: WritableArray = WritableNativeArray()
|
||||
for (childFile in files!!) {
|
||||
val fileMap: WritableMap = WritableNativeMap()
|
||||
fileMap.putString("name", childFile.name)
|
||||
fileMap.putString("path", childFile.absolutePath)
|
||||
fileMap.putBoolean("isDirectory", childFile.isDirectory)
|
||||
fileMaps.pushMap(fileMap)
|
||||
}
|
||||
return fileMaps
|
||||
}
|
||||
|
||||
private fun decompressStream(input: InputStream?): InputStream {
|
||||
val pb = PushbackInputStream(input, 2)
|
||||
val signature = ByteArray(2)
|
||||
val len = pb.read(signature)
|
||||
if(len == -1) return pb;
|
||||
pb.unread(signature, 0, len)
|
||||
return if (signature[0] == 0x1f.toByte() && signature[1] == 0x8b.toByte())
|
||||
GZIPInputStream(pb) else pb
|
||||
}
|
||||
|
||||
override fun downloadFile(
|
||||
url: String,
|
||||
destPath: String,
|
||||
method: String,
|
||||
headers: ReadableMap,
|
||||
body: String?,
|
||||
promise: Promise
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val headersBuilder = Headers.Builder()
|
||||
headers.entryIterator.forEach { entry ->
|
||||
headersBuilder.add(entry.key, entry.value.toString())
|
||||
}
|
||||
val requestBuilder = Request.Builder()
|
||||
.url(url)
|
||||
.headers(headersBuilder.build())
|
||||
if (method.lowercase() == "get") {
|
||||
requestBuilder.get()
|
||||
} else if (body != null) {
|
||||
requestBuilder.post(body.toRequestBody())
|
||||
}
|
||||
|
||||
okHttpClient.newCall(requestBuilder.build())
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
promise.reject(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful || response.body == null) {
|
||||
promise.reject(Exception("Failed to download load: ${response.code}"))
|
||||
return
|
||||
}
|
||||
try {
|
||||
val inputStream = decompressStream(response.body!!.byteStream())
|
||||
FileOutputStream(destPath).use { fos ->
|
||||
inputStream.copyTo(fos, BUFFER_SIZE)
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTypedExportedConstants(): MutableMap<String, Any> {
|
||||
val constants: MutableMap<String, Any> = HashMap()
|
||||
val externalDirectory = this.reactApplicationContext.getExternalFilesDir(null)
|
||||
if (externalDirectory != null) {
|
||||
constants["ExternalDirectoryPath"] = externalDirectory.absolutePath
|
||||
}
|
||||
val externalCachesDirectory = this.reactApplicationContext.externalCacheDir
|
||||
if (externalCachesDirectory != null) {
|
||||
constants["ExternalCachesDirectoryPath"] = externalCachesDirectory.absolutePath
|
||||
}
|
||||
return constants
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.NativeFile
|
||||
|
||||
import com.facebook.react.BaseReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.module.model.ReactModuleInfo
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||
import com.lnreader.spec.NativeFileSpec
|
||||
|
||||
class NativePackage : BaseReactPackage() {
|
||||
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
||||
if (name == NativeFileSpec.NAME) {
|
||||
NativeFile(reactContext)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
||||
mapOf(
|
||||
NativeFileSpec.NAME to ReactModuleInfo(
|
||||
NativeFileSpec.NAME,
|
||||
NativeFileSpec.NAME,
|
||||
canOverrideExistingModule = false,
|
||||
needsEagerInit = false,
|
||||
isCxxModule = false,
|
||||
isTurboModule = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.rajarsheechatterjee.NativeVolumeButtonListener
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||
import com.lnreader.spec.NativeVolumeButtonListenerSpec
|
||||
|
||||
class NativeVolumeButtonListener(appContext: ReactApplicationContext) :
|
||||
NativeVolumeButtonListenerSpec(appContext) {
|
||||
init {
|
||||
NativeVolumeButtonListener.appContext = appContext
|
||||
}
|
||||
|
||||
override fun addListener(eventName: String?) {
|
||||
isActive = true
|
||||
}
|
||||
|
||||
override fun removeListeners(count: Double) {
|
||||
isActive = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var appContext: ReactApplicationContext
|
||||
var isActive = false
|
||||
|
||||
fun sendEvent(up: Boolean) {
|
||||
if (isActive) {
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
.emit(if (up) "VolumeUp" else "VolumeDown", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.NativeVolumeButtonListener
|
||||
|
||||
import com.facebook.react.BaseReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.module.model.ReactModuleInfo
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||
import com.lnreader.spec.NativeVolumeButtonListenerSpec
|
||||
|
||||
class NativeVolumeButtonListenerPackage : BaseReactPackage() {
|
||||
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
||||
if (name == NativeVolumeButtonListenerSpec.NAME) {
|
||||
NativeVolumeButtonListener(reactContext)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
||||
mapOf(
|
||||
NativeVolumeButtonListenerSpec.NAME to ReactModuleInfo(
|
||||
NativeVolumeButtonListenerSpec.NAME,
|
||||
NativeVolumeButtonListenerSpec.NAME,
|
||||
canOverrideExistingModule = false,
|
||||
needsEagerInit = false,
|
||||
isCxxModule = false,
|
||||
isTurboModule = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.rajarsheechatterjee.NativeZipArchive
|
||||
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.lnreader.spec.NativeZipArchiveSpec
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class NativeZipArchive(context: ReactApplicationContext) : NativeZipArchiveSpec(context) {
|
||||
@ReactMethod
|
||||
override fun unzip(sourceFilePath: String, distDirPath: String, promise: Promise) {
|
||||
Thread {
|
||||
try {
|
||||
ZipFile(sourceFilePath).use { zis ->
|
||||
zis.entries().asSequence().filterNot { it.isDirectory }.forEach { zipEntry ->
|
||||
val newFile = File(distDirPath, zipEntry.name)
|
||||
newFile.parentFile?.mkdirs()
|
||||
zis.getInputStream(zipEntry).use { inputStream ->
|
||||
FileOutputStream(newFile).use { fos -> inputStream.copyTo(fos, 4096) }
|
||||
}
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun zip(sourceDirPath: String, zipFilePath: String, promise: Promise) {
|
||||
Thread {
|
||||
try {
|
||||
FileOutputStream(zipFilePath).use { fos ->
|
||||
ZipOutputStream(fos).use { zos -> zipProcess(sourceDirPath, zos) }
|
||||
}
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun remoteUnzip(
|
||||
distDirPath: String,
|
||||
urlString: String,
|
||||
headers: ReadableMap,
|
||||
promise: Promise
|
||||
) {
|
||||
val connection = URL(urlString).openConnection() as HttpURLConnection
|
||||
Thread {
|
||||
try {
|
||||
connection.requestMethod = "GET"
|
||||
val it = headers.entryIterator
|
||||
while (it.hasNext()) {
|
||||
val (key, value) = it.next()
|
||||
connection.setRequestProperty(key, value.toString())
|
||||
}
|
||||
ZipInputStream(connection.inputStream).use { zis ->
|
||||
generateSequence { zis.nextEntry }
|
||||
.filterNot { it.isDirectory }
|
||||
.forEach { zipEntry ->
|
||||
val newFile = File(distDirPath, zipEntry.name)
|
||||
newFile.parentFile?.mkdirs()
|
||||
FileOutputStream(newFile).use { fos -> zis.copyTo(fos, 4096) }
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
if (connection.responseCode == 200) {
|
||||
promise.resolve(null)
|
||||
} else {
|
||||
throw Exception("Network request failed")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun zipProcess(sourceDirPath: String, zos: ZipOutputStream) {
|
||||
val sourceDir = File(sourceDirPath)
|
||||
sourceDir.walkBottomUp().filter { it.isFile }.forEach { file ->
|
||||
val zipFileName =
|
||||
file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/")
|
||||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
|
||||
zos.putNextEntry(entry)
|
||||
file.inputStream().use { fis ->
|
||||
fis.copyTo(zos, 4096)
|
||||
fis.close()
|
||||
}
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun remoteZip(
|
||||
sourceDirPath: String,
|
||||
urlString: String,
|
||||
headers: ReadableMap,
|
||||
promise: Promise
|
||||
) {
|
||||
Thread {
|
||||
val connection = URL(urlString).openConnection() as HttpURLConnection
|
||||
try {
|
||||
connection.requestMethod = "POST"
|
||||
val it = headers.entryIterator
|
||||
while (it.hasNext()) {
|
||||
val (key, value) = it.next()
|
||||
connection.setRequestProperty(key, value.toString())
|
||||
}
|
||||
ZipOutputStream(connection.outputStream).use { zipProcess(sourceDirPath, it) }
|
||||
if (connection.responseCode == 200) {
|
||||
promise.resolve(
|
||||
connection.inputStream.bufferedReader().use { it.readText() })
|
||||
} else {
|
||||
throw Exception("Network request failed")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
promise.reject(e)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.NativeZipArchive
|
||||
|
||||
import com.facebook.react.BaseReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.module.model.ReactModuleInfo
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider
|
||||
import com.lnreader.spec.NativeZipArchiveSpec
|
||||
|
||||
class NativeZipArchivePackage : BaseReactPackage() {
|
||||
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
||||
if (name == NativeZipArchiveSpec.NAME) {
|
||||
NativeZipArchive(reactContext)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
||||
mapOf(
|
||||
NativeZipArchiveSpec.NAME to ReactModuleInfo(
|
||||
NativeZipArchiveSpec.NAME,
|
||||
NativeZipArchiveSpec.NAME,
|
||||
canOverrideExistingModule = false,
|
||||
needsEagerInit = false,
|
||||
isCxxModule = false,
|
||||
isTurboModule = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Define the library name here.
|
||||
project(appmodules)
|
||||
|
||||
# This file includes all the necessary to let you build your React Native application
|
||||
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
|
||||
|
||||
# Define where the additional source code lives. We need to crawl back the jni, main, src, app, android folders
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
../../../../../shared/NativeEpub.cpp
|
||||
../../../../../shared/Epub.cpp
|
||||
../../../../../shared/pugixml.cpp
|
||||
)
|
||||
|
||||
# Define where CMake can find the additional header files. We need to crawl back the jni, main, src, app, android folders
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../../../../shared)
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// This C++ file is part of the default configuration used by apps and is placed
|
||||
// inside react-native to encapsulate it from user space (so you won't need to
|
||||
// touch C++/Cmake code at all on Android).
|
||||
//
|
||||
// If you wish to customize it (because you want to manually link a C++ library
|
||||
// or pass a custom compilation flag) you can:
|
||||
//
|
||||
// 1. Copy this CMake file inside the `android/app/src/main/jni` folder of your
|
||||
// project
|
||||
// 2. Copy the OnLoad.cpp (in this same folder) file inside the same folder as
|
||||
// above.
|
||||
// 3. Extend your `android/app/build.gradle` as follows
|
||||
//
|
||||
// android {
|
||||
// // Other config here...
|
||||
// externalNativeBuild {
|
||||
// cmake {
|
||||
// path "src/main/jni/CMakeLists.txt"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#include <DefaultComponentsRegistry.h>
|
||||
#include <DefaultTurboModuleManagerDelegate.h>
|
||||
#include <autolinking.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
||||
#include <FBReactNativeSpec.h>
|
||||
#include <NativeEpub.hpp>
|
||||
|
||||
#ifdef REACT_NATIVE_APP_CODEGEN_HEADER
|
||||
#include REACT_NATIVE_APP_CODEGEN_HEADER
|
||||
#endif
|
||||
#ifdef REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
|
||||
#include REACT_NATIVE_APP_COMPONENT_DESCRIPTORS_HEADER
|
||||
#endif
|
||||
|
||||
namespace facebook::react
|
||||
{
|
||||
|
||||
void registerComponents(
|
||||
std::shared_ptr<const ComponentDescriptorProviderRegistry> registry)
|
||||
{
|
||||
// Custom Fabric Components go here. You can register custom
|
||||
// components coming from your App or from 3rd party libraries here.
|
||||
//
|
||||
// providerRegistry->add(concreteComponentDescriptorProvider<
|
||||
// MyComponentDescriptor>());
|
||||
|
||||
// We link app local components if available
|
||||
#ifdef REACT_NATIVE_APP_COMPONENT_REGISTRATION
|
||||
REACT_NATIVE_APP_COMPONENT_REGISTRATION(registry);
|
||||
#endif
|
||||
|
||||
// And we fallback to the components autolinked
|
||||
autolinking_registerProviders(registry);
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule> cxxModuleProvider(
|
||||
const std::string &name,
|
||||
const std::shared_ptr<CallInvoker> &jsInvoker)
|
||||
{
|
||||
// Here you can provide your CXX Turbo Modules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a module called `NativeCxxModuleExample`):
|
||||
//
|
||||
// if (name == NativeCxxModuleExample::kModuleName) {
|
||||
// return std::make_shared<NativeCxxModuleExample>(jsInvoker);
|
||||
// }
|
||||
|
||||
if (name == NativeEpub::kModuleName)
|
||||
{
|
||||
return std::make_shared<NativeEpub>(jsInvoker);
|
||||
}
|
||||
|
||||
// And we fallback to the CXX module providers autolinked
|
||||
return autolinking_cxxModuleProvider(name, jsInvoker);
|
||||
}
|
||||
|
||||
std::shared_ptr<TurboModule> javaModuleProvider(
|
||||
const std::string &name,
|
||||
const JavaTurboModule::InitParams ¶ms)
|
||||
{
|
||||
// Here you can provide your own module provider for TurboModules coming from
|
||||
// either your application or from external libraries. The approach to follow
|
||||
// is similar to the following (for a library called `samplelibrary`):
|
||||
//
|
||||
// auto module = samplelibrary_ModuleProvider(name, params);
|
||||
// if (module != nullptr) {
|
||||
// return module;
|
||||
// }
|
||||
// return FBReactNativeSpec_ModuleProvider(name, params);
|
||||
|
||||
// We link app local modules if available
|
||||
#ifdef REACT_NATIVE_APP_MODULE_PROVIDER
|
||||
auto module = REACT_NATIVE_APP_MODULE_PROVIDER(name, params);
|
||||
if (module != nullptr)
|
||||
{
|
||||
return module;
|
||||
}
|
||||
#endif
|
||||
|
||||
// We first try to look up core modules
|
||||
if (auto module = FBReactNativeSpec_ModuleProvider(name, params))
|
||||
{
|
||||
return module;
|
||||
}
|
||||
|
||||
// And we fallback to the module providers autolinked
|
||||
if (auto module = autolinking_ModuleProvider(name, params))
|
||||
{
|
||||
return module;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace facebook::react
|
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *)
|
||||
{
|
||||
return facebook::jni::initialize(vm, []
|
||||
{
|
||||
facebook::react::DefaultTurboModuleManagerDelegate::cxxModuleProvider =
|
||||
&facebook::react::cxxModuleProvider;
|
||||
facebook::react::DefaultTurboModuleManagerDelegate::javaModuleProvider =
|
||||
&facebook::react::javaModuleProvider;
|
||||
facebook::react::DefaultComponentsRegistry::
|
||||
registerComponentDescriptorsFromEntryPoint =
|
||||
&facebook::react::registerComponents; });
|
||||
}
|
||||
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 696 B |
|
After Width: | Height: | Size: 617 B |
|
After Width: | Height: | Size: 500 B |
|
After Width: | Height: | Size: 976 B |
|
After Width: | Height: | Size: 837 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,93 @@
|
||||
<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>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
@@ -2,5 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -2,5 +2,4 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 874 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |