Compare commits

...

74 Commits
2.11.1 ... 4.0

Author SHA1 Message Date
tom5079
f2a2656837 Merge pull request #20 from tom5079/development
Development
2019-11-02 20:30:04 +09:00
tom5079
2011572270 Merge remote-tracking branch 'origin/development' into development 2019-11-02 20:29:24 +09:00
tom5079
3b682667e1 Fixed bug caused by updated hitomi server structure
Version 4.0
2019-11-02 20:25:03 +09:00
tom5079
6da8de6463 Merge pull request #19 from tom5079/master
merge readme
2019-11-02 20:08:28 +09:00
tom5079
039d415871 Update README.md
r/engrish
2019-08-31 23:59:41 +09:00
tom5079
776f53bde0 Update README.md 2019-08-30 22:28:07 +09:00
tom5079
58e535595e Update README.md 2019-08-30 22:27:32 +09:00
tom5079
96ad5f6a6c Update README.md 2019-08-30 22:27:08 +09:00
tom5079
043f7bedd8 Added quick download/delete 2019-08-30 15:24:51 +09:00
tom5079
69bcd8f7c0 Merge pull request #17 from tom5079/development
Version 3.2
2019-08-29 12:11:22 +09:00
tom5079
8a58564812 Version 3.2 2019-08-29 12:10:51 +09:00
tom5079
d346cf431f Merge conflicts 2019-08-29 11:47:01 +09:00
tom5079
c0bce4f3b1 Merge conflicts 2019-08-29 11:43:20 +09:00
tom5079
94d258ddbb Merge conflicts 2019-08-29 11:42:31 +09:00
tom5079
6bdba49284 added missing file 2019-08-29 11:41:08 +09:00
tom5079
9b99baf4bc Added missing files 2019-08-29 11:39:01 +09:00
tom5079
5ad2a538bc Merge pull request #16 from tom5079/master
merge to development branch
2019-08-29 11:29:27 +09:00
tom5079
28703e9bf2 added missing file 2019-08-29 11:25:08 +09:00
tom5079
e664efefe9 Fixed image broken after download finishes 2019-08-28 09:21:36 +09:00
tom5079
27a8694938 Fixed not moving cached gallery to download folder 2019-08-28 09:01:39 +09:00
tom5079
e0a6102d4d Fixed viewer flickering
Added moving with volume button
Added nomedia
Added help link to error snackbar
2019-08-28 08:56:29 +09:00
tom5079
7106cf04ed Delete google-services.json 2019-08-25 09:52:47 +09:00
tom5079
2afdc5591a Added gallery details
Added dark mode
2019-07-21 20:51:50 +09:00
tom5079
8eed4b67c3 Added gallery details
Added dark mode
2019-07-21 20:51:50 +09:00
tom5079
edacef0f2b Update ignore feature added
Bug fixed
2019-07-14 10:53:42 +09:00
tom5079
d28894f8cd Update ignore feature added
Bug fixed
2019-07-14 10:53:42 +09:00
tom5079
ee8e921e1a Image loading optimized
Adds gallery to history when opened directly by gallery ID
Fixed blurred image
2019-07-11 21:24:25 +09:00
tom5079
480d4b1e9a Image loading optimized
Adds gallery to history when opened directly by gallery ID
Fixed blurred image
2019-07-11 21:24:25 +09:00
tom5079
a79c023220 Fixed invisible favorite tag bug 2019-07-07 17:30:03 +09:00
tom5079
efc50df243 Fixed invisible favorite tag bug 2019-07-07 17:30:03 +09:00
tom5079
905ea766b1 App crash fix 2019-07-07 16:36:42 +09:00
tom5079
bce26f4557 App crash fix 2019-07-07 16:36:42 +09:00
tom5079
474d3ad80a Merge pull request #15 from tom5079/development
Version fix
2019-07-07 15:26:57 +09:00
tom5079
a74b2c9b49 Version fix 2019-07-07 15:26:22 +09:00
tom5079
22bdf61bb3 Version fix 2019-07-07 15:26:22 +09:00
tom5079
69f9b099b7 Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:25:54 +09:00
tom5079
1d812487a6 Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:25:54 +09:00
tom5079
7c2bf8fb9d Version fix 2019-07-07 15:25:36 +09:00
tom5079
dfb78bed69 Version fix 2019-07-07 15:25:36 +09:00
tom5079
fb42b48880 Merge pull request #14 from tom5079/development
Version 3.0
2019-07-07 15:25:02 +09:00
tom5079
bb0988a188 Merge branch 'master' into development 2019-07-07 15:24:53 +09:00
tom5079
c64b6f112b Merge branch 'master' into development 2019-07-07 15:24:53 +09:00
tom5079
9ac7fb490e Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:23:42 +09:00
tom5079
bd88a8a8d3 Merge remote-tracking branch 'origin/development' into development 2019-07-07 15:23:42 +09:00
tom5079
1eb75acb40 UI update
Added sort by popularity functionality
Added auto update
2019-07-07 15:21:56 +09:00
tom5079
5ccc96aeb9 UI update
Added sort by popularity functionality
Added auto update
2019-07-07 15:21:56 +09:00
tom5079
ef72d10344 Create README.md 2019-07-03 20:49:34 +09:00
tom5079
573f0b40d1 Create LICENSE 2019-07-03 20:49:13 +09:00
tom5079
48f49edb19 Update LICENSE 2019-07-03 20:46:43 +09:00
tom5079
aa22d9fdd8 Create LICENSE 2019-07-03 20:45:56 +09:00
tom5079
8410a2fdb3 Update LICENSE 2019-07-03 20:44:57 +09:00
tom5079
ec98e4e9a4 Update LICENSE 2019-07-03 20:44:57 +09:00
tom5079
dca6ba457b Added license 2019-07-03 20:44:10 +09:00
tom5079
5b10a781a6 Added license 2019-07-03 20:44:10 +09:00
tom5079
b103188faf Merge pull request #13 from tom5079/master
updated license
2019-07-03 20:07:21 +09:00
tom5079
29637b234c Merge pull request #13 from tom5079/master
updated license
2019-07-03 20:07:21 +09:00
tom5079
34dc238ef1 Create LICENSE 2019-07-03 19:49:40 +09:00
tom5079
3c2675e650 Create README.md 2019-07-03 19:47:13 +09:00
tom5079
7e87bb6838 Search algorithm improved
Language settings in default tag fixed
2019-07-03 19:40:19 +09:00
tom5079
3992a07340 Search algorithm improved
Language settings in default tag fixed
2019-07-03 19:40:19 +09:00
tom5079
bd4b61d7ac Utilizing Glide
Fixed Reader FAB icon
Changed to use gallery id instead of galleryblock to open Reader
2019-06-30 22:04:35 +09:00
tom5079
2046d87031 Utilizing Glide
Fixed Reader FAB icon
Changed to use gallery id instead of galleryblock to open Reader
2019-06-30 22:04:35 +09:00
tom5079
0618d8c6f8 Merge pull request #12 from tom5079/development
Version 2.11.1
2019-06-23 23:33:23 +09:00
tom5079
5bfc27835b Fixed app icon
Version 2.11.1
2019-06-23 23:32:38 +09:00
tom5079
cdc545ea32 Fixed bug for older devices
Hoping that the viewer crashing bug is fixed
Version 2.11
2019-06-23 23:09:01 +09:00
tom5079
449db97a2b Merge pull request #11 from tom5079/development
Pupil v2.10
2019-06-23 16:17:49 +09:00
tom5079
e01380090d Added lock 2019-06-23 10:27:07 +09:00
tom5079
6d1505241e Migrate to Android 29
Re-Added Cache clear to prevent deleting downloading images
2019-06-23 00:23:17 +09:00
tom5079
f303e49e97 Memory optimization 2019-06-14 20:01:32 +09:00
tom5079
0e6b50e302 Merge pull request #10 from tom5079/development
Version 2.9
2019-06-13 22:08:57 +09:00
tom5079
868af1e6a2 Changed to non-scrolling horizontal image view 2019-06-13 21:45:31 +09:00
tom5079
34f7b111ee Bug fix 2019-06-13 21:29:57 +09:00
tom5079
df27907c57 Bug fix 2019-06-13 21:18:26 +09:00
tom5079
75583b9e65 Added URL support
Added Firebase
Added progressbar to the full screen horizontal reader view
2019-06-13 21:05:52 +09:00
98 changed files with 2785 additions and 1038 deletions

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@
#Github pages #Github pages
/gh-pages /gh-pages
#Private files
/app/google-services.json

View File

@@ -1,18 +1,12 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<AndroidXmlCodeStyleSettings> <AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" /> <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings> </AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<XML>
<option name="XML_KEEP_LINE_BREAKS" value="false" />
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</XML>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions> </indentOptions>
@@ -23,6 +17,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:android</NAME> <NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -33,6 +28,7 @@
<match> <match>
<AND> <AND>
<NAME>xmlns:.*</NAME> <NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -44,6 +40,7 @@
<match> <match>
<AND> <AND>
<NAME>.*:id</NAME> <NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -54,6 +51,7 @@
<match> <match>
<AND> <AND>
<NAME>.*:name</NAME> <NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -64,6 +62,7 @@
<match> <match>
<AND> <AND>
<NAME>name</NAME> <NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -74,6 +73,7 @@
<match> <match>
<AND> <AND>
<NAME>style</NAME> <NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -84,6 +84,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE> <XML_NAMESPACE>^$</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -95,6 +96,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND> </AND>
</match> </match>
@@ -106,6 +108,7 @@
<match> <match>
<AND> <AND>
<NAME>.*</NAME> <NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE> <XML_NAMESPACE>.*</XML_NAMESPACE>
</AND> </AND>
</match> </match>

6
.idea/copyright/Apache.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Copyright &amp;#36;today.year tom5079&#10;&#10; Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10; you may not use this file except in compliance with the License.&#10; You may obtain a copy of the License at&#10;&#10; http://www.apache.org/licenses/LICENSE-2.0&#10;&#10; Unless required by applicable law or agreed to in writing, software&#10; distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10; See the License for the specific language governing permissions and&#10; limitations under the License." />
<option name="myName" value="Apache" />
</copyright>
</component>

6
.idea/copyright/GPL.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value=" Pupil, Hitomi.la viewer for Android&#10; Copyright (C) &amp;#36;today.year tom5079&#10;&#10; This program is free software: you can redistribute it and/or modify&#10; it under the terms of the GNU General Public License as published by&#10; the Free Software Foundation, either version 3 of the License, or&#10; (at your option) any later version.&#10;&#10; This program is distributed in the hope that it will be useful,&#10; but WITHOUT ANY WARRANTY; without even the implied warranty of&#10; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10; GNU General Public License for more details.&#10;&#10; You should have received a copy of the GNU General Public License&#10; along with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;." />
<option name="myName" value="GPL" />
</copyright>
</component>

8
.idea/copyright/profiles_settings.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="Pupil" copyright="GPL" />
<element module="libpupil" copyright="Apache" />
</module2copyright>
</settings>
</component>

1
.idea/gradle.xml generated
View File

@@ -13,7 +13,6 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -1,10 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

5
.idea/misc.xml generated
View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component> </component>
</project> </project>

3
.idea/scopes/Pupil.xml generated Normal file
View File

@@ -0,0 +1,3 @@
<component name="DependencyValidationManager">
<scope name="Pupil" pattern="file[app]:*/" />
</component>

3
.idea/scopes/libpupil.xml generated Normal file
View File

@@ -0,0 +1,3 @@
<component name="DependencyValidationManager">
<scope name="libpupil" pattern="file[libpupil]:*/" />
</component>

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -1,2 +1,27 @@
# Pupil # Pupil
Hitomi.la viewer for Android
![Banner](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/pupil-banner.png?raw=true)
*Pupil, Hitomi.la viewer for Android*
# Screenshot
![Main Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/main-screenshot.png?raw=true)
*Main Screen*
![Reader Screen](https://github.com/tom5079/Pupil/blob/gh-pages/assets/images/reader-screenshot.png?raw=true)
*Reader Screen*
Images are censored to be SFW
# Installation
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
or Build app yourself
# Manual
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
# Contribution
Any kind of contribution is appriciated. Feel free to leave PR!

View File

@@ -1,5 +1,6 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
@@ -12,8 +13,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 20 versionCode 29
versionName "2.11.1" versionName "4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -23,10 +24,18 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
buildTypes.each {
it.buildConfigField('boolean', 'PRERELEASE', 'false')
it.buildConfigField('boolean', 'CENSOR', 'false')
}
} }
kotlinOptions { kotlinOptions {
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental' freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
} }
dependencies { dependencies {
@@ -38,22 +47,30 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"
implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0-beta01' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.0-rc02"
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation 'com.google.android.material:material:1.0.0' implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.firebase:firebase-core:17.0.0' implementation 'com.google.android.material:material:1.2.0-alpha01'
implementation 'com.google.firebase:firebase-perf:18.0.1' implementation 'com.google.firebase:firebase-core:17.2.1'
implementation 'com.google.firebase:firebase-perf:19.0.1'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4' implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
transitive = false
}
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}" implementation "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.9.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":20,"versionName":"2.11.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":29,"versionName":"4.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -1,3 +1,23 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@file:Suppress("UNUSED_VARIABLE")
package xyz.quaver.pupil package xyz.quaver.pupil
import android.content.Intent import android.content.Intent
@@ -6,11 +26,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import xyz.quaver.hitomi.fetchNozomi
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.getReader import xyz.quaver.hiyobi.getReader
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.LockActivity
@@ -30,13 +49,11 @@ class ExampleInstrumentedTest {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("xyz.quaver.pupil", appContext.packageName) assertEquals("xyz.quaver.pupil", appContext.packageName)
Log.d("Pupil", fetchNozomi().first.size.toString())
} }
@Test @Test
fun checkCacheDir() { fun checkCacheDir() {
val activityTestRule = ActivityTestRule<LockActivity>(LockActivity::class.java) val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
activityTestRule.launchActivity(Intent()) activityTestRule.launchActivity(Intent())
@@ -50,7 +67,7 @@ class ExampleInstrumentedTest {
val data: ByteArray val data: ByteArray
with(URL(reader[0].url).openConnection() as HttpsURLConnection) { with(URL(createImgList(1426382, reader)[0].path).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)

View File

@@ -1,9 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="xyz.quaver.pupil"> package="xyz.quaver.pupil">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -13,7 +17,9 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
tools:replace="android:theme">
<activity android:name=".ui.LockActivity"/> <activity android:name=".ui.LockActivity"/>
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil package xyz.quaver.pupil
import android.app.Notification import android.app.Notification
@@ -55,6 +73,11 @@ class Pupil : MultiDexApplication() {
preference.edit().putBoolean("channel_created", true).apply() preference.edit().putBoolean("channel_created", true).apply()
} }
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
super.onCreate() super.onCreate()
} }

View File

@@ -1,20 +1,39 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.app.AlertDialog
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.Log
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.daimajia.swipe.SwipeLayout
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
import com.daimajia.swipe.interfaces.SwipeAdapterInterface
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.item_galleryblock.view.* import kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -23,21 +42,23 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.ReaderItem import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.wordCapitalize
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
enum class ViewType { enum class ViewType {
NEXT, NEXT,
@@ -47,7 +68,7 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
private lateinit var favorites: Histories private lateinit var favorites: Histories
inner class GalleryViewHolder(private val view: CardView) : RecyclerView.ViewHolder(view) { inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Pair<GalleryBlock, Deferred<String>>) { fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
with(view) { with(view) {
val resources = context.resources val resources = context.resources
@@ -62,29 +83,38 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
val artists = galleryBlock.artists val artists = galleryBlock.artists
val series = galleryBlock.series val series = galleryBlock.series
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Main).launch {
val cache = thumbnail.await() val cache = thumbnail.await()
if (!File(cache).exists()) glide
return@launch .load(cache)
.skipMemoryCache(true)
val bitmap = BitmapFactory.decodeFile(thumbnail.await()) .diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
launch(Dispatchers.Main) { .apply {
galleryblock_thumbnail.setImageBitmap(bitmap) if (BuildConfig.CENSOR)
override(5, 8)
} }
.into(galleryblock_thumbnail)
} }
//Check cache //Check cache
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") } val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") }
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") } val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") }
try {
Json(JsonConfiguration.Stable)
.parse(Reader.serializer(), readerCache.invoke().readText())
} catch(e: Exception) {
readerCache.invoke().delete()
}
if (readerCache.invoke().exists()) { if (readerCache.invoke().exists()) {
val reader = Json(JsonConfiguration.Stable) val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.invoke().readText()) .parse(Reader.serializer(), readerCache.invoke().readText())
with(galleryblock_progressbar) { with(galleryblock_progressbar) {
max = reader.size max = reader.galleryInfo.size
progress = imageCache.invoke().list()?.size ?: 0 progress = imageCache.invoke().list()?.size ?: 0
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -108,8 +138,8 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} else { } else {
if (visibility == View.GONE) { if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable) val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.invoke().readText()) .parse(Reader.serializer(), readerCache.invoke().readText())
max = reader.size max = reader.galleryInfo.size
visibility = View.VISIBLE visibility = View.VISIBLE
} }
@@ -130,8 +160,6 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
} else } else
view.galleryblock_progress_complete.visibility = View.INVISIBLE view.galleryblock_progress_complete.visibility = View.INVISIBLE
null
} }
} }
} }
@@ -147,19 +175,6 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
artists.isNotEmpty() -> View.VISIBLE artists.isNotEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
setOnClickListener {
if (artists.size > 1) {
AlertDialog.Builder(context).apply {
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, artists)) { _, index ->
for (callback in onChipClickedHandler)
callback.invoke(Tag("artist", artists[index]))
}
}.show()
} else {
for(callback in onChipClickedHandler)
callback.invoke(Tag("artist", artists.first()))
}
}
} }
with(galleryblock_series) { with(galleryblock_series) {
text = text =
@@ -170,31 +185,8 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
series.isNotEmpty() -> View.VISIBLE series.isNotEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
setOnClickListener {
setOnClickListener {
if (series.size > 1) {
AlertDialog.Builder(context).apply {
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, series)) { _, index ->
for (callback in onChipClickedHandler)
callback.invoke(Tag("series", series[index]))
}
}.show()
} else {
for(callback in onChipClickedHandler)
callback.invoke(Tag("series", series.first()))
}
}
}
}
with(galleryblock_type) {
text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
setOnClickListener {
setOnClickListener {
for(callback in onChipClickedHandler)
callback.invoke(Tag("type", galleryBlock.type))
}
}
} }
galleryblock_type.text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
with(galleryblock_language) { with(galleryblock_language) {
text = text =
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language]) resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
@@ -202,16 +194,11 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
galleryBlock.language.isNotEmpty() -> View.VISIBLE galleryBlock.language.isNotEmpty() -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
setOnClickListener {
setOnClickListener {
for(callback in onChipClickedHandler)
callback.invoke(Tag("language", galleryBlock.language))
}
}
} }
galleryblock_tag_group.removeAllViews() galleryblock_tag_group.removeAllViews()
galleryBlock.relatedTags.forEach { galleryBlock.relatedTags.forEach {
galleryblock_tag_group.addView(Chip(context).apply {
val tag = Tag.parse(it).let { tag -> val tag = Tag.parse(it).let { tag ->
when { when {
tag.area != null -> tag tag.area != null -> tag
@@ -219,31 +206,25 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
} }
val chip = LayoutInflater.from(context) chipIcon = when(tag.area) {
.inflate(R.layout.tag_chip, this, false) as Chip
val icon = when(tag.area) {
"male" -> { "male" -> {
chip.setChipBackgroundColorResource(R.color.material_blue_700) setChipBackgroundColorResource(R.color.material_blue_700)
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white) ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
} }
"female" -> { "female" -> {
chip.setChipBackgroundColorResource(R.color.material_pink_600) setChipBackgroundColorResource(R.color.material_pink_600)
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white)) setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white) ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
} }
else -> null else -> null
} }
text = tag.tag.wordCapitalize()
chip.chipIcon = icon setOnClickListener {
chip.text = tag.tag.wordCapitalize()
chip.setOnClickListener {
for (callback in onChipClickedHandler) for (callback in onChipClickedHandler)
callback.invoke(tag) callback.invoke(tag)
} }
})
galleryblock_tag_group.addView(chip)
} }
galleryblock_id.text = galleryBlock.id.toString() galleryblock_id.text = galleryBlock.id.toString()
@@ -295,19 +276,12 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
} }
private fun String.wordCapitalize() : String {
val result = ArrayList<String>()
for (word in this.split(" "))
result.add(word.capitalize())
return result.joinToString(" ")
}
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>() private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
val completeFlag = SparseBooleanArray() val completeFlag = SparseBooleanArray()
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>() val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
var onDownloadClickedHandler: ((Int) -> Unit)? = null
var onDeleteClickedHandler: ((Int) -> Unit)? = null
var showNext = false var showNext = false
var showPrev = false var showPrev = false
@@ -333,8 +307,47 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GalleryViewHolder) if (holder is GalleryViewHolder) {
holder.bind(galleries[position-(if (showPrev) 1 else 0)]) val gallery = galleries[position-(if (showPrev) 1 else 0)]
holder.bind(gallery)
with(holder.view.galleryblock_primary) {
setOnClickListener {
holder.view.performClick()
}
setOnLongClickListener {
holder.view.performLongClick()
}
}
holder.view.galleryblock_download.setOnClickListener {
onDownloadClickedHandler?.invoke(position)
}
holder.view.galleryblock_delete.setOnClickListener {
onDeleteClickedHandler?.invoke(position)
}
mItemManger.bindView(holder.view, position)
holder.view.galleryblock_swipe_layout.addSwipeListener(object: SwipeLayout.SwipeListener {
override fun onStartOpen(layout: SwipeLayout?) {
mItemManger.closeAllExcept(layout)
holder.view.galleryblock_download.text = when(GalleryDownloader.get(gallery.first.id)) {
null -> holder.view.context.getString(R.string.main_download)
else -> holder.view.context.getString(R.string.main_cancel_download)
}
}
override fun onClose(layout: SwipeLayout?) {}
override fun onHandRelease(layout: SwipeLayout?, xvel: Float, yvel: Float) {}
override fun onOpen(layout: SwipeLayout?) {}
override fun onStartClose(layout: SwipeLayout?) {}
override fun onUpdate(layout: SwipeLayout?, leftOffset: Int, topOffset: Int) {}
})
}
} }
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
@@ -360,4 +373,6 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
else -> ViewType.GALLERY else -> ViewType.GALLERY
}.ordinal }.ordinal
} }
override fun getSwipeLayoutResourceId(position: Int) = R.id.galleryblock_swipe_layout
} }

View File

@@ -1,71 +1,65 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters package xyz.quaver.pupil.adapters
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.getCachedGallery
import java.io.File
class ReaderAdapter(private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { class ReaderAdapter(private val glide: RequestManager,
private val galleryID: Int,
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
var isFullScreen = false var isFullScreen = false
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
LayoutInflater.from(parent.context).inflate( return LayoutInflater.from(parent.context).inflate(
R.layout.item_reader, parent, false R.layout.item_reader, parent, false
).let { ).let {
return ViewHolder(it) ViewHolder(it)
} }
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ImageView
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { glide
// Raw height and width of image .load(File(getCachedGallery(holder.view.context, galleryID), images[position]))
val (height: Int, width: Int) = options.run { outHeight to outWidth } .dontTransform()
var inSampleSize = 1 .diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
if (height > reqHeight || width > reqWidth) { .error(R.drawable.image_broken_variant)
.apply {
val halfHeight: Int = height / 2 if (BuildConfig.CENSOR)
val halfWidth: Int = width / 2 override(5, 8)
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
with(holder.view as ImageView) {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(images[position], options)
val (reqWidth, reqHeight) = context.resources.displayMetrics.let {
Pair(it.widthPixels, it.heightPixels)
}
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inPreferredConfig = Bitmap.Config.RGB_565
options.inJustDecodeBounds = false
val image = BitmapFactory.decodeFile(images[position], options)
setImageBitmap(image)
} }
.into(holder.view)
} }
override fun getItemCount() = images.size override fun getItemCount() = images.size

View File

@@ -0,0 +1,47 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.adapters
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.RequestManager
import xyz.quaver.pupil.BuildConfig
class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails: List<String>) : RecyclerView.Adapter<ThumbnailAdapter.ViewHolder>() {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ImageView(parent.context))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
glide
.load(thumbnails[position])
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view)
}
override fun getItemCount() = thumbnails.size
}

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
@@ -5,7 +23,7 @@ import kotlinx.android.parcel.Parcelize
import xyz.quaver.hitomi.Suggestion import xyz.quaver.hitomi.Suggestion
@Parcelize @Parcelize
data class TagSuggestion constructor(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
override fun getBody(): String { override fun getBody(): String {

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.types package xyz.quaver.pupil.types
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -90,8 +108,8 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
} }
} }
fun removeByArea(area: String) { fun removeByArea(area: String, isNegative: Boolean? = null) {
filter { it.area == area }.forEach { filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
remove(it) remove(it)
} }
} }

View File

@@ -0,0 +1,275 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout.LayoutParams
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.dialog_galleryblock.*
import kotlinx.android.synthetic.main.gallery_details.view.*
import kotlinx.android.synthetic.main.item_gallery_details.view.*
import kotlinx.coroutines.*
import xyz.quaver.hitomi.Gallery
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGallery
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailAdapter
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.wordCapitalize
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
private val languages = context.resources.getStringArray(R.array.languages).map {
it.split("|").let { split ->
Pair(split[0], split[1])
}
}.toMap()
private val glide = Glide.with(context)
val onChipClickedHandler = ArrayList<((Tag) -> (Unit))>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_galleryblock)
window?.attributes.apply {
this ?: return@apply
width = LayoutParams.MATCH_PARENT
height = LayoutParams.MATCH_PARENT
}
with(gallery_fab) {
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.arrow_right))
setOnClickListener {
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
})
(context.applicationContext as Pupil).histories.add(galleryID)
}
}
CoroutineScope(Dispatchers.IO).launch {
try {
val gallery = getGallery(galleryID)
launch(Dispatchers.Main) {
gallery_progressbar.visibility = View.GONE
gallery_title.text = gallery.title
gallery_artist.text = gallery.artists.joinToString(", ") { it.wordCapitalize() }
with(gallery_type) {
text = gallery.type.wordCapitalize()
setOnClickListener {
gallery.type.let {
when (it) {
"artist CG" -> "artistcg"
"game CG" -> "gamecg"
else -> it
}
}.let {
onChipClickedHandler.forEach { handler ->
handler.invoke(Tag("type", it))
}
}
}
}
Glide.with(context)
.load(gallery.cover)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}.into(gallery_cover)
addDetails(gallery)
addThumbnails(gallery)
addRelated(gallery)
}
} catch (e: Exception) {
Snackbar.make(gallery_layout, R.string.unable_to_connect, Snackbar.LENGTH_INDEFINITE).show()
}
}
}
private fun addDetails(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_details)
listOf(
R.string.gallery_artists,
R.string.gallery_groups,
R.string.gallery_language,
R.string.gallery_series,
R.string.gallery_characters,
R.string.gallery_tags
).zip(
listOf(
gallery.artists.map { Tag("artist", it) },
gallery.groups.map { Tag("group", it) },
listOf(gallery.language).map { Tag("language", it) },
gallery.series.map { Tag("series", it) },
gallery.characters.map { Tag("character", it) },
gallery.tags.map {
Tag.parse(it).let { tag ->
when {
tag.area != null -> tag
else -> Tag("tag", it)
}
}
}
)
).filter {
(_, content) -> content.isNotEmpty()
}.forEach { (title, content) ->
inflater.inflate(R.layout.item_gallery_details, gallery_details_contents, false).apply {
gallery_details_type.setText(title)
content.forEach { tag ->
gallery_details_tags.addView(
Chip(context).apply {
chipIcon = when(tag.area) {
"male" -> {
setChipBackgroundColorResource(R.color.material_blue_700)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
}
"female" -> {
setChipBackgroundColorResource(R.color.material_pink_600)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
}
else -> null
}
text = when (tag.area) {
"language" -> languages[tag.tag]
else -> tag.tag.wordCapitalize()
}
setOnClickListener {
onChipClickedHandler.forEach { handler ->
handler.invoke(tag)
}
}
}
)
}
}.let {
gallery_details_contents.addView(it)
}
}
}.let {
gallery_contents.addView(it)
}
}
private fun addThumbnails(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_thumbnails)
RecyclerView(context).apply {
layoutManager = GridLayoutManager(context, 3)
adapter = ThumbnailAdapter(glide, gallery.thumbnails)
}.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
}
}.let {
gallery_contents.addView(it)
}
}
private fun addRelated(gallery: Gallery) {
val inflater = LayoutInflater.from(context)
val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
val adapter = GalleryBlockAdapter(glide, galleries).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
handler.invoke(tag)
}
}
}
CoroutineScope(Dispatchers.Main).launch {
gallery.related.forEachIndexed { i, galleryID ->
async(Dispatchers.IO) {
getGalleryBlock(galleryID)
}.let {
val galleryBlock = it.await() ?: return@let
galleries.add(Pair(galleryBlock, GlobalScope.async { galleryBlock.thumbnails.first() }))
adapter.notifyItemInserted(i)
}
}
}
inflater.inflate(R.layout.gallery_details, gallery_contents, false).apply {
gallery_details.setText(R.string.gallery_related)
RecyclerView(context).apply {
layoutManager = LinearLayoutManager(context)
this.adapter = adapter
ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, _ ->
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleries[position].first.id)
})
(context.applicationContext as Pupil).histories.add(galleries[position].first.id)
}
.setOnItemLongClickListener { _, position, _ ->
GalleryDialog(context, galleries[position].first.id).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
}
}.show()
true
}
}.let {
gallery_details_contents.addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
}
}.let {
gallery_contents.addView(it)
}
}
}

View File

@@ -1,6 +1,25 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.andrognito.patternlockview.PatternLockView import com.andrognito.patternlockview.PatternLockView
@@ -17,7 +36,18 @@ class LockActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock) setContentView(R.layout.activity_lock)
val lockManager = LockManager(this) val lockManager = try {
LockManager(this)
} catch (e: Exception) {
AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
}.show()
return
}
val mode = intent.getStringExtra("mode") val mode = intent.getStringExtra("mode")
@@ -28,9 +58,10 @@ class LockActivity : AppCompatActivity() {
when(mode) { when(mode) {
null -> { null -> {
if (lockManager.empty()) { if (lockManager.isEmpty()) {
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
return
} }
} }
"add_lock" -> { "add_lock" -> {

View File

@@ -1,15 +1,40 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.IntentFilter
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.text.* import android.text.*
import android.text.style.AlignmentSpan import android.text.style.AlignmentSpan
import android.view.* import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -26,11 +51,11 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.FloatingSearchView
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
import com.arlib.floatingsearchview.util.view.SearchInputView import com.arlib.floatingsearchview.util.view.SearchInputView
import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.android.synthetic.main.activity_main_content.*
import kotlinx.android.synthetic.main.dialog_galleryblock.view.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -41,7 +66,6 @@ import kotlinx.serialization.list
import kotlinx.serialization.stringify import kotlinx.serialization.stringify
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.* import xyz.quaver.hitomi.*
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter import xyz.quaver.pupil.adapters.GalleryBlockAdapter
@@ -55,6 +79,8 @@ import java.net.URL
import java.util.* import java.util.*
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -67,16 +93,24 @@ class MainActivity : AppCompatActivity() {
FAVORITE FAVORITE
} }
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>() private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
private var query = "" private var query = ""
set(value) { set(value) {
field = value field = value
findViewById<SearchInputView>(R.id.search_bar_text) with(findViewById<SearchInputView>(R.id.search_bar_text)) {
.setText(query, TextView.BufferType.EDITABLE) if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
}
} }
private var mode = Mode.SEARCH private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST
private val REQUEST_SETTINGS = 45162 private val REQUEST_SETTINGS = 45162
private val REQUEST_LOCK = 561 private val REQUEST_LOCK = 561
@@ -93,6 +127,20 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val lockManager = try {
LockManager(this)
} catch (e: Exception) {
android.app.AlertDialog.Builder(this).apply {
setTitle(R.string.warning)
setMessage(R.string.lock_corrupted)
setPositiveButton(android.R.string.ok) { _, _ ->
finish()
}
}.show()
return
}
if (lockManager.isNotEmpty())
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK) startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
checkPermissions() checkPermissions()
@@ -132,7 +180,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
@@ -155,23 +203,9 @@ class MainActivity : AppCompatActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")!!.toInt() val perPage = preference.getString("per_page", "25")!!.toInt()
val maxPage = Math.ceil(totalItems / perPage.toDouble()).roundToInt() val maxPage = ceil(totalItems / perPage.toDouble()).roundToInt()
return when(keyCode) { return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) {
runOnUiThread {
currentPage++
cancelFetch()
clearGalleries()
fetchGalleries(query)
loadBlocks()
}
}
true
}
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
if (currentPage > 0) { if (currentPage > 0) {
runOnUiThread { runOnUiThread {
@@ -179,7 +213,21 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks()
}
}
true
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (currentPage < maxPage) {
runOnUiThread {
currentPage++
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -197,7 +245,7 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -210,6 +258,12 @@ class MainActivity : AppCompatActivity() {
private fun checkUpdate() { private fun checkUpdate() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
if (ignoreUpdateUntil > System.currentTimeMillis())
return
fun extractReleaseNote(update: JsonObject, locale: String) : String { fun extractReleaseNote(update: JsonObject, locale: String) : String {
val markdown = update["body"]!!.content val markdown = update["body"]!!.content
@@ -254,16 +308,60 @@ class MainActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
val update = val update =
checkUpdate(getString(R.string.release_url), BuildConfig.VERSION_NAME) ?: return@launch checkUpdate(getString(R.string.release_url)) ?: return@launch
val (url, fileName) = getApkUrl(update) ?: return@launch
fileName ?: return@launch
val dialog = AlertDialog.Builder(this@MainActivity).apply { val dialog = AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.update_title) setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault().language) val msg = extractReleaseNote(update, Locale.getDefault().language)
setMessage(Markwon.create(context).toMarkdown(msg)) setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ -> setPositiveButton(android.R.string.yes) { _, _ ->
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.update)))) if (!this@MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.warning)
setMessage(R.string.update_no_permission)
setPositiveButton(android.R.string.ok) { _, _ -> }
}.show()
return@setPositiveButton
}
val request = DownloadManager.Request(Uri.parse(url)).apply {
setDescription(getString(R.string.update_notification_description))
setTitle(getString(R.string.app_name))
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
}
val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val id = manager.enqueue(request)
registerReceiver(object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
try {
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(manager.getUriForDownloadedFile(id), manager.getMimeTypeForDownloadedFile(id))
}
startActivity(install)
unregisterReceiver(this)
} catch (e: Exception) {
AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.update_failed)
setMessage(R.string.update_failed_message)
setPositiveButton(android.R.string.ok) { _, _ -> }
}.show()
}
}
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
setNegativeButton(R.string.ignore_update) { _, _ ->
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
.apply()
} }
setNegativeButton(android.R.string.no) { _, _ ->}
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
@@ -273,7 +371,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun checkPermissions() { private fun checkPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) if (!hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489) ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489)
} }
@@ -284,6 +382,13 @@ class MainActivity : AppCompatActivity() {
main_searchview.translationY = p1.toFloat() main_searchview.translationY = p1.toFloat()
main_recyclerview.scrollBy(0, prevP1 - p1) main_recyclerview.scrollBy(0, prevP1 - p1)
with(main_fab) {
if (prevP1 > p1)
hideMenuButton(true)
else if (prevP1 < p1)
showMenuButton(true)
}
prevP1 = p1 prevP1 = p1
} }
) )
@@ -300,7 +405,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.SEARCH mode = Mode.SEARCH
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_history -> { R.id.main_drawer_history -> {
@@ -309,7 +414,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.HISTORY mode = Mode.HISTORY
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_downloads -> { R.id.main_drawer_downloads -> {
@@ -318,7 +423,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.DOWNLOAD mode = Mode.DOWNLOAD
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_favorite -> { R.id.main_drawer_favorite -> {
@@ -327,7 +432,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0 currentPage = 0
query = "" query = ""
mode = Mode.FAVORITE mode = Mode.FAVORITE
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
R.id.main_drawer_help -> { R.id.main_drawer_help -> {
@@ -351,15 +456,74 @@ class MainActivity : AppCompatActivity() {
true true
} }
with(main_fab_jump) {
setImageResource(R.drawable.ic_jump)
setOnClickListener {
val preference = PreferenceManager.getDefaultSharedPreferences(context)
val perPage = preference.getString("per_page", "25")!!.toInt()
val editText = EditText(context)
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
currentPage+1,
ceil(totalItems / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
runOnUiThread {
cancelFetch()
clearGalleries()
loadBlocks()
}
}
}.show()
}
}
with(main_fab_id) {
setImageResource(R.drawable.numeric)
setOnClickListener {
val editText = EditText(context)
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ ->
CoroutineScope(Dispatchers.Default).launch {
try {
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery =
getGalleryBlock(editText.text.toString().toInt()) ?: throw Exception()
intent.putExtra("galleryID", gallery.id)
startActivity(intent)
histories.add(gallery.id)
} catch (e: Exception) {
Snackbar.make(main_layout,
R.string.main_open_gallery_by_id_error, Snackbar.LENGTH_LONG).show()
}
}
}
}.show()
}
}
setupSearchBar() setupSearchBar()
setupRecyclerView() setupRecyclerView()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
with(main_recyclerview) { with(main_recyclerview) {
adapter = GalleryBlockAdapter(galleries).apply { adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
onChipClickedHandler.add { onChipClickedHandler.add {
runOnUiThread { runOnUiThread {
query = it.toQuery() query = it.toQuery()
@@ -367,85 +531,91 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
onDownloadClickedHandler = { position ->
val galleryID = galleries[position].first.id
if (!completeFlag.get(galleryID, false)) {
val downloader = GalleryDownloader.get(galleryID)
if (downloader == null)
GalleryDownloader(context, galleryID, true).start()
else {
downloader.cancel()
downloader.clearNotification()
}
}
closeAllItems()
}
onDeleteClickedHandler = { position ->
val galleryID = galleries[position].first.id
CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryID]) {
this?.cancelAndJoin()
this?.clearNotification()
}
val cache = File(cacheDir, "imageCache/${galleryID}")
val data = getCachedGallery(context, galleryID)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryID)
if (this@MainActivity.mode == Mode.DOWNLOAD) {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
completeFlag.put(galleryID, false)
}
closeAllItems()
}
} }
ItemClickSupport.addTo(this) ItemClickSupport.addTo(this)
.setOnItemClickListener { _, position, v -> .setOnItemClickListener { _, position, v ->
if (v !is CardView) if (v !is CardView)
return@setOnItemClickListener return@setOnItemClickListener
val intent = Intent(this@MainActivity, ReaderActivity::class.java) val intent = Intent(this@MainActivity, ReaderActivity::class.java)
val gallery = galleries[position].first val gallery = galleries[position].first
intent.putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), gallery)) intent.putExtra("galleryID", gallery.id)
//TODO: Maybe sprinke some transitions will be nice :D //TODO: Maybe sprinkling some transitions will be nice :D
startActivity(intent) startActivity(intent)
histories.add(gallery.id) histories.add(gallery.id)
}.setOnItemLongClickListener { recyclerView, position, v -> }.setOnItemLongClickListener { _, position, v ->
if (v !is CardView) if (v !is CardView)
return@setOnItemLongClickListener true return@setOnItemLongClickListener true
val galleryBlock = galleries[position].first val galleryID = galleries[position].first.id
val view = LayoutInflater.from(this@MainActivity)
.inflate(R.layout.dialog_galleryblock, recyclerView, false)
val dialog = AlertDialog.Builder(this@MainActivity).apply { GalleryDialog(this@MainActivity, galleryID).apply {
setView(view) onChipClickedHandler.add {
}.create()
with(view.main_dialog_download) {
text = when(GalleryDownloader.get(galleryBlock.id)) {
null -> getString(R.string.reader_fab_download)
else -> getString(R.string.reader_fab_download_cancel)
}
isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(galleryBlock.id, false)
setOnClickListener {
val downloader = GalleryDownloader.get(galleryBlock.id)
if (downloader == null)
GalleryDownloader(context, galleryBlock, true).start()
else {
downloader.cancel()
downloader.clearNotification()
}
dialog.dismiss()
}
}
view.main_dialog_delete.setOnClickListener {
CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryBlock.id]) {
this?.cancelAndJoin()
this?.clearNotification()
}
val cache = File(cacheDir, "imageCache/${galleryBlock.id}")
val data = getCachedGallery(context, galleryBlock.id)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryBlock.id)
if (mode == Mode.DOWNLOAD) {
runOnUiThread { runOnUiThread {
query = it.toQuery()
currentPage = 0
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
dismiss()
} }
}.show()
(adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false)
}
dialog.dismiss()
}
dialog.show()
true true
} }
@@ -503,7 +673,6 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query)
loadBlocks() loadBlocks()
} }
@@ -583,7 +752,7 @@ class MainActivity : AppCompatActivity() {
//BOTTOM //BOTTOM
//Scrolling DOWN //Scrolling DOWN
if (dist < 0 && currentPage != Math.ceil(totalItems.toDouble()/perPage).roundToInt()-1) { if (dist < 0 && currentPage != ceil(totalItems.toDouble()/perPage).roundToInt()-1) {
with(main_recyclerview.adapter as GalleryBlockAdapter) { with(main_recyclerview.adapter as GalleryBlockAdapter) {
if(!showNext) { if(!showNext) {
showNext = true showNext = true
@@ -595,13 +764,14 @@ class MainActivity : AppCompatActivity() {
getChildAt(childCount-1) getChildAt(childCount-1)
} }
val absDist = Math.abs(dist) val absDist = abs(dist)
if (next is LinearLayout) { if (next is LinearLayout) {
val icon = next.findViewById<ImageView>(R.id.icon_next) val icon = next.findViewById<ImageView>(R.id.icon_next)
val text = next.findViewById<TextView>(R.id.text_next).apply { val text = next.findViewById<TextView>(R.id.text_next).apply {
text = getString(R.string.main_move, currentPage+2) text = getString(R.string.main_move, currentPage+2)
} }
if (absDist < 360) { if (absDist < 360) {
next.layoutParams.height = (absDist/2).roundToInt() next.layoutParams.height = (absDist/2).roundToInt()
icon.layoutParams.height = (absDist/2).roundToInt() icon.layoutParams.height = (absDist/2).roundToInt()
@@ -609,8 +779,7 @@ class MainActivity : AppCompatActivity() {
text.layoutParams.width = absDist.roundToInt() text.layoutParams.width = absDist.roundToInt()
target = -1 target = -1
} } else {
else {
next.layoutParams.height = 180 next.layoutParams.height = 180
icon.layoutParams.height = 180 icon.layoutParams.height = 180
icon.rotation = 0f icon.rotation = 0f
@@ -673,7 +842,7 @@ class MainActivity : AppCompatActivity() {
s ?: return s ?: return
if (s.any { it.isUpperCase() }) if (s.any { it.isUpperCase() })
s.replace(0, s.length, s.toString().toLowerCase()) s.replace(0, s.length, s.toString().toLowerCase(Locale.getDefault()))
} }
}) })
@@ -690,72 +859,52 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener { setOnMenuItemClickListener {
when(it.itemId) { when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS) R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_jump -> { R.id.main_menu_sort_newest -> {
val preference = PreferenceManager.getDefaultSharedPreferences(context) sortMode = SortMode.NEWEST
val perPage = preference.getString("per_page", "25")!!.toInt() it.isChecked = true
val editText = EditText(context)
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_jump_title)
setMessage(getString(
R.string.main_jump_message,
currentPage+1,
Math.ceil(totalItems / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
runOnUiThread { runOnUiThread {
currentPage = 0
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
}.show() R.id.main_menu_sort_popular -> {
} sortMode = SortMode.POPULAR
R.id.main_menu_id -> { it.isChecked = true
val editText = EditText(context)
AlertDialog.Builder(context).apply { runOnUiThread {
setView(editText) currentPage = 0
setTitle(R.string.main_open_gallery_by_id)
setPositiveButton(android.R.string.ok) { _, _ -> cancelFetch()
CoroutineScope(Dispatchers.Default).launch { clearGalleries()
try { fetchGalleries(query, sortMode)
val intent = Intent(this@MainActivity, ReaderActivity::class.java) loadBlocks()
val gallery =
getGalleryBlock(editText.text.toString().toInt()) ?: throw Exception()
intent.putExtra(
"galleryblock",
Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), gallery)
)
startActivity(intent)
} catch (e: Exception) {
Snackbar.make(main_layout,
R.string.main_open_gallery_by_id_error, Snackbar.LENGTH_LONG).show()
} }
} }
} }
}.show()
}
}
} }
setOnQueryChangeListener { _, query -> setOnQueryChangeListener { _, query ->
clearSuggestions() this@MainActivity.query = query
if (query.isEmpty() or query.endsWith(' '))
return@setOnQueryChangeListener
val currentQuery = query.split(" ").last().replace('_', ' ')
suggestionJob?.cancel() suggestionJob?.cancel()
clearSuggestions()
if (query.isEmpty() or query.endsWith(' ')) {
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
return@setOnQueryChangeListener
}
val currentQuery = query.split(" ").last().replace('_', ' ')
suggestionJob = CoroutineScope(Dispatchers.IO).launch { suggestionJob = CoroutineScope(Dispatchers.IO).launch {
val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) }) val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) })
@@ -774,13 +923,14 @@ class MainActivity : AppCompatActivity() {
} }
setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ -> setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ ->
val suggestion = item as TagSuggestion item as TagSuggestion
val tag = "${suggestion.n}:${suggestion.s.replace(Regex("\\s"), "_")}"
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
leftIcon.setImageDrawable( leftIcon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
resources, resources,
when(suggestion.n) { when(item.n) {
"female" -> R.drawable.ic_gender_female "female" -> R.drawable.ic_gender_female
"male" -> R.drawable.ic_gender_male "male" -> R.drawable.ic_gender_male
"language" -> R.drawable.ic_translate "language" -> R.drawable.ic_translate
@@ -804,8 +954,6 @@ class MainActivity : AppCompatActivity() {
rotation = 0f rotation = 0f
isEnabled = true isEnabled = true
setColorFilter(ContextCompat.getColor(context, R.color.material_orange_500))
isClickable = true isClickable = true
setOnClickListener { setOnClickListener {
val favorites = Tags(json.parse(serializer, favoritesFile.readText())) val favorites = Tags(json.parse(serializer, favoritesFile.readText()))
@@ -827,13 +975,13 @@ class MainActivity : AppCompatActivity() {
} }
} }
if (suggestion.t == -1) { if (item.t == -1) {
textView.text = suggestion.s textView.text = item.s
} else { } else {
val text = "${suggestion.s}\n ${suggestion.t}" val text = "${item.s}\n ${item.t}"
val len = text.length val len = text.length
val left = suggestion.s.length val left = item.s.length
textView.text = SpannableString(text).apply { textView.text = SpannableString(text).apply {
val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE) val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)
@@ -846,14 +994,13 @@ class MainActivity : AppCompatActivity() {
setOnSearchListener(object : FloatingSearchView.OnSearchListener { setOnSearchListener(object : FloatingSearchView.OnSearchListener {
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) { override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
val suggestion = searchSuggestion as TagSuggestion if (searchSuggestion !is TagSuggestion)
return
with(searchInputView.text) { with(searchInputView.text) {
delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length) delete(if (lastIndexOf(' ') == -1) 0 else lastIndexOf(' ')+1, length)
append("${suggestion.n}:${suggestion.s.replace(Regex("\\s"), "_")} ") append("${searchSuggestion.n}:${searchSuggestion.s.replace(Regex("\\s"), "_")} ")
} }
clearSuggestions()
} }
override fun onSearchAction(currentQuery: String?) { override fun onSearchAction(currentQuery: String?) {
@@ -863,7 +1010,7 @@ class MainActivity : AppCompatActivity() {
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener { setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() { override fun onFocus() {
if (searchInputView.text.isEmpty()) if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map { swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag") TagSuggestion(it.tag, -1, "", it.area ?: "tag")
}) })
@@ -872,20 +1019,14 @@ class MainActivity : AppCompatActivity() {
override fun onFocusCleared() { override fun onFocusCleared() {
suggestionJob?.cancel() suggestionJob?.cancel()
val query = searchInputView.text.toString()
if (query != this@MainActivity.query) {
this@MainActivity.query = query
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
currentPage = 0 currentPage = 0
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
}
}) })
attachNavigationDrawerToMenuButton(main_drawer_layout) attachNavigationDrawerToMenuButton(main_drawer_layout)
@@ -912,9 +1053,8 @@ class MainActivity : AppCompatActivity() {
main_progressbar.show() main_progressbar.show()
} }
private fun fetchGalleries(query: String) { private fun fetchGalleries(query: String, sortMode: SortMode) {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
val defaultQuery = preference.getString("default_query", "")!! val defaultQuery = preference.getString("default_query", "")!!
galleryIDs = null galleryIDs = null
@@ -927,12 +1067,14 @@ class MainActivity : AppCompatActivity() {
Mode.SEARCH -> { Mode.SEARCH -> {
when { when {
query.isEmpty() and defaultQuery.isEmpty() -> { query.isEmpty() and defaultQuery.isEmpty() -> {
fetchNozomi(start = currentPage*perPage, count = perPage).let { when(sortMode) {
totalItems = it.second SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
it.first else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply {
totalItems = size
} }
} }
else -> doSearch("$defaultQuery $query").apply { else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
totalItems = size totalItems = size
} }
} }
@@ -985,7 +1127,6 @@ class MainActivity : AppCompatActivity() {
private fun loadBlocks() { private fun loadBlocks() {
val preference = PreferenceManager.getDefaultSharedPreferences(this) val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25 val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
val defaultQuery = preference.getString("default_query", "")!!
loadingJob = CoroutineScope(Dispatchers.IO).launch { loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = galleryIDs?.await() val galleryIDs = galleryIDs?.await()
@@ -999,12 +1140,7 @@ class MainActivity : AppCompatActivity() {
return@launch return@launch
} }
when { galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
query.isEmpty() and defaultQuery.isEmpty() and (mode == Mode.SEARCH) ->
galleryIDs
else ->
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size))
}.chunked(5).let { chunks ->
for (chunk in chunks) for (chunk in chunks)
chunk.map { galleryID -> chunk.map { galleryID ->
async { async {

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.os.Bundle import android.os.Bundle
@@ -11,8 +29,6 @@ import com.andrognito.patternlockview.utils.PatternLockUtils
import kotlinx.android.synthetic.main.fragment_pattern_lock.* import kotlinx.android.synthetic.main.fragment_pattern_lock.*
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.* import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.hash
import xyz.quaver.pupil.util.hashWithSalt
class PatternLockFragment : Fragment(), PatternLockViewListener { class PatternLockFragment : Fragment(), PatternLockViewListener {

View File

@@ -1,8 +1,28 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.Manifest
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -13,6 +33,7 @@ import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
@@ -21,24 +42,19 @@ import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.io.IOException
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.getGalleryBlock
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.util.GalleryDownloader import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.ItemClickSupport import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.hasPermission
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
private var galleryID = 0
private val images = ArrayList<String>() private val images = ArrayList<String>()
private lateinit var galleryBlock: GalleryBlock
private var gallerySize = 0 private var gallerySize = 0
private var currentPage = 0 private var currentPage = 0
@@ -66,6 +82,9 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
favorites = (application as Pupil).favorites favorites = (application as Pupil).favorites
window.setFlags( window.setFlags(
@@ -76,16 +95,13 @@ class ReaderActivity : AppCompatActivity() {
handleIntent(intent) handleIntent(intent)
Crashlytics.setInt("GalleryID", galleryBlock.id) Crashlytics.setInt("GalleryID", galleryID)
if (!::galleryBlock.isInitialized) { if (galleryID == 0) {
onBackPressed() onBackPressed()
return return
} }
supportActionBar?.title = galleryBlock.title
supportActionBar?.setDisplayHomeAsUpEnabled(false)
initDownloader() initDownloader()
initView() initView()
@@ -106,25 +122,16 @@ class ReaderActivity : AppCompatActivity() {
if (uri != null && lastPathSegment != null) { if (uri != null && lastPathSegment != null) {
val nonNumber = Regex("[^-?0-9]+") val nonNumber = Regex("[^-?0-9]+")
val galleryID = when (uri.host) { galleryID = when (uri.host) {
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt() "hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
"히요비.asia" -> lastPathSegment.toInt() "히요비.asia" -> lastPathSegment.toInt()
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt() "xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt() "e-hentai.org" -> uri.pathSegments[1].toInt()
else -> return else -> return
} }
runBlocking {
CoroutineScope(Dispatchers.IO).launch {
galleryBlock = getGalleryBlock(galleryID) ?: return@launch
}.join()
}
} }
} else { } else {
galleryBlock = Json(JsonConfiguration.Stable).parse( galleryID = intent.getIntExtra("galleryID", 0)
GalleryBlock.serializer(),
intent.getStringExtra("galleryblock")!!
)
} }
} }
@@ -148,7 +155,7 @@ class ReaderActivity : AppCompatActivity() {
with(menu?.findItem(R.id.reader_menu_favorite)) { with(menu?.findItem(R.id.reader_menu_favorite)) {
this ?: return@with this ?: return@with
if (favorites.contains(galleryBlock.id)) if (favorites.contains(galleryID))
(icon as Animatable).start() (icon as Animatable).start()
} }
@@ -176,7 +183,7 @@ class ReaderActivity : AppCompatActivity() {
dialog.show() dialog.show()
} }
R.id.reader_menu_favorite -> { R.id.reader_menu_favorite -> {
val id = galleryBlock.id val id = galleryID
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
if (favorites.contains(id)) { if (favorites.contains(id)) {
@@ -214,33 +221,44 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
private fun initDownloader() { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id) //currentPage is 1-based
return when(keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage-2, 0)
if (d == null) { true
try { }
d = GalleryDownloader(this, galleryBlock) KeyEvent.KEYCODE_VOLUME_DOWN -> {
} catch (e: IOException) { (reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(currentPage, 0)
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
finish() true
return }
else -> super.onKeyDown(keyCode, event)
} }
} }
private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
if (d == null)
d = GalleryDownloader(this, galleryID)
downloader = d.apply { downloader = d.apply {
onReaderLoadedHandler = { onReaderLoadedHandler = {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
title = it.title
with(reader_download_progressbar) { with(reader_download_progressbar) {
max = it.size max = it.galleryInfo.size
progress = 0 progress = 0
} }
with(reader_progressbar) { with(reader_progressbar) {
max = it.size max = it.galleryInfo.size
progress = 0 progress = 0
} }
gallerySize = it.size gallerySize = it.galleryInfo.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}" menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.galleryInfo.size}"
} }
} }
onProgressHandler = { onProgressHandler = {
@@ -262,8 +280,12 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
onErrorHandler = { onErrorHandler = {
if (it is IOException) Snackbar
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show() .make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.reader_help) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
}
.show()
downloader.download = false downloader.download = false
} }
onCompleteHandler = { onCompleteHandler = {
@@ -311,12 +333,17 @@ class ReaderActivity : AppCompatActivity() {
private fun initView() { private fun initView() {
with(reader_recyclerview) { with(reader_recyclerview) {
adapter = ReaderAdapter(images) adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images)
addOnScrollListener(object: RecyclerView.OnScrollListener() { addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
if (dy < 0)
this@ReaderActivity.reader_fab.showMenuButton(true)
else if (dy > 0)
this@ReaderActivity.reader_fab.hideMenuButton(true)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager val layoutManager = recyclerView.layoutManager as LinearLayoutManager
if (layoutManager.findFirstVisibleItemPosition() == -1) if (layoutManager.findFirstVisibleItemPosition() == -1)
@@ -336,19 +363,25 @@ class ReaderActivity : AppCompatActivity() {
scrollMode(false) scrollMode(false)
fullscreen(true) fullscreen(true)
} else { } else {
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) (reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
} }
} }
} }
reader_fab_fullscreen.setOnClickListener { with(reader_fab_download) {
isFullscreen = true setImageResource(R.drawable.ic_download)
fullscreen(isFullscreen) setOnClickListener {
reader_fab.close(true) if (!this@ReaderActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
AlertDialog.Builder(this@ReaderActivity).apply {
setTitle(R.string.warning)
setMessage(R.string.update_no_permission)
setPositiveButton(android.R.string.ok) { _, _ -> }
}.show()
return@setOnClickListener
} }
reader_fab_download.setOnClickListener {
downloader.download = !downloader.download downloader.download = !downloader.download
if (!downloader.download) if (!downloader.download)
@@ -356,6 +389,17 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
with(reader_fab_fullscreen) {
setImageResource(R.drawable.ic_fullscreen)
setOnClickListener {
isFullscreen = true
fullscreen(isFullscreen)
this@ReaderActivity.reader_fab.close(true)
}
}
}
private fun fullscreen(isFullscreen: Boolean) { private fun fullscreen(isFullscreen: Boolean) {
with(window.attributes) { with(window.attributes) {
if (isFullscreen) { if (isFullscreen) {

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui package xyz.quaver.pupil.ui
import android.app.Activity import android.app.Activity
@@ -13,6 +31,7 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -222,14 +241,14 @@ class SettingsActivity : AppCompatActivity() {
addAll(languages.values) addAll(languages.values)
} }
) )
if (tags.any { it.area == "language" }) { if (tags.any { it.area == "language" && !it.isNegative }) {
val tag = languages[tags.first { it.area == "language" }.tag] val tag = languages[tags.first { it.area == "language" }.tag]
if (tag != null) { if (tag != null) {
setSelection( setSelection(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(adapter as ArrayAdapter<String>).getPosition(tag) (adapter as ArrayAdapter<String>).getPosition(tag)
) )
tags.removeByArea("language") tags.removeByArea("language", false)
} }
} }
} }
@@ -320,6 +339,19 @@ class SettingsActivity : AppCompatActivity() {
true true
} }
} }
with(findPreference<Preference>("dark_mode")) {
this!!
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(when (newValue as Boolean) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO
})
true
}
}
} }
} }

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.app.PendingIntent import android.app.PendingIntent
@@ -9,16 +27,20 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.io.IOException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list import xyz.quaver.availableInHiyobi
import xyz.quaver.hitomi.* import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.R
import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -30,7 +52,7 @@ import kotlin.concurrent.schedule
class GalleryDownloader( class GalleryDownloader(
base: Context, base: Context,
private val galleryBlock: GalleryBlock, private val galleryID: Int,
_notify: Boolean = false _notify: Boolean = false
) : ContextWrapper(base) { ) : ContextWrapper(base) {
@@ -41,20 +63,21 @@ class GalleryDownloader(
set(value) { set(value) {
if (value) { if (value) {
field = true field = true
notificationManager.notify(galleryBlock.id, notificationBuilder.build()) notificationManager.notify(galleryID, notificationBuilder.build())
val data = getCachedGallery(this, galleryBlock.id) if (reader?.isActive == false && downloadJob?.isActive != true) {
val cache = File(cacheDir, "imageCache/${galleryBlock.id}") val data = File(getDownloadDirectory(this), galleryID.toString())
val cache = File(cacheDir, "imageCache/$galleryID")
if (File(cache, "images").exists() && !data.exists()) { if (File(cache, "images").exists() && !data.exists()) {
cache.copyRecursively(data, true) cache.copyRecursively(data, true)
cache.deleteRecursively() cache.deleteRecursively()
} }
if (reader?.isActive == false && downloadJob?.isActive != true)
field = false field = false
}
downloads.add(galleryBlock.id) downloads.add(galleryID)
} else { } else {
field = false field = false
} }
@@ -62,7 +85,7 @@ class GalleryDownloader(
onNotifyChangedHandler?.invoke(value) onNotifyChangedHandler?.invoke(value)
} }
private val reader: Deferred<Reader>? private val reader: Deferred<Reader?>?
private var downloadJob: Job? = null private var downloadJob: Job? = null
private lateinit var notificationBuilder: NotificationCompat.Builder private lateinit var notificationBuilder: NotificationCompat.Builder
@@ -78,26 +101,30 @@ class GalleryDownloader(
companion object : SparseArray<GalleryDownloader>() companion object : SparseArray<GalleryDownloader>()
init { init {
put(galleryBlock.id, this) put(galleryID, this)
initNotification() initNotification()
reader = CoroutineScope(Dispatchers.IO).async { reader = CoroutineScope(Dispatchers.IO).async {
try {
download = _notify download = _notify
val json = Json(JsonConfiguration.Stable) val json = Json(JsonConfiguration.Stable)
val serializer = ReaderItem.serializer().list val serializer = Reader.serializer()
//Check cache //Check cache
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "reader.json") val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
try {
json.parse(serializer, cache.readText())
} catch(e: Exception) {
cache.delete()
}
if (cache.exists()) { if (cache.exists()) {
val cached = json.parse(serializer, cache.readText()) val cached = json.parse(serializer, cache.readText())
if (cached.isNotEmpty()) { if (cached.galleryInfo.isNotEmpty()) {
useHiyobi = when { useHiyobi = availableInHiyobi(galleryID)
cached.first().url.contains("hitomi.la") -> false
else -> true
}
onReaderLoadedHandler?.invoke(cached) onReaderLoadedHandler?.invoke(cached)
@@ -108,22 +135,19 @@ class GalleryDownloader(
//Cache doesn't exist. Load from internet //Cache doesn't exist. Load from internet
val reader = when { val reader = when {
useHiyobi -> { useHiyobi -> {
xyz.quaver.hiyobi.getReader(galleryBlock.id).let { try {
when { xyz.quaver.hiyobi.getReader(galleryID)
it.isEmpty() -> { } catch(e: Exception) {
useHiyobi = false useHiyobi = false
getReader(galleryBlock.id) getReader(galleryID)
}
else -> it
}
} }
} }
else -> { else -> {
getReader(galleryBlock.id) getReader(galleryID)
} }
} }
if (reader.isNotEmpty()) { if (reader.galleryInfo.isNotEmpty()) {
//Save cache //Save cache
if (cache.parentFile?.exists() == false) if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs() cache.parentFile!!.mkdirs()
@@ -132,6 +156,11 @@ class GalleryDownloader(
} }
reader reader
} catch (e: Exception) {
Crashlytics.logException(e)
onErrorHandler?.invoke(e)
null
}
} }
} }
@@ -139,39 +168,41 @@ class GalleryDownloader(
fun start() { fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch { downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await() val reader = reader!!.await() ?: return@launch
if (reader.isEmpty()) notificationBuilder.setContentTitle(reader.title)
onErrorHandler?.invoke(IOException("Couldn't retrieve Reader"))
val list = ArrayList<String>() val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader) onReaderLoadedHandler?.invoke(reader)
notificationBuilder notificationBuilder
.setProgress(reader.size, 0, false) .setProgress(reader.galleryInfo.size, 0, false)
.setContentText("0/${reader.size}") .setContentText("0/${reader.galleryInfo.size}")
reader.chunked(4).forEachIndexed { chunkIndex, chunked -> reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunked ->
chunked.mapIndexed { i, it -> chunked.mapIndexed { i, galleryInfo ->
val index = chunkIndex*4+i val index = chunkIndex*4+i
onProgressHandler?.invoke(index)
notificationBuilder
.setProgress(reader.size, index, false)
.setContentText("$index/${reader.size}")
if (download)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
async(Dispatchers.IO) { async(Dispatchers.IO) {
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url val url = when(useHiyobi) {
true -> createImgList(galleryID, reader)[index].path
false -> when (galleryInfo.haswebp) {
1 -> webpUrlFromUrl(
urlFromUrlFromHash(
galleryID,
galleryInfo,
true
)
)
else -> urlFromUrlFromHash(galleryID, galleryInfo)
}
}
val name = "$index".padStart(4, '0') val name = "$index".padStart(4, '0')
val ext = url.split('.').last() val ext = url.split('.').last()
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "images/$name.$ext") val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "images/$name.$ext")
if (!cache.exists()) if (!cache.exists())
try { try {
@@ -180,7 +211,7 @@ class GalleryDownloader(
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
} else } else
setRequestProperty("Referer", getReferer(galleryBlock.id)) setRequestProperty("Referer", getReferer(galleryID))
if (cache.parentFile?.exists() == false) if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs() cache.parentFile!!.mkdirs()
@@ -193,31 +224,43 @@ class GalleryDownloader(
onErrorHandler?.invoke(e) onErrorHandler?.invoke(e)
notificationBuilder notificationBuilder
.setContentTitle(galleryBlock.title) .setContentTitle(reader.title)
.setContentText(getString(R.string.reader_notification_error)) .setContentText(getString(R.string.reader_notification_error))
.setProgress(0, 0, false) .setProgress(0, 0, false)
notificationManager.notify(galleryBlock.id, notificationBuilder.build()) notificationManager.notify(galleryID, notificationBuilder.build())
} }
cache.absolutePath "images/$name.$ext"
} }
}.forEach { }.forEach {
list.add(it.await()) list.add(it.await())
val index = list.size
onProgressHandler?.invoke(index)
notificationBuilder
.setProgress(reader.galleryInfo.size, index, false)
.setContentText("$index/${reader.galleryInfo.size}")
if (download)
notificationManager.notify(galleryID, notificationBuilder.build())
onDownloadedHandler?.invoke(list) onDownloadedHandler?.invoke(list)
} }
} }
Timer(false).schedule(1000) { Timer(false).schedule(1000) {
notificationBuilder notificationBuilder
.setContentTitle(galleryBlock.title) .setContentTitle(reader.title)
.setContentText(getString(R.string.reader_notification_complete)) .setContentText(getString(R.string.reader_notification_complete))
.setProgress(0, 0, false) .setProgress(0, 0, false)
if (download) { if (download) {
File(cacheDir, "imageCache/${galleryBlock.id}").let { File(cacheDir, "imageCache/${galleryID}").let {
if (it.exists()) { if (it.exists()) {
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryBlock.id.toString()) val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
if (!target.exists()) if (!target.exists())
target.mkdirs() target.mkdirs()
@@ -227,7 +270,7 @@ class GalleryDownloader(
} }
} }
notificationManager.notify(galleryBlock.id, notificationBuilder.build()) notificationManager.notify(galleryID, notificationBuilder.build())
download = false download = false
} }
@@ -235,20 +278,20 @@ class GalleryDownloader(
onCompleteHandler?.invoke() onCompleteHandler?.invoke()
} }
remove(galleryBlock.id) remove(galleryID)
} }
} }
fun cancel() { fun cancel() {
downloadJob?.cancel() downloadJob?.cancel()
remove(galleryBlock.id) remove(galleryID)
} }
suspend fun cancelAndJoin() { suspend fun cancelAndJoin() {
downloadJob?.cancelAndJoin() downloadJob?.cancelAndJoin()
remove(galleryBlock.id) remove(galleryID)
} }
fun invokeOnReaderLoaded() { fun invokeOnReaderLoaded() {
@@ -258,7 +301,7 @@ class GalleryDownloader(
} }
fun clearNotification() { fun clearNotification() {
notificationManager.cancel(galleryBlock.id) notificationManager.cancel(galleryID)
} }
fun invokeOnNotifyChanged() { fun invokeOnNotifyChanged() {
@@ -267,22 +310,23 @@ class GalleryDownloader(
private fun initNotification() { private fun initNotification() {
val intent = Intent(this, ReaderActivity::class.java).apply { val intent = Intent(this, ReaderActivity::class.java).apply {
putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), galleryBlock)) putExtra("galleryID", galleryID)
} }
val pendingIntent = TaskStackBuilder.create(this).run { val pendingIntent = TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent) addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
} }
notificationManager = NotificationManagerCompat.from(this)
notificationBuilder = NotificationCompat.Builder(this, "download").apply { notificationBuilder = NotificationCompat.Builder(this, "download").apply {
setContentTitle(galleryBlock.title) setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text)) setContentText(getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_download) setSmallIcon(android.R.drawable.stat_sys_download)
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
setProgress(0, 0, true) setProgress(0, 0, true)
priority = NotificationCompat.PRIORITY_LOW priority = NotificationCompat.PRIORITY_LOW
} }
notificationManager = NotificationManagerCompat.from(this)
} }
} }

View File

@@ -1,10 +1,26 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import androidx.core.content.ContextCompat
import java.io.File import java.io.File
fun getCachedGallery(context: Context, galleryID: Int): File { fun getCachedGallery(context: Context, galleryID: Int): File {
@@ -16,6 +32,7 @@ fun getCachedGallery(context: Context, galleryID: Int): File {
} }
} }
@Suppress("DEPRECATION")
fun getDownloadDirectory(context: Context): File? { fun getDownloadDirectory(context: Context): File? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getExternalFilesDir("Pupil") context.getExternalFilesDir("Pupil")

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
@@ -11,7 +29,7 @@ class Histories(private val file: File) : ArrayList<Int>() {
init { init {
if (!file.exists()) if (!file.exists())
file.parentFile.mkdirs() file.parentFile?.mkdirs()
try { try {
load() load()

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
@@ -94,10 +112,12 @@ class LockManager(base: Context): ContextWrapper(base) {
} }
} }
fun empty(): Boolean { fun isEmpty(): Boolean {
return locks.isNullOrEmpty() return locks.isNullOrEmpty()
} }
fun isNotEmpty(): Boolean = !isEmpty()
fun contains(type: Lock.Type): Boolean { fun contains(type: Lock.Type): Boolean {
return locks?.any { it.type == type } ?: false return locks?.any { it.type == type } ?: false
} }

View File

@@ -0,0 +1,35 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
fun Context.hasPermission(permission: String) =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
fun String.wordCapitalize() : String {
val result = ArrayList<String>()
for (word in this.split(" "))
result.add(word.capitalize())
return result.joinToString(" ")
}

View File

@@ -1,7 +1,25 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.util.Log
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import xyz.quaver.pupil.BuildConfig
import java.net.URL import java.net.URL
fun getReleases(url: String) : JsonArray { fun getReleases(url: String) : JsonArray {
@@ -14,26 +32,33 @@ fun getReleases(url: String) : JsonArray {
} }
} }
fun checkUpdate(url: String, currentVersion: String) : JsonObject? { fun checkUpdate(url: String) : JsonObject? {
val releases = getReleases(url) val releases = getReleases(url)
if (releases.isEmpty()) if (releases.isEmpty())
return null return null
val latestVersion = releases[0].jsonObject["tag_name"]?.content return releases.firstOrNull {
if (BuildConfig.PRERELEASE) {
true
} else {
it.jsonObject["prerelease"]?.boolean == false
}
}?.let {
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
null
else
it.jsonObject
}
}
return when { fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
currentVersion.split('-').size == 1 -> { return releases["assets"]?.jsonArray?.firstOrNull {
when { Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
currentVersion != latestVersion -> releases[0].jsonObject }.let {
else -> null if (it == null)
} null
} else
else -> { Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
when {
(currentVersion.split('-')[0] == latestVersion) -> releases[0].jsonObject
else -> null
}
}
} }
} }

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<path
android:fillColor="#FF000000"
android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
</vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/arrow_right.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
</vector>

View File

@@ -13,14 +13,14 @@
<path <path
android:name="path" android:name="path"
android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"
android:fillColor="#000"/> android:fillColor="@color/material_orange_500"/>
<clip-path <clip-path
android:name="clip" android:name="clip"
android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/> android:pathData="M 2 21 L 2 21 L 22 21 L 22 21 Z"/>
<path <path
android:name="path_1" android:name="path_1"
android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"
android:fillColor="#000"/> android:fillColor="@color/material_orange_500"/>
</vector> </vector>
</aapt:attr> </aapt:attr>
<target android:name="clip"> <target android:name="clip">

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#fff" <vector android:height="24dp" android:tint="#fff"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ff000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> <path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector> </vector>

View File

@@ -1,3 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<animated-vector <animated-vector
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"

View File

@@ -1,8 +0,0 @@
<!-- drawable/numeric.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" />
</vector>

View File

@@ -1,4 +1,4 @@
<vector android:height="24dp" android:width="24dp" <vector android:height="24dp" android:width="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"/> <path android:fillColor="@color/material_orange_500" android:pathData="M 12 15.39 L 8.24 17.66 L 9.23 13.38 L 5.91 10.5 L 10.29 10.13 L 12 6.09 L 13.71 10.13 L 18.09 10.5 L 14.77 13.38 L 15.76 17.66 M 22 9.24 L 14.81 8.63 L 12 2 L 9.19 8.63 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 Z"/>
</vector> </vector>

View File

@@ -1,4 +1,4 @@
<vector android:height="24dp" android:width="24dp" <vector android:height="24dp" android:width="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportHeight="24.0" android:viewportWidth="24.0" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"/> <path android:fillColor="@color/material_orange_500" android:pathData="M 12 17.27 L 18.18 21 L 16.54 13.97 L 22 9.24 L 14.81 8.62 L 12 2 L 9.19 8.62 L 2 9.24 L 7.45 13.97 L 5.82 21 L 12 17.27 Z"/>
</vector> </vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/image_broken_variant.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M21,5V11.59L18,8.58L14,12.59L10,8.59L6,12.59L3,9.58V5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5M18,11.42L21,14.43V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V12.42L6,15.41L10,11.41L14,15.41" />
</vector>

View File

@@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" /> <path android:fillColor="#fff" android:pathData="M4,17V9H2V7H6V17H4M22,15C22,16.11 21.1,17 20,17H16V15H20V13H18V11H20V9H16V7H20A2,2 0 0,1 22,9V10.5A1.5,1.5 0 0,1 20.5,12A1.5,1.5 0 0,1 22,13.5V15M14,15V17H8V13C8,11.89 8.9,11 10,11H12V9H8V7H12A2,2 0 0,1 14,9V11C14,12.11 13.1,13 12,13H10V15H14Z" />
</vector> </vector>

View File

@@ -0,0 +1,8 @@
<!-- drawable/sort_variant.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M3,13H15V11H3M3,6V8H21V6M3,18H9V16H3V18Z" />
</vector>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -11,7 +29,29 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/lock_button_layout"/> app:layout_constraintBottom_toTopOf="@id/lock_fingerprint_layout"/>
<LinearLayout
android:id="@+id/lock_fingerprint_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="32dp"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/lock_content"
app:layout_constraintBottom_toTopOf="@id/lock_button_layout">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/lock_button_layout" android:id="@+id/lock_button_layout"
@@ -19,7 +59,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="32dp" android:layout_marginBottom="32dp"
app:layout_constraintTop_toBottomOf="@id/lock_content" app:layout_constraintTop_toBottomOf="@id/lock_fingerprint_layout"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"> android:gravity="center">
@@ -41,16 +81,6 @@
app:backgroundTint="@color/dark_gray" app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/> app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/fingerprint"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:backgroundTint="@color/dark_gray"
app:fabSize="mini"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/lock_password" android:id="@+id/lock_password"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.drawerlayout.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout <RelativeLayout
android:id="@+id/main_layout" android:id="@+id/main_layout"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
@@ -56,12 +74,37 @@
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<com.github.clans.fab.FloatingActionMenu
android:id="@+id/main_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:menu_colorNormal="@color/colorAccent">
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_jump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_jump_title"
app:fab_size="mini"/>
<com.github.clans.fab.FloatingActionButton
android:id="@+id/main_fab_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_label="@string/main_open_gallery_by_id"
app:fab_size="mini"/>
</com.github.clans.fab.FloatingActionMenu>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.arlib.floatingsearchview.FloatingSearchView <com.arlib.floatingsearchview.FloatingSearchView
android:id="@+id/main_searchview" android:id="@+id/main_searchview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:floatingSearch_backgroundColor="?attr/colorSurface"
app:floatingSearch_searchBarMarginLeft="8dp" app:floatingSearch_searchBarMarginLeft="8dp"
app:floatingSearch_searchBarMarginRight="8dp" app:floatingSearch_searchBarMarginRight="8dp"
app:floatingSearch_searchBarMarginTop="8dp" app:floatingSearch_searchBarMarginTop="8dp"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/reader_layout" android:id="@+id/reader_layout"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -39,6 +57,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="4dp" android:layout_height="4dp"
android:progressTint="@color/material_green_a700" android:progressTint="@color/material_green_a700"
tools:ignore="UnusedAttribute"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
@@ -55,7 +74,6 @@
android:id="@+id/reader_fab_download" android:id="@+id/reader_fab_download"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_downloading"
app:fab_label="@string/reader_fab_download" app:fab_label="@string/reader_fab_download"
app:fab_size="mini"/> app:fab_size="mini"/>
@@ -63,7 +81,6 @@
android:id="@+id/reader_fab_fullscreen" android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen" app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/> app:fab_size="mini"/>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -1,23 +1,132 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gallery_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp"> android:orientation="vertical">
<Button <com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_dialog_download" android:id="@+id/gallery_toolbar"
style="?borderlessButtonStyle" android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"/> app:layout_scrollFlags="scroll|exitUntilCollapsed">
<Button <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_dialog_delete"
style="?borderlessButtonStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/main_dialog_delete" android:padding="8dp">
app:layout_constraintTop_toBottomOf="@id/main_dialog_download"/>
<ImageView
android:id="@+id/gallery_cover"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/gallery_title"/>
<TextView
android:id="@+id/gallery_title"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:id="@+id/gallery_artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/gallery_title"
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
<View
android:id="@+id/gallery_padding"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/gallery_artist"
app:layout_constraintBottom_toTopOf="@id/gallery_type"/>
<com.google.android.material.chip.Chip
android:id="@+id/gallery_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/gallery_cover"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/gallery_contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/gallery_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/gallery_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_anchor="@id/gallery_toolbar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/gallery_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"/>
<LinearLayout
android:id="@+id/gallery_details_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
style="@style/TextAppearance.MaterialComponents.Body2"
android:id="@+id/gallery_details_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="8dp"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/gallery_details_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingVertical="8dp"/>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"/>

View File

@@ -1,24 +1,82 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:paddingStart="0dp"
android:paddingLeft="0dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
android:clipChildren="true">
<com.daimajia.swipe.SwipeLayout
android:id="@+id/galleryblock_swipe_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:drag_edge="right"
app:show_mode="pull_out">
<LinearLayout
android:id="@+id/galleryblock_secondary"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/galleryblock_download"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_blue_dark"
android:textColor="@android:color/white"
android:text="@string/main_download"
android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"/>
<TextView
android:id="@+id/galleryblock_delete"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="70dp"
android:padding="8dp"
android:gravity="center"
android:background="@android:color/holo_red_dark"
android:textColor="@android:color/white"
android:text="@string/main_delete"
android:foreground="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"/>
</LinearLayout>
<LinearLayout
android:id="@+id/galleryblock_primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true"> android:clickable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@@ -69,8 +127,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/> app:layout_constraintTop_toBottomOf="@id/galleryblock_title"/>
@@ -81,8 +137,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/galleryblock_artist" app:layout_constraintTop_toBottomOf="@id/galleryblock_artist"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
@@ -93,8 +147,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/galleryblock_series" app:layout_constraintTop_toBottomOf="@id/galleryblock_series"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" /> app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
@@ -105,8 +157,6 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/galleryblock_type" app:layout_constraintTop_toBottomOf="@id/galleryblock_type"
app:layout_constraintBottom_toTopOf="@id/galleryblock_padding" app:layout_constraintBottom_toTopOf="@id/galleryblock_padding"
app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" /> app:layout_constraintStart_toEndOf="@id/galleryblock_thumbnail" />
@@ -127,6 +177,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:chipSpacing="2dp"
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding" app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail" app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
@@ -163,11 +214,12 @@
android:contentDescription="@string/app_name" android:contentDescription="@string/app_name"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
app:srcCompat="@drawable/ic_star_empty" app:srcCompat="@drawable/ic_star_empty"/>
app:backgroundTint="@color/material_orange_500"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</com.daimajia.swipe.SwipeLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
android:layout_width="match_parent" android:layout_height="wrap_content"> android:layout_width="match_parent" android:layout_height="wrap_content">

View File

@@ -1,8 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android" <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:contentDescription="@string/reader_imageview_description" android:contentDescription="@string/reader_imageview_description"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="100dp"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:adjustViewBounds="true"/> android:adjustViewBounds="true"/>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<ImageView <ImageView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"

View File

@@ -1,3 +1,21 @@
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="24dp"
app:chipIconSize="16dp"
app:chipStartPadding="8dp"
app:chipCornerRadius="100dp"/>

View File

@@ -1,4 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single"> <group android:checkableBehavior="single">

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/gallery_favorite"
android:icon="@drawable/ic_star_empty"
android:title=""
app:showAsAction="ifRoom"/>
<item
android:id="@+id/gallery_download"
android:icon="@drawable/ic_download"
android:title=""
app:showAsAction="always"/>
</menu>

View File

@@ -1,16 +1,38 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/main_menu_jump" <item
android:icon="@drawable/ic_jump" android:id="@+id/main_menu_sort"
android:title="@string/main_jump_title" android:title="@string/main_menu_sort">
app:showAsAction="ifRoom"/> <menu>
<group android:checkableBehavior="single">
<item android:id="@+id/main_menu_id" <item android:id="@+id/main_menu_sort_newest"
android:icon="@drawable/ic_numeric" android:title="@string/main_menu_sort_newest"
android:title="@string/main_open_gallery_by_id" android:checked="true"/>
app:showAsAction="ifRoom"/> <item android:id="@+id/main_menu_sort_popular"
android:title="@string/main_menu_sort_popular"/>
</group>
</menu>
</item>
<item <item
android:id="@+id/main_menu_settings" android:id="@+id/main_menu_settings"

View File

@@ -1,10 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Pupil, Hitomi.la viewer for Android
~ Copyright (C) 2019 tom5079
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/reader_menu_favorite" <item android:id="@+id/reader_menu_favorite"
android:title="" android:title=""
android:iconTint="@color/material_orange_500"
android:icon="@drawable/avd_star" android:icon="@drawable/avd_star"
app:showAsAction="always"/> app:showAsAction="always"/>

View File

@@ -70,7 +70,7 @@
<string name="settings_app_lock">アプリロック</string> <string name="settings_app_lock">アプリロック</string>
<string name="settings_app_lock_type">アップロックの種類</string> <string name="settings_app_lock_type">アップロックの種類</string>
<string name="settings_app_version_title">バージョン</string> <string name="settings_app_version_title">バージョン</string>
<string name="settings_lock_biomatrics">生体認識</string> <string name="settings_lock_biometrics">生体認識</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string> <string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string> <string name="settings_lock_enabled">有効</string>
<string name="settings_lock_fingerprint">指紋</string> <string name="settings_lock_fingerprint">指紋</string>
@@ -79,4 +79,30 @@
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string> <string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="settings_lock_none">なし</string> <string name="settings_lock_none">なし</string>
<string name="settings_lock_remove_message">ロックを無効にしますか?</string> <string name="settings_lock_remove_message">ロックを無効にしますか?</string>
<string name="reader_loading">ロード中</string>
<string name="main_menu_sort">ソート</string>
<string name="main_menu_sort_newest">投稿日時順</string>
<string name="main_menu_sort_popular">人気順</string>
<string name="update_failed">アップデートに失敗しました</string>
<string name="update_failed_message">マニュアルインストールが必要です。APKファイルは</string>
<string name="ignore_update">無視</string>
<string name="lock_corrupted">ロックファイルが破損されています。Pupilを再再インストールしてください。</string>
<string name="update_no_permission">権限がないため自動アップデートを行えません。ホームページで直接ダウンロードしてください。</string>
<string name="settings_dark_mode_title">ダークモード</string>
<string name="settings_dark_mode_summary">夜にシコりたい方々へ</string>
<string name="gallery_details">ギャラリー情報</string>
<string name="gallery_artists">アーティスト</string>
<string name="gallery_characters">キャラクター</string>
<string name="gallery_groups">グループ</string>
<string name="gallery_language">言語</string>
<string name="gallery_series">シリーズ</string>
<string name="gallery_tags">タグ</string>
<string name="gallery_thumbnails">サムネイル</string>
<string name="gallery_related">おすすめ</string>
<string name="settings_nomedia_summary">イメージをギャラリーから見えなくする</string>
<string name="settings_nomedia_title">イメージを隠す</string>
<string name="reader_help">ヘルプ</string>
<string name="main_delete">削除</string>
<string name="main_download">ダウンロード</string>
<string name="main_cancel_download">キャンセル</string>
</resources> </resources>

View File

@@ -54,7 +54,7 @@
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string> <string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
<string name="main_move">%1$d 페이지로 이동</string> <string name="main_move">%1$d 페이지로 이동</string>
<string name="https_block_alert_title">접속 불가 현상 안내</string> <string name="https_block_alert_title">접속 불가 현상 안내</string>
<string name="https_block_alert">최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다\n이 경우 플레이스토어에서 SNIper앱을 이용하시면 정상이용이 가능합니다.</string> <string name="https_block_alert">최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다 이 경우 플레이스토어에서 Intra앱을 이용하시면 정상이용이 가능합니다.</string>
<string name="main_dialog_export">갤러리 내보내기</string> <string name="main_dialog_export">갤러리 내보내기</string>
<string name="main_export_complete">내보내기 완료</string> <string name="main_export_complete">내보내기 완료</string>
<string name="main_export_open_folder">폴더 열기</string> <string name="main_export_open_folder">폴더 열기</string>
@@ -70,7 +70,7 @@
<string name="settings_app_lock">앱 잠금</string> <string name="settings_app_lock">앱 잠금</string>
<string name="settings_app_lock_type">앱 잠금 종류</string> <string name="settings_app_lock_type">앱 잠금 종류</string>
<string name="settings_app_version_title">앱 버전</string> <string name="settings_app_version_title">앱 버전</string>
<string name="settings_lock_biomatrics">생체 인식</string> <string name="settings_lock_biometrics">생체 인식</string>
<string name="settings_lock_confirm">잠금 확인을 위해 한번 더 입력해주세요</string> <string name="settings_lock_confirm">잠금 확인을 위해 한번 더 입력해주세요</string>
<string name="settings_lock_enabled">사용 중</string> <string name="settings_lock_enabled">사용 중</string>
<string name="settings_lock_fingerprint">지문</string> <string name="settings_lock_fingerprint">지문</string>
@@ -79,4 +79,30 @@
<string name="settings_lock_wrong_confirm">잠금이 일치하지 않습니다. 다시 시도하세요.</string> <string name="settings_lock_wrong_confirm">잠금이 일치하지 않습니다. 다시 시도하세요.</string>
<string name="settings_lock_none">없음</string> <string name="settings_lock_none">없음</string>
<string name="settings_lock_remove_message">잠금을 해제할까요?</string> <string name="settings_lock_remove_message">잠금을 해제할까요?</string>
<string name="reader_loading">로딩중</string>
<string name="main_menu_sort">정렬</string>
<string name="main_menu_sort_popular">인기순</string>
<string name="main_menu_sort_newest">시간순</string>
<string name="update_failed">"업데이트 "</string>
<string name="update_failed_message">수동 업데이트가 필요합니다. APK 파일은 다운로드 폴더에 있습니다.</string>
<string name="ignore_update">무시</string>
<string name="lock_corrupted">잠금 파일이 손상되었습니다! 앱을 재설치 해 주시기 바랍니다.</string>
<string name="update_no_permission">권한이 부여되어 있지 않아 자동 업데이트를 진행할 수 없습니다. 홈페이지에서 직접 다운로드 받으시기 바랍니다.</string>
<string name="settings_dark_mode_title">다크 모드</string>
<string name="settings_dark_mode_summary">딥 다크한 모오드</string>
<string name="gallery_details">갤러리 정보</string>
<string name="gallery_artists">작가</string>
<string name="gallery_characters">캐릭터</string>
<string name="gallery_groups">그룹</string>
<string name="gallery_language">언어</string>
<string name="gallery_series">시리즈</string>
<string name="gallery_tags">태그</string>
<string name="gallery_related">관련 갤러리</string>
<string name="gallery_thumbnails">미리보기</string>
<string name="settings_nomedia_summary">갤러리에서 이미지 검색이 되지 않도록 합니다</string>
<string name="settings_nomedia_title">이미지 숨기기</string>
<string name="reader_help">도움말</string>
<string name="main_delete">삭제</string>
<string name="main_download">다운로드</string>
<string name="main_cancel_download">취소</string>
</resources> </resources>

View File

@@ -8,4 +8,6 @@
<dimen name="activity_vertical_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen> <dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen> <dimen name="nav_header_height">176dp</dimen>
<dimen name="thumbnail_margin">8dp</dimen>
</resources> </resources>

View File

@@ -1,7 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false" tools:override="true">Pupil</string> <string name="app_name" translatable="false" tools:override="true">Pupil</string>
<string name="release_url" translatable="false">https://api.github.com/repos/tom5079/Pupil-issue/releases</string> <string name="release_url" translatable="false">https://api.github.com/repos/tom5079/Pupil/releases</string>
<string name="release_name" translatable="false">Pupil-v(\\d+\\.)+\\d+\\.apk</string> <string name="release_name" translatable="false">Pupil-v(\\d+\\.)+\\d+\\.apk</string>
<string name="home_page" translatable="false">http://bit.ly/2EZDClw</string> <string name="home_page" translatable="false">http://bit.ly/2EZDClw</string>
@@ -10,6 +10,7 @@
<string name="github" translatable="false">https://github.com/tom5079/Pupil-issue/issues/new/choose</string> <string name="github" translatable="false">https://github.com/tom5079/Pupil-issue/issues/new/choose</string>
<string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string> <string name="email" translatable="false">mailto:pupil.hentai@gmail.com</string>
<string name="kakaotalk" translatable="false">https://open.kakao.com/o/gvNrncsb</string> <string name="kakaotalk" translatable="false">https://open.kakao.com/o/gvNrncsb</string>
<string name="error_help" translatable="false">http://bit.ly/2KYYhto</string>
<string name="main_settings" translatable="false">Settings</string> <string name="main_settings" translatable="false">Settings</string>
<string name="galleryblock_thumbnail_description" translatable="false">Thumbnail</string> <string name="galleryblock_thumbnail_description" translatable="false">Thumbnail</string>
@@ -26,11 +27,18 @@
<string name="https_block_alert_title">(Korean only)</string> <string name="https_block_alert_title">(Korean only)</string>
<string name="https_block_alert">(Korean only)</string> <string name="https_block_alert">(Korean only)</string>
<string name="update_failed">Update failed</string>
<string name="update_failed_message">Please install manually. APK file is in the Downloads folder.</string>
<string name="update_no_permission">Cannot auto update because permission is denied. Please download manually from the webpage.</string>
<string name="ignore_update">Ignore</string>
<string name="channel_download">Download</string> <string name="channel_download">Download</string>
<string name="channel_download_description">Shows download status</string> <string name="channel_download_description">Shows download status</string>
<string name="unable_to_connect">Unable to connect to hitomi.la</string> <string name="unable_to_connect">Unable to connect to hitomi.la</string>
<string name="lock_corrupted">Lock file corrupted! Please re-install Pupil</string>
<string name="main_search">Search</string> <string name="main_search">Search</string>
<string name="main_no_result">No result</string> <string name="main_no_result">No result</string>
@@ -45,6 +53,10 @@
<string name="main_drawer_group_contact_email">Email me!</string> <string name="main_drawer_group_contact_email">Email me!</string>
<string name="main_drawer_grouop_contact_kakaotalk">Kakaotalk</string> <string name="main_drawer_grouop_contact_kakaotalk">Kakaotalk</string>
<string name="main_menu_sort">Sort</string>
<string name="main_menu_sort_newest">Newest</string>
<string name="main_menu_sort_popular">Popular</string>
<string name="main_jump_title">Jump to page</string> <string name="main_jump_title">Jump to page</string>
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string> <string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
<string name="main_open_gallery_by_id">Open Gallery by ID</string> <string name="main_open_gallery_by_id">Open Gallery by ID</string>
@@ -59,6 +71,10 @@
<string name="main_export_open_folder">Open Folder</string> <string name="main_export_open_folder">Open Folder</string>
<string name="main_export_error">Error occurred during export</string> <string name="main_export_error">Error occurred during export</string>
<string name="main_download">DOWNLOAD</string>
<string name="main_cancel_download">CANCEL</string>
<string name="main_delete">DELETE</string>
<string name="update_title">Update available</string> <string name="update_title">Update available</string>
<string name="update_download_started">Download started</string> <string name="update_download_started">Download started</string>
<string name="update_notification_description">Downloading apk&#8230;</string> <string name="update_notification_description">Downloading apk&#8230;</string>
@@ -67,10 +83,21 @@
<string name="search_hint">Search galleries</string> <string name="search_hint">Search galleries</string>
<string name="search_hint_with_page">Search galleries</string> <string name="search_hint_with_page">Search galleries</string>
<string name="gallery_details">Details</string>
<string name="gallery_thumbnails">Thumbnails</string>
<string name="gallery_related">Related Galleries</string>
<string name="gallery_artists">Artists</string>
<string name="gallery_groups">Groups</string>
<string name="gallery_language">Language</string>
<string name="gallery_series">Series</string>
<string name="gallery_characters">Characters</string>
<string name="gallery_tags">Tags</string>
<string name="galleryblock_series">Series: %1$s</string> <string name="galleryblock_series">Series: %1$s</string>
<string name="galleryblock_type">Type: %1$s</string> <string name="galleryblock_type">Type: %1$s</string>
<string name="galleryblock_language">Language: %1$s</string> <string name="galleryblock_language">Language: %1$s</string>
<string name="reader_loading">Loading</string>
<string name="reader_go_to_page">Go to page</string> <string name="reader_go_to_page">Go to page</string>
<string name="reader_fab_fullscreen">Fullscreen</string> <string name="reader_fab_fullscreen">Fullscreen</string>
<string name="reader_fab_download">Background download</string> <string name="reader_fab_download">Background download</string>
@@ -79,6 +106,8 @@
<string name="reader_notification_complete">Download complete</string> <string name="reader_notification_complete">Download complete</string>
<string name="reader_notification_error">Download error</string> <string name="reader_notification_error">Download error</string>
<string name="reader_help">Help</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="settings_app_version_title">App version</string> <string name="settings_app_version_title">App version</string>
<string name="settings_search_title">Search Settings</string> <string name="settings_search_title">Search Settings</string>
@@ -100,12 +129,16 @@
<string name="settings_use_hiyobi_summary">Load images from hiyobi.me to improve loading speed (if available)</string> <string name="settings_use_hiyobi_summary">Load images from hiyobi.me to improve loading speed (if available)</string>
<string name="settings_security_mode_title">Enable security mode</string> <string name="settings_security_mode_title">Enable security mode</string>
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string> <string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
<string name="settings_dark_mode_title">Dark mode</string>
<string name="settings_dark_mode_summary">Protect yourself against light attacks!</string>
<string name="settings_nomedia_title">Hide image from gallery</string>
<string name="settings_nomedia_summary">Hides image from gallery</string>
<string name="settings_lock_none">None</string> <string name="settings_lock_none">None</string>
<string name="settings_lock_pattern">Pattern</string> <string name="settings_lock_pattern">Pattern</string>
<string name="settings_lock_pin" translatable="false">PIN</string> <string name="settings_lock_pin" translatable="false">PIN</string>
<string name="settings_lock_password">Password</string> <string name="settings_lock_password">Password</string>
<string name="settings_lock_biomatrics">Biomatrics</string> <string name="settings_lock_biometrics">Biometrics</string>
<string name="settings_lock_fingerprint">Fingerprint</string> <string name="settings_lock_fingerprint">Fingerprint</string>
<string name="settings_lock_enabled">Enabled</string> <string name="settings_lock_enabled">Enabled</string>
<string name="settings_lock_confirm">Input same lock once more to confirm Lock</string> <string name="settings_lock_confirm">Input same lock once more to confirm Lock</string>

View File

@@ -1,14 +1,14 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
</style> </style>
<style name="NoActionBarAppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <style name="NoActionBarAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>

View File

@@ -15,7 +15,7 @@
app:key="lock_password"/> app:key="lock_password"/>
<PreferenceCategory <PreferenceCategory
app:title="@string/settings_lock_biomatrics"> app:title="@string/settings_lock_biometrics">
<Preference <Preference
app:title="@string/settings_lock_fingerprint" app:title="@string/settings_lock_fingerprint"

View File

@@ -20,7 +20,6 @@
<Preference <Preference
app:key="default_query" app:key="default_query"
app:title="@string/settings_default_query" app:title="@string/settings_default_query"
app:defaultValue=""
app:useSimpleSummaryProvider="true"/> app:useSimpleSummaryProvider="true"/>
</PreferenceCategory> </PreferenceCategory>
@@ -65,6 +64,15 @@
app:summary="@string/settings_security_mode_summary" app:summary="@string/settings_security_mode_summary"
app:defaultValue="true"/> app:defaultValue="true"/>
<SwitchPreference
app:key="dark_mode"
app:title="@string/settings_dark_mode_title"
app:summary="@string/settings_dark_mode_summary"/>
<SwitchPreference
app:key="nomedia"
app:title="@string/settings_nomedia_title"
app:summary="@string/settings_nomedia_title"/>
</PreferenceCategory> </PreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View File

@@ -1,6 +1,25 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@file:Suppress("UNUSED_VARIABLE")
package xyz.quaver.pupil package xyz.quaver.pupil
import kotlinx.serialization.ImplicitReflectionSerializer
import org.junit.Test import org.junit.Test
/** /**
@@ -13,7 +32,10 @@ class ExampleUnitTest {
@Test @Test
fun test() { fun test() {
val current = "0.1"
val latest = "0.2"
print(current < latest)
} }
} }

View File

@@ -1,24 +1,22 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.3.31' ext.kotlin_version = '1.3.50'
repositories { repositories {
google() google()
jcenter() jcenter()
maven { maven { url 'https://maven.fabric.io/public' }
url 'https://maven.fabric.io/public'
}
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.4.1' classpath 'com.android.tools.build:gradle:3.5.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.3.2'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath 'io.fabric.tools:gradle:1.29.0' classpath 'io.fabric.tools:gradle:1.29.0'
classpath 'com.google.firebase:perf-plugin:1.2.1' classpath 'com.google.firebase:perf-plugin:1.3.1'
} }
} }

View File

@@ -1,6 +1,6 @@
#Thu Apr 25 10:57:40 KST 2019 #Fri Aug 23 08:21:15 KST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

View File

@@ -14,7 +14,6 @@ dependencies {
sourceCompatibility = "7" sourceCompatibility = "7"
targetCompatibility = "7" targetCompatibility = "7"
buildscript { buildscript {
ext.kotlin_version = '1.3.31'
repositories { repositories {
mavenCentral() mavenCentral()
} }

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver
fun availableInHiyobi(galleryID: Int) : Boolean {
return try {
xyz.quaver.hiyobi.getReader(galleryID)
true
} catch (e: Exception) {
false
}
}

View File

@@ -1,10 +1,40 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import java.net.URL
const val protocol = "https:" const val protocol = "https:"
fun getGalleryInfo(galleryID: Int): List<GalleryInfo> {
return Json(JsonConfiguration.Stable).parse(
GalleryInfo.serializer().list,
Regex("""\[.+]""").find(
URL("$protocol//$domain/galleries/$galleryID.js").readText()
)?.value ?: "[]"
)
}
//common.js //common.js
var adapose = false var adapose = false
const val numberOfFrontends = 2 const val numberOfFrontends = 3
const val domain = "ltn.hitomi.la" const val domain = "ltn.hitomi.la"
const val galleryblockdir = "galleryblock" const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi" const val nozomiextension = ".nozomi"
@@ -21,20 +51,22 @@ fun subdomainFromGalleryID(g: Int) : String {
fun subdomainFromURL(url: String, base: String? = null) : String { fun subdomainFromURL(url: String, base: String? = null) : String {
var retval = "a" var retval = "a"
if (base != null) if (!base.isNullOrEmpty())
retval = base retval = base
val r = Regex("""/\d*(\d)/""") val r = Regex("""/galleries/\d*(\d)/""")
val m = r.find(url) var m = r.find(url)
var b = 10
m ?: return retval if (m == null) {
b = 16
val r2 = Regex("""/images/[0-9a-f]/([0-9a-f]{2})/""")
m = r2.find(url)
if (m == null)
return retval
}
var g = m.groups[1]!!.value.toIntOrNull() val g = m.groupValues[1].toIntOrNull(b) ?: return retval
g ?: return retval
if (g == 1)
g = 0
retval = subdomainFromGalleryID(g) + retval retval = subdomainFromGalleryID(g) + retval
@@ -42,5 +74,22 @@ fun subdomainFromURL(url: String, base: String? = null) : String {
} }
fun urlFromURL(url: String, base: String? = null) : String { fun urlFromURL(url: String, base: String? = null) : String {
return url.replace(Regex("//..?\\.hitomi\\.la/"), "//${subdomainFromURL(url, base)}.hitomi.la/") return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
} }
fun fullPathFromHash(hash: String?) : String? {
return when {
hash?.length ?: 0 < 3 -> hash
else -> hash!!.replace(Regex("^.*(..)(.)$"), "$2/$1/$hash")
}
}
fun urlFromHash(galleryID: Int, image: GalleryInfo, oldMethod: Boolean) : String {
return when {
oldMethod or image.hash.isNullOrEmpty() -> "$protocol//a.hitomi.la/galleries/$galleryID/${image.name}"
else -> "$protocol//a.hitomi.la/images/${fullPathFromHash(image.hash)}.${image.name.split('.').last()}"
}
}
fun urlFromUrlFromHash(galleryID: Int, image: GalleryInfo, oldMethod: Boolean = false) =
urlFromURL(urlFromHash(galleryID, image, oldMethod))

View File

@@ -1,12 +1,28 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hitomi package xyz.quaver.hitomi
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URL import java.net.URLDecoder
data class Gallery( data class Gallery(
val related: List<Int>, val related: List<Int>,
val langList: List<Pair<String, String>>, val langList: List<Pair<String, String>>,
val cover: URL, val cover: String,
val title: String, val title: String,
val artists: List<String>, val artists: List<String>,
val groups: List<String>, val groups: List<String>,
@@ -15,10 +31,11 @@ data class Gallery(
val series: List<String>, val series: List<String>,
val characters: List<String>, val characters: List<String>,
val tags: List<String>, val tags: List<String>,
val thumbnails: List<URL> val thumbnails: List<String>
) )
fun getGallery(galleryID: Int) : Gallery { fun getGallery(galleryID: Int) : Gallery {
val url = "https://hitomi.la/galleries/$galleryID.html" val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").get()
.select("a").attr("href")
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).get()
@@ -29,10 +46,10 @@ fun getGallery(galleryID: Int) : Gallery {
}.toList() }.toList()
val langList = doc.select("#lang-list a").map { val langList = doc.select("#lang-list a").map {
Pair(it.text(), it.attr("href").replace(".html", "")) Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
} }
val cover = URL(protocol + doc.selectFirst(".cover img").attr("src")) val cover = protocol + doc.selectFirst(".cover img").attr("src")
val title = doc.selectFirst(".gallery h1 a").text() val title = doc.selectFirst(".gallery h1 a").text()
val artists = doc.select(".gallery h2 a").map { it.text() } val artists = doc.select(".gallery h2 a").map { it.text() }
val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() } val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() }
@@ -47,15 +64,13 @@ fun getGallery(galleryID: Int) : Gallery {
val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() } val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() }
val tags = doc.select(".gallery-info a[href~=^/tag/]").map { val tags = doc.select(".gallery-info a[href~=^/tag/]").map {
val href = it.attr("href") val href = URLDecoder.decode(it.attr("href"), "UTF-8")
href.slice(5 until href.indexOf('-')) href.slice(5 until href.indexOf('-'))
} }
val thumbnails = Regex("'(//tn.hitomi.la/smalltn/\\d+/\\d+.+)',") val thumbnails = getGalleryInfo(galleryID).map {
.findAll(doc.select("script").last().html()) "$protocol//tn.hitomi.la/smalltn/$galleryID/${it.name}.jpg"
.map { }
URL(protocol + it.groups[1]!!.value)
}.toList()
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails) return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
} }

View File

@@ -1,8 +1,23 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jsoup.Jsoup import org.jsoup.Jsoup
import sun.rmi.runtime.Log
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -53,6 +68,7 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
@Serializable @Serializable
data class GalleryBlock( data class GalleryBlock(
val id: Int, val id: Int,
val galleryUrl: String,
val thumbnails: List<String>, val thumbnails: List<String>,
val title: String, val title: String,
val artists: List<String>, val artists: List<String>,
@@ -67,6 +83,8 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
try { try {
val doc = Jsoup.connect(url).get() val doc = Jsoup.connect(url).get()
val galleryUrl = doc.selectFirst(".lillie").attr("href")
val thumbnails = doc.select("img").map { protocol + it.attr("data-src") } val thumbnails = doc.select("img").map { protocol + it.attr("data-src") }
val title = doc.selectFirst("h1.lillie > a").text() val title = doc.selectFirst("h1.lillie > a").text()
@@ -84,7 +102,7 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
href.slice(5 until href.indexOf("-all")) href.slice(5 until href.indexOf("-all"))
} }
return GalleryBlock(galleryID, thumbnails, title, artists, series, type, language, relatedTags) return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hitomi
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
@Serializable
data class GalleryInfo(
val width: Int,
val hash: String?,
val haswebp: Int,
val name: String,
val height: Int
)
@Serializable
open class Reader(val title: String, val galleryInfo: List<GalleryInfo>)
//Set header `Referer` to reader url to avoid 403 error
fun getReader(galleryID: Int) : Reader {
val readerUrl = "https://hitomi.la/reader/$galleryID.html"
val doc = Jsoup.connect(readerUrl).get()
return Reader(doc.title(), getGalleryInfo(galleryID))
}

View File

@@ -1,57 +0,0 @@
package xyz.quaver.hitomi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import org.jsoup.Jsoup
import java.net.URL
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
@Serializable
data class GalleryInfo(
val width: Int,
val haswebp: Int,
val name: String,
val height: Int
)
@Serializable
data class ReaderItem(
val url: String,
val galleryInfo: GalleryInfo?
)
typealias Reader = List<ReaderItem>
//Set header `Referer` to reader url to avoid 403 error
fun getReader(galleryID: Int) : Reader {
val readerUrl = "https://hitomi.la/reader/$galleryID.html"
val galleryInfoUrl = "https://ltn.hitomi.la/galleries/$galleryID.js"
try {
val doc = Jsoup.connect(readerUrl).get()
val images = doc.select(".img-url").map {
protocol + urlFromURL(it.text())
}
val galleryInfo = ArrayList<GalleryInfo?>()
galleryInfo.addAll(
Json(JsonConfiguration.Stable).parse(
GalleryInfo.serializer().list,
Regex("""\[.+]""").find(
URL(galleryInfoUrl).readText()
)?.value ?: "[]"
)
)
if (images.size > galleryInfo.size)
galleryInfo.addAll(arrayOfNulls(images.size - galleryInfo.size))
return (images zip galleryInfo).map {
ReaderItem(it.first, it.second)
}
} catch (e: Exception) {
return emptyList()
}
}

View File

@@ -1,13 +1,28 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hitomi package xyz.quaver.hitomi
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
import java.util.concurrent.Executors
val searchDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() fun doSearch(query: String, sortByPopularity: Boolean = false) : List<Int> {
fun doSearch(query: String) : List<Int> {
val terms = query val terms = query
.trim() .trim()
.replace(Regex("""^\?"""), "") .replace(Regex("""^\?"""), "")
@@ -27,40 +42,50 @@ fun doSearch(query: String) : List<Int> {
positiveTerms.push(term) positiveTerms.push(term)
} }
val positiveResults = positiveTerms.map {
CoroutineScope(Dispatchers.IO).async {
getGalleryIDsForQuery(it)
}
}
val negativeResults = negativeTerms.map {
CoroutineScope(Dispatchers.IO).async {
getGalleryIDsForQuery(it)
}
}
var results = when { var results = when {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all") positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
else -> getGalleryIDsForQuery(positiveTerms.poll()) else -> listOf()
} }
runBlocking { runBlocking {
@Synchronized fun filterPositive(newResults: List<Int>) { @Synchronized fun filterPositive(newResults: List<Int>) {
results = results.filter { newResults.binarySearch(it) >= 0 } results = when {
results.isEmpty() -> newResults
else -> newResults.sorted().let { sorted ->
results.filter { sorted.binarySearch(it) >= 0 }
}
}
} }
@Synchronized fun filterNegative(newResults: List<Int>) { @Synchronized fun filterNegative(newResults: List<Int>) {
results = results.filter { newResults.binarySearch(it) < 0 } results = newResults.sorted().let { sorted ->
results.filter { sorted.binarySearch(it) < 0 }
}
} }
//positive results //positive results
positiveTerms.map { positiveResults.forEach {
launch(searchDispatcher) { filterPositive(it.await())
val newResults = getGalleryIDsForQuery(it)
filterPositive(newResults.sorted())
}
}.forEach {
it.join()
} }
//negative results //negative results
negativeTerms.map { negativeResults.forEach {
launch(searchDispatcher) { filterNegative(it.await())
filterNegative(getGalleryIDsForQuery(it).sorted())
}
}.forEach {
it.join()
} }
} }
return results return results
} }

View File

@@ -1,3 +1,19 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hitomi package xyz.quaver.hitomi
import java.net.URL import java.net.URL
@@ -158,20 +174,18 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
} }
try { try {
with (URL(nozomiAddress).openConnection() as HttpsURLConnection) { val bytes = URL(nozomiAddress).readBytes()
requestMethod = "GET"
val nozomi = ArrayList<Int>() val nozomi = ArrayList<Int>()
val arrayBuffer = ByteBuffer val arrayBuffer = ByteBuffer
.wrap(inputStream.readBytes()) .wrap(bytes)
.order(ByteOrder.BIG_ENDIAN) .order(ByteOrder.BIG_ENDIAN)
while (arrayBuffer.hasRemaining()) while (arrayBuffer.hasRemaining())
nozomi.add(arrayBuffer.int) nozomi.add(arrayBuffer.int)
return nozomi return nozomi
}
} catch (e: Exception) { } catch (e: Exception) {
return emptyList() return emptyList()
} }

View File

@@ -1,11 +1,28 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
package xyz.quaver.hiyobi package xyz.quaver.hiyobi
import kotlinx.io.IOException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.content import kotlinx.serialization.list
import org.jsoup.Jsoup
import xyz.quaver.hitomi.GalleryInfo
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.ReaderItem import xyz.quaver.hitomi.protocol
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -20,6 +37,12 @@ get() {
return field return field
} }
data class Images(
val path: String,
val no: Int,
val name: String
)
fun renewCookie() : String { fun renewCookie() : String {
val url = "https://$hiyobi/" val url = "https://$hiyobi/"
@@ -35,11 +58,14 @@ fun renewCookie() : String {
} }
} }
fun getReader(galleryId: Int) : Reader { fun getReader(galleryID: Int) : Reader {
val url = "https://$hiyobi/data/json/${galleryId}_list.json" val reader = "https://$hiyobi/reader/$galleryID"
val url = "https://$hiyobi/data/json/${galleryID}_list.json"
try { val title = Jsoup.connect(reader).get().title()
val json = Json(JsonConfiguration.Stable).parseJson(
val galleryInfo = Json(JsonConfiguration.Stable).parse(
GalleryInfo.serializer().list,
with(URL(url).openConnection() as HttpsURLConnection) { with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)
@@ -50,11 +76,8 @@ fun getReader(galleryId: Int) : Reader {
} }
) )
return json.jsonArray.map { return Reader(title, galleryInfo)
val name = it.jsonObject["name"]!!.content
ReaderItem("https://$hiyobi/data/$galleryId/$name", null)
}
} catch (e: Exception) {
return emptyList()
}
} }
fun createImgList(galleryID: Int, reader: Reader) =
reader.galleryInfo.map { Images("$protocol//$hiyobi/data/$galleryID/${it.name}", galleryID, it.name) }

View File

@@ -1,31 +1,40 @@
/*
* Copyright 2019 tom5079
*
* 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.
*/
@file:Suppress("UNUSED_VARIABLE")
package xyz.quaver.hitomi package xyz.quaver.hitomi
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.net.InetAddress
import java.net.UnknownHostException
class UnitTest { class UnitTest {
@Test @Test
fun test() { fun test() {
assertEquals(
} "6/2d/c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6",
fullPathFromHash("c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6")
private fun getByIp(host: String): InetAddress { )
try {
return InetAddress.getByName(host)
} catch (e: UnknownHostException) {
// unlikely
throw RuntimeException(e)
}
} }
@Test @Test
fun test_nozomi() { fun test_nozomi() {
val nozomi = fetchNozomi(start = 0, count = 5) val nozomi = getGalleryIDsFromNozomi(null, "popular", "all")
nozomi.first print(nozomi.size)
} }
@Test @Test
@@ -44,7 +53,7 @@ class UnitTest {
@Test @Test
fun test_doSearch() { fun test_doSearch() {
val r = doSearch("female:loli female:bondage language:korean -male:yaoi -male:guro -female:guro") val r = doSearch("female:loli female:bondage language:korean -male:yaoi -male:guro -female:guro", true)
print(r.size) print(r.size)
} }
@@ -58,22 +67,29 @@ class UnitTest {
@Test @Test
fun test_getGallery() { fun test_getGallery() {
val gallery = getGallery(1405267) val gallery = getGallery(1510566)
print(gallery) print(gallery)
} }
@Test @Test
fun test_getReader() { fun test_getReader() {
val reader = getReader(1404693) val reader = getReader(1442740)
print(reader) print(reader)
} }
@Test @Test
fun test_hiyobi() { fun test_hiyobi() {
xyz.quaver.hiyobi.getReader(1415416).forEach { xyz.quaver.hiyobi.getReader(1510567)
println(it.url) }
}
@Test
fun test_urlFromUrlFromHash() {
val url = urlFromUrlFromHash(1510702, GalleryInfo(
210, "56e9e1b8bb72194777ed93fee11b06070b905039dd11348b070bcf1793aaed7b", 1, "6.jpg", 300
))
print(url)
} }
} }