Compare commits

..

12 Commits

Author SHA1 Message Date
tom5079
474d3ad80a Merge pull request #15 from tom5079/development
Version fix
2019-07-07 15:26:57 +09:00
tom5079
69f9b099b7 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
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
9ac7fb490e 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
8410a2fdb3 Update LICENSE 2019-07-03 20:44:57 +09:00
tom5079
dca6ba457b 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
7e87bb6838 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
52 changed files with 1078 additions and 498 deletions

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>

7
.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_7" 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,4 +1,4 @@
License is specified in following module separately License is specified in following module separately
app/ app/
libpupil/ libpupil/

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 21
versionName "2.11.1" versionName "3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -23,6 +24,9 @@ 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')
}
} }
kotlinOptions { kotlinOptions {
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental' freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
@@ -52,8 +56,13 @@ dependencies {
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":21,"versionName":"2.12","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
@@ -36,7 +56,7 @@ class ExampleInstrumentedTest {
@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 +70,7 @@ class ExampleInstrumentedTest {
val data: ByteArray val data: ByteArray
with(URL(reader[0].url).openConnection() as HttpsURLConnection) { with(URL(reader.readerItems[0].url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie) setRequestProperty("Cookie", cookie)

View File

@@ -3,7 +3,9 @@
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"/>
<application <application
android:name=".Pupil" android:name=".Pupil"
@@ -14,6 +16,7 @@
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">
<activity android:name=".ui.LockActivity"/> <activity android:name=".ui.LockActivity"/>
<activity <activity
android:name=".ui.ReaderActivity" android:name=".ui.ReaderActivity"

View File

@@ -1,9 +1,30 @@
/*
* 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.DownloadManager
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat

View File

@@ -1,9 +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.adapters package xyz.quaver.pupil.adapters
import android.app.AlertDialog import android.app.AlertDialog
import android.graphics.BitmapFactory 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
@@ -15,6 +32,8 @@ 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.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
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,9 +42,8 @@ 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.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
@@ -47,8 +65,8 @@ 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: CardView) : RecyclerView.ViewHolder(view) {
fun bind(item: Pair<GalleryBlock, Deferred<String>>) { fun bind(holder: GalleryViewHolder, item: Pair<GalleryBlock, Deferred<String>>) {
with(view) { with(view) {
val resources = context.resources val resources = context.resources
val languages = resources.getStringArray(R.array.languages).map { val languages = resources.getStringArray(R.array.languages).map {
@@ -62,17 +80,15 @@ 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.with(holder.view)
return@launch .load(cache)
.skipMemoryCache(true)
val bitmap = BitmapFactory.decodeFile(thumbnail.await()) .diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
launch(Dispatchers.Main) { .into(galleryblock_thumbnail)
galleryblock_thumbnail.setImageBitmap(bitmap)
}
} }
//Check cache //Check cache
@@ -81,10 +97,10 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
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.readerItems.size
progress = imageCache.invoke().list()?.size ?: 0 progress = imageCache.invoke().list()?.size ?: 0
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -108,8 +124,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.readerItems.size
visibility = View.VISIBLE visibility = View.VISIBLE
} }
@@ -334,7 +350,7 @@ 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)]) holder.bind(holder, galleries[position-(if (showPrev) 1 else 0)])
} }
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {

View File

@@ -1,13 +1,31 @@
/*
* 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 androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
class ReaderAdapter(private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() { class ReaderAdapter(private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
@@ -25,47 +43,19 @@ class ReaderAdapter(private val images: List<String>) : RecyclerView.Adapter<Rea
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val progressDrawable = CircularProgressDrawable(holder.view.context).apply {
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { strokeWidth = 10f
// Raw height and width of image centerRadius = 100f
val (height: Int, width: Int) = options.run { outHeight to outWidth } start()
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// 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) { Glide.with(holder.view)
val options = BitmapFactory.Options() .load(images[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
options.inJustDecodeBounds = true .skipMemoryCache(true)
BitmapFactory.decodeFile(images[position], options) .placeholder(progressDrawable)
.error(R.drawable.image_broken_variant)
val (reqWidth, reqHeight) = context.resources.displayMetrics.let { .into(holder.view as ImageView)
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)
}
} }
override fun getItemCount() = images.size override fun getItemCount() = images.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

@@ -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

View File

@@ -1,12 +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.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.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
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.*
@@ -19,6 +42,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@@ -41,7 +65,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 +78,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
@@ -66,17 +91,25 @@ class MainActivity : AppCompatActivity() {
DOWNLOAD, DOWNLOAD,
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
@@ -132,7 +165,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
@@ -155,7 +188,7 @@ 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 -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
@@ -165,7 +198,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -179,7 +212,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -197,7 +230,7 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -254,14 +287,36 @@ 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)))) 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?) {
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)
}
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
} }
setNegativeButton(android.R.string.no) { _, _ ->} setNegativeButton(android.R.string.no) { _, _ ->}
} }
@@ -284,6 +339,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 +362,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 +371,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 +380,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 +389,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,9 +413,67 @@ 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()
fetchGalleries(query, sortMode)
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)
} 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()
} }
@@ -367,7 +487,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
@@ -380,9 +500,9 @@ class MainActivity : AppCompatActivity() {
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)
@@ -391,7 +511,7 @@ class MainActivity : AppCompatActivity() {
if (v !is CardView) if (v !is CardView)
return@setOnItemLongClickListener true return@setOnItemLongClickListener true
val galleryBlock = galleries[position].first val gallery = galleries[position].first
val view = LayoutInflater.from(this@MainActivity) val view = LayoutInflater.from(this@MainActivity)
.inflate(R.layout.dialog_galleryblock, recyclerView, false) .inflate(R.layout.dialog_galleryblock, recyclerView, false)
@@ -400,15 +520,15 @@ class MainActivity : AppCompatActivity() {
}.create() }.create()
with(view.main_dialog_download) { with(view.main_dialog_download) {
text = when(GalleryDownloader.get(galleryBlock.id)) { text = when(GalleryDownloader.get(gallery.id)) {
null -> getString(R.string.reader_fab_download) null -> getString(R.string.reader_fab_download)
else -> getString(R.string.reader_fab_download_cancel) else -> getString(R.string.reader_fab_download_cancel)
} }
isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(galleryBlock.id, false) isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(gallery.id, false)
setOnClickListener { setOnClickListener {
val downloader = GalleryDownloader.get(galleryBlock.id) val downloader = GalleryDownloader.get(gallery.id)
if (downloader == null) if (downloader == null)
GalleryDownloader(context, galleryBlock, true).start() GalleryDownloader(context, gallery.id, true).start()
else { else {
downloader.cancel() downloader.cancel()
downloader.clearNotification() downloader.clearNotification()
@@ -420,27 +540,27 @@ class MainActivity : AppCompatActivity() {
view.main_dialog_delete.setOnClickListener { view.main_dialog_delete.setOnClickListener {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryBlock.id]) { with(GalleryDownloader[gallery.id]) {
this?.cancelAndJoin() this?.cancelAndJoin()
this?.clearNotification() this?.clearNotification()
} }
val cache = File(cacheDir, "imageCache/${galleryBlock.id}") val cache = File(cacheDir, "imageCache/${gallery.id}")
val data = getCachedGallery(context, galleryBlock.id) val data = getCachedGallery(context, gallery.id)
cache.deleteRecursively() cache.deleteRecursively()
data.deleteRecursively() data.deleteRecursively()
downloads.remove(galleryBlock.id) downloads.remove(gallery.id)
if (mode == Mode.DOWNLOAD) { if (mode == Mode.DOWNLOAD) {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query) fetchGalleries(query, sortMode)
loadBlocks() loadBlocks()
} }
} }
(adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false) (adapter as GalleryBlockAdapter).completeFlag.put(gallery.id, false)
} }
dialog.dismiss() dialog.dismiss()
} }
@@ -503,7 +623,6 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
cancelFetch() cancelFetch()
clearGalleries() clearGalleries()
fetchGalleries(query)
loadBlocks() loadBlocks()
} }
@@ -583,7 +702,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,7 +714,7 @@ 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)
@@ -690,72 +809,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 { runOnUiThread {
setView(editText) currentPage = 0
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) { _, _ -> cancelFetch()
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 clearGalleries()
fetchGalleries(query, sortMode)
runOnUiThread { loadBlocks()
cancelFetch() }
clearGalleries()
fetchGalleries(query)
loadBlocks()
}
}
}.show()
} }
R.id.main_menu_id -> { R.id.main_menu_sort_popular -> {
val editText = EditText(context) sortMode = SortMode.POPULAR
it.isChecked = true
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 +873,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
@@ -800,7 +900,6 @@ class MainActivity : AppCompatActivity() {
else else
setImageResource(R.drawable.ic_star_empty) setImageResource(R.drawable.ic_star_empty)
visibility = View.VISIBLE
rotation = 0f rotation = 0f
isEnabled = true isEnabled = true
@@ -827,13 +926,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 +945,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 +961,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,18 +970,12 @@ class MainActivity : AppCompatActivity() {
override fun onFocusCleared() { override fun onFocusCleared() {
suggestionJob?.cancel() suggestionJob?.cancel()
val query = searchInputView.text.toString() runOnUiThread {
cancelFetch()
if (query != this@MainActivity.query) { clearGalleries()
this@MainActivity.query = query currentPage = 0
fetchGalleries(query, sortMode)
runOnUiThread { loadBlocks()
cancelFetch()
clearGalleries()
currentPage = 0
fetchGalleries(query)
loadBlocks()
}
} }
} }
}) })
@@ -912,9 +1004,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 +1018,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 +1078,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 +1091,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

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.content.Intent import android.content.Intent
@@ -21,13 +39,7 @@ 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
@@ -37,8 +49,8 @@ import xyz.quaver.pupil.util.ItemClickSupport
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 +78,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 +91,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 +118,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 +151,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 +179,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)) {
@@ -215,32 +218,26 @@ class ReaderActivity : AppCompatActivity() {
} }
private fun initDownloader() { private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id) var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
if (d == null) { if (d == null)
try { d = GalleryDownloader(this, galleryID)
d = GalleryDownloader(this, galleryBlock)
} catch (e: IOException) {
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
finish()
return
}
}
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.readerItems.size
progress = 0 progress = 0
} }
with(reader_progressbar) { with(reader_progressbar) {
max = it.size max = it.readerItems.size
progress = 0 progress = 0
} }
gallerySize = it.size gallerySize = it.readerItems.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}" menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.readerItems.size}"
} }
} }
onProgressHandler = { onProgressHandler = {
@@ -262,8 +259,7 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
onErrorHandler = { onErrorHandler = {
if (it is IOException) Snackbar.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE).show()
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
downloader.download = false downloader.download = false
} }
onCompleteHandler = { onCompleteHandler = {
@@ -317,6 +313,11 @@ class ReaderActivity : AppCompatActivity() {
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)
@@ -341,18 +342,24 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
reader_fab_fullscreen.setOnClickListener { with(reader_fab_download) {
isFullscreen = true setImageResource(R.drawable.ic_download)
fullscreen(isFullscreen) setOnClickListener {
downloader.download = !downloader.download
reader_fab.close(true) if (!downloader.download)
downloader.clearNotification()
}
} }
reader_fab_download.setOnClickListener { with(reader_fab_fullscreen) {
downloader.download = !downloader.download setImageResource(R.drawable.ic_fullscreen)
setOnClickListener {
isFullscreen = true
fullscreen(isFullscreen)
if (!downloader.download) this@ReaderActivity.reader_fab.close(true)
downloader.clearNotification() }
} }
} }

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
@@ -222,14 +240,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)
} }
} }
} }

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
@@ -13,12 +31,13 @@ import kotlinx.coroutines.*
import kotlinx.io.IOException 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.hitomi.Reader
import xyz.quaver.hitomi.* import xyz.quaver.hitomi.getReader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
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 +49,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,10 +60,10 @@ 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) val data = getCachedGallery(this, galleryID)
val cache = File(cacheDir, "imageCache/${galleryBlock.id}") 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)
@@ -54,7 +73,7 @@ class GalleryDownloader(
if (reader?.isActive == false && downloadJob?.isActive != true) if (reader?.isActive == false && downloadJob?.isActive != true)
field = false field = false
downloads.add(galleryBlock.id) downloads.add(galleryID)
} else { } else {
field = false field = false
} }
@@ -78,60 +97,64 @@ 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 {
download = _notify try {
val json = Json(JsonConfiguration.Stable) download = _notify
val serializer = ReaderItem.serializer().list val json = Json(JsonConfiguration.Stable)
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")
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.readerItems.isNotEmpty()) {
useHiyobi = when { useHiyobi = when {
cached.first().url.contains("hitomi.la") -> false cached.readerItems[0].url.contains("hitomi.la") -> false
else -> true else -> true
}
onReaderLoadedHandler?.invoke(cached)
return@async cached
} }
onReaderLoadedHandler?.invoke(cached)
return@async cached
} }
}
//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 { xyz.quaver.hiyobi.getReader(galleryID).let {
when { when {
it.isEmpty() -> { it.readerItems.isEmpty() -> {
useHiyobi = false useHiyobi = false
getReader(galleryBlock.id) getReader(galleryID)
}
else -> it
} }
else -> it
} }
} }
else -> {
getReader(galleryID)
}
} }
else -> {
getReader(galleryBlock.id) if (reader.readerItems.isNotEmpty()) {
//Save cache
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, reader))
} }
reader
} catch (e: Exception) {
Reader("", listOf())
} }
if (reader.isNotEmpty()) {
//Save cache
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
cache.writeText(json.stringify(serializer, reader))
}
reader
} }
} }
@@ -141,37 +164,30 @@ class GalleryDownloader(
downloadJob = CoroutineScope(Dispatchers.Default).launch { downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await() val reader = reader!!.await()
if (reader.isEmpty()) if (reader.readerItems.isEmpty()) {
onErrorHandler?.invoke(IOException("Couldn't retrieve Reader")) onErrorHandler?.invoke(IOException(getString(R.string.unable_to_connect)))
return@launch
}
val list = ArrayList<String>() val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader) onReaderLoadedHandler?.invoke(reader)
notificationBuilder notificationBuilder
.setProgress(reader.size, 0, false) .setProgress(reader.readerItems.size, 0, false)
.setContentText("0/${reader.size}") .setContentText("0/${reader.readerItems.size}")
reader.chunked(4).forEachIndexed { chunkIndex, chunked -> reader.readerItems.chunked(4).forEachIndexed { chunkIndex, chunked ->
chunked.mapIndexed { i, it -> chunked.mapIndexed { i, it ->
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 = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
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 +196,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 +209,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 cache.absolutePath
} }
}.forEach { }.forEach {
list.add(it.await()) list.add(it.await())
val index = list.size
onProgressHandler?.invoke(index)
notificationBuilder
.setProgress(reader.readerItems.size, index, false)
.setContentText("$index/${reader.readerItems.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 +255,7 @@ class GalleryDownloader(
} }
} }
notificationManager.notify(galleryBlock.id, notificationBuilder.build()) notificationManager.notify(galleryID, notificationBuilder.build())
download = false download = false
} }
@@ -235,20 +263,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 +286,7 @@ class GalleryDownloader(
} }
fun clearNotification() { fun clearNotification() {
notificationManager.cancel(galleryBlock.id) notificationManager.cancel(galleryID)
} }
fun invokeOnNotifyChanged() { fun invokeOnNotifyChanged() {
@@ -267,22 +295,28 @@ 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(R.drawable.ic_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)
CoroutineScope(Dispatchers.Default).launch {
while (reader == null) ;
notificationBuilder.setContentTitle(reader.await().title)
}
} }
} }

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
@@ -16,6 +34,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

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,27 @@ 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) {
BuildConfig.VERSION_NAME != it.jsonObject["tag_name"]?.content
} else {
it.jsonObject["prerelease"]?.boolean == false &&
BuildConfig.VERSION_NAME != (it.jsonObject["tag_name"]?.content ?: "")
}
}?.jsonObject
}
return when { fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
currentVersion.split('-').size == 1 -> { releases["assets"]?.jsonArray?.forEach {
when { if (Regex("Pupil-v(\\d+\\.)+\\d+\\.apk").matches(it.jsonObject["name"]?.content ?: ""))
currentVersion != latestVersion -> releases[0].jsonObject return Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
else -> null
}
}
else -> {
when {
(currentVersion.split('-')[0] == latestVersion) -> releases[0].jsonObject
else -> null
}
}
} }
return null
} }

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,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

@@ -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

@@ -56,6 +56,30 @@
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

View File

@@ -39,6 +39,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 +56,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 +63,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

@@ -3,6 +3,7 @@
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

@@ -2,15 +2,19 @@
<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

@@ -79,4 +79,8 @@
<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>
</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>
@@ -79,4 +79,8 @@
<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>
</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>
@@ -45,6 +45,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>
@@ -71,6 +75,7 @@
<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>

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,7 +1,7 @@
// 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.41'
repositories { repositories {
google() google()
jcenter() jcenter()
@@ -14,7 +14,7 @@ buildscript {
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.0'
// 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'

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
const val protocol = "https:" const val protocol = "https:"

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 org.jsoup.Jsoup import org.jsoup.Jsoup

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 kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,88 @@
/*
* 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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import org.jsoup.Jsoup
import xyz.quaver.hiyobi.HiyobiReader
import java.net.URL
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
fun webpReaderFromReader(reader: Reader) : Reader {
if (reader is HiyobiReader)
return reader
return Reader(reader.title, reader.readerItems.map {
ReaderItem(
if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url,
it.galleryInfo
)
})
}
@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?
)
@Serializable
open class Reader(val title: String, val readerItems: 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"
val doc = Jsoup.connect(readerUrl).get()
val title = doc.title()
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 Reader(title, (images zip galleryInfo).map {
ReaderItem(it.first, it.second)
})
}

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,7 +42,20 @@ 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 -> getGalleryIDsForQuery(positiveTerms.poll())
} }
@@ -42,25 +70,19 @@ fun doSearch(query: String) : List<Int> {
} }
//positive results //positive results
positiveTerms.map { positiveResults.forEach {
launch(searchDispatcher) { val result = it.await()
val newResults = getGalleryIDsForQuery(it)
filterPositive(newResults.sorted()) filterPositive(result.sorted())
}
}.forEach {
it.join()
} }
//negative results //negative results
negativeTerms.map { negativeResults.forEach {
launch(searchDispatcher) { val result = it.await()
filterNegative(getGalleryIDsForQuery(it).sorted())
} filterNegative(result.sorted())
}.forEach {
it.join()
} }
} }
return results return results
} }

View File

@@ -1,5 +1,22 @@
/*
* 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.io.ByteArrayOutputStream
import java.net.URL import java.net.URL
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@@ -163,8 +180,10 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
val nozomi = ArrayList<Int>() val nozomi = ArrayList<Int>()
val bytes = inputStream.readBytes()
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())

View File

@@ -1,9 +1,25 @@
/*
* 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.json.content
import org.jsoup.Jsoup
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.ReaderItem import xyz.quaver.hitomi.ReaderItem
import java.net.URL import java.net.URL
@@ -12,13 +28,15 @@ import javax.net.ssl.HttpsURLConnection
const val hiyobi = "xn--9w3b15m8vo.asia" const val hiyobi = "xn--9w3b15m8vo.asia"
const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"
var cookie: String = "" class HiyobiReader(title: String, readerItems: List<ReaderItem>) : Reader(title, readerItems)
get() {
if (field.isEmpty())
field = renewCookie()
return field var cookie: String = ""
} get() {
if (field.isEmpty())
field = renewCookie()
return field
}
fun renewCookie() : String { fun renewCookie() : String {
val url = "https://$hiyobi/" val url = "https://$hiyobi/"
@@ -35,26 +53,25 @@ 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(
with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
connectTimeout = 2000
connect()
inputStream.bufferedReader().use { it.readText() } val json = Json(JsonConfiguration.Stable).parseJson(
} with(URL(url).openConnection() as HttpsURLConnection) {
) setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
connectTimeout = 2000
connect()
return json.jsonArray.map { inputStream.bufferedReader().use { it.readText() }
val name = it.jsonObject["name"]!!.content
ReaderItem("https://$hiyobi/data/$galleryId/$name", null)
} }
} catch (e: Exception) { )
return emptyList()
} return Reader(title, json.jsonArray.map {
val name = it.jsonObject["name"]!!.content
ReaderItem("https://$hiyobi/data/$galleryID/$name", null)
})
} }

View File

@@ -1,9 +1,24 @@
/*
* 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.Test import org.junit.Test
import java.net.InetAddress
import java.net.UnknownHostException
class UnitTest { class UnitTest {
@Test @Test
@@ -11,21 +26,11 @@ class UnitTest {
} }
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 +49,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)
} }
@@ -72,8 +77,6 @@ class UnitTest {
@Test @Test
fun test_hiyobi() { fun test_hiyobi() {
xyz.quaver.hiyobi.getReader(1415416).forEach {
println(it.url)
}
} }
} }