Compare commits
483 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0c20e209c | |||
| 648477931b | |||
| e42e8f66f2 | |||
| de5358727a | |||
| 706459783f | |||
| 1ca9a679cf | |||
| 920f88aee4 | |||
| cef3ae5bdf | |||
| 39c788e576 | |||
| d40713ac99 | |||
| f53bed7190 | |||
| 0b9c415681 | |||
| 38003c59e0 | |||
| f1119d8c30 | |||
| a5e2b4d4f0 | |||
| a12cba030d | |||
| 56d1140aad | |||
| 0379141304 | |||
| b6e9f7157d | |||
| edb3df719f | |||
| 9bde3df74d | |||
| 2426e6fe3e | |||
| 93209abebd | |||
| 158786ccd5 | |||
| 77be5624f2 | |||
| 4b580c90ef | |||
| cb1a5d9e82 | |||
| 7e20f7dccb | |||
| 0fab0143b1 | |||
| 5a0717d2c7 | |||
| 09652f9962 | |||
| eef58f212f | |||
| 523aa90c19 | |||
| eb310f7821 | |||
| af2a224781 | |||
| 990cd4f2eb | |||
| 3ac611f633 | |||
| 909504a721 | |||
| 63349de1bb | |||
| c6679b7f41 | |||
| 15560b67b1 | |||
| 7883b28cd9 | |||
| 64707409bb | |||
| 7f1f76408b | |||
| 51560195ba | |||
| 084dcccabc | |||
| 3ad6e372fd | |||
| 179feb56ee | |||
| 67e01bc2df | |||
| 3ece098b94 | |||
| 586e085141 | |||
| e0c89cdd9f | |||
| 3bf025108a | |||
| f69e5d6a7e | |||
| a727c229ca | |||
| 57eca11a9e | |||
| 197d8670f8 | |||
| 675f19ef90 | |||
| 8a12529ba8 | |||
| c3482a8510 | |||
| e4246dee59 | |||
| 23f9b183be | |||
| 1eb8c587c7 | |||
| c3b75ebeb2 | |||
| 91358ad3dd | |||
| 13885320ae | |||
| b9d1abcf27 | |||
| c3260e8e01 | |||
| c0877a9b06 | |||
| 96fb52df17 | |||
| 76106317b6 | |||
| e75f4fba5d | |||
| 4dc933ee5a | |||
| bfb12b04aa | |||
| b7639ab55a | |||
| ecc5b84a64 | |||
| eb12bdfd62 | |||
| 9612bf6f89 | |||
| 0ed7d878b4 | |||
| 5f45e4c7f2 | |||
| ca24671a2f | |||
| ee3a4f20bc | |||
| 0c3542836c | |||
| f71c9c8594 | |||
| f53546cc01 | |||
| 594b078fad | |||
| ac1b2f5ce4 | |||
| 648d21062f | |||
| 89d715ce6b | |||
| bd527a02ef | |||
| 11117c1b54 | |||
| ba25d5b8fe | |||
| 31cb4b994a | |||
| 72ef618af1 | |||
| 936fd223bb | |||
| 6d26438d2d | |||
| 5eb5a55c4c | |||
| 6cd86d1422 | |||
| 9783c4d5e3 | |||
| 87f602a1fd | |||
| 3d32ddd6e9 | |||
| 7ddd934c2b | |||
| b53c91f748 | |||
| 9d106a3716 | |||
| 8b0a8dc4c0 | |||
| 72bfcad034 | |||
| 345d084eba | |||
| 10b6042a9c | |||
| bd71611725 | |||
| 8f53550d2c | |||
| 52914b978d | |||
| ff12759a66 | |||
| e62868b82c | |||
| 97be48dfe1 | |||
| cbe7d70e2d | |||
| 6548e9a6f8 | |||
| 138fdff2e5 | |||
| f1dd4b77a8 | |||
| 9b356ffd17 | |||
| 5cf7e15d76 | |||
| 6a7e90c179 | |||
| 99c31d56bb | |||
| 4d98247980 | |||
| 164f5d2592 | |||
| 89cddf9e3d | |||
| eac6ad584e | |||
| 93fe04c28c | |||
| c9358b3a9f | |||
| 67d2c7c01b | |||
| c30441b949 | |||
| 6d2a9f8e15 | |||
| 799845426c | |||
| 8ac628b3de | |||
| d02291c22c | |||
| 7af0911f5b | |||
| 098782d6aa | |||
| 732628c40b | |||
| 5e62c36a9b | |||
| 3d102a52a7 | |||
| 4dcf213037 | |||
| 4de82472ee | |||
| 58db0352e6 | |||
| 74808f086e | |||
| b96351ea81 | |||
| eb8dc6df36 | |||
| 0c8546188e | |||
| e9f6bdaa20 | |||
| 3d34658d0f | |||
| d44ce7412a | |||
| 17c891e133 | |||
| 9805da63cf | |||
| afc20277e0 | |||
| 14061d207b | |||
| 916374b5df | |||
| 0a2a6c96ca | |||
| 65e8610030 | |||
| 1322065290 | |||
| 22a1efd1e5 | |||
| 269606ebb8 | |||
| 4cb1890b62 | |||
| a421177f0c | |||
| a16ec87631 | |||
| dfd620aa29 | |||
| a5a6e8326f | |||
| 7f48a8ba82 | |||
| ae34d7b4ec | |||
| 44c8e54ed8 | |||
| 4a4208e336 | |||
| d2385b8dc2 | |||
| 9babe1676d | |||
| 8c38f499e4 | |||
| 89c43c1eef | |||
| c3c891cea0 | |||
| e4cf64a112 | |||
| 3e1e211098 | |||
| 02b6b9848f | |||
| 71da09ed44 | |||
| 4404494ec9 | |||
| 98a2a1d725 | |||
| 788f6cede7 | |||
| 7abee71ac3 | |||
| 52d5c99c46 | |||
| 93bc5e5e4b | |||
| 5d996f1388 | |||
| bbb2645a6f | |||
| 54fedc2f53 | |||
| e8c5550256 | |||
| 57b9d41b5c | |||
| 34be7a1234 | |||
| 60f42ed624 | |||
| 25c93afabe | |||
| 2cd5a97ee8 | |||
| 8f47e8fd1e | |||
| eafa545b2a | |||
| b680a07750 | |||
| 2a919ec0f9 | |||
| 01596c045a | |||
| 3de93c5c09 | |||
| 306f5b27e2 | |||
| 06f0f66e3e | |||
| 64921a054e | |||
| 44e035ad38 | |||
| 1803cd071d | |||
| e4d7ba306b | |||
| cc0428752b | |||
| b8c177bc55 | |||
| 7083c51dea | |||
| f0693204e1 | |||
| 3f8071e48e | |||
| a1c5350e8e | |||
| 8f237909d5 | |||
| 11ff69d4f5 | |||
| d15cf98742 | |||
| 29bd6612e9 | |||
| ab1e36d280 | |||
| f1fdafd37f | |||
| 4c7684a3a7 | |||
| 2f45db659b | |||
| 9e38ad114d | |||
| 45c4ea8ca0 | |||
| d2b95ae316 | |||
| 02121152b3 | |||
| caed965b6b | |||
| 4088d5966b | |||
| a062beee87 | |||
| 3095882516 | |||
| 4ad0639796 | |||
| b53ec3352f | |||
| c40edd5b2c | |||
| bdd42caaaf | |||
| ab4eee31ce | |||
| 3f9886a2e5 | |||
| 198b7443ae | |||
| f9c5ca303d | |||
| 9922d43940 | |||
| ae99b077d4 | |||
| 0cb9da9027 | |||
| 467a97dcf7 | |||
| 8d15e418e7 | |||
| 3849b797c9 | |||
| d43c95f98c | |||
| 52d9d9ea1d | |||
| 2cabc9849c | |||
| 17049aca1c | |||
| 9608d59efe | |||
| a7942212a0 | |||
| a22aa53c51 | |||
| 1ce16dc643 | |||
| 0ed6e9a463 | |||
| b7ecd1d749 | |||
| 44e177a5ca | |||
| b09cd3d587 | |||
| b70ebcf5f9 | |||
| 9d58161729 | |||
| e07eed4a95 | |||
| e09704c711 | |||
| 3af24f0815 | |||
| 4c0e7f6013 | |||
| ea298273af | |||
| f60a5af3f2 | |||
| 6ae5c1cdac | |||
| dcf667b176 | |||
| 3a313b9441 | |||
| ae8211a4c0 | |||
| 3452478aa1 | |||
| 0c14018996 | |||
| 41ea7eb7b7 | |||
| e31eb9d457 | |||
| 7842e860cb | |||
| daf7bf2ccb | |||
| a712100cfc | |||
| 92e1210fc8 | |||
| 5b4058c093 | |||
| 77ebdebcca | |||
| ea72de0113 | |||
| a1c0820da8 | |||
| c9eb42ff4d | |||
| 8f0c39ab41 | |||
| 4de059d5bb | |||
| 969572b5a0 | |||
| 4236037b6b | |||
| a800c98d02 | |||
| f1c825f826 | |||
| fba8974699 | |||
| 10b90de171 | |||
| 3785dddebc | |||
| 259dd6acc7 | |||
| 90160f7f40 | |||
| 8bdc86c974 | |||
| 5b441562f8 | |||
| 4e1a67efc8 | |||
| cc217e2b04 | |||
| f8d4f50307 | |||
| d0e14d518e | |||
| c06d6f9326 | |||
| 1200b7f75b | |||
| ee7d96daf9 | |||
| 5a6f324638 | |||
| 4b1ff0efc1 | |||
| 45174af3c2 | |||
| baabc073f0 | |||
| 37969da3e9 | |||
| a81b32ae7f | |||
| 389fee8ecb | |||
| 8fcfd22767 | |||
| b216d3739a | |||
| dcceea064a | |||
| 64c027c695 | |||
| 0dd9f2bd94 | |||
| 80228ada1e | |||
| 00c180927e | |||
| 7f61219bb0 | |||
| 00d3a441e3 | |||
| f6b82939de | |||
| 3f2cfc253b | |||
| b107d3ca54 | |||
| 6bda33efd4 | |||
| 1baa3e914c | |||
| ccbbe36ce6 | |||
| 9e81f805c0 | |||
| 47428ec484 | |||
| 58f9427008 | |||
| 5e0f375f7f | |||
| 21611de3aa | |||
| 70520223cd | |||
| 2c8880c9af | |||
| ee864d3e81 | |||
| 6730c773f3 | |||
| 241cb0a664 | |||
| 3d59b8f774 | |||
| 188700112a | |||
| 03f582da98 | |||
| ed215a1d54 | |||
| c487bd2ca6 | |||
| f90a4d1103 | |||
| 8306296736 | |||
| ae022052d5 | |||
| 7299e53df6 | |||
| c513361a5e | |||
| 429829b80b | |||
| ad1ccd815a | |||
| 036608f7a8 | |||
| 8747e8ad39 | |||
| 537cf48fb4 | |||
| 9f51adb196 | |||
| f2d3de4470 | |||
| 5f4507cfd5 | |||
| ecc7a7a6e7 | |||
| 8d5b92bc9e | |||
| 0f5eee1bdb | |||
| 4e3f817792 | |||
| c61c4d0105 | |||
| 659988330c | |||
| 430a067105 | |||
| c2c8935068 | |||
| 9636aab53f | |||
| 22915914fb | |||
| d39dabef68 | |||
| e01cbefdae | |||
| 73fcba007a | |||
| 54a3feb08c | |||
| ecb3875fe2 | |||
| 2c9df1d93f | |||
| 5db79a09eb | |||
| ff771fdd68 | |||
| 169c4b9de1 | |||
| 4bcd22b9de | |||
| 8a2dc15da6 | |||
| 3f3a407820 | |||
| c72808d72f | |||
| dabe4710bb | |||
| cdc4ddcc97 | |||
| 8f783fd4c2 | |||
| 048119e530 | |||
| b68d44d500 | |||
| 4bc3638b38 | |||
| bc5e2f1fd1 | |||
| e8e7519c40 | |||
| 5460047e41 | |||
| bdd00cd87d | |||
| f83c017e09 | |||
| e38cb9972f | |||
| ac9c789a89 | |||
| 445187cf11 | |||
| 52ec11a33d | |||
| c498d7e9cd | |||
| 0fad9b907f | |||
| 0a170f14bc | |||
| 8245e77672 | |||
| 9c68260592 | |||
| b1778c5802 | |||
| be4ed6de79 | |||
| 3597b542d3 | |||
| fd6d912391 | |||
| d9dc18da43 | |||
| a22d754f95 | |||
| d9d7d3aaf0 | |||
| a399927264 | |||
| b4b00a5c66 | |||
| 9861f81589 | |||
| d06aecb5f8 | |||
| 86c82c1a61 | |||
| 1131446ddc | |||
| f6b2516643 | |||
| 7cf8692e77 | |||
| d6290294f7 | |||
| d5cb0a234f | |||
| 306d8d1d42 | |||
| 7b1eb7dd9d | |||
| 1ad2c98473 | |||
| f0b8df93b4 | |||
| 19ce7da32e | |||
| 9d35f6d303 | |||
| 31d0aa1787 | |||
| ea5fde4156 | |||
| 03e3816ab8 | |||
| 9d6e407c40 | |||
| 6781b5d6a4 | |||
| 352913f00e | |||
| 73f7ebf7b5 | |||
| 045d6419d8 | |||
| acc9d8e7ed | |||
| a29cce9eba | |||
| 1292f520bb | |||
| 8bd6131862 | |||
| ed6c2b7ba2 | |||
| f68a7a4e6a | |||
| c9d2ce382f | |||
| 5262567d61 | |||
| b45a30f0cc | |||
| 867b63af22 | |||
| b3a5e237f4 | |||
| 7aae3a81c3 | |||
| 078796784b | |||
| 15aed61aa6 | |||
| b67d6f8088 | |||
| 9b4bdfea89 | |||
| 70220a5a87 | |||
| aa95743d1c | |||
| eb818ef55b | |||
| 0bda5fd055 | |||
| ea336902f2 | |||
| 58a633e5b9 | |||
| 74ed5a446d | |||
| a5d073a1ec | |||
| f4d3d9d4eb | |||
| 772a9ef3a2 | |||
| 6f3278f6ff | |||
| f11a10ba22 | |||
| 02304fd394 | |||
| 94d11a12ff | |||
| fca37868f1 | |||
| 574f54a9bc | |||
| fb0649f07f | |||
| e3def3821e | |||
| 334c981013 | |||
| b62c8e3aea | |||
| 09a482e4b7 | |||
| b7bbbbe962 | |||
| 30265471b5 | |||
| b4b038dd42 | |||
| 6517028cd4 | |||
| 3f62f6f225 | |||
| 822256229f | |||
| 4f23e1814e | |||
| aec6c34b66 | |||
| e50b6c4a47 | |||
| 1ac6d116a2 | |||
| 6864d07dd9 | |||
| 671e4f4bfd | |||
| e2d22a5b05 | |||
| 3cb2eb6b11 | |||
| 4597a95e9c | |||
| cf4d97e7f9 | |||
| 09f7319f2f | |||
| c77dc52f29 | |||
| 3cd5b40c25 | |||
| 42570d29f3 | |||
| 7e7a8866ca | |||
| ed5e599edb | |||
| addcadeba2 | |||
| 484c2b7e33 | |||
| b6fea9111c |
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-shadow': ['warn'],
|
||||
'no-shadow': 'off',
|
||||
'no-undef': 'off',
|
||||
'react-native/no-inline-styles': 'warn',
|
||||
'no-console': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
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.0.0"
|
||||
Example: "2.1.4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -86,9 +86,9 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
- label: If this is an issue with a plugin, I should be opening an issue in the [plugins repository](https://github.com/lnreader/lnreader-plugins/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.0.0](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.1.4](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 Request]
|
||||
labels: [feature]
|
||||
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.0.0](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.1.4](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -1,57 +1,208 @@
|
||||
name: Build
|
||||
name: Build Preview
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '30 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
EXPO_NO_TELEMETRY: '1'
|
||||
NODE_ENV: 'production'
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
name: Build App
|
||||
runs-on: ubuntu-latest
|
||||
check-preview:
|
||||
name: Check Preview
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
should-build: ${{ steps.preview.outputs.should-build }}
|
||||
|
||||
steps:
|
||||
- name: Clone Repository (Latest)
|
||||
uses: actions/checkout@v3
|
||||
- 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
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set Up pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '18'
|
||||
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: Configure Release Environment
|
||||
env:
|
||||
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_ID }}
|
||||
run: |
|
||||
npm install
|
||||
- name: Create .env
|
||||
pnpm generate:env:release \
|
||||
--build-type "GitHub Action" \
|
||||
--node-env "production"
|
||||
|
||||
- name: Generate Native Android Project
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
MYANIMELIST_CLIENT_ID=${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID=${{ vars.ANILIST_CLIENT_ID }}
|
||||
EOF
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v3
|
||||
pnpm exec expo prebuild \
|
||||
--platform android \
|
||||
--clean \
|
||||
--no-install \
|
||||
--non-interactive
|
||||
|
||||
- name: Configure Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
cache-provider: enhanced
|
||||
cache-read-only: >-
|
||||
${{ github.ref_name != github.event.repository.default_branch }}
|
||||
cache-cleanup: on-success
|
||||
add-job-summary: always
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd android && chmod +x ./gradlew
|
||||
|
||||
- name: Build Android Release
|
||||
- name: Generate Preview Metadata
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
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")"
|
||||
|
||||
- name: Set Env
|
||||
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: |
|
||||
set -x
|
||||
echo "COMMIT_COUNT=$(git rev-list --count HEAD)" >> $GITHUB_ENV
|
||||
mkdir -p app/build/intermediates/sourcemaps/react/preRelease
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
./gradlew :app:assemblePreRelease \
|
||||
--build-cache \
|
||||
--parallel \
|
||||
-PpreReleaseVersionCode="$PRERELEASE_VERSION_CODE" \
|
||||
-PpreReleaseVersionName="$PRERELEASE_VERSION_NAME" \
|
||||
--stacktrace
|
||||
|
||||
mv \
|
||||
app/build/outputs/apk/preRelease/app-preRelease.apk \
|
||||
"app/build/outputs/apk/preRelease/$PRERELEASE_APK_NAME"
|
||||
|
||||
- name: Upload Preview Sourcemap
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: LNReader-r${{ env.COMMIT_COUNT }}
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
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"
|
||||
|
||||
@@ -7,13 +7,45 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm install
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Environment Variables
|
||||
run: pnpm generate:env:debug
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
run: pnpm run lint
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
name: Release Android
|
||||
run-name: Draft ${{ inputs.bump }} Android release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: Version component to increment
|
||||
required: true
|
||||
default: Patch
|
||||
type: choice
|
||||
options:
|
||||
- Patch
|
||||
- Minor
|
||||
- Major
|
||||
|
||||
concurrency:
|
||||
group: release-android
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CI: 'true'
|
||||
EXPO_NO_TELEMETRY: '1'
|
||||
NODE_ENV: 'production'
|
||||
RELEASE_BUMP: ${{ inputs.bump }}
|
||||
|
||||
jobs:
|
||||
release-android:
|
||||
name: Build Draft Release
|
||||
if: >-
|
||||
github.ref == 'refs/heads/master' &&
|
||||
github.actor_id == vars.RELEASE_ACTOR_ID &&
|
||||
github.triggering_actor == vars.RELEASE_ACTOR_LOGIN
|
||||
environment: release
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Set Up pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Resolve Release Version
|
||||
id: version
|
||||
run: |
|
||||
RELEASE_VERSION="$(
|
||||
node scripts/resolve-release-version.cjs "$RELEASE_BUMP"
|
||||
)"
|
||||
|
||||
echo "Releasing $RELEASE_BUMP version: $RELEASE_VERSION"
|
||||
echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV"
|
||||
echo "version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if ! [[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Version must use stable semver, for example 2.0.4"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git rev-parse --verify --quiet "refs/tags/v$RELEASE_VERSION"; then
|
||||
echo "Tag v$RELEASE_VERSION already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if gh release view "v$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||
echo "Release v$RELEASE_VERSION already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Prepare Release Commit
|
||||
run: |
|
||||
pnpm release:prepare "$RELEASE_VERSION"
|
||||
|
||||
if git diff --quiet -- \
|
||||
package.json \
|
||||
app.json \
|
||||
.github/ISSUE_TEMPLATE/report_issue.yml \
|
||||
.github/ISSUE_TEMPLATE/request_feature.yml; then
|
||||
CURRENT_VERSION="$(node -p "require('./package.json').version")"
|
||||
if [ "$CURRENT_VERSION" != "$RELEASE_VERSION" ]; then
|
||||
echo "No release changes were generated for $RELEASE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release version $RELEASE_VERSION is already committed; resuming"
|
||||
else
|
||||
git diff --check
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add \
|
||||
package.json \
|
||||
app.json \
|
||||
.github/ISSUE_TEMPLATE/report_issue.yml \
|
||||
.github/ISSUE_TEMPLATE/request_feature.yml
|
||||
git commit -m "chore: Release v$RELEASE_VERSION"
|
||||
fi
|
||||
|
||||
git fetch origin master
|
||||
REMOTE_VERSION="$(
|
||||
node -e "
|
||||
const { execFileSync } = require('child_process');
|
||||
const contents = execFileSync(
|
||||
'git',
|
||||
['show', 'origin/master:package.json'],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
process.stdout.write(JSON.parse(contents).version);
|
||||
"
|
||||
)"
|
||||
|
||||
if [ "$REMOTE_VERSION" = "$RELEASE_VERSION" ]; then
|
||||
echo "Release commit is already on master; resuming from it"
|
||||
git checkout --detach origin/master
|
||||
echo "RELEASE_ALREADY_PUSHED=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "RELEASE_ALREADY_PUSHED=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
echo "RELEASE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure Release Environment
|
||||
env:
|
||||
MYANIMELIST_CLIENT_ID: ${{ vars.MYANIMELIST_CLIENT_ID }}
|
||||
ANILIST_CLIENT_ID: ${{ vars.ANILIST_CLIENT_ID }}
|
||||
run: |
|
||||
pnpm generate:env:release \
|
||||
--build-type "Release" \
|
||||
--node-env "production" \
|
||||
--myanimelist-client-id "$MYANIMELIST_CLIENT_ID" \
|
||||
--anilist-client-id "$ANILIST_CLIENT_ID"
|
||||
|
||||
- name: Generate Native Android Project
|
||||
run: |
|
||||
pnpm exec expo prebuild \
|
||||
--platform android \
|
||||
--clean \
|
||||
--no-install
|
||||
|
||||
- name: Configure Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
cache-provider: enhanced
|
||||
cache-cleanup: on-success
|
||||
add-job-summary: always
|
||||
|
||||
- name: Build Release APKs
|
||||
working-directory: android
|
||||
run: |
|
||||
mkdir -p app/build/intermediates/sourcemaps/react/release
|
||||
|
||||
./gradlew :app:assembleRelease \
|
||||
--build-cache \
|
||||
--parallel \
|
||||
-PreleaseAbiSplits=true \
|
||||
--stacktrace
|
||||
|
||||
- name: Package Release APKs
|
||||
run: pnpm release:package "$RELEASE_VERSION"
|
||||
|
||||
- name: Upload Sourcemaps
|
||||
run: |
|
||||
cp android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
|
||||
"release-artifacts/LNReader-v${RELEASE_VERSION}-sourcemap.map"
|
||||
|
||||
- name: Verify APK Signatures
|
||||
run: |
|
||||
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner |
|
||||
sort -V |
|
||||
tail -1)"
|
||||
|
||||
if [ -z "$APKSIGNER" ]; then
|
||||
echo "Android apksigner was not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for APK in release-artifacts/*.apk; do
|
||||
"$APKSIGNER" verify "$APK"
|
||||
done
|
||||
|
||||
- name: Upload Release APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: LNReader-v${{ steps.version.outputs.version }}
|
||||
path: release-artifacts/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
- name: Push Release Commit
|
||||
run: |
|
||||
if [ "$RELEASE_ALREADY_PUSHED" = "true" ]; then
|
||||
echo "Release commit is already on master"
|
||||
else
|
||||
git push origin HEAD:master
|
||||
fi
|
||||
|
||||
- name: Create Draft GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create "v$RELEASE_VERSION" \
|
||||
release-artifacts/* \
|
||||
--target "$RELEASE_SHA" \
|
||||
--title "LNReader v$RELEASE_VERSION" \
|
||||
--generate-notes \
|
||||
--draft
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get pnpm Store Path
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache pnpm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Environment
|
||||
run: pnpm generate:env:debug
|
||||
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Type Check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
name: TypeScript
|
||||
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: Check Types
|
||||
run: pnpm run type-check
|
||||
@@ -8,7 +8,8 @@ npm-debug.*
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
.env
|
||||
.*env
|
||||
.env.local
|
||||
reader_playground/index.html
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -38,7 +39,7 @@ DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
**/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
@@ -48,6 +49,10 @@ build/
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
.kotlin/
|
||||
|
||||
# node.js
|
||||
#
|
||||
@@ -66,6 +71,7 @@ buck-out/
|
||||
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
Gemfile.lock
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
@@ -74,4 +80,37 @@ web-build/
|
||||
# @end expo-cli
|
||||
|
||||
.vscode/
|
||||
src/sources/en/generators/
|
||||
src/sources/en/generators/
|
||||
*.lockb
|
||||
*.lock
|
||||
!pnpm-lock.yaml
|
||||
.tool-versions
|
||||
flake.lock
|
||||
|
||||
# pnpm
|
||||
.pnpm-store
|
||||
|
||||
src/generated/**/*
|
||||
|
||||
.cursor/
|
||||
.agents/
|
||||
.claude/
|
||||
.jj/
|
||||
.sisyphus/
|
||||
|
||||
# Rock
|
||||
.rock/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
*/**/org.eclipse.buildship.core.prefs
|
||||
*/**/org.eclipse.jdt.core.prefs
|
||||
*/**/.classpath
|
||||
*/**/.project
|
||||
android/
|
||||
release-artifacts/
|
||||
!modules/**/android/
|
||||
!plugins/android/
|
||||
ios/
|
||||
!modules/**/ios/
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm run type-check
|
||||
@@ -1 +1,3 @@
|
||||
**/node_modules
|
||||
**/node_modules
|
||||
android/**/js/van.js
|
||||
android/**/js/text-vibe.js
|
||||
|
||||
@@ -7,4 +7,5 @@ module.exports = {
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
quoteProps: 'preserve',
|
||||
endOfLine: 'auto', // stop prettier from getting mad on windows
|
||||
};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for AI coding agents working in this repository.
|
||||
|
||||
## Project overview
|
||||
|
||||
LNReader is a free, open-source light novel reader for Android, built with
|
||||
React Native (Expo) and TypeScript. Content is fetched at runtime by
|
||||
JavaScript "plugins" (per-source scrapers) rather than bundled with the app.
|
||||
Plugin _requests_ live in a separate repo, [lnreader-plugins](https://github.com/lnreader/lnreader-plugins);
|
||||
this repo is the reader app itself.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Node >= 22.11.0, pnpm (see `packageManager` in `package.json`, currently pnpm@11.20.0).
|
||||
- This is a React Native/Expo app targeting Android; there is no web or iOS
|
||||
build target maintained here. Building/running the app requires an Android
|
||||
SDK and a device/emulator, which is generally not available in an agent
|
||||
sandbox — prefer running lint, type-check, and tests to verify changes.
|
||||
- See `CONTRIBUTING.md` (and `CONTRIBUTING-NIX.md` for a Nix-based dev shell)
|
||||
for full local Android setup instructions.
|
||||
|
||||
## Commands
|
||||
|
||||
Run these from the repo root:
|
||||
|
||||
```bash
|
||||
pnpm run lint # eslint ./src
|
||||
pnpm run lint:fix # eslint --fix
|
||||
pnpm run format:check # prettier --check
|
||||
pnpm run format # prettier --write
|
||||
pnpm run type-check # tsc --noEmit
|
||||
pnpm run check # format:check + lint + type-check (run before finishing any change)
|
||||
pnpm test # jest (all projects)
|
||||
pnpm run test:rn # jest --selectProjects rn (React Native/component/hook tests)
|
||||
pnpm run test:db # jest --selectProjects db (database/query tests, node env)
|
||||
pnpm run test:watch
|
||||
pnpm run test:coverage
|
||||
```
|
||||
|
||||
Always run `pnpm run check` (or at minimum lint + type-check + the relevant
|
||||
test project) before considering a change complete. A pre-commit hook
|
||||
(`husky` + `lint-staged`) runs prettier/eslint on staged files, and a
|
||||
pre-push hook runs `type-check`, so failures here will also block commits/pushes.
|
||||
|
||||
## Code style
|
||||
|
||||
- TypeScript, functional React components, hooks-based state.
|
||||
- Formatting is enforced by Prettier (`.prettierrc.js`) — don't hand-format,
|
||||
run `pnpm run format` instead.
|
||||
- ESLint config is `eslint.config.js` (flat config, based on `eslint-config-expo`).
|
||||
Notable rules: `no-console` is an error (use the project's logging/toast
|
||||
utilities, not `console.*`), `prefer-const`, `no-var`, and several
|
||||
`react-hooks/*` correctness rules are enforced.
|
||||
- Use the `@alias/*` path aliases defined in `tsconfig.json` (and mirrored in
|
||||
`jest.config.js`) instead of deep relative imports, e.g. `@components`,
|
||||
`@hooks`, `@database/*`, `@screens/*`, `@services/*`, `@utils/*`,
|
||||
`@api/*`, `@theme/*`, `@plugins/*`, `@navigators/*`, `@i18n/*`, `@type/*`.
|
||||
- Don't add comments that just restate what the code does; only comment
|
||||
non-obvious "why" (workarounds, invariants, native/platform quirks).
|
||||
|
||||
## Localization (i18n)
|
||||
|
||||
- `src/i18n/languages/en/strings.json` is the source-of-truth string file —
|
||||
it's the only locale you should hand-edit. Every other folder under
|
||||
`src/i18n/languages/*` is a translation synced automatically from Crowdin
|
||||
(see `crowdin.yml`); don't edit or "fix" translated strings in those files.
|
||||
- After adding/changing keys in `en/strings.json`, run
|
||||
`pnpm run generate:string-types` to regenerate the associated TypeScript types.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `src/api` – external API clients (drive/cloud backup providers, etc.)
|
||||
- `src/components` – shared/reusable UI components
|
||||
- `src/database` – SQLite (op-sqlite/Drizzle) schema, queries, and migrations
|
||||
helpers; `src/database/queries/*Queries.ts` are the main data-access layer
|
||||
- `src/hooks` – shared and "persisted" (MMKV-backed) React hooks
|
||||
- `src/i18n` – translated strings (see Crowdin integration in `crowdin.yml`)
|
||||
- `src/navigators` – React Navigation stack/tab setup
|
||||
- `src/plugins` – plugin manager and plugin type definitions (not the plugin
|
||||
source scrapers themselves, which live in the separate lnreader-plugins repo)
|
||||
- `src/screens` – feature screens (library, browse, novel, reader, settings, etc.)
|
||||
- `src/services` – background tasks, downloads, backups, trackers, EPUB export, updates
|
||||
- `src/theme` – Material3 theming
|
||||
- `src/utils` – general utilities
|
||||
- `drizzle/` – generated SQL migrations (see `pnpm run generate:db-migration`)
|
||||
- `modules/` – native modules (Nitro modules: epub, tts, file, zip-archive, etc.)
|
||||
- `test/` – Jest global mocks and the shared `test-utils.tsx` render helpers
|
||||
- `scripts/` – Node scripts for env file generation, release packaging, etc.
|
||||
|
||||
## Testing notes
|
||||
|
||||
- Don't add tests for the sake of coverage. Write tests for user-facing
|
||||
interactions (screens, components a user directly touches) and for core
|
||||
complex logic (parsing, database queries, sync/backup, chapter/novel
|
||||
matching, etc.), not for trivial passthroughs, simple type/prop plumbing,
|
||||
or generated/boilerplate code.
|
||||
- Tests live in `__tests__` directories next to the code under test and use
|
||||
Jest + `@testing-library/react-native`.
|
||||
- Jest is split into two projects (see `jest.config.js`):
|
||||
- `db` — pure logic/query tests under `src/database`, run in a Node environment.
|
||||
- `rn` — component/hook/integration tests under `src/`, run with the `jest-expo` preset.
|
||||
- Import the shared render helpers from `@test-utils` (`render`, `renderNovel`,
|
||||
`AllTheProviders`) rather than using `@testing-library/react-native`'s
|
||||
`render` directly, so providers (theme, gesture handler, safe area, novel
|
||||
context) are set up consistently.
|
||||
- Mock at module level with `jest.mock(...)`, not inside `beforeEach` — see
|
||||
`TESTING.md` for the reasoning and common pitfalls (ESM transform errors,
|
||||
`mockReturnValue` failures, test isolation).
|
||||
- Global mocks for native modules and database queries live in `test/mocks/`;
|
||||
hook-specific mocks live in `src/hooks/__mocks__` and `src/hooks/__tests__/mocks.ts`.
|
||||
|
||||
## Database changes
|
||||
|
||||
- Schema lives under `src/database/schema`; the ORM is Drizzle
|
||||
(`drizzle-orm`, config in `drizzle.config.ts`).
|
||||
- After changing the schema, generate a migration rather than hand-writing
|
||||
SQL: `pnpm run generate:db-migration`. Do not edit files under `drizzle/`
|
||||
by hand.
|
||||
|
||||
## PR / commit conventions
|
||||
|
||||
- Keep changes scoped and consistent with existing patterns in the touched
|
||||
directory; prefer editing existing files over introducing new abstractions.
|
||||
- Match existing code style; don't reformat unrelated code.
|
||||
- If you are an AI agent opening a PR or making a commit, attribute it as
|
||||
such (e.g. a "Generated with <agent/tool name>" note in the commit
|
||||
message/PR description, and a co-author trailer if your tool supports one).
|
||||
@@ -1,57 +1,126 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { PropsWithChildren, Suspense, useEffect, useMemo } from 'react';
|
||||
import { StatusBar, StyleSheet } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import LottieSplashScreen from 'react-native-lottie-splash-screen';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { Provider as PaperProvider } from 'react-native-paper';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
import {
|
||||
MD3DarkTheme,
|
||||
MD3LightTheme,
|
||||
Provider as PaperProvider,
|
||||
} from 'react-native-paper';
|
||||
|
||||
import { createTables } from '@database/db';
|
||||
import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import { deserializePlugins } from '@plugins/pluginManager';
|
||||
import AppErrorBoundary, {
|
||||
ErrorFallback,
|
||||
} from '@components/AppErrorBoundary/AppErrorBoundary';
|
||||
import CrashRecoveryGate from '@components/AppErrorBoundary/CrashRecoveryGate';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { MMKVStorage } from '@utils/mmkv/mmkv';
|
||||
import { BACKGROUND_ACTION } from '@services/constants';
|
||||
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';
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => {
|
||||
return {
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
enableFreeze(true);
|
||||
const sqliteAdapters = __DEV__ && opSqliteAdapter ? [opSqliteAdapter] : [];
|
||||
|
||||
createTables();
|
||||
deserializePlugins().then(() => LottieSplashScreen.hide());
|
||||
if (!BackgroundService.isRunning()) {
|
||||
MMKVStorage.delete(BACKGROUND_ACTION);
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
<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;
|
||||
|
||||
return {
|
||||
...baseTheme,
|
||||
colors: {
|
||||
...baseTheme.colors,
|
||||
...theme,
|
||||
},
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
|
||||
};
|
||||
|
||||
const Application = () => {
|
||||
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
|
||||
const { success: databaseReady, error: databaseError } = useInitDatabase();
|
||||
const { ready: servicesReady, error: servicesError } =
|
||||
useInitializeAppServices(Boolean(databaseReady));
|
||||
|
||||
useEffect(() => {
|
||||
if ((databaseReady && servicesReady) || databaseError || servicesError) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [databaseReady, databaseError, servicesReady, servicesError]);
|
||||
|
||||
const initializationError = databaseError || servicesError;
|
||||
|
||||
if (initializationError) {
|
||||
return (
|
||||
<ErrorFallback error={initializationError} resetError={() => null} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!databaseReady || !servicesReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedRootView>
|
||||
<KeyboardProvider>
|
||||
<AppErrorBoundary>
|
||||
<SafeAreaProvider>
|
||||
<ThemedPaperProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</BottomSheetModalProvider>
|
||||
</ThemedPaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</AppErrorBoundary>
|
||||
</KeyboardProvider>
|
||||
</ThemedRootView>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider>
|
||||
<CrashRecoveryGate>
|
||||
<Application />
|
||||
</CrashRecoveryGate>
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# React Native Development Nix Environment Setup
|
||||
|
||||
This document outlines how to set up your React Native development environment using Nix flakes for reproducibility and isolation.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This was tested on an Arch Linux system with AMD CPU and GPU. There are likely problems with NVIDIA GPUs.
|
||||
|
||||
Compatibility with Microsoft WSL2 is not guaranteed, but should work without major issues, except for the emulator.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Nix installed**: [https://nixos.org/download.html](https://nixos.org/download.html)
|
||||
- **Git**: Required for cloning the repository.
|
||||
- **KVM (for emulator, if enabled)**: Ensure KVM is enabled and your user is in the `kvm` group for optimal emulator performance:
|
||||
```bash
|
||||
sudo usermod -aG kvm $USER && newgrp kvm # (or log out/in)
|
||||
kvm-ok # Verify KVM acceleration
|
||||
```
|
||||
- **GPU Drivers (for emulator, if enabled)**: For hardware-accelerated emulator (AMD/Intel), ensure host Vulkan drivers are working:
|
||||
```bash
|
||||
vulkaninfo | grep -i "device name" # Should show your GPU
|
||||
```
|
||||
If not, install `vulkan-radeon` (AMD) or `vulkan-intel` (Intel) on your host system and reboot.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Clone the Project:**
|
||||
|
||||
```bash
|
||||
git clone <your-fork-url>
|
||||
cd <your-fork-name>
|
||||
```
|
||||
|
||||
2. **Enter the Development Shell:**
|
||||
|
||||
If you have `pnpm` installed, use the defined scripts:
|
||||
|
||||
- **For Physical Device Only (default):**
|
||||
```bash
|
||||
pnpm run nix:shell
|
||||
```
|
||||
- **For Emulator Development (explicitly enable):**
|
||||
```bash
|
||||
pnpm run nix:emulator
|
||||
```
|
||||
|
||||
If you do not have `pnpm` installed, execute the commands manually:
|
||||
|
||||
- **For Physical Device Only (default):**
|
||||
```bash
|
||||
nix develop --extra-experimental-features nix-command --extra-experimental-features flakes
|
||||
```
|
||||
- **For Emulator Development (explicitly enable):**
|
||||
```
|
||||
nix develop .#emulator --extra-experimental-features nix-command --extra-experimental-features flakes
|
||||
```
|
||||
|
||||
The first time, this will download and set up all required tools, which can take considerable time. Subsequent entries will be much faster. You will also see a message about "Setting up writable Android SDK..." which only happens once.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Once inside the `nix develop` shell, all tools are available.
|
||||
|
||||
### 1. Start Metro Bundler
|
||||
|
||||
Open a new terminal or run in the background:
|
||||
|
||||
```bash
|
||||
pnpm run dev:start
|
||||
```
|
||||
|
||||
### 2. Physical Device Development
|
||||
|
||||
1. **Connect Device**: Enable USB debugging on your Android device and connect it via USB.
|
||||
2. **Verify Device**:
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
3. **Mirror Screen (Optional)**:
|
||||
```bash
|
||||
scrcpy # Mirrors screen, provides control
|
||||
scrcpy -s <device_serial> # If multiple devices connected
|
||||
```
|
||||
4. **Run App**:
|
||||
```bash
|
||||
npx run android
|
||||
```
|
||||
|
||||
### 3. Android Emulator Development (if enabled)
|
||||
|
||||
1. **Launch Emulator**:
|
||||
|
||||
```bash
|
||||
emulator -avd ReactNative_API35 -gpu host -no-metrics -no-audio
|
||||
```
|
||||
|
||||
- Use `-gpu swiftshader_indirect` if hardware acceleration (`-gpu host`) fails.
|
||||
|
||||
2. **Verify Emulator**:
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
3. **Run App**:
|
||||
Launches the the emulater automatically, if it is not already running.
|
||||
```bash
|
||||
npx run android
|
||||
```
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
Contributions are welcome and are greatly appreciated!
|
||||
|
||||
## Setting up your environment
|
||||
## Setup Your Environment with Nix
|
||||
|
||||
After forking to your own github org or account, do the following steps to get started:
|
||||
If you are on a Linux system, you can install the nix package manager and use the nix flakes to set up your development environment.
|
||||
See [CONTRIBUTING-NIX.md](CONTRIBUTING-NIX.md).
|
||||
|
||||
## Setting Up Your Environment
|
||||
|
||||
After forking to your own GitHub organization or account, take the following steps to get started:
|
||||
|
||||
```bash
|
||||
# prerequisites
|
||||
node --version <= 16.13.1 (for version management, get nvm [recommended])
|
||||
java sdk --version <= 11 (for version management, get jenv [optional])
|
||||
node --version >= 20 (for version management, get nvm [recommended])
|
||||
java sdk --version >= 17 (for version management, get jenv [optional])
|
||||
android sdk (https://developer.android.com/studio)
|
||||
|
||||
# clone your fork to your local machine
|
||||
@@ -18,16 +23,19 @@ git clone https://github.com/<your-account-name>/lnreader.git
|
||||
# step into local repo
|
||||
cd lnreader
|
||||
|
||||
# install pnpm (if not already installed)
|
||||
npm install -g pnpm
|
||||
|
||||
# install dependencies
|
||||
npm install
|
||||
pnpm install
|
||||
|
||||
# build the apk (the built apk will be found in ~/lnreader/android/app/build/outputs/apk/release/)
|
||||
npm run buildRelease
|
||||
pnpm run build:release:android
|
||||
```
|
||||
|
||||
### Developing on Android
|
||||
|
||||
You will need an Android device or emulator connected to your computer as well as an IDE of your choice. (eg: vscode)
|
||||
You will need an Android device or emulator connected to your computer as well as an IDE of your choice (e.g., VS Code).
|
||||
|
||||
```bash
|
||||
# prerequisites
|
||||
@@ -38,65 +46,19 @@ IDE
|
||||
adb devices
|
||||
|
||||
# run metro for development
|
||||
npm start
|
||||
pnpm run dev:start
|
||||
|
||||
# then to view on your android device (new terminal)
|
||||
npm run android
|
||||
pnpm run dev:android
|
||||
```
|
||||
|
||||
To view any changes to the app with new code, save your code and press "r" on the metro terminal to
|
||||
reload it. The app on the android device/emulator will reload shortly.
|
||||
|
||||
### Style & Linting
|
||||
|
||||
This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
|
||||
|
||||
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
|
||||
npm run lint
|
||||
```
|
||||
# Database
|
||||
```mermaid
|
||||
erDiagram
|
||||
Category ||--|{ NovelCategory : contains
|
||||
Category{
|
||||
INTEGER id PK
|
||||
TEXT name
|
||||
INTEGER sort
|
||||
}
|
||||
NovelCategory{
|
||||
INTEGER id PK
|
||||
INTEGER novelId FK
|
||||
INTEGER categoryId FK "default: whose sort = 1"
|
||||
}
|
||||
Novel ||--|{ NovelCategory : contains
|
||||
Novel{
|
||||
INTEGER id PK
|
||||
TEXT url
|
||||
TEXT pluginId
|
||||
TEXT name
|
||||
TEXT cover
|
||||
TEXT summary
|
||||
TEXT author
|
||||
TEXT artist
|
||||
TEXT status
|
||||
TEXT genres
|
||||
INTEGER inLibrary "default: 0"
|
||||
}
|
||||
Novel ||--o| Chapter : has
|
||||
Chapter{
|
||||
INTEGER id PK
|
||||
INTEGER novelId FK
|
||||
text url
|
||||
TEXT name
|
||||
TEXT releaseTime
|
||||
INTEGER bookmark "default: 0"
|
||||
INTEGER unread "default: 1"
|
||||
TEXT readTime
|
||||
INTEGER isDownloaded "default: 0"
|
||||
TEXT updatedTime
|
||||
TEXT chapterNumber
|
||||
}
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Debugging Production Builds
|
||||
|
||||
Symbolicate production stack traces from LNReader using uploaded sourcemaps.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** (v18 or later)
|
||||
- The **sourcemap** matching the crashing APK version
|
||||
- A **stack trace** from either `adb logcat` (device attached) or pasted from a Discord / GitHub issue report
|
||||
|
||||
## Getting the Sourcemap
|
||||
|
||||
### Release builds
|
||||
|
||||
Sourcemaps are uploaded as GitHub release assets. Download the one matching the crashing version:
|
||||
|
||||
```
|
||||
LNReader-v{version}-sourcemap.map
|
||||
```
|
||||
|
||||
Example for `v2.5.0`:
|
||||
|
||||
1. Go to the [releases page](https://github.com/lnreader/lnreader/releases)
|
||||
2. Find release `v2.5.0`
|
||||
3. Under **Assets**, download `LNReader-v2.5.0-sourcemap.map`
|
||||
|
||||
### Preview / nightly builds
|
||||
|
||||
Sourcemaps are uploaded as a separate artifact alongside the preview APK, named `LNReader-{sha}-sourcemap`.
|
||||
|
||||
1. Go to the [Actions tab](https://github.com/lnreader/lnreader/actions) → **Build Preview**
|
||||
2. Find the workflow run matching the build hash
|
||||
3. Under **Artifacts**, download the one with the `-sourcemap` suffix
|
||||
|
||||
## Symbolicating a Stack Trace
|
||||
|
||||
Save the raw stack trace to a file (from `adb logcat`, or paste from Discord/GitHub into `trace.txt`), then run:
|
||||
|
||||
```bash
|
||||
npx metro-symbolicate <sourcemap.map> < trace.txt
|
||||
```
|
||||
|
||||
Or pipe it directly:
|
||||
|
||||
```bash
|
||||
adb logcat -d | npx metro-symbolicate <sourcemap.map>
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Before (Hermes-obfuscated):
|
||||
|
||||
```
|
||||
com.lnreader E Error: Unexpected token
|
||||
at 93795 (address at index.js:1:729193)
|
||||
at 28471 (address at index.js:1:219482)
|
||||
at 10384 (address at index.js:1:81729)
|
||||
```
|
||||
|
||||
After symbolication:
|
||||
|
||||
```
|
||||
com.lnreader E Error: Unexpected token
|
||||
at handleResponse (src/api/plugins.ts:120:12)
|
||||
at fetchPlugin (src/hooks/usePlugins.ts:45:8)
|
||||
at getPluginList (src/screens/browse/BrowseScreen.tsx:89:20)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Version mismatch**: the sourcemap must match the exact APK build. A mismatched version produces wrong file names and line numbers.
|
||||
- **No sourcemap artifact**: verify the workflow that built the APK ran successfully and that the "Upload Sourcemaps" / "Upload Preview Sourcemap" step completed without errors.
|
||||
- **Hermes bytecode**: Hermes compiles JS to bytecode. The sourcemap is generated during this step and maps bytecode offsets back to source. `metro-symbolicate` handles this transparently.
|
||||
@@ -0,0 +1,17 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Ruby 3.4.0 has removed some libraries from the standard library.
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
gem 'nkf'
|
||||
@@ -1,47 +1,68 @@
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
|
||||
<p>
|
||||
<h1 align="center">
|
||||
LNReader
|
||||
</h1>
|
||||
<p align="center">
|
||||
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
|
||||
<a href="https://lnreader.app">
|
||||
<img src="./.github/readme-images/icon_new.png" align="center" width="128" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">LNReader</h1>
|
||||
|
||||
<p align="center">
|
||||
LNReader is a free and open source light novel reader for Android, inspired by Tachiyomi.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/LNReader/lnreader/build.yml?branch=main">
|
||||
</a>
|
||||
<a href="https://discord.gg/QdcWN4MD63">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=5865F2&color=4752C4&label=discord&style=flat">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader-preview/releases/latest">
|
||||
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader-preview?color=red&label=preview">
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader"><img src="https://badges.crowdin.net/lnreader/localized.svg"></a>
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
|
||||
<a href="https://github.com/lnreader/lnreader/releases">
|
||||
<img alt="GitHub Downloads" src="https://img.shields.io/github/downloads/lnreader/lnreader/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<div align="center">
|
||||
<img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/lnreader/lnreader/build.yml?labelColor=27303D&style=flat">
|
||||
<a href="https://github.com/lnreader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/lnreader/lnreader?labelColor=27303D&color=1a73e8&style=flat">
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader">
|
||||
<img src="https://badges.crowdin.net/lnreader/localized.svg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 align="center">Download</h2>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/lnreader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/lnreader/lnreader?label=Stable&labelColor=0d7377&color=084c4e&style=flat">
|
||||
</a>
|
||||
<a href="https://github.com/lnreader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="Build Preview" src="https://github.com/lnreader/lnreader/actions/workflows/build.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Get the app from our <a href="https://github.com/lnreader/lnreader/releases">releases page</a>.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<em>Android 7.0 or higher.</em>
|
||||
</p>
|
||||
|
||||
<h2 align="center">Screenshots</h2>
|
||||
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" />
|
||||
</p>
|
||||
|
||||
## Download
|
||||
|
||||
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
|
||||
|
||||
## Sources
|
||||
## Plugins
|
||||
|
||||
LNReader does not have any affiliation with the content providers available.
|
||||
|
||||
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
|
||||
Plugin requests should be created at [lnreader-plugins](https://github.com/lnreader/lnreader-plugins).
|
||||
|
||||
## Translation
|
||||
|
||||
Help translate LNReader into your language on [Crowdin](https://crowdin.com/project/lnreader).
|
||||
|
||||
## Building & Contributing
|
||||
|
||||
@@ -49,4 +70,4 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
|
||||
[MIT](https://github.com/lnreader/lnreader/blob/main/LICENSE)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Testing Guide for LNReader
|
||||
|
||||
This guide explains how to write tests in this React Native project using Jest and React Testing Library.
|
||||
|
||||
|
||||
## Existing Mocks
|
||||
|
||||
### Global Mocks
|
||||
|
||||
The project has global mocks configured in Jest. These are automatically applied:
|
||||
|
||||
- `test/mocks/` - Global mocks for native modules (react-native-mmkv, react-navigation, all database queries, etc.)
|
||||
- `src/hooks/__mocks__/index.ts` - Hook-specific mocks (showToast, getString, parseChapterNumber, etc.)
|
||||
- `src/hooks/__tests__/mocks.ts` - Extended mocks for persisted hooks
|
||||
|
||||
### Using @test-utils
|
||||
|
||||
There is a custom render wrapper at `test/test-utils.tsx` with:
|
||||
|
||||
- `render` - wraps with GestureHandlerRootView, SafeAreaProvider, PaperProvider, etc.
|
||||
- `renderNovel` - includes NovelContextProvider
|
||||
- `AllTheProviders` - the full provider wrapper
|
||||
|
||||
Usage:
|
||||
|
||||
```typescript
|
||||
import { render, renderNovel } from '@test-utils';
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### 1. ESM Modules Not Transforming
|
||||
|
||||
If you see `Cannot use import statement outside a module`, you need to add mocks for the module:
|
||||
|
||||
```typescript
|
||||
jest.mock('@hooks/persisted/usePlugins');
|
||||
// Add more specific mocks as needed
|
||||
```
|
||||
|
||||
### 2. Mock Functions Not Working
|
||||
|
||||
If `mockReturnValue` throws "not a function", create mock functions at the module level:
|
||||
|
||||
```typescript
|
||||
// CORRECT: Module-level mock functions
|
||||
const mockUseTheme = jest.fn();
|
||||
jest.mock('@hooks/persisted', () => ({
|
||||
useTheme: () => mockUseTheme(),
|
||||
}));
|
||||
|
||||
// INCORRECT: Trying to use jest.Mock type casting
|
||||
// (useTheme as jest.Mock).mockReturnValue(...) // This fails!
|
||||
```
|
||||
|
||||
### 3. Test Isolation
|
||||
|
||||
Tests must mock at module level, not in `beforeEach`:
|
||||
|
||||
```typescript
|
||||
// CORRECT
|
||||
const mockFn = jest.fn();
|
||||
jest.mock('module', () => ({ useHook: () => mockFn() }));
|
||||
|
||||
// INCORRECT - mocks get reset between tests
|
||||
jest.mock('module');
|
||||
beforeEach(() => {
|
||||
// This doesn't work properly
|
||||
});
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
pnpm test # Run all tests
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With coverage
|
||||
pnpm test:rn # React Native only
|
||||
pnpm test:db # Database only
|
||||
```
|
||||
@@ -1,55 +0,0 @@
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
# - `npm start` - to start the packager
|
||||
# - `cd android`
|
||||
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
|
||||
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
|
||||
# - `buck install -r android/app` - compile, install and run application
|
||||
#
|
||||
|
||||
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
|
||||
|
||||
lib_deps = []
|
||||
|
||||
create_aar_targets(glob(["libs/*.aar"]))
|
||||
|
||||
create_jar_targets(glob(["libs/*.jar"]))
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "com.rajarsheechatterjee.LNReader",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "com.rajarsheechatterjee.LNReader",
|
||||
res = "src/main/res",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
)
|
||||
@@ -1,147 +0,0 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
/**
|
||||
* 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"]
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
ext {
|
||||
versionMajor = 2
|
||||
versionMinor = 0
|
||||
versionPatch = 0
|
||||
}
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
namespace "com.rajarsheechatterjee.LNReader"
|
||||
defaultConfig {
|
||||
applicationId '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
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation 'com.facebook.soloader:soloader:0.10.5+'
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled || isWebpEnabled) {
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.0.0'
|
||||
}
|
||||
|
||||
if(isGifEnabled){
|
||||
// For animated gif support
|
||||
implementation 'com.facebook.fresco:animated-gif:2.0.0'
|
||||
}
|
||||
|
||||
if(isWebpEnabled){
|
||||
// For webp support
|
||||
implementation 'com.facebook.fresco:webpsupport:2.0.0'
|
||||
if(isWebpAnimatedEnabled){
|
||||
// Animated webp support
|
||||
implementation 'com.facebook.fresco:animated-webp:2.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "523872485654",
|
||||
"project_id": "lnreader-backup",
|
||||
"storage_bucket": "lnreader-backup.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:523872485654:android:169cd062458db962ce1037",
|
||||
"android_client_info": {
|
||||
"package_name": "com.rajarsheechatterjee.LNReader"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "523872485654-14jtut8orr7dbrk2chea279d7k8889sr.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "com.rajarsheechatterjee.LNReader",
|
||||
"certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyBH_j-0Jyo9aFJ_KV8Nr3te2hs_L_ZYhrE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "523872485654-liarmq8nl0g5an2cki3bpg9jc0d8a21j.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
@@ -1,12 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,71 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.rajarsheechatterjee.LNReader">
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<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:label="@string/app_name"
|
||||
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" />
|
||||
<data android:scheme="com.rajarsheechatterjee.LNReader" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,419 +0,0 @@
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: var(--StatusBar-currentHeight);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
body {
|
||||
padding-left: var(--readerSettings-padding);
|
||||
padding-right: var(--readerSettings-padding);
|
||||
padding-bottom: 40px;
|
||||
|
||||
font-size: var(--readerSettings-textSize);
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
text-align: var(--readerSettings-textAlign);
|
||||
line-height: var(--readerSettings-lineHeight);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
}
|
||||
|
||||
chapter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nextButton,
|
||||
.infoText {
|
||||
width: 100%;
|
||||
border-radius: 50px;
|
||||
border-width: 1;
|
||||
color: var(--theme-onPrimary);
|
||||
background-color: var(--theme-primary);
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
font-size: 16px;
|
||||
border-width: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nextButton {
|
||||
min-height: 40px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.infoText {
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
border: none;
|
||||
margin: 0px;
|
||||
color: inherit;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.chapterCtn {
|
||||
min-height: var(--chapterCtn-height);
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ToolWrapper {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 5vw;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal {
|
||||
top: unset;
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 120px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 500px) {
|
||||
#ToolWrapper.horizontal {
|
||||
bottom: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden {
|
||||
opacity: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#ToolWrapper.hidden.horizontal {
|
||||
opacity: 0;
|
||||
bottom: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#TTS-Controller {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #TTS-Controller {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
#TTS-Controller button{
|
||||
background: var(--theme-surface-0-9);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#TTS-Controller svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper > #ScrollBar {
|
||||
margin-top: 8px;
|
||||
width: 2.4rem;
|
||||
height: 45vh;
|
||||
min-height: 200px;
|
||||
border-radius: 1.2rem;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
touch-action: none;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
height: 12.5%;
|
||||
}
|
||||
|
||||
#ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
height: 75%;
|
||||
align-items: unset;
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-track {
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-progress {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
top: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar{
|
||||
display: flex;
|
||||
width: 80vw;
|
||||
height: 2.4rem;
|
||||
min-height: unset;
|
||||
align-items: center;
|
||||
margin-top: unset;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--theme-onSurface);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item.scrollbar-text {
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar > .scrollbar-item#scrollbar-slider {
|
||||
width: 75%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-track {
|
||||
height: 0.1rem;
|
||||
width: 100%;
|
||||
background-color: var(--theme-outline);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-progress {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb-wrapper {
|
||||
position: relative;
|
||||
left: 100%;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translate(-0.95rem, -1rem);
|
||||
}
|
||||
|
||||
#ToolWrapper.horizontal > #ScrollBar #scrollbar-thumb {
|
||||
top: 100%;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 100%;
|
||||
background-color: var(--theme-primary);
|
||||
}
|
||||
|
||||
#reader-footer-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#reader-footer {
|
||||
display: flex;
|
||||
flex: 3;
|
||||
padding: 0.2rem 2rem;
|
||||
background-color: var(--readerSettings-theme);
|
||||
color: var(--readerSettings-textColor);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.reader-footer-item {
|
||||
flex: 1;
|
||||
}
|
||||
.reader-footer-item:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.reader-footer-item:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tts.highlight {
|
||||
color: var(--theme-onSecondary);
|
||||
background-color: var(--theme-secondary);
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--theme-surface-0-9);
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
position: fixed;
|
||||
top: var(--top);
|
||||
left: var(--left);
|
||||
-webkit-animation: menuAnimation 0.4s 0s both;
|
||||
animation: menuAnimation 0.4s 0s both;
|
||||
transform-origin: left;
|
||||
list-style: none;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 999999999;
|
||||
}
|
||||
|
||||
.contextMenu-item {
|
||||
padding: 4px,
|
||||
}
|
||||
|
||||
.contextMenu-button {
|
||||
color: var(--theme-onSurface);
|
||||
background: 0;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
padding: 6px 24px 6px 7px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
-webkit-animation: menuItemAnimation 0.2s 0s both;
|
||||
animation: menuItemAnimation 0.2s 0s both;
|
||||
font-family: var(--readerSettings-fontFamily);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: var(--theme-rippleColor);
|
||||
}
|
||||
|
||||
.contextMenu-button svg {
|
||||
fill: var(--theme-onSurface);
|
||||
}
|
||||
.contextMenu-button span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes menuAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height);
|
||||
opacity: 1;
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--height);
|
||||
opacity: 1;
|
||||
border-radius: 8px;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes menuItemAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuItemAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
#Image-Modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#Image-Modal.show {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--theme-surface-0-9);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#Image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,658 +0,0 @@
|
||||
/**
|
||||
* https://fonts.google.com/icons
|
||||
* Filled
|
||||
* other settings are default
|
||||
*/
|
||||
const copyIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Z"/></svg>';
|
||||
const volumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320Z"/></svg>';
|
||||
const selectAllIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M280-280v-400h400v400H280Zm80-80h240v-240H360v240ZM200-200v80q-33 0-56.5-23.5T120-200h80Zm-80-80v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm80-160h-80q0-33 23.5-56.5T200-840v80Zm80 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80v80h-80Zm0-640v-80h80v80h-80Zm160 640v-80h80q0 33-23.5 56.5T760-120Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80h80v80h-80Zm0-160v-80q33 0 56.5 23.5T840-760h-80Z"/></svg>';
|
||||
const translateIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>';
|
||||
const resumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M240-240v-480h80v480h-80Zm160 0 400-240-400-240v480Z"/></svg>';
|
||||
const pauseIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-200v-560h160v560H560Zm-320 0v-560h160v560H240Z"/></svg>';
|
||||
const stopIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M320-320h320v-320H320v320ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>';
|
||||
const selectVolumeIcon =
|
||||
'<svg height="24" viewBox="0 -960 960 960" width="24"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131Zm-80-29L280-360H120v-240h160l200-200v640Zm80-160v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM40-680v-240h240v80H120v160H40ZM680-40v-80h160v-160h80v240H680Z"/></svg>';
|
||||
/**
|
||||
* @type {import("./type").Reader}
|
||||
*/
|
||||
class Reader {
|
||||
constructor() {
|
||||
this.selection = window.getSelection();
|
||||
this.viewport = document.querySelector('meta[name=viewport]');
|
||||
this.footerWrapper = document.getElementById('reader-footer-wrapper');
|
||||
this.percentage = document.getElementById('reader-percentage');
|
||||
this.battery = document.getElementById('reader-battery');
|
||||
this.time = document.getElementById('reader-time');
|
||||
this.paddingTop = parseInt(
|
||||
getComputedStyle(document.querySelector('html')).getPropertyValue(
|
||||
'padding-top',
|
||||
),
|
||||
);
|
||||
this.chapter = document.querySelector('chapter');
|
||||
this.rawHTML = this.chapter.innerHTML;
|
||||
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
|
||||
this.layoutHeight = window.innerHeight;
|
||||
this.pluginId = this.chapter.getAttribute('data-plugin-id');
|
||||
this.novelId = this.chapter.getAttribute('data-novel-id');
|
||||
this.chapterId = this.chapter.getAttribute('data-chapter-id');
|
||||
this.saveProgressInterval = setInterval(
|
||||
() =>
|
||||
this.post({
|
||||
type: 'save',
|
||||
data: parseInt(
|
||||
((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100,
|
||||
),
|
||||
}),
|
||||
autoSaveInterval,
|
||||
);
|
||||
this.time.innerText = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
this.timeInterval = setInterval(() => {
|
||||
this.time.innerText = new Date().toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}, 60000);
|
||||
this.updateBatteryLevel(batteryLevel);
|
||||
this.updateGeneralSettings(initSettings);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
this.chapterHeight = this.chapter.scrollHeight + this.paddingTop;
|
||||
};
|
||||
post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj));
|
||||
updateReaderSettings = settings => {
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-theme',
|
||||
settings.theme,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-padding',
|
||||
settings.padding + '%',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textSize',
|
||||
settings.textSize + 'px',
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textColor',
|
||||
settings.textColor,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-textAlign',
|
||||
settings.textAlign,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-lineHeight',
|
||||
settings.lineHeight,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--readerSettings-fontFamily',
|
||||
settings.fontFamily,
|
||||
);
|
||||
new FontFace(
|
||||
settings.fontFamily,
|
||||
'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")',
|
||||
)
|
||||
.load()
|
||||
.then(function (loadedFont) {
|
||||
document.fonts.add(loadedFont);
|
||||
});
|
||||
};
|
||||
updateGeneralSettings = settings => {
|
||||
this.showScrollPercentage = settings.showScrollPercentage;
|
||||
this.showBatteryAndTime = settings.showBatteryAndTime;
|
||||
this.verticalSeekbar = settings.verticalSeekbar;
|
||||
this.bionicReading = settings.bionicReading;
|
||||
if (this.bionicReading) {
|
||||
this.chapter.innerHTML = textVide.textVide(this.rawHTML);
|
||||
} else {
|
||||
this.chapter.innerHTML = this.rawHTML;
|
||||
}
|
||||
if (settings.swipeGestures) {
|
||||
swipeHandler.enable();
|
||||
} else {
|
||||
swipeHandler.disable();
|
||||
}
|
||||
if (!this.showScrollPercentage && !this.showBatteryAndTime) {
|
||||
this.footerWrapper.classList.add('d-none');
|
||||
} else {
|
||||
this.footerWrapper.classList.remove('d-none');
|
||||
if (this.showScrollPercentage) {
|
||||
this.percentage.classList.remove('hidden');
|
||||
} else {
|
||||
this.percentage.classList.add('hidden');
|
||||
}
|
||||
if (this.showBatteryAndTime) {
|
||||
this.battery.classList.remove('hidden');
|
||||
this.time.classList.remove('hidden');
|
||||
} else {
|
||||
this.battery.classList.add('hidden');
|
||||
this.time.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
if (!this.verticalSeekbar) {
|
||||
toolWrapper.$.classList.add('horizontal');
|
||||
} else {
|
||||
toolWrapper.$.classList.remove('horizontal');
|
||||
}
|
||||
if (scrollHandler) {
|
||||
scrollHandler.refresh();
|
||||
scrollHandler.update();
|
||||
}
|
||||
};
|
||||
updateBatteryLevel = level => {
|
||||
this.battery.innerText = Math.ceil(level * 100) + '%';
|
||||
};
|
||||
}
|
||||
|
||||
class ToolWrapper {
|
||||
constructor() {
|
||||
this.$ = document.getElementById('ToolWrapper');
|
||||
this.$.onclick = e => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
this.tools = [];
|
||||
}
|
||||
hide = () => {
|
||||
this.$.classList.add('hidden');
|
||||
this.visible = false;
|
||||
};
|
||||
show = () => {
|
||||
this.$.classList.remove('hidden');
|
||||
this.visible = true;
|
||||
for (const tool of this.tools) {
|
||||
tool.onShow?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ScrollHandler {
|
||||
constructor(reader, toolWrapper) {
|
||||
this.reader = reader;
|
||||
this.toolWrapper = toolWrapper;
|
||||
this.$ = document.getElementById('ScrollBar');
|
||||
this.$.innerHTML = `
|
||||
<div class="scrollbar-item scrollbar-text" id="scrollbar-percentage">
|
||||
0
|
||||
</div>
|
||||
<div class="scrollbar-item" id="scrollbar-slider">
|
||||
<div id="scrollbar-track">
|
||||
<div id="scrollbar-progress">
|
||||
<div id="scrollbar-thumb-wrapper">
|
||||
<div id="scrollbar-thumb"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="scrollbar-item scrollbar-text">
|
||||
100
|
||||
</div>
|
||||
`;
|
||||
this.percentage = this.$.querySelector('#scrollbar-percentage');
|
||||
this.progress = this.$.querySelector('#scrollbar-progress');
|
||||
this.thumb = this.$.querySelector('#scrollbar-thumb-wrapper');
|
||||
this.slider = this.$.querySelector('#scrollbar-slider');
|
||||
this.sliderHeight = this.slider.clientHeight;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
|
||||
this.lock = false;
|
||||
window.onscroll = () => !this.lock && this.update();
|
||||
this.thumb.ontouchstart = () => (this.lock = true);
|
||||
this.thumb.ontouchend = () => (this.lock = false);
|
||||
this.thumb.ontouchmove = e => {
|
||||
const ratio =
|
||||
((this.reader.verticalSeekbar
|
||||
? e.changedTouches[0].clientY
|
||||
: e.changedTouches[0].clientX) -
|
||||
this.sliderOffsetY) /
|
||||
this.sliderHeight;
|
||||
this.update(ratio < 0 ? 0 : ratio);
|
||||
};
|
||||
}
|
||||
update = ratio => {
|
||||
if (ratio === undefined) {
|
||||
ratio =
|
||||
(window.scrollY + this.reader.layoutHeight) / this.reader.chapterHeight;
|
||||
}
|
||||
if (ratio > 1) {
|
||||
ratio = 1;
|
||||
}
|
||||
const percentage = parseInt(ratio * 100);
|
||||
if (this.toolWrapper.visible) {
|
||||
if (this.reader.verticalSeekbar) {
|
||||
this.progress.style.height = percentage + '%';
|
||||
this.progress.style.width = '100%';
|
||||
} else {
|
||||
this.progress.style.height = '100%';
|
||||
this.progress.style.width = percentage + '%';
|
||||
}
|
||||
this.percentage.innerText = percentage;
|
||||
}
|
||||
if (this.lock) {
|
||||
window.scrollTo({
|
||||
top: this.reader.chapterHeight * ratio - this.reader.layoutHeight,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
if (this.reader.percentage) {
|
||||
this.reader.percentage.innerText = percentage + '%';
|
||||
}
|
||||
};
|
||||
refresh = () => {
|
||||
if (this.reader.verticalSeekbar) {
|
||||
this.sliderHeight = this.slider.clientHeight;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().top;
|
||||
} else {
|
||||
this.sliderHeight = this.slider.clientWidth;
|
||||
this.sliderOffsetY = this.slider.getBoundingClientRect().left;
|
||||
}
|
||||
};
|
||||
onShow = () => {
|
||||
this.reader.refresh();
|
||||
this.refresh();
|
||||
this.update();
|
||||
};
|
||||
}
|
||||
|
||||
class SwipeHandler {
|
||||
constructor() {
|
||||
this.initialX = null;
|
||||
this.initialY = null;
|
||||
}
|
||||
|
||||
touchStartHandler = e => {
|
||||
this.initialX = e.changedTouches[0].screenX;
|
||||
this.initialY = e.changedTouches[0].screenY;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} e
|
||||
*/
|
||||
touchEndHandler = e => {
|
||||
let diffX = e.changedTouches[0].screenX - this.initialX;
|
||||
let diffY = e.changedTouches[0].screenY - this.initialY;
|
||||
if (
|
||||
e.target.id?.startsWith('scrollbar') ||
|
||||
e.target.id === 'Image-Modal-img'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(diffX) > Math.abs(diffY) * 2 && Math.abs(diffX) > 180) {
|
||||
if (diffX < 0 && this.initialX >= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'next' });
|
||||
} else if (diffX > 0 && this.initialX <= window.innerWidth / 2) {
|
||||
e.preventDefault();
|
||||
reader.post({ type: 'prev' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
enable = () => {
|
||||
document.addEventListener('touchstart', this.touchStartHandler);
|
||||
document.addEventListener('touchend', this.touchEndHandler);
|
||||
};
|
||||
|
||||
disable = () => {
|
||||
document.removeEventListener('touchstart', this.touchStartHandler);
|
||||
document.removeEventListener('touchend', this.touchEndHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('./type').TextToSpeech}
|
||||
*/
|
||||
class TextToSpeech {
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
(this.leaf = this.reader.chapter), (this.TTSWrapper = null); // swap these 2 elements
|
||||
this.TTSEle = null;
|
||||
this.reading = false;
|
||||
this.$ = document.getElementById('TTS-Controller');
|
||||
this.$.classList.add('d-none');
|
||||
this.$.onclick = () => {
|
||||
if (this.reading) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.resume();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
start = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.$.classList.remove('d-none');
|
||||
this.stop();
|
||||
this.next();
|
||||
};
|
||||
|
||||
startHere = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.$.classList.remove('d-none');
|
||||
this.stop();
|
||||
if (this.reader.selection.type === 'Range') {
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
}
|
||||
this.leaf = this.reader.selection.anchorNode;
|
||||
this.makeLeafSpeakable();
|
||||
this.speak();
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
};
|
||||
|
||||
resume = () => {
|
||||
this.$.innerHTML = `<button>${pauseIcon}</button>`;
|
||||
this.next();
|
||||
};
|
||||
|
||||
findLeaf() {
|
||||
while (this.leaf.firstChild) {
|
||||
this.leaf = this.leaf.firstChild;
|
||||
}
|
||||
}
|
||||
|
||||
findNextLeaf() {
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
this.findLeaf();
|
||||
} else if (this.leaf.nextSibling) {
|
||||
this.leaf = this.leaf.nextSibling;
|
||||
this.findLeaf();
|
||||
} else {
|
||||
this.leaf = this.leaf.parentNode;
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.findNextLeaf();
|
||||
}
|
||||
}
|
||||
|
||||
readable() {
|
||||
return (
|
||||
this.leaf.nodeName === '#text' && /[^\s\n,.?!;":“”]/.test(this.leaf.data)
|
||||
);
|
||||
}
|
||||
|
||||
findNextTextNode() {
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
this.TTSWrapper = null;
|
||||
}
|
||||
do {
|
||||
this.findNextLeaf();
|
||||
} while (!this.readable() && !this.reader.chapter.isSameNode(this.leaf));
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.makeLeafSpeakable();
|
||||
}
|
||||
|
||||
makeLeafSpeakable() {
|
||||
this.TTSWrapper = document.createElement('tts-wrapper');
|
||||
this.TTSWrapper.innerHTML = this.leaf.data.replace(
|
||||
/([^\n,.?!;":“”]+)/g,
|
||||
matched => {
|
||||
if (matched.trim().length) {
|
||||
return `<tts>${matched}</tts>`;
|
||||
}
|
||||
return matched;
|
||||
},
|
||||
);
|
||||
this.TTSEle = this.TTSWrapper.firstElementChild;
|
||||
this.leaf.replaceWith(this.TTSWrapper);
|
||||
}
|
||||
|
||||
next = () => {
|
||||
try {
|
||||
if (this.TTSEle) {
|
||||
this.TTSEle.classList.remove('highlight');
|
||||
}
|
||||
if (this.reading) {
|
||||
if (this.TTSEle && this.TTSEle.nextElementSibling) {
|
||||
this.TTSEle = this.TTSEle.nextElementSibling;
|
||||
} else {
|
||||
this.findNextTextNode();
|
||||
}
|
||||
} else {
|
||||
if (!this.TTSEle) {
|
||||
this.findNextTextNode();
|
||||
}
|
||||
}
|
||||
this.speak();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
};
|
||||
|
||||
stop = () => {
|
||||
this.$.classList.add('d-none');
|
||||
this.reading = false;
|
||||
if (this.leaf && this.TTSWrapper) {
|
||||
this.TTSWrapper.replaceWith(this.leaf);
|
||||
this.TTSWrapper = null;
|
||||
this.TTSEle = null;
|
||||
this.leaf = this.reader.chapter;
|
||||
}
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
pause = () => {
|
||||
this.$.innerHTML = `<button>${resumeIcon}</button>`;
|
||||
this.reading = false;
|
||||
this.reader.post({ type: 'stop-speak' });
|
||||
};
|
||||
|
||||
started = () => this.TTSWrapper !== null;
|
||||
|
||||
speak = () => {
|
||||
this.reading = true;
|
||||
if (this.reader.chapter.isSameNode(this.leaf)) {
|
||||
return;
|
||||
}
|
||||
this.TTSEle.classList.add('highlight');
|
||||
this.reader.post({ type: 'speak', data: this.TTSEle?.innerText.trim() });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("./type").ContextMenu}
|
||||
*/
|
||||
class ContextMenu {
|
||||
/**
|
||||
* @param {import("./type").Reader} reader
|
||||
*/
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
this.items = {
|
||||
START_READING: this.renderItem({
|
||||
name: 'Start Reading',
|
||||
icon: volumeIcon,
|
||||
action: () => {
|
||||
tts.start();
|
||||
this.closeMenu();
|
||||
},
|
||||
}),
|
||||
START_HERE: this.renderItem({
|
||||
name: 'Start Here',
|
||||
icon: selectVolumeIcon,
|
||||
action: () => {
|
||||
tts.startHere();
|
||||
this.closeMenu();
|
||||
},
|
||||
}),
|
||||
COPY: this.renderItem({
|
||||
name: 'Copy',
|
||||
icon: copyIcon,
|
||||
action: () => {
|
||||
this.reader.post({
|
||||
type: 'copy',
|
||||
data: this.reader.selection.toString(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
SELECT_ALL: this.renderItem({
|
||||
name: 'Select All',
|
||||
icon: selectAllIcon,
|
||||
action: () => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(this.reader.chapter);
|
||||
this.reader.selection.removeAllRanges();
|
||||
this.reader.selection.addRange(range);
|
||||
},
|
||||
}),
|
||||
};
|
||||
this.commonItems = [this.items.COPY, this.items.SELECT_ALL];
|
||||
this.contextMenu = document.createElement('ul');
|
||||
this.contextMenu.classList.add('contextMenu');
|
||||
this.isOpened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{name: string, icon: string, action: () => void}} data
|
||||
* @returns
|
||||
*/
|
||||
renderItem(data) {
|
||||
const item = document.createElement('li');
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = `${data.icon} <span>${data.name}</span>`;
|
||||
button.classList.add('contextMenu-button');
|
||||
item.classList.add('contextMenu-item');
|
||||
item.appendChild(button);
|
||||
button.addEventListener('click', event => {
|
||||
event.stopPropagation();
|
||||
data.action();
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
renderMenu(items) {
|
||||
this.contextMenu.innerHTML = '';
|
||||
items.concat(this.commonItems).forEach((item, index) => {
|
||||
item.firstChild.setAttribute('style', `animation-delay: ${index * 0.1}s`);
|
||||
this.contextMenu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
if (this.isOpened) {
|
||||
this.isOpened = false;
|
||||
this.contextMenu.remove();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.reader.chapter.addEventListener('click', () => {
|
||||
this.closeMenu(this.contextMenu);
|
||||
});
|
||||
document.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
if (e.target instanceof HTMLImageElement) {
|
||||
if (imageModal.showing) {
|
||||
imageModal.hide();
|
||||
} else {
|
||||
imageModal.show(e.target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.isOpened = true;
|
||||
if (this.reader.selection.type === 'Range') {
|
||||
this.renderMenu([this.items.START_HERE, this.items.START_READING]);
|
||||
} else {
|
||||
this.renderMenu([this.items.START_READING]);
|
||||
}
|
||||
const { clientX, clientY } = e;
|
||||
e.target.appendChild(this.contextMenu);
|
||||
const positionY =
|
||||
clientY + this.contextMenu.scrollHeight >= window.innerHeight
|
||||
? window.innerHeight - this.contextMenu.scrollHeight - 20
|
||||
: clientY;
|
||||
const positionX =
|
||||
clientX + this.contextMenu.scrollWidth >= window.innerWidth
|
||||
? window.innerWidth - this.contextMenu.scrollWidth - 20
|
||||
: clientX;
|
||||
this.contextMenu.setAttribute(
|
||||
'style',
|
||||
`--width: ${this.contextMenu.scrollWidth}px;
|
||||
--height: ${this.contextMenu.scrollHeight}px;
|
||||
--top: ${positionY}px;
|
||||
--left: ${positionX}px;`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("./type").ImageModal}
|
||||
*/
|
||||
class ImageModal {
|
||||
/**
|
||||
* @param {import('./type').Reader} reader
|
||||
*/
|
||||
constructor(reader) {
|
||||
this.reader = reader;
|
||||
this.$ = document.getElementById('Image-Modal');
|
||||
this.img = this.$.querySelector('img');
|
||||
this.showing = false;
|
||||
this.$.onclick = e => {
|
||||
if (e.target.id !== 'Image-Modal-img') {
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
this.$.ontouchmove = e => {
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLImageElement} image
|
||||
*/
|
||||
show(image) {
|
||||
this.img.src = image.src;
|
||||
this.img.alt = `Can not render image from ${image.src}`;
|
||||
this.reader.viewport.setAttribute(
|
||||
'content',
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=10',
|
||||
);
|
||||
this.$.classList.add('show');
|
||||
this.showing = true;
|
||||
}
|
||||
hide() {
|
||||
this.$.classList.remove('show');
|
||||
this.showing = false;
|
||||
this.img.src = '';
|
||||
this.img.alt = '';
|
||||
this.reader.viewport.setAttribute(
|
||||
'content',
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=1.0',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var swipeHandler = new SwipeHandler();
|
||||
var toolWrapper = new ToolWrapper();
|
||||
var reader = new Reader();
|
||||
var scrollHandler = new ScrollHandler(reader, toolWrapper);
|
||||
var tts = new TextToSpeech(reader);
|
||||
toolWrapper.tools = [scrollHandler, tts];
|
||||
imageModal = new ImageModal(reader);
|
||||
const contextMenu = new ContextMenu(reader);
|
||||
contextMenu.init();
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
var textVide = (function (L) {
|
||||
'use strict';
|
||||
const f = t => t == null || t === '',
|
||||
h = (t, n) =>
|
||||
Object.keys(t).reduce((e, s) => (n(e[s]) && delete e[s], e), t),
|
||||
I = (t, n) => ({ ...n, ...h(t, f) }),
|
||||
_ = ['<b>', '</b>'],
|
||||
y = t =>
|
||||
I(t, {
|
||||
sep: _,
|
||||
fixationPoint: 1,
|
||||
ignoreHtmlTag: !0,
|
||||
ignoreHtmlEntity: !0,
|
||||
}),
|
||||
u = [
|
||||
[0, 4, 12, 17, 24, 29, 35, 42, 48],
|
||||
[1, 2, 7, 10, 13, 14, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46, 49],
|
||||
[
|
||||
1, 2, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37,
|
||||
39, 41, 43, 45, 47, 49,
|
||||
],
|
||||
[
|
||||
0, 2, 4, 5, 6, 8, 9, 11, 14, 15, 17, 18, 20, 0, 21, 23, 24, 26, 27, 29,
|
||||
30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48,
|
||||
],
|
||||
[
|
||||
0, 2, 3, 5, 6, 7, 8, 10, 11, 12, 14, 15, 17, 19, 20, 21, 23, 24, 25, 26,
|
||||
28, 29, 30, 32, 33, 34, 35, 37, 38, 39, 41, 42, 43, 44, 46, 47, 48,
|
||||
],
|
||||
],
|
||||
A = (t, n) => {
|
||||
const { length: i } = t,
|
||||
e = u[n - 1] ?? u[0],
|
||||
s = e.findIndex(c => i <= c);
|
||||
let o = i - s;
|
||||
return s === -1 && (o = i - e.length), Math.max(o, 0);
|
||||
},
|
||||
R = (t, n) =>
|
||||
typeof n === 'string' ? `${n}${t}${n}` : `${n[0]}${t}${n[1]}`,
|
||||
m = t =>
|
||||
Array.from(t).map(n => {
|
||||
const i = n.index,
|
||||
[e] = n,
|
||||
{ length: s } = e;
|
||||
return [i, i + s - 1];
|
||||
}),
|
||||
H = /(<!--[\s\S]*?-->)|(<[^>]*>)/g,
|
||||
N = t => {
|
||||
const n = t.matchAll(H),
|
||||
e = m(n).reverse();
|
||||
return s => {
|
||||
const o = s.index,
|
||||
c = e.find(([r]) => o > r);
|
||||
if (!c) {
|
||||
return !1;
|
||||
}
|
||||
const [, a] = c;
|
||||
return o < a;
|
||||
};
|
||||
},
|
||||
O = /(&[\w#]+;)/g,
|
||||
M = t => {
|
||||
const n = t.matchAll(O),
|
||||
e = m(n).reverse();
|
||||
return s => {
|
||||
const o = s.index,
|
||||
c = e.find(([r]) => o > r);
|
||||
if (!c) {
|
||||
return !1;
|
||||
}
|
||||
const [, a] = c;
|
||||
return o < a;
|
||||
};
|
||||
},
|
||||
x = /(\p{L}|\p{Nd})*\p{L}(\p{L}|\p{Nd})*/gu,
|
||||
F = (t, n = {}) => {
|
||||
if (!(t != null && t.length)) {
|
||||
return '';
|
||||
}
|
||||
const {
|
||||
fixationPoint: i,
|
||||
sep: e,
|
||||
ignoreHtmlTag: s,
|
||||
ignoreHtmlEntity: o,
|
||||
} = y(n),
|
||||
c = Array.from(t.matchAll(x));
|
||||
let a = '',
|
||||
l = 0,
|
||||
r;
|
||||
s && (r = N(t));
|
||||
let d;
|
||||
o && (d = M(t));
|
||||
for (const g of c) {
|
||||
if ((r == null ? void 0 : r(g)) || (d == null ? void 0 : d(g))) {
|
||||
continue;
|
||||
}
|
||||
const [D] = g,
|
||||
T = g.index,
|
||||
E = T + A(D, i),
|
||||
U = t.slice(l, T);
|
||||
(a += U), T !== E && (a += R(t.slice(T, E), e)), (l = E);
|
||||
}
|
||||
const G = t.slice(l);
|
||||
return a + G;
|
||||
};
|
||||
return (
|
||||
(L.textVide = F),
|
||||
Object.defineProperty(L, Symbol.toStringTag, { value: 'Module' }),
|
||||
L
|
||||
);
|
||||
})({});
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
ChapterGeneralSettings,
|
||||
ChapterReaderSettings,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
import 'typescript';
|
||||
import 'typescript/lib/lib.dom';
|
||||
|
||||
interface Reader {
|
||||
selection: Selection;
|
||||
viewport: HTMLMetaElement;
|
||||
footerWrapper: HTMLDivElement;
|
||||
percentage: HTMLDivElement;
|
||||
battery: HTMLDivElement;
|
||||
time: HTMLDivElement;
|
||||
paddingTop: number;
|
||||
chapter: HTMLElement;
|
||||
chapterHeight: number;
|
||||
layoutHeight: number;
|
||||
pluginId: string;
|
||||
novelId: string;
|
||||
chapterId: string;
|
||||
saveProgressInterval: NodeJS.Timeout;
|
||||
timeInterval: NodeJS.Timeout;
|
||||
refresh: () => void;
|
||||
post: (message: { type: string; data?: any }) => void;
|
||||
updateReaderSettings: (settings: ChapterReaderSettings) => void;
|
||||
updateGeneralSettings: (settings: ChapterGeneralSettings) => void;
|
||||
updateBatteryLevel: (level: number) => void;
|
||||
}
|
||||
|
||||
interface TextToSpeech {
|
||||
reader: Reader;
|
||||
leaf: HTMLElement;
|
||||
TTSWrapper: HTMLElement;
|
||||
TTSEle: HTMLElement;
|
||||
reading: boolean;
|
||||
start: () => void;
|
||||
startHere: () => void;
|
||||
resume: () => void;
|
||||
pause: () => void;
|
||||
stop: () => void;
|
||||
started: () => boolean;
|
||||
}
|
||||
|
||||
export enum ContextMenuItem {
|
||||
START_READING = 'START_READING',
|
||||
START_HERE = 'START_HERE',
|
||||
COPY = 'COPY',
|
||||
SELECT_ALL = 'SELECT_ALL',
|
||||
}
|
||||
interface ContextMenu {
|
||||
reader: Reader;
|
||||
contextMenu: HTMLUListElement;
|
||||
items: Record<ContextMenuItem, HTMLLIElement>;
|
||||
renderItem: (data: {
|
||||
name: string;
|
||||
icon: string;
|
||||
action: () => void;
|
||||
}) => HTMLLIElement;
|
||||
renderMenu: (items: HTMLLIElement[]) => void;
|
||||
closeMenu: (menu: HTMLUListElement) => void;
|
||||
init: () => void;
|
||||
}
|
||||
|
||||
interface ImageModal {
|
||||
reader: Reader;
|
||||
showing: boolean;
|
||||
show: (image: HTMLImageElement) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package com.rajarsheechatterjee.EpubUtil;
|
||||
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
public class EpubUtil extends ReactContextBaseJavaModule {
|
||||
EpubUtil(ReactApplicationContext context){super(context);}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "EpubUtil";
|
||||
}
|
||||
|
||||
private XmlPullParser initParse(File file) throws XmlPullParserException, IOException {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
|
||||
parser.setInput(new FileInputStream(file), null);
|
||||
parser.nextTag();
|
||||
return parser;
|
||||
}
|
||||
|
||||
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
String result = "";
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
result = parser.getText();
|
||||
parser.nextTag();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String cleanUrl(String url){
|
||||
if(url != null){
|
||||
return url.replaceFirst("#[^.]+?$", "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void parseNovelAndChapters(String epubDirPath, Promise promise) {
|
||||
try {
|
||||
File containerFile = new File(epubDirPath, "META-INF/container.xml");
|
||||
File contentMetaFile = new File(epubDirPath, getContentMetaFilePath(containerFile));
|
||||
String contentDir = contentMetaFile.getParent();
|
||||
|
||||
ReadableMap novel = getNovelMetadata(contentMetaFile, contentDir);
|
||||
promise.resolve(novel);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentMetaFilePath(File file) throws XmlPullParserException, IOException {
|
||||
XmlPullParser parser = initParse(file);
|
||||
while (parser.next() != XmlPullParser.END_TAG){
|
||||
@Nullable String tag = parser.getName();
|
||||
if(tag != null && tag.equals("rootfile")) {
|
||||
return parser.getAttributeValue(null, "full-path");
|
||||
}
|
||||
}
|
||||
return "OEBPS/content.opf"; // default
|
||||
}
|
||||
private ReadableMap getNovelMetadata(File file, String contentDir) throws XmlPullParserException, IOException {
|
||||
WritableMap novel = new WritableNativeMap();
|
||||
WritableArray chapters = new WritableNativeArray();
|
||||
XmlPullParser parser = initParse(file);
|
||||
HashMap<String, String> refMap = new HashMap<>();
|
||||
HashMap<String, String> tocMap = new HashMap<>();
|
||||
File tocFile = new File(contentDir, "toc.ncx");
|
||||
if(tocFile.exists()){
|
||||
XmlPullParser tocParser = initParse(tocFile);
|
||||
String label = null;
|
||||
while (tocParser.next() != XmlPullParser.END_DOCUMENT){
|
||||
String tag = tocParser.getName();
|
||||
if(tag != null){
|
||||
if(tag.equals("text")){
|
||||
label = readText(tocParser);
|
||||
}else if(tag.equals("content")){
|
||||
String href = cleanUrl(tocParser.getAttributeValue(null, "src"));
|
||||
if(href != null){
|
||||
tocMap.put(href, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String cover = null;
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT){
|
||||
@Nullable String tag = parser.getName();
|
||||
if(tag != null){
|
||||
switch (tag) {
|
||||
case "item": {
|
||||
String id = parser.getAttributeValue(null, "id");
|
||||
String href = parser.getAttributeValue(null, "href");
|
||||
if (id != null) {
|
||||
refMap.put(id, href);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "itemref": {
|
||||
String idRef = parser.getAttributeValue(null, "idref");
|
||||
String href = refMap.get(idRef);
|
||||
if (href != null) {
|
||||
WritableMap chapter = new WritableNativeMap();
|
||||
chapter.putString("path", contentDir + "/" + href);
|
||||
String name = tocMap.get(href);
|
||||
chapter.putString("name", name == null ? href : name);
|
||||
chapters.pushMap(chapter);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "title":
|
||||
novel.putString("name", readText(parser));
|
||||
break;
|
||||
case "creator":
|
||||
novel.putString("author", readText(parser));
|
||||
break;
|
||||
case "contributor":
|
||||
novel.putString("artist", readText(parser));
|
||||
break;
|
||||
case "description":
|
||||
novel.putString("summary", readText(parser));
|
||||
break;
|
||||
case "meta":
|
||||
String metaName = parser.getAttributeValue(null, "name");
|
||||
if(metaName != null && metaName.equals("cover")){
|
||||
cover = parser.getAttributeValue(null, "content");
|
||||
}
|
||||
break;
|
||||
}
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
if(cover != null){
|
||||
String coverPath = contentDir + "/" + refMap.get(cover);
|
||||
novel.putString("cover", coverPath);
|
||||
}
|
||||
novel.putArray("chapters", chapters);
|
||||
return novel;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.rajarsheechatterjee.EpubUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.rajarsheechatterjee.ZipArchive.ZipArchive;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class EpubUtilPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new EpubUtil(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper;
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
}
|
||||
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this,R.style.SplashScreenTheme, R.id.lottie);
|
||||
SplashScreen.setAnimationFinished(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (VolumeButtonListener.isActive()) {
|
||||
|
||||
int action = event.getAction();
|
||||
int keyCode = event.getKeyCode();
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
// volup
|
||||
VolumeButtonListener.up();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
// voldown
|
||||
VolumeButtonListener.down();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "main";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Configuration;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.rajarsheechatterjee.EpubUtil.EpubUtilPackage;
|
||||
import com.rajarsheechatterjee.NavigationBarColor.NavigationBarColorPackage;
|
||||
import com.rajarsheechatterjee.TextFile.TextFilePackage;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
import com.rajarsheechatterjee.ZipArchive.ZipArchivePackage;
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
packages.add(new ZipArchivePackage());
|
||||
packages.add(new TextFilePackage());
|
||||
packages.add(new EpubUtilPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package com.rajarsheechatterjee.NavigationBarColor;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
||||
|
||||
public class NavigationBarColorModule extends ReactContextBaseJavaModule {
|
||||
public static final String REACT_CLASS = "NavigationBarColor";
|
||||
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
|
||||
private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity";
|
||||
private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
public NavigationBarColorModule(ReactApplicationContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setNavigationBarTheme(Activity activity, Boolean light) {
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Window window = activity.getWindow();
|
||||
int flags = window.getDecorView().getSystemUiVisibility();
|
||||
if (light) {
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
} else {
|
||||
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
}
|
||||
window.getDecorView().setSystemUiVisibility(flags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> constants = new HashMap<>();
|
||||
constants.put("EXAMPLE_CONSTANT", "example");
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) {
|
||||
final WritableMap map = Arguments.createMap();
|
||||
if (getCurrentActivity() != null) {
|
||||
try {
|
||||
final Window window = getCurrentActivity().getWindow();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (color.equals("transparent") || color.equals("translucent")) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
if (color.equals("transparent")) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
} else {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
return;
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
if (animated) {
|
||||
Integer colorFrom = window.getNavigationBarColor();
|
||||
Integer colorTo = Color.parseColor(color);
|
||||
//window.setNavigationBarColor(colorTo);
|
||||
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
window.setNavigationBarColor((Integer) animator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
colorAnimation.start();
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.parseColor(color));
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE));
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void hideNavigationBar(Promise promise) {
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentActivity() != null) {
|
||||
View decorView = getCurrentActivity().getWindow().getDecorView();
|
||||
decorView.setSystemUiVisibility(UI_FLAG_HIDE_NAV_BAR);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void showNavigationBar(Promise promise) {
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentActivity() != null) {
|
||||
Window w = getCurrentActivity().getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
View decorView = w.getDecorView();
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.rajarsheechatterjee.NavigationBarColor;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class NavigationBarColorPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new NavigationBarColorModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.rajarsheechatterjee.TextFile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
|
||||
public class TextFile extends ReactContextBaseJavaModule {
|
||||
TextFile(ReactApplicationContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "TextFile";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void writeFile(String path, String content, Promise promise) {
|
||||
try {
|
||||
FileWriter fw = new FileWriter(path);
|
||||
fw.write(content);
|
||||
fw.close();
|
||||
promise.resolve(null);
|
||||
} catch (Exception e) {
|
||||
promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod void readFile(String path, Promise promise) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
BufferedReader br = new BufferedReader(new FileReader(path));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line).append('\n');
|
||||
promise.resolve(sb.toString());
|
||||
} catch (Exception e) {
|
||||
promise.reject(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.rajarsheechatterjee.TextFile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class TextFilePackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new TextFile(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
|
||||
public class VolumeButtonListener extends ReactContextBaseJavaModule {
|
||||
|
||||
static ReactApplicationContext appContext;
|
||||
|
||||
public VolumeButtonListener(ReactApplicationContext context) {
|
||||
super(context);
|
||||
appContext = context;
|
||||
}
|
||||
|
||||
private static boolean active = false;
|
||||
|
||||
public static boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public static boolean prevent = false;
|
||||
|
||||
public static void sendEvent(boolean up) {
|
||||
if (active) {
|
||||
WritableMap args = Arguments.createMap();
|
||||
if (up)
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeUp", args);
|
||||
else
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeDown", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void down() {
|
||||
sendEvent(false);
|
||||
}
|
||||
|
||||
public static void up() {
|
||||
sendEvent(true);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void connect() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
void disconnect() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void pause() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unpause() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void preventDefault() {
|
||||
prevent = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void noPreventDefault() {
|
||||
prevent = false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VolumeButtonListener";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addListener(String eventName) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeListeners(Integer count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class VolumeButtonListenerPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<NativeModule>();
|
||||
|
||||
modules.add(new VolumeButtonListener(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package com.rajarsheechatterjee.ZipArchive;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
public class ZipArchive extends ReactContextBaseJavaModule {
|
||||
ZipArchive(ReactApplicationContext context) throws Exception {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ZipArchive";
|
||||
}
|
||||
|
||||
private String escapeFilePath(String filePath){
|
||||
return filePath.replaceAll(":", "\uA789");
|
||||
}
|
||||
|
||||
private void unzipProcess(ZipInputStream zis, String distDirPath) throws Exception {
|
||||
ZipEntry zipEntry;
|
||||
int len;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((zipEntry = zis.getNextEntry()) != null) {
|
||||
if(zipEntry.getName().endsWith("/")) continue;
|
||||
String escapedFilePath = this.escapeFilePath(zipEntry.getName());
|
||||
File newFile = new File(distDirPath, escapedFilePath);
|
||||
newFile.getParentFile().mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
while ((len = zis.read(buffer)) > 0) fos.write(buffer, 0, len);
|
||||
fos.close();
|
||||
}
|
||||
zis.closeEntry();
|
||||
}
|
||||
@ReactMethod
|
||||
public void unzip(String sourceFilePath, String distDirPath, Promise promise) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try{
|
||||
ZipInputStream zis = new ZipInputStream(new FileInputStream(sourceFilePath));
|
||||
unzipProcess(zis, distDirPath);
|
||||
zis.close();
|
||||
promise.resolve(null);
|
||||
} catch (Exception e) {
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void remoteUnzip(String distDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
URL url = new URL(_url);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
if(headers != null){
|
||||
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
|
||||
while (it.hasNext()){
|
||||
Map.Entry<String, Object> entry = it.next();
|
||||
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
ZipInputStream zis = new ZipInputStream(connection.getInputStream());
|
||||
unzipProcess(zis, distDirPath);
|
||||
connection.disconnect();
|
||||
promise.resolve(null);
|
||||
}catch (Exception e){
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private ArrayList<String> walkDir(String path) {
|
||||
ArrayList<String> res = new ArrayList<>();
|
||||
File node = new File(path);
|
||||
if(node.isFile()){
|
||||
res.add(path);
|
||||
}else{
|
||||
String[] children = node.list();
|
||||
if(children != null){
|
||||
for(String filename: children){
|
||||
ArrayList<String> childPaths = walkDir(new File(path, filename).toString());
|
||||
res.addAll(childPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
private void zipProcess(String sourceDirPath, ZipOutputStream zos) throws Exception{
|
||||
ArrayList<String> paths = walkDir(sourceDirPath);
|
||||
byte[] buffer = new byte[4096];
|
||||
int len;
|
||||
for(String path: paths){
|
||||
ZipEntry zipEntry = new ZipEntry(path.replace(sourceDirPath + "/", ""));
|
||||
zos.putNextEntry(zipEntry);
|
||||
try (FileInputStream fis = new FileInputStream(path)) {
|
||||
while ((len = fis.read(buffer)) > 0) {
|
||||
zos.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
zos.closeEntry();
|
||||
}
|
||||
zos.close();
|
||||
}
|
||||
@ReactMethod
|
||||
public void remoteZip(String sourceDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
URL url = new URL(_url);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
if(headers != null){
|
||||
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
|
||||
while (it.hasNext()){
|
||||
Map.Entry<String, Object> entry = it.next();
|
||||
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
ZipOutputStream zos = new ZipOutputStream(connection.getOutputStream());
|
||||
zipProcess(sourceDirPath, zos);
|
||||
if(connection.getResponseCode() == 200){
|
||||
InputStream is = connection.getInputStream();
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
for (int length; (length = is.read(buffer)) != -1; ) {
|
||||
result.write(buffer, 0, length);
|
||||
}
|
||||
is.close();
|
||||
connection.disconnect();
|
||||
promise.resolve(result.toString());
|
||||
}else{
|
||||
throw new Exception("Network request failed");
|
||||
}
|
||||
}catch (Exception e){
|
||||
promise.reject(e.getCause());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.rajarsheechatterjee.ZipArchive;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ZipArchivePackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
try {
|
||||
modules.add(new ZipArchive(reactApplicationContext));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 801 B |
|
Before Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 617 B |
|
Before Width: | Height: | Size: 500 B |
|
Before Width: | Height: | Size: 976 B |
|
Before Width: | Height: | Size: 837 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,93 +0,0 @@
|
||||
<vector android:alpha="0" android:height="1dp"
|
||||
android:viewportHeight="1024" android:viewportWidth="1024"
|
||||
android:width="1dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.994" android:fillColor="#132c33"
|
||||
android:pathData="M479.5,-0.5C500.8,-0.5 522.2,-0.5 543.5,-0.5C704.4,13.5 832.9,84.8 929,213.5C985.1,293.3 1016.5,382 1023.5,479.5C1023.5,500.8 1023.5,522.2 1023.5,543.5C1009.2,709.4 934.5,840.2 799.5,936C722,987.8 636.6,1017 543.5,1023.5C522.2,1023.5 500.8,1023.5 479.5,1023.5C333.8,1011.4 213.3,950.4 118,840.5C47.3,754.3 7.8,655.3 -0.5,543.5C-0.5,522.2 -0.5,500.8 -0.5,479.5C13.5,318.6 84.8,190.1 213.5,94C293.3,37.9 382,6.4 479.5,-0.5Z" android:strokeAlpha="0.994"/>
|
||||
<path android:fillColor="#106476" android:pathData="M870.5,325.5C876.8,340.6 882.7,355.9 888,371.5C889.7,373.2 891.1,375.2 892,377.5C895.1,386.3 897.4,395.3 899,404.5C900,406.7 901.5,408.4 903.5,409.5C903.5,410.2 903.5,410.8 903.5,411.5C907.2,436.2 910.8,460.8 914.5,485.5C915.4,500.5 915.7,515.5 915.5,530.5C911.7,612.8 885.4,686.8 836.5,752.5C836.5,750.7 837,749 838,747.5C894.4,667.2 918.8,577.8 911,479.5C899,365 848.5,271.8 759.5,200C652.9,120.8 534.9,97.5 405.5,130C299,161.6 218.1,225.4 163,321.5C108.7,424.4 99.1,531.4 134,642.5C147,680.2 165,715.2 188,747.5C189,749 189.5,750.7 189.5,752.5C141.4,688 115.1,615.3 110.5,534.5C110.3,518.5 110.5,502.5 111,486.5C112.8,477.3 113.8,467.9 114,458.5C119,435.8 124.4,413.1 130,390.5C140,359.6 153.3,330.6 170,303.5C184.2,283.1 199.4,263.5 215.5,244.5C231.1,229.1 246.7,213.9 262.5,199C273.8,191.7 285.1,184.4 296.5,177C297.3,175.5 298.3,174.2 299.5,173C303.2,172.6 306.5,171.3 309.5,169C313,166 316.3,162.9 319.5,159.5C330.1,155.2 340.1,149.9 349.5,143.5C360.1,142.3 370.1,139.2 379.5,134C385.7,132.1 392.1,130.5 398.5,129C400.5,128 402.5,127 404.5,126C452.1,113.9 500.4,109.7 549.5,113.5C567,115.2 584.3,117.7 601.5,121C620.6,126.7 639.8,132.6 659,138.5C668,141.9 676.8,145.8 685.5,150C695.4,156.1 705.8,161.5 716.5,166C717.2,167 717.8,168 718.5,169C733.7,175.2 747.1,184.2 758.5,196C764.2,199.9 769.5,204.2 774.5,209C776.6,210.8 778.9,212.1 781.5,213C785.3,218.2 789.8,222.7 795,226.5C797.1,228.3 798.4,230.7 799,233.5C807,241.6 815,249.6 823,257.5C823.3,258.8 823.7,260.2 824,261.5C828,266.5 832.4,271.2 837,275.5C841.8,284.4 847.1,293.1 853,301.5C858.3,307.4 862.6,314.1 866,321.5C867,323.4 868.5,324.7 870.5,325.5Z"/>
|
||||
<path android:fillColor="#126e81" android:pathData="M836.5,752.5C791,813.4 733,858.3 662.5,887C545.5,931 431.5,924.3 320.5,867C268.3,838.7 224.7,800.5 189.5,752.5C189.5,750.7 189,749 188,747.5C165,715.2 147,680.2 134,642.5C99.1,531.4 108.7,424.4 163,321.5C218.1,225.4 299,161.6 405.5,130C534.9,97.5 652.9,120.8 759.5,200C848.5,271.8 899,365 911,479.5C918.8,577.8 894.4,667.2 838,747.5C837,749 836.5,750.7 836.5,752.5Z"/>
|
||||
<path android:fillColor="#a1abaf" android:pathData="M660.5,203.5C657.7,203.4 655,202.8 652.5,201.5C641.6,195.4 629.9,191.5 617.5,190C609.7,188 602.4,185 595.5,181C580.9,179.2 566.6,176.6 552.5,173C521.2,169 490.2,169.9 459.5,175.5C447.6,178.3 435.6,180.5 423.5,182C418.2,183.8 412.9,185.3 407.5,186.5C388.5,192.1 370.1,199.6 352.5,209C351.2,209.5 349.9,209.7 348.5,209.5C423.4,170 502.4,159.1 585.5,177C611.8,182.6 636.8,191.4 660.5,203.5Z"/>
|
||||
<path android:fillColor="#d7e2e6" android:pathData="M688.5,223.5C690.9,224.3 692.9,225.7 694.5,227.5C695.4,229.8 696.7,232 698.5,234C734,257.8 763.8,287.3 788,322.5C789.3,323.9 790.8,324.9 792.5,325.5C793.2,325.8 793.8,326.2 794.5,326.5C794.3,327.9 794.5,329.2 795,330.5C851.6,424.2 862.9,523.2 829,627.5C824.9,638.7 820.3,649.7 815,660.5C812.6,667.1 810.4,673.7 808.5,680.5C806.4,682.7 803.9,684.7 801,686.5C790.6,704.3 778.8,720.7 765.5,735.5C765.5,737.2 765.5,738.8 765.5,740.5C721,790.4 666.4,824.1 601.5,841.5C586.8,843.9 572.2,846.4 557.5,849C523.2,853.2 488.9,853.4 454.5,849.5C364.7,834.1 292.3,790.4 237.5,718.5C236.2,715.7 235.4,712.7 235,709.5C188.1,642.5 167.8,567.8 174,485.5C176.7,456.8 182.5,428.8 191.5,401.5C194.1,391.7 197.3,382 201,372.5C201.9,372 202.8,371.3 203.5,370.5C208.1,361.7 212.3,352.7 216,343.5C220.2,338.6 223.9,333.3 227,327.5C242.1,304 260,282.5 280.5,263C290.7,256.1 300.3,248.4 309.5,240C314.3,237.6 318.9,235.1 323.5,232.5C408.4,179.1 499.7,164.2 597.5,188C611.5,191.7 625.2,196.3 638.5,202C641.5,202.3 644.5,202.7 647.5,203C661.2,209.8 674.9,216.7 688.5,223.5Z"/>
|
||||
<path android:fillColor="#cfdade" android:pathData="M556.5,177.5C577.5,179.3 597.9,184.2 617.5,192C621.3,192.3 624.9,192.9 628.5,194C636.6,198 645,201.2 653.5,203.5C663.6,208.4 673.6,213.6 683.5,219C685.6,220 687.3,221.5 688.5,223.5C674.9,216.7 661.2,209.8 647.5,203C644.5,202.7 641.5,202.3 638.5,202C625.2,196.3 611.5,191.7 597.5,188C499.7,164.2 408.4,179.1 323.5,232.5C330.8,226.2 338.8,220.9 347.5,216.5C348.5,215.1 349.5,213.8 350.5,212.5C363.9,207 377.2,201.1 390.5,195C402.2,191.3 413.8,187.7 425.5,184C448.7,180 472.1,177 495.5,175C505.5,174.3 515.5,174.3 525.5,175C535.8,176.1 546.2,176.9 556.5,177.5Z"/>
|
||||
<path android:fillColor="#c4ced2" android:pathData="M556.5,177.5C546.2,176.9 535.8,176.1 525.5,175C515.5,174.3 505.5,174.3 495.5,175C472.1,177 448.7,180 425.5,184C413.8,187.7 402.2,191.3 390.5,195C377.2,201.1 363.9,207 350.5,212.5C368.9,202.5 387.9,193.8 407.5,186.5C412.9,185.3 418.2,183.8 423.5,182C435.6,180.5 447.6,178.3 459.5,175.5C491.2,171.4 522.9,171.5 554.5,176C555.4,176.3 556.1,176.8 556.5,177.5Z"/>
|
||||
<path android:fillColor="#bac3c7" android:pathData="M652.5,201.5C652.8,202.2 653.2,202.8 653.5,203.5C645,201.2 636.6,198 628.5,194C624.9,192.9 621.3,192.3 617.5,192C597.9,184.2 577.5,179.3 556.5,177.5C556.1,176.8 555.4,176.3 554.5,176C522.9,171.5 491.2,171.4 459.5,175.5C490.2,169.9 521.2,169 552.5,173C566.6,176.6 580.9,179.2 595.5,181C602.4,185 609.7,188 617.5,190C629.9,191.5 641.6,195.4 652.5,201.5Z"/>
|
||||
<path android:fillColor="#0e5565" android:pathData="M870.5,325.5C868.5,324.7 867,323.4 866,321.5C862.6,314.1 858.3,307.4 853,301.5C847.1,293.1 841.8,284.4 837,275.5C832.4,271.2 828,266.5 824,261.5C823.7,260.2 823.3,258.8 823,257.5C815,249.6 807,241.6 799,233.5C798.4,230.7 797.1,228.3 795,226.5C789.8,222.7 785.3,218.2 781.5,213C778.9,212.1 776.6,210.8 774.5,209C769.5,204.2 764.2,199.9 758.5,196C747.1,184.2 733.7,175.2 718.5,169C717.8,168 717.2,167 716.5,166C705.8,161.5 695.4,156.1 685.5,150C676.8,145.8 668,141.9 659,138.5C639.8,132.6 620.6,126.7 601.5,121C584.3,117.7 567,115.2 549.5,113.5C500.4,109.7 452.1,113.9 404.5,126C402.5,127 400.5,128 398.5,129C392.1,130.5 385.7,132.1 379.5,134C370.1,139.2 360.1,142.3 349.5,143.5C420.7,111.5 495,101.3 572.5,113C678.8,130.2 765.3,180.7 832,264.5C846.7,283.7 859.5,304 870.5,325.5Z"/>
|
||||
<path android:fillColor="#a9b3b7" android:pathData="M652.5,201.5C655,202.8 657.7,203.4 660.5,203.5C704.9,224.6 742.6,254.2 773.5,292.5C765,283.5 756.4,274.7 747.5,266C744,263.6 741,260.8 738.5,257.5C733.8,254.1 729.1,250.8 724.5,247.5C722.4,245.3 720.1,243.3 717.5,241.5C709.8,236.7 702.1,232 694.5,227.5C692.9,225.7 690.9,224.3 688.5,223.5C687.3,221.5 685.6,220 683.5,219C673.6,213.6 663.6,208.4 653.5,203.5C653.2,202.8 652.8,202.2 652.5,201.5Z"/>
|
||||
<path android:fillColor="#cfdadd" android:pathData="M694.5,227.5C702.1,232 709.8,236.7 717.5,241.5C720.1,243.3 722.4,245.3 724.5,247.5C740.5,264 756.6,280.3 773,296.5C779.5,304.9 785.8,313.3 792,321.5C792.5,322.8 792.7,324.1 792.5,325.5C790.8,324.9 789.3,323.9 788,322.5C763.8,287.3 734,257.8 698.5,234C696.7,232 695.4,229.8 694.5,227.5Z"/>
|
||||
<path android:fillColor="#a8b3b6" android:pathData="M407.5,186.5C387.9,193.8 368.9,202.5 350.5,212.5C348.1,212.5 345.7,213 343.5,214C280.9,251.6 233.3,303.1 200.5,368.5C200,369.3 199.4,369.6 198.5,369.5C231,299.7 281,246.4 348.5,209.5C349.9,209.7 351.2,209.5 352.5,209C370.1,199.6 388.5,192.1 407.5,186.5Z"/>
|
||||
<path android:fillColor="#c7d1d5" android:pathData="M350.5,212.5C349.5,213.8 348.5,215.1 347.5,216.5C338.8,220.9 330.8,226.2 323.5,232.5C318.9,235.1 314.3,237.6 309.5,240C300.3,248.4 290.7,256.1 280.5,263C260,282.5 242.1,304 227,327.5C223.9,333.3 220.2,338.6 216,343.5C212.3,352.7 208.1,361.7 203.5,370.5C202.8,371.3 201.9,372 201,372.5C197.3,382 194.1,391.7 191.5,401.5C187.7,410.3 184.4,419.3 181.5,428.5C183.2,412.5 188.1,397.5 196,383.5C196.7,378.7 197.5,374.1 198.5,369.5C199.4,369.6 200,369.3 200.5,368.5C233.3,303.1 280.9,251.6 343.5,214C345.7,213 348.1,212.5 350.5,212.5Z"/>
|
||||
<path android:fillColor="#0f5969" android:pathData="M349.5,143.5C340.1,149.9 330.1,155.2 319.5,159.5C316.3,162.9 313,166 309.5,169C306.5,171.3 303.2,172.6 299.5,173C298.3,174.2 297.3,175.5 296.5,177C285.1,184.4 273.8,191.7 262.5,199C246.7,213.9 231.1,229.1 215.5,244.5C199.4,263.5 184.2,283.1 170,303.5C153.3,330.6 140,359.6 130,390.5C124.4,413.1 119,435.8 114,458.5C113.8,467.9 112.8,477.3 111,486.5C110.5,502.5 110.3,518.5 110.5,534.5C105.6,408 149,301.1 240.5,214C273,184.7 309.3,161.2 349.5,143.5Z"/>
|
||||
<path android:fillColor="#b5bfc3" android:pathData="M724.5,247.5C729.1,250.8 733.8,254.1 738.5,257.5C741,260.8 744,263.6 747.5,266C756.4,274.7 765,283.5 773.5,292.5C804,327.4 825.6,367.4 838.5,412.5C837.5,412 836.7,411.4 836,410.5C833.5,403.3 831,396.2 828.5,389C820.3,372.7 812.4,356.2 805,339.5C803.9,338.7 802.7,338 801.5,337.5C799.9,333.3 797.6,329.6 794.5,326.5C793.8,326.2 793.2,325.8 792.5,325.5C792.7,324.1 792.5,322.8 792,321.5C785.8,313.3 779.5,304.9 773,296.5C756.6,280.3 740.5,264 724.5,247.5Z"/>
|
||||
<path android:fillColor="#d0dade" android:pathData="M558.5,314.5C570.2,314.3 581.8,314.5 593.5,315C603.6,318.9 605.1,325 598,333.5C597.2,342.4 597.3,351.2 598.5,360C629.8,360.3 661.2,360.7 692.5,361C694.7,361.8 696.2,363.3 697,365.5C697.7,376.5 697.7,387.5 697,398.5C696.2,400 695,401.2 693.5,402C661.6,403.5 629.6,404 597.5,403.5C597.2,411.8 597.5,419.9 598.5,428C623.3,428 647.9,428.7 672.5,430C673.7,430.9 674.5,432.1 675,433.5C675.7,444.2 675.7,454.8 675,465.5C674.2,468.8 672,470.7 668.5,471C638.7,471.2 608.8,471.3 579,471.5C547.8,471.7 516.6,471.2 485.5,470C483.5,468.7 482.3,466.8 482,464.5C481.3,454.5 481.3,444.5 482,434.5C482.3,432.7 483.1,431.2 484.5,430C507.1,428.5 529.7,428 552.5,428.5C552.5,420.2 552.5,411.8 552.5,403.5C525.2,403.7 497.8,403.5 470.5,403C467.2,403.1 465.1,401.6 464,398.5C462,387.2 462,375.8 464,364.5C465.2,363 466.7,361.8 468.5,361C496.5,360.5 524.5,360.3 552.5,360.5C552.1,346.5 552.6,332.5 554,318.5C555.4,316.9 556.9,315.6 558.5,314.5Z"/>
|
||||
<path android:fillColor="#849094" android:pathData="M591.5,317.5C580.5,317.5 569.5,317.5 558.5,317.5C558.8,332.7 558.5,347.7 557.5,362.5C557.5,347.2 557.5,331.8 557.5,316.5C569,316.2 580.3,316.5 591.5,317.5Z"/>
|
||||
<path android:fillColor="#132c33" android:pathData="M591.5,317.5C599,318.8 599.5,321.8 593,326.5C592.5,338.5 592.3,350.5 592.5,362.5C625.8,362.5 659.2,362.5 692.5,362.5C692.5,373.2 692.5,383.8 692.5,394.5C658.8,394.5 625.2,394.5 591.5,394.5C591.2,406.7 591.5,418.7 592.5,430.5C618,431.5 643.7,431.8 669.5,431.5C669.5,441.8 669.5,452.2 669.5,462.5C608.8,462.5 548.2,462.5 487.5,462.5C487.5,451.8 487.5,441.2 487.5,430.5C510.2,430.5 532.8,430.5 555.5,430.5C556.2,430.5 556.8,430.5 557.5,430.5C558.5,418.7 558.8,406.7 558.5,394.5C528.5,394.5 498.5,394.5 468.5,394.5C468.5,383.8 468.5,373.2 468.5,362.5C498.2,362.5 527.8,362.5 557.5,362.5C558.5,347.7 558.8,332.7 558.5,317.5C569.5,317.5 580.5,317.5 591.5,317.5Z"/>
|
||||
<path android:fillColor="#cfdade" android:pathData="M331.5,324.5C366.9,324 402.2,324.5 437.5,326C439.5,327.3 440.7,329.2 441,331.5C441.9,342.2 441.6,352.9 440,363.5C439,365.3 437.5,366.4 435.5,367C399.8,368 364.1,367.6 328.5,366C327.3,365.1 326.5,363.9 326,362.5C325.3,351.5 325.3,340.5 326,329.5C327.4,327.3 329.2,325.6 331.5,324.5Z"/>
|
||||
<path android:fillColor="#b8c1c4" android:pathData="M330.5,331.5C330.2,341 330.5,350.3 331.5,359.5C331.6,360.1 331.9,360.6 332.5,361C349.3,361.2 366.2,361.3 383,361.5C400.2,361.5 417,360.8 433.5,359.5C434.2,359.5 434.8,359.5 435.5,359.5C435.8,359.5 436.2,359.5 436.5,359.5C436.4,361.4 435.4,362.6 433.5,363C399.2,363.7 364.8,363.7 330.5,363C329.9,362.3 329.4,361.4 329,360.5C328.3,351.2 328.3,341.8 329,332.5C329.4,331.9 329.9,331.6 330.5,331.5Z"/>
|
||||
<path android:fillColor="#a3abaf" android:pathData="M331.5,359.5C365.5,359.5 399.5,359.5 433.5,359.5C417,360.8 400.2,361.5 383,361.5C366.2,361.3 349.3,361.2 332.5,361C331.9,360.6 331.6,360.1 331.5,359.5Z"/>
|
||||
<path android:fillColor="#142d34" android:pathData="M330.5,327.5C365.5,327.5 400.5,327.5 435.5,327.5C435.5,338.2 435.5,348.8 435.5,359.5C434.8,359.5 434.2,359.5 433.5,359.5C399.5,359.5 365.5,359.5 331.5,359.5C330.5,350.3 330.2,341 330.5,331.5C330.5,330.2 330.5,328.8 330.5,327.5Z"/>
|
||||
<path android:fillColor="#79868a" android:pathData="M330.5,327.5C365.7,326.5 401,326.2 436.5,326.5C436.5,337.5 436.5,348.5 436.5,359.5C436.2,359.5 435.8,359.5 435.5,359.5C435.5,348.8 435.5,338.2 435.5,327.5C400.5,327.5 365.5,327.5 330.5,327.5Z"/>
|
||||
<path android:fillColor="#0f5a6b" android:pathData="M870.5,325.5C884.8,352.1 895.8,380.1 903.5,409.5C901.5,408.4 900,406.7 899,404.5C897.4,395.3 895.1,386.3 892,377.5C891.1,375.2 889.7,373.2 888,371.5C882.7,355.9 876.8,340.6 870.5,325.5Z"/>
|
||||
<path android:fillColor="#8e979a" android:pathData="M468.5,394.5C498.5,394.5 528.5,394.5 558.5,394.5C558.8,406.7 558.5,418.7 557.5,430.5C558.1,419.1 557.5,407.9 555.5,397C528.2,396.7 500.8,396.3 473.5,396C471.6,395.8 470,395.3 468.5,394.5Z"/>
|
||||
<path android:fillColor="#a6aeb1" android:pathData="M690.5,395.5C690.4,396.1 690.1,396.6 689.5,397C658.2,397.3 626.8,397.7 595.5,398C594.6,399 593.6,399.9 592.5,400.5C592.5,398.8 592.5,397.2 592.5,395.5C625.2,395.5 657.8,395.5 690.5,395.5Z"/>
|
||||
<path android:fillColor="#b4bec1" android:pathData="M468.5,362.5C468.5,373.2 468.5,383.8 468.5,394.5C470,395.3 471.6,395.8 473.5,396C500.8,396.3 528.2,396.7 555.5,397C557.5,407.9 558.1,419.1 557.5,430.5C556.8,430.5 556.2,430.5 555.5,430.5C556.6,420.9 556.8,411.2 556,401.5C555.5,401 555,400.5 554.5,400C526.5,399.7 498.5,399.3 470.5,399C469.7,398.2 468.8,397.3 468,396.5C467.2,385 467.3,373.7 468.5,362.5Z"/>
|
||||
<path android:fillColor="#b9c3c6" android:pathData="M690.5,395.5C691.2,395.5 691.8,395.5 692.5,395.5C691.9,396.5 691.2,397.5 690.5,398.5C658.8,399.3 627.2,399.8 595.5,400C595,400.5 594.5,401 594,401.5C593.2,411.2 593.4,420.9 594.5,430.5C593.8,430.5 593.2,430.5 592.5,430.5C592.5,420.5 592.5,410.5 592.5,400.5C593.6,399.9 594.6,399 595.5,398C626.8,397.7 658.2,397.3 689.5,397C690.1,396.6 690.4,396.1 690.5,395.5Z"/>
|
||||
<path android:fillColor="#515f63" android:pathData="M692.5,394.5C692.5,394.8 692.5,395.2 692.5,395.5C691.8,395.5 691.2,395.5 690.5,395.5C657.8,395.5 625.2,395.5 592.5,395.5C592.5,397.2 592.5,398.8 592.5,400.5C592.5,410.5 592.5,420.5 592.5,430.5C591.5,418.7 591.2,406.7 591.5,394.5C625.2,394.5 658.8,394.5 692.5,394.5Z"/>
|
||||
<path android:fillColor="#ced9dd" android:pathData="M191.5,401.5C182.5,428.8 176.7,456.8 174,485.5C167.8,567.8 188.1,642.5 235,709.5C235.4,712.7 236.2,715.7 237.5,718.5C214.2,688.9 196.9,655.9 185.5,619.5C182.6,606.1 179.5,592.8 176,579.5C174,566 171.8,552.6 169.5,539.5C167.9,501.8 171.9,464.8 181.5,428.5C184.4,419.3 187.7,410.3 191.5,401.5Z"/>
|
||||
<path android:fillColor="#ced8dc" android:pathData="M313.5,384.5C359.2,384.3 404.8,384.5 450.5,385C452,386.2 453.2,387.7 454,389.5C454.7,400.5 454.7,411.5 454,422.5C451.8,425.8 448.6,427.3 444.5,427C400.5,428 356.5,427.6 312.5,426C311,424.8 309.8,423.3 309,421.5C308.3,410.8 308.3,400.2 309,389.5C310,387.3 311.5,385.6 313.5,384.5Z"/>
|
||||
<path android:fillColor="#bbc4c8" android:pathData="M313.5,389.5C313.5,399.2 313.5,408.8 313.5,418.5C314.4,419.3 315.4,420.1 316.5,421C360.2,421.7 403.8,421.7 447.5,421C448.4,420.3 449,419.5 449.5,418.5C449.5,408.8 449.5,399.2 449.5,389.5C450.7,399.7 450.8,410 450,420.5C449.1,421.7 447.9,422.5 446.5,423C402.5,423.7 358.5,423.7 314.5,423C313.8,421.7 313,420.6 312,419.5C311.3,409.8 311.3,400.2 312,390.5C312.4,389.9 312.9,389.6 313.5,389.5Z"/>
|
||||
<path android:fillColor="#9da6a9" android:pathData="M313.5,418.5C358.8,418.5 404.2,418.5 449.5,418.5C449,419.5 448.4,420.3 447.5,421C403.8,421.7 360.2,421.7 316.5,421C315.4,420.1 314.4,419.3 313.5,418.5Z"/>
|
||||
<path android:fillColor="#132c33" android:pathData="M449.5,389.5C449.5,399.2 449.5,408.8 449.5,418.5C404.2,418.5 358.8,418.5 313.5,418.5C313.5,408.8 313.5,399.2 313.5,389.5C313.5,388.5 313.5,387.5 313.5,386.5C358.8,386.5 404.2,386.5 449.5,386.5C449.5,387.5 449.5,388.5 449.5,389.5Z"/>
|
||||
<path android:fillColor="#273b41" android:pathData="M592.5,430.5C593.2,430.5 593.8,430.5 594.5,430.5C620.2,429.8 645.5,430.5 670.5,432.5C670.5,432.8 670.5,433.2 670.5,433.5C670.5,443.2 670.5,452.8 670.5,462.5C670.2,462.5 669.8,462.5 669.5,462.5C669.5,452.2 669.5,441.8 669.5,431.5C643.7,431.8 618,431.5 592.5,430.5Z"/>
|
||||
<path android:fillColor="#b8c4c8" android:pathData="M198.5,369.5C197.5,374.1 196.7,378.7 196,383.5C188.1,397.5 183.2,412.5 181.5,428.5C171.9,464.8 167.9,501.8 169.5,539.5C163.8,480.3 173.5,423.6 198.5,369.5Z"/>
|
||||
<path android:fillColor="#cfdadd" android:pathData="M330.5,445.5C365.8,445.3 401.2,445.5 436.5,446C439.1,447.3 440.6,449.5 441,452.5C441.9,463.2 441.6,473.9 440,484.5C439.2,485.3 438.3,486.2 437.5,487C401.5,487.7 365.5,487.7 329.5,487C327.3,486.3 326.1,484.8 326,482.5C325.3,471.8 325.3,461.2 326,450.5C327,448.3 328.5,446.6 330.5,445.5Z"/>
|
||||
<path android:fillColor="#132c32" android:pathData="M435.5,448.5C435.5,458.8 435.5,469.2 435.5,479.5C400.8,479.5 366.2,479.5 331.5,479.5C331.5,469.2 331.5,458.8 331.5,448.5C366.2,448.5 400.8,448.5 435.5,448.5Z"/>
|
||||
<path android:fillColor="#273b41" android:pathData="M435.5,447.5C435.5,447.8 435.5,448.2 435.5,448.5C400.8,448.5 366.2,448.5 331.5,448.5C331.5,458.8 331.5,469.2 331.5,479.5C330.5,470.7 330.2,461.7 330.5,452.5C330.5,450.8 330.5,449.2 330.5,447.5C365.5,447.5 400.5,447.5 435.5,447.5Z"/>
|
||||
<path android:fillColor="#6b7679" android:pathData="M487.5,430.5C487.5,441.2 487.5,451.8 487.5,462.5C548.2,462.5 608.8,462.5 669.5,462.5C669.8,462.5 670.2,462.5 670.5,462.5C670.2,463.3 669.5,463.7 668.5,463.5C608.5,463.5 548.5,463.5 488.5,463.5C488.2,463.5 487.8,463.5 487.5,463.5C486.5,453 486.2,442.3 486.5,431.5C486.5,430.8 486.8,430.5 487.5,430.5Z"/>
|
||||
<path android:fillColor="#c8d3d6" android:pathData="M486.5,431.5C486.2,442.3 486.5,453 487.5,463.5C487.8,465.8 489.1,466.9 491.5,467C550.8,467.7 610.2,467.7 669.5,467C670.9,465.8 671.7,464.3 672,462.5C672.7,453.2 672.7,443.8 672,434.5C671.6,433.9 671.1,433.6 670.5,433.5C670.5,433.2 670.5,432.8 670.5,432.5C672.4,432.6 673.6,433.6 674,435.5C674.7,445.2 674.7,454.8 674,464.5C672.6,466.1 671.1,467.6 669.5,469C608.8,469.7 548.2,469.7 487.5,469C486.2,467.6 485,466.1 484,464.5C483.3,454.2 483.3,443.8 484,433.5C484.7,432.6 485.5,432 486.5,431.5Z"/>
|
||||
<path android:fillColor="#bac3c7" android:pathData="M670.5,433.5C671.1,433.6 671.6,433.9 672,434.5C672.7,443.8 672.7,453.2 672,462.5C671.7,464.3 670.9,465.8 669.5,467C610.2,467.7 550.8,467.7 491.5,467C489.1,466.9 487.8,465.8 487.5,463.5C487.8,463.5 488.2,463.5 488.5,463.5C488.6,464.1 488.9,464.6 489.5,465C548.8,465.7 608.2,465.7 667.5,465C668.1,464.6 668.4,464.1 668.5,463.5C669.5,463.7 670.2,463.3 670.5,462.5C670.5,452.8 670.5,443.2 670.5,433.5Z"/>
|
||||
<path android:fillColor="#a6aeb1" android:pathData="M488.5,463.5C548.5,463.5 608.5,463.5 668.5,463.5C668.4,464.1 668.1,464.6 667.5,465C608.2,465.7 548.8,465.7 489.5,465C488.9,464.6 488.6,464.1 488.5,463.5Z"/>
|
||||
<path android:fillColor="#0f5b6c" android:pathData="M903.5,411.5C914.1,450.5 918.1,490.2 915.5,530.5C915.7,515.5 915.4,500.5 914.5,485.5C910.8,460.8 907.2,436.2 903.5,411.5Z"/>
|
||||
<path android:fillColor="#9fa8ab" android:pathData="M435.5,447.5C436.4,448.6 436.8,449.9 436.5,451.5C436.8,461.5 436.5,471.5 435.5,481.5C434.7,481.8 434,482.3 433.5,483C399.5,483.7 365.5,483.7 331.5,483C331,482.3 330.3,481.8 329.5,481.5C329.2,471.7 329.5,462 330.5,452.5C330.2,461.7 330.5,470.7 331.5,479.5C366.2,479.5 400.8,479.5 435.5,479.5C435.5,469.2 435.5,458.8 435.5,448.5C435.5,448.2 435.5,447.8 435.5,447.5Z"/>
|
||||
<path android:fillColor="#c2ccd0" android:pathData="M436.5,451.5C437.1,451.6 437.6,451.9 438,452.5C438.7,462.2 438.7,471.8 438,481.5C437.6,482.4 437.1,483.3 436.5,484C418.2,485.1 399.9,485.6 381.5,485.5C364.5,485.2 347.5,484.8 330.5,484.5C329.9,483.6 329.5,482.6 329.5,481.5C330.3,481.8 331,482.3 331.5,483C365.5,483.7 399.5,483.7 433.5,483C434,482.3 434.7,481.8 435.5,481.5C436.5,471.5 436.8,461.5 436.5,451.5Z"/>
|
||||
<path android:fillColor="#142c33" android:pathData="M459.5,485.5C538.8,485.5 618.2,485.5 697.5,485.5C697.5,509.5 697.5,533.5 697.5,557.5C686.2,557.5 674.8,557.5 663.5,557.5C663.5,545.8 663.5,534.2 663.5,522.5C663.5,521.8 663.5,521.2 663.5,520.5C663.5,519.5 663.5,518.5 663.5,517.5C606.8,517.5 550.2,517.5 493.5,517.5C493.5,531.5 493.5,545.5 493.5,559.5C482.2,559.5 470.8,559.5 459.5,559.5C459.5,536.5 459.5,513.5 459.5,490.5C459.5,489.2 459.5,487.8 459.5,486.5C459.5,486.2 459.5,485.8 459.5,485.5Z"/>
|
||||
<path android:fillColor="#8f9a9e" android:pathData="M459.5,485.5C499.2,484.8 539,484.5 579,484.5C619.2,484.5 659,485.2 698.5,486.5C698.8,510.7 698.5,534.7 697.5,558.5C686,558.8 674.7,558.5 663.5,557.5C674.8,557.5 686.2,557.5 697.5,557.5C697.5,533.5 697.5,509.5 697.5,485.5C618.2,485.5 538.8,485.5 459.5,485.5Z"/>
|
||||
<path android:fillColor="#bac6ca" android:pathData="M838.5,412.5C841.3,419.5 843.4,426.9 845,434.5C854.7,478.4 855.8,522.4 848.5,566.5C851.3,530.5 851.1,494.5 848,458.5C846.2,447.9 843.5,437.6 840,427.5C839.5,422.5 839,417.5 838.5,412.5Z"/>
|
||||
<path android:fillColor="#132c32" android:pathData="M331.5,507.5C366.2,507.5 400.8,507.5 435.5,507.5C435.5,517.8 435.5,528.2 435.5,538.5C400.8,538.5 366.2,538.5 331.5,538.5C331.5,528.2 331.5,517.8 331.5,507.5Z"/>
|
||||
<path android:fillColor="#cbd6d9" android:pathData="M459.5,486.5C459.5,487.8 459.5,489.2 459.5,490.5C458.3,514.3 458.2,538.3 459,562.5C459.5,563 460,563.5 460.5,564C471.2,564.7 481.8,564.7 492.5,564C493.3,563.7 494,563.2 494.5,562.5C495.3,549.9 495.8,537.2 496,524.5C496.9,523.3 498.1,522.5 499.5,522C553.2,521.3 606.8,521.3 660.5,522C661,522.5 661.5,523 662,523.5C662.3,535.5 662.7,547.5 663,559.5C663.8,560.3 664.7,561.2 665.5,562C675.2,562.7 684.8,562.7 694.5,562C695.7,561 696.7,559.8 697.5,558.5C698.5,534.7 698.8,510.7 698.5,486.5C699.5,487 700.3,487.6 701,488.5C701.7,511.8 701.7,535.2 701,558.5C700.3,561.3 698.4,562.8 695.5,563C684.5,563.7 673.5,563.7 662.5,563C662,562.5 661.5,562 661,561.5C659.9,549.4 659,537.2 658.5,525C605.2,524.5 551.8,524.3 498.5,524.5C498.7,537.5 498.5,550.5 498,563.5C496.2,566.3 493.7,567.8 490.5,568C479.8,568.9 469.1,568.5 458.5,567C456.3,565.4 454.9,563.2 454.5,560.5C455.3,537.2 455.8,513.9 456,490.5C456.7,488.7 457.8,487.3 459.5,486.5Z"/>
|
||||
<path android:fillColor="#aab3b7" android:pathData="M663.5,520.5C663.5,521.2 663.5,521.8 663.5,522.5C662.7,522.2 662,521.7 661.5,521C605.8,520.2 550.1,520.3 494.5,521.5C494.9,520.1 495.9,519.2 497.5,519C552.2,518.3 606.8,518.3 661.5,519C662,519.7 662.7,520.2 663.5,520.5Z"/>
|
||||
<path android:fillColor="#9ca4a7" android:pathData="M663.5,520.5C662.7,520.2 662,519.7 661.5,519C606.8,518.3 552.2,518.3 497.5,519C495.9,519.2 494.9,520.1 494.5,521.5C494.8,534.3 494.5,547 493.5,559.5C493.5,545.5 493.5,531.5 493.5,517.5C550.2,517.5 606.8,517.5 663.5,517.5C663.5,518.5 663.5,519.5 663.5,520.5Z"/>
|
||||
<path android:fillColor="#132c33" android:pathData="M629.5,538.5C629.9,541.8 628.5,544.1 625.5,545.5C624.2,582 624.2,618.3 625.5,654.5C625.7,658.6 627.7,661.4 631.5,663C641.5,663.7 651.5,663.7 661.5,663C665.1,661.1 667.6,658.3 669,654.5C671.2,646.6 673,638.6 674.5,630.5C675,629.1 675.6,627.8 676.5,626.5C686.4,629.9 696.4,632.9 706.5,635.5C707.1,636.4 707.4,637.4 707.5,638.5C704.9,648.7 702.4,659 700,669.5C698.8,673.6 697.3,677.6 695.5,681.5C691.1,688.5 684.8,692.9 676.5,694.5C655.5,695.5 634.5,695.8 613.5,695.5C600.6,694.3 593.1,687.3 591,674.5C590.5,630.5 590.3,586.5 590.5,542.5C590.5,541.2 590.5,539.8 590.5,538.5C596.1,537.2 602.1,536.5 608.5,536.5C614.5,536.7 620.5,536.8 626.5,537C627.7,537.2 628.7,537.7 629.5,538.5Z"/>
|
||||
<path android:fillColor="#2b3e44" android:pathData="M331.5,507.5C331.5,517.8 331.5,528.2 331.5,538.5C366.2,538.5 400.8,538.5 435.5,538.5C435.2,539.3 434.5,539.7 433.5,539.5C399.8,539.5 366.2,539.5 332.5,539.5C330.5,531.2 329.9,522.6 330.5,513.5C330.5,512.2 330.5,510.8 330.5,509.5C330.3,508.5 330.7,507.8 331.5,507.5Z"/>
|
||||
<path android:fillColor="#b3bcbf" android:pathData="M435.5,507.5C436.2,508.8 437.1,510.1 438,511.5C438.7,521.2 438.7,530.8 438,540.5C437.6,542.4 436.4,543.4 434.5,543.5C400.5,543.7 366.5,543.5 332.5,543C331.7,542.2 330.8,541.3 330,540.5C329.2,531.3 329.3,522.3 330.5,513.5C329.9,522.6 330.5,531.2 332.5,539.5C332.8,540.2 333.1,540.8 333.5,541.5C366.5,542.7 399.5,542.8 432.5,542C433.3,541.3 433.6,540.5 433.5,539.5C434.5,539.7 435.2,539.3 435.5,538.5C435.5,528.2 435.5,517.8 435.5,507.5Z"/>
|
||||
<path android:fillColor="#a5aeb1" android:pathData="M332.5,539.5C366.2,539.5 399.8,539.5 433.5,539.5C433.6,540.5 433.3,541.3 432.5,542C399.5,542.8 366.5,542.7 333.5,541.5C333.1,540.8 332.8,540.2 332.5,539.5Z"/>
|
||||
<path android:fillColor="#c7d1d5" android:pathData="M330.5,509.5C330.5,510.8 330.5,512.2 330.5,513.5C329.3,522.3 329.2,531.3 330,540.5C330.8,541.3 331.7,542.2 332.5,543C366.5,543.5 400.5,543.7 434.5,543.5C417.5,544.8 400.2,545.5 382.5,545.5C365.2,545.3 347.8,545.2 330.5,545C330,544.1 329.3,543.2 328.5,542.5C327.3,532.2 327.2,521.8 328,511.5C328.7,510.6 329.5,510 330.5,509.5Z"/>
|
||||
<path android:fillColor="#cdd8dc" android:pathData="M794.5,326.5C797.6,329.6 799.9,333.3 801.5,337.5C802.7,338 803.9,338.7 805,339.5C812.4,356.2 820.3,372.7 828.5,389C831,396.2 833.5,403.3 836,410.5C836.7,411.4 837.5,412 838.5,412.5C839,417.5 839.5,422.5 840,427.5C843.5,437.6 846.2,447.9 848,458.5C851.1,494.5 851.3,530.5 848.5,566.5C842.5,607.2 829.2,645.2 808.5,680.5C810.4,673.7 812.6,667.1 815,660.5C820.3,649.7 824.9,638.7 829,627.5C862.9,523.2 851.6,424.2 795,330.5C794.5,329.2 794.3,327.9 794.5,326.5Z"/>
|
||||
<path android:fillColor="#b4bdc0" android:pathData="M459.5,490.5C459.5,513.5 459.5,536.5 459.5,559.5C470.8,559.5 482.2,559.5 493.5,559.5C494.5,547 494.8,534.3 494.5,521.5C550.1,520.3 605.8,520.2 661.5,521C662,521.7 662.7,522.2 663.5,522.5C663.5,534.2 663.5,545.8 663.5,557.5C674.7,558.5 686,558.8 697.5,558.5C696.7,559.8 695.7,561 694.5,562C684.8,562.7 675.2,562.7 665.5,562C664.7,561.2 663.8,560.3 663,559.5C662.7,547.5 662.3,535.5 662,523.5C661.5,523 661,522.5 660.5,522C606.8,521.3 553.2,521.3 499.5,522C498.1,522.5 496.9,523.3 496,524.5C495.8,537.2 495.3,549.9 494.5,562.5C494,563.2 493.3,563.7 492.5,564C481.8,564.7 471.2,564.7 460.5,564C460,563.5 459.5,563 459,562.5C458.2,538.3 458.3,514.3 459.5,490.5Z"/>
|
||||
<path android:fillColor="#becacd" android:pathData="M169.5,539.5C171.8,552.6 174,566 176,579.5C179.5,592.8 182.6,606.1 185.5,619.5C176.5,593.7 171.1,567 169.5,539.5Z"/>
|
||||
<path android:fillColor="#ccd7db" android:pathData="M674.5,630.5C673,638.6 671.2,646.6 669,654.5C667.6,658.3 665.1,661.1 661.5,663C651.5,663.7 641.5,663.7 631.5,663C627.7,661.4 625.7,658.6 625.5,654.5C625.3,619.5 625.5,584.5 626,549.5C630.1,546.6 631.3,542.9 629.5,538.5C628.7,537.7 627.7,537.2 626.5,537C620.5,536.8 614.5,536.7 608.5,536.5C602.1,536.5 596.1,537.2 590.5,538.5C589.8,538.8 589.2,539.1 588.5,539.5C587.3,586.2 587.2,632.8 588,679.5C588.6,683.6 590.1,687.2 592.5,690.5C593.3,691.4 594.3,691.7 595.5,691.5C599.4,695.2 604.1,697.7 609.5,699C631.2,699.7 652.8,699.7 674.5,699C678.4,697.5 682.4,696.4 686.5,695.5C691,693.2 694.3,689.7 696.5,685C697.8,684.7 698.9,684.2 700,683.5C702.3,679.1 703.9,674.4 705,669.5C705.5,659.1 706.3,648.7 707.5,638.5C707.4,637.4 707.1,636.4 706.5,635.5C708,636.2 708.9,637.6 709,639.5C710.4,656.8 707.1,673.1 699,688.5C693.4,696.1 685.9,701 676.5,703C653.8,703.7 631.2,703.7 608.5,703C595.4,699.9 587.9,691.8 586,678.5C585.3,631.8 585.3,585.2 586,538.5C587.1,536.2 588.9,534.7 591.5,534C602.5,533.3 613.5,533.3 624.5,534C630.5,534.3 633.8,537.3 634.5,543C633.9,546.9 632.4,550.4 630,553.5C629,588.5 629.4,623.5 631,658.5C632.5,660 634.3,660.8 636.5,661C644.6,661.9 652.6,661.6 660.5,660C667.7,652.6 671.7,643.8 672.5,633.5C672.6,632 673.3,631 674.5,630.5Z"/>
|
||||
<path android:fillColor="#aeb7ba" android:pathData="M590.5,542.5C590.3,586.5 590.5,630.5 591,674.5C593.1,687.3 600.6,694.3 613.5,695.5C633.7,696.7 654,696.8 674.5,696C675.4,695.7 676.1,695.2 676.5,694.5C684.8,692.9 691.1,688.5 695.5,681.5C694,687.1 691,691.8 686.5,695.5C682.4,696.4 678.4,697.5 674.5,699C652.8,699.7 631.2,699.7 609.5,699C604.1,697.7 599.4,695.2 595.5,691.5C594.5,687.4 592.7,683.8 590,680.5C589.2,634.3 589.3,588.3 590.5,542.5Z"/>
|
||||
<path android:fillColor="#838d90" android:pathData="M676.5,694.5C676.1,695.2 675.4,695.7 674.5,696C654,696.8 633.7,696.7 613.5,695.5C634.5,695.8 655.5,695.5 676.5,694.5Z"/>
|
||||
<path android:fillColor="#bdc7ca" android:pathData="M707.5,638.5C706.3,648.7 705.5,659.1 705,669.5C703.9,674.4 702.3,679.1 700,683.5C698.9,684.2 697.8,684.7 696.5,685C694.3,689.7 691,693.2 686.5,695.5C691,691.8 694,687.1 695.5,681.5C697.3,677.6 698.8,673.6 700,669.5C702.4,659 704.9,648.7 707.5,638.5Z"/>
|
||||
<path android:fillColor="#aeb7bb" android:pathData="M629.5,538.5C631.3,542.9 630.1,546.6 626,549.5C625.5,584.5 625.3,619.5 625.5,654.5C624.2,618.3 624.2,582 625.5,545.5C628.5,544.1 629.9,541.8 629.5,538.5Z"/>
|
||||
<path android:fillColor="#c3cdd1" android:pathData="M590.5,538.5C590.5,539.8 590.5,541.2 590.5,542.5C589.3,588.3 589.2,634.3 590,680.5C592.7,683.8 594.5,687.4 595.5,691.5C594.3,691.7 593.3,691.4 592.5,690.5C590.1,687.2 588.6,683.6 588,679.5C587.2,632.8 587.3,586.2 588.5,539.5C589.2,539.1 589.8,538.8 590.5,538.5Z"/>
|
||||
<path android:fillColor="#cad6db" android:pathData="M808.5,680.5C796.6,702.3 782.3,722.3 765.5,740.5C765.5,738.8 765.5,737.2 765.5,735.5C778.8,720.7 790.6,704.3 801,686.5C803.9,684.7 806.4,682.7 808.5,680.5Z"/>
|
||||
<path android:fillColor="#d0dbdf" android:pathData="M474.5,666.5C466.2,669.7 457.6,671.4 448.5,671.5C447.8,671.9 447.3,672.6 447,673.5C446.7,673.2 446.3,672.8 446,672.5C445.7,640.8 445.3,609.2 445,577.5C444.8,576.3 444.3,575.3 443.5,574.5C443.5,572.8 443.5,571.2 443.5,569.5C403.3,569.2 363.3,569.5 323.5,570.5C323.5,571.5 323.5,572.5 323.5,573.5C322.9,573.6 322.4,573.9 322,574.5C321.3,619.5 321.3,664.5 322,709.5C322.8,710.3 323.7,711.2 324.5,712C333.8,714 343.2,714 352.5,712C353.9,710.8 354.7,709.3 355,707.5C355.3,703.8 355.7,700.2 356,696.5C356.4,695.6 356.9,694.8 357.5,694C374.2,693.3 390.8,693.3 407.5,694C408.4,694.4 409.3,694.9 410,695.5C409.7,700.1 411.2,703.9 414.5,707C423.2,707.7 431.8,707.7 440.5,707C442.3,706.2 443.8,705 445,703.5C446,697.9 446.9,692.2 447.5,686.5C448.7,687.3 449.7,688.3 450.5,689.5C452.8,693.9 454.6,698.5 456,703.5C459.1,710.8 464.6,713.3 472.5,711C481.2,708.8 489.2,705.3 496.5,700.5C502.5,698 507.8,694.5 512.5,690C513.8,689.5 515.1,689.3 516.5,689.5C518.3,688.9 519.9,688.1 521.5,687C527.7,679.8 534.2,673 541,666.5C552.5,646.5 558.9,624.8 560,601.5C560.3,584.5 560.7,567.5 561,550.5C563.7,548.3 564.8,545.5 564.5,542C564.4,540 563.8,538.5 562.5,537.5C560.9,537 559.2,536.6 557.5,536.5C546.3,535.5 535,535.2 523.5,535.5C523.5,536.5 523.5,537.5 523.5,538.5C522.2,539.1 521.4,540.1 521,541.5C520.8,565.5 520.2,589.5 519,613.5C517.2,619.1 515.7,624.8 514.5,630.5C508.7,644 499.4,654.3 486.5,661.5C486.6,660.5 486.3,659.7 485.5,659C499.8,650.4 509.3,637.9 514,621.5C515.8,614.9 517.2,608.3 518,601.5C518.3,582.2 518.7,562.8 519,543.5C518.8,538.6 521,535.5 525.5,534C536.5,533.3 547.5,533.3 558.5,534C569.5,537.3 571.3,543.5 564,552.5C563.9,572.5 563.2,592.5 562,612.5C554.2,670.8 521.4,705.1 463.5,715.5C461,715.2 458.8,714.2 457,712.5C454.1,706.2 451.5,699.9 449,693.5C448.2,697.1 447.6,700.8 447,704.5C445.2,707.3 442.7,708.8 439.5,709C430.4,709.8 421.4,709.5 412.5,708C411.7,707.2 410.8,706.3 410,705.5C409.8,702.2 409.3,699.1 408.5,696C391.8,695.3 375.2,695.3 358.5,696C357.7,701.1 357.2,706.3 357,711.5C356.5,713.3 355.3,714.5 353.5,715C343.5,715.7 333.5,715.7 323.5,715C322.1,714.5 320.9,713.7 320,712.5C318.3,666.3 318,619.9 319,573.5C319.9,570.2 322.1,568.4 325.5,568C365.2,567 404.9,567.4 444.5,569C445.7,569.9 446.5,571.1 447,572.5C447.5,605.2 447.7,637.8 447.5,670.5C456.5,669.2 465.5,667.8 474.5,666.5Z"/>
|
||||
<path android:fillColor="#c5d0d3" android:pathData="M323.5,573.5C323.5,574.2 323.5,574.8 323.5,575.5C322.5,619.7 322.2,664 322.5,708.5C331.6,710.5 341,711 350.5,710C351.4,709.3 352,708.5 352.5,707.5C353.5,703 353.8,698.4 353.5,693.5C354.3,693.2 355,692.7 355.5,692C373.5,691.3 391.5,691.3 409.5,692C410,692.7 410.7,693.2 411.5,693.5C412.2,696.3 412.8,699 413.5,701.5C413.8,701.5 414.2,701.5 414.5,701.5C414.4,702.5 414.7,703.3 415.5,704C424.9,704.9 434.2,704.6 443.5,703C444.1,693.9 444.9,684.7 446,675.5C446.9,677.7 448,679.7 449.5,681.5C450.5,684.1 450.8,686.8 450.5,689.5C449.7,688.3 448.7,687.3 447.5,686.5C446.9,692.2 446,697.9 445,703.5C443.8,705 442.3,706.2 440.5,707C431.8,707.7 423.2,707.7 414.5,707C411.2,703.9 409.7,700.1 410,695.5C409.3,694.9 408.4,694.4 407.5,694C390.8,693.3 374.2,693.3 357.5,694C356.9,694.8 356.4,695.6 356,696.5C355.7,700.2 355.3,703.8 355,707.5C354.7,709.3 353.9,710.8 352.5,712C343.2,714 333.8,714 324.5,712C323.7,711.2 322.8,710.3 322,709.5C321.3,664.5 321.3,619.5 322,574.5C322.4,573.9 322.9,573.6 323.5,573.5Z"/>
|
||||
<path android:fillColor="#737e82" android:pathData="M413.5,687.5C414.5,691.6 414.8,696 414.5,700.5C423.8,700.5 433.2,700.5 442.5,700.5C433.3,701.5 424,701.8 414.5,701.5C414.2,701.5 413.8,701.5 413.5,701.5C413.5,698.5 413.5,695.5 413.5,692.5C413.5,690.8 413.5,689.2 413.5,687.5Z"/>
|
||||
<path android:fillColor="#b6bfc2" android:pathData="M413.5,692.5C413.5,695.5 413.5,698.5 413.5,701.5C412.8,699 412.2,696.3 411.5,693.5C410.7,693.2 410,692.7 409.5,692C391.5,691.3 373.5,691.3 355.5,692C355,692.7 354.3,693.2 353.5,693.5C353.5,692.2 353.5,690.8 353.5,689.5C372.2,689.3 390.8,689.5 409.5,690C411,690.7 412.3,691.5 413.5,692.5Z"/>
|
||||
<path android:fillColor="#a2aaae" android:pathData="M413.5,687.5C413.5,689.2 413.5,690.8 413.5,692.5C412.3,691.5 411,690.7 409.5,690C390.8,689.5 372.2,689.3 353.5,689.5C353.5,690.8 353.5,692.2 353.5,693.5C353.8,698.4 353.5,703 352.5,707.5C352.5,700.8 352.5,694.2 352.5,687.5C372.8,687.5 393.2,687.5 413.5,687.5Z"/>
|
||||
<path android:fillColor="#a5aeb1" android:pathData="M547.5,645.5C546.6,651.6 544.5,657.3 541,662.5C533.3,672 525.1,681 516.5,689.5C515.1,689.3 513.8,689.5 512.5,690C507.8,694.5 502.5,698 496.5,700.5C486.3,703.7 475.9,706.7 465.5,709.5C461.2,709.8 459.2,707.8 459.5,703.5C460.6,704.6 461.4,705.9 462,707.5C490.8,703.1 514.8,690.1 534,668.5C539.2,661.2 543.7,653.5 547.5,645.5Z"/>
|
||||
<path android:fillColor="#b8c2c6" android:pathData="M562.5,537.5C563.8,538.5 564.4,540 564.5,542C564.8,545.5 563.7,548.3 561,550.5C560.7,567.5 560.3,584.5 560,601.5C558.9,624.8 552.5,646.5 541,666.5C534.2,673 527.7,679.8 521.5,687C519.9,688.1 518.3,688.9 516.5,689.5C525.1,681 533.3,672 541,662.5C544.5,657.3 546.6,651.6 547.5,645.5C553,630.3 556.5,614.6 558,598.5C558.3,580.8 558.7,563.2 559,545.5C562.5,543.6 563.7,540.9 562.5,537.5Z"/>
|
||||
<path android:fillColor="#b4bdc0" android:pathData="M323.5,575.5C323.5,619.5 323.5,663.5 323.5,707.5C333.2,707.5 342.8,707.5 352.5,707.5C352,708.5 351.4,709.3 350.5,710C341,711 331.6,710.5 322.5,708.5C322.2,664 322.5,619.7 323.5,575.5Z"/>
|
||||
<path android:fillColor="#bcc5c9" android:pathData="M443.5,574.5C444.3,575.3 444.8,576.3 445,577.5C445.3,609.2 445.7,640.8 446,672.5C446.3,672.8 446.7,673.2 447,673.5C447.3,672.6 447.8,671.9 448.5,671.5C452.2,682.2 455.8,692.8 459.5,703.5C459.2,707.8 461.2,709.8 465.5,709.5C475.9,706.7 486.3,703.7 496.5,700.5C489.2,705.3 481.2,708.8 472.5,711C464.6,713.3 459.1,710.8 456,703.5C454.6,698.5 452.8,693.9 450.5,689.5C450.8,686.8 450.5,684.1 449.5,681.5C448,679.7 446.9,677.7 446,675.5C444.9,684.7 444.1,693.9 443.5,703C434.2,704.6 424.9,704.9 415.5,704C414.7,703.3 414.4,702.5 414.5,701.5C424,701.8 433.3,701.5 442.5,700.5C443.5,658.7 443.8,616.7 443.5,574.5Z"/>
|
||||
<path android:fillColor="#8e999d" android:pathData="M323.5,570.5C363.3,569.5 403.3,569.2 443.5,569.5C443.5,571.2 443.5,572.8 443.5,574.5C443.8,616.7 443.5,658.7 442.5,700.5C442.5,657.2 442.5,613.8 442.5,570.5C402.8,570.5 363.2,570.5 323.5,570.5Z"/>
|
||||
<path android:fillColor="#142c33" android:pathData="M323.5,570.5C363.2,570.5 402.8,570.5 442.5,570.5C442.5,613.8 442.5,657.2 442.5,700.5C433.2,700.5 423.8,700.5 414.5,700.5C414.8,696 414.5,691.6 413.5,687.5C393.2,687.5 372.8,687.5 352.5,687.5C352.5,694.2 352.5,700.8 352.5,707.5C342.8,707.5 333.2,707.5 323.5,707.5C323.5,663.5 323.5,619.5 323.5,575.5C323.5,574.8 323.5,574.2 323.5,573.5C323.5,572.5 323.5,571.5 323.5,570.5Z"/>
|
||||
<path android:fillColor="#728084" android:pathData="M413.5,597.5C414.5,618.3 414.8,639.3 414.5,660.5C393.7,660.8 373,660.5 352.5,659.5C352.8,659.5 353.2,659.5 353.5,659.5C354.2,659.5 354.8,659.5 355.5,659.5C374.5,659.5 393.5,659.5 412.5,659.5C413.5,640 413.8,620.3 413.5,600.5C413.5,599.5 413.5,598.5 413.5,597.5Z"/>
|
||||
<path android:fillColor="#bcc6ca" android:pathData="M413.5,600.5C413.8,620.3 413.5,640 412.5,659.5C412.8,640.8 412.5,622.2 411.5,603.5C393.2,602.3 374.9,602.2 356.5,603C356,603.5 355.5,604 355,604.5C354.2,622.9 354.3,641.2 355.5,659.5C354.8,659.5 354.2,659.5 353.5,659.5C353.2,639.9 353.5,620.4 354.5,601C364.4,599.9 374.4,599.4 384.5,599.5C394.2,599.5 403.9,599.9 413.5,600.5Z"/>
|
||||
<path android:fillColor="#cfdade" android:pathData="M412.5,659.5C393.5,659.5 374.5,659.5 355.5,659.5C354.3,641.2 354.2,622.9 355,604.5C355.5,604 356,603.5 356.5,603C374.9,602.2 393.2,602.3 411.5,603.5C412.5,622.2 412.8,640.8 412.5,659.5Z"/>
|
||||
<path android:fillColor="#d7e2e6" android:pathData="M358.5,605.5C375.2,605.5 391.8,605.5 408.5,605.5C408.5,622.8 408.5,640.2 408.5,657.5C391.6,657.8 374.8,657.5 358,656.5C357.2,639.4 357.3,622.4 358.5,605.5Z"/>
|
||||
<path android:fillColor="#a0a8ab" android:pathData="M413.5,597.5C413.5,598.5 413.5,599.5 413.5,600.5C403.9,599.9 394.2,599.5 384.5,599.5C374.4,599.4 364.4,599.9 354.5,601C353.5,620.4 353.2,639.9 353.5,659.5C353.2,659.5 352.8,659.5 352.5,659.5C352.5,638.8 352.5,618.2 352.5,597.5C372.8,597.5 393.2,597.5 413.5,597.5Z"/>
|
||||
<path android:fillColor="#8e999d" android:pathData="M557.5,536.5C546.5,536.5 535.5,536.5 524.5,536.5C524.8,555 524.5,573.3 523.5,591.5C523.5,573.8 523.5,556.2 523.5,538.5C523.5,537.5 523.5,536.5 523.5,535.5C535,535.2 546.3,535.5 557.5,536.5Z"/>
|
||||
<path android:fillColor="#c4ced2" android:pathData="M523.5,538.5C523.5,556.2 523.5,573.8 523.5,591.5C523.6,605.4 520.6,618.4 514.5,630.5C515.7,624.8 517.2,619.1 519,613.5C520.2,589.5 520.8,565.5 521,541.5C521.4,540.1 522.2,539.1 523.5,538.5Z"/>
|
||||
<path android:fillColor="#142c33" android:pathData="M557.5,536.5C559.2,536.6 560.9,537 562.5,537.5C563.7,540.9 562.5,543.6 559,545.5C558.7,563.2 558.3,580.8 558,598.5C556.5,614.6 553,630.3 547.5,645.5C543.7,653.5 539.2,661.2 534,668.5C514.8,690.1 490.8,703.1 462,707.5C461.4,705.9 460.6,704.6 459.5,703.5C455.8,692.8 452.2,682.2 448.5,671.5C457.6,671.4 466.2,669.7 474.5,666.5C478.9,665.6 482.9,663.9 486.5,661.5C499.4,654.3 508.7,644 514.5,630.5C520.6,618.4 523.6,605.4 523.5,591.5C524.5,573.3 524.8,555 524.5,536.5C535.5,536.5 546.5,536.5 557.5,536.5Z"/>
|
||||
<path android:fillColor="#cad8dc" android:pathData="M601.5,841.5C553.1,855.4 504.1,858.1 454.5,849.5C488.9,853.4 523.2,853.2 557.5,849C572.2,846.4 586.8,843.9 601.5,841.5Z"/>
|
||||
</vector>
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
</inset>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
This file was created by '@expo/configure-splash-screen' and some of it's content shouldn't be modified by hand
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
</layer-list>
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/splashscreen_background"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:scaleType="centerCrop"
|
||||
android:navigationBarColor="#333333"
|
||||
tools:context=".MainActivity">
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottie"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="130dp"
|
||||
app:lottie_rawRes="@raw/loading"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="false"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 874 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<color name="iconBackground">#FFFFFF</color>
|
||||
<color name="splashscreen_background">#1F2024</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#202125</color>
|
||||
<color name="navigationBarColor">#202125</color>
|
||||
<color name="notification_icon_color">#00adb5</color>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<string name="app_name">LNReader</string>
|
||||
</resources>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml
|
||||
version="1.0"
|
||||
encoding="UTF-8"
|
||||
standalone="yes"
|
||||
?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="colorControlActivated">#2596be</item>
|
||||
</style>
|
||||
|
||||
<style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<!-- Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually -->
|
||||
<!-- Customize your splash screen theme here -->
|
||||
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/colorPrimaryDark</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s" >@drawable/invisible</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="s">1000</item>
|
||||
<!-- <item name="icon">@drawable/no_icon</item> -->
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
||||
* flavor of it so it's empty as we don't want to load Flipper.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
// Do nothing as we don't want to initialize Flipper on Release.
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "33.0.0"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
kotlinVersion = "1.8.10"
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
ndkVersion = "23.1.7779620"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=false
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
expo.webp.enabled=false
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
@@ -1,6 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -1,244 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -1,92 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,9 +0,0 @@
|
||||
rootProject.name = 'LNReader'
|
||||
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
|
||||
useExpoModules()
|
||||
@@ -1,10 +1,94 @@
|
||||
{
|
||||
"expo": {
|
||||
"version": "2.1.4",
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
"icon": "./assets/native/ic_launcher.png",
|
||||
"plugins": [
|
||||
"expo-localization"
|
||||
]
|
||||
"./plugins/withAndroidCustomizations",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/native/ic_launcher_round.png",
|
||||
"backgroundColor": "#1D1B20",
|
||||
"resizeMode": "contain",
|
||||
"dark": {
|
||||
"image": "./assets/native/ic_launcher_round.png",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"android": {
|
||||
"image": "./assets/native/splash_icon.png",
|
||||
"backgroundColor": "#132C33",
|
||||
"imageWidth": 288,
|
||||
"resizeMode": "native",
|
||||
"dark": {
|
||||
"image": "./assets/native/splash_icon.png",
|
||||
"backgroundColor": "#132C33"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-localization",
|
||||
[
|
||||
"react-native-edge-to-edge",
|
||||
{
|
||||
"android": {
|
||||
"enforceNavigationBarContrast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-web-browser",
|
||||
"./plugins/withReaderAssets",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"usesCleartextTraffic": true,
|
||||
"manifestApplicationAttributes": {
|
||||
"android:largeHeap": "true",
|
||||
"android:allowBackup": "true"
|
||||
},
|
||||
"enableMinifyInReleaseBuilds": true,
|
||||
"enableShrinkResourcesInReleaseBuilds": true
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"android": {
|
||||
"versionCode": 4196356,
|
||||
"package": "com.rajarsheechatterjee.LNReader",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/native/ic_launcher_foreground.png",
|
||||
"backgroundImage": "./assets/native/ic_launcher_background.png",
|
||||
"monochromeImage": "./assets/native/ic_launcher_monochrome.png"
|
||||
},
|
||||
"permissions": [
|
||||
"android.permission.DOWNLOAD_WITHOUT_NOTIFICATION",
|
||||
"android.permission.WAKE_LOCK",
|
||||
"android.permission.FOREGROUND_SERVICE",
|
||||
"android.permission.FOREGROUND_SERVICE_DATA_SYNC",
|
||||
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
|
||||
"android.permission.POST_NOTIFICATIONS"
|
||||
],
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "SEND",
|
||||
"category": ["DEFAULT"],
|
||||
"data": [{ "mimeType": "text/plain" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "3a63596f-e80c-4342-96b7-a8ac48910489"
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.rajarsheechatterjee.LNReader",
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": ["audio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 672 B |