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>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</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"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/classes" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</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"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,5 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.gms.google-services'
@@ -12,8 +13,8 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 20
versionName "2.11.1"
versionCode 21
versionName "3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -23,6 +24,9 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
buildTypes.each {
it.buildConfigField('boolean', 'PRERELEASE', 'false')
}
}
kotlinOptions {
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
@@ -52,8 +56,13 @@ dependencies {
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
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 "ru.noties.markwon:core:${markwonVersion}"
kapt 'com.github.bumptech.glide:compiler:4.9.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
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
import android.content.Intent
@@ -36,7 +56,7 @@ class ExampleInstrumentedTest {
@Test
fun checkCacheDir() {
val activityTestRule = ActivityTestRule<LockActivity>(LockActivity::class.java)
val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
activityTestRule.launchActivity(Intent())
@@ -50,7 +70,7 @@ class ExampleInstrumentedTest {
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("Cookie", cookie)

View File

@@ -3,7 +3,9 @@
package="xyz.quaver.pupil">
<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
android:name=".Pupil"
@@ -14,6 +16,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".ui.LockActivity"/>
<activity
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
import android.app.DownloadManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
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
import android.app.AlertDialog
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.util.Log
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
@@ -15,6 +32,8 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
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 kotlinx.android.synthetic.main.item_galleryblock.view.*
import kotlinx.coroutines.CoroutineScope
@@ -23,9 +42,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.ReaderItem
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tag
@@ -47,8 +65,8 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
private lateinit var favorites: Histories
inner class GalleryViewHolder(private val view: CardView) : RecyclerView.ViewHolder(view) {
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
inner class GalleryViewHolder(val view: CardView) : RecyclerView.ViewHolder(view) {
fun bind(holder: GalleryViewHolder, item: Pair<GalleryBlock, Deferred<String>>) {
with(view) {
val resources = context.resources
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 series = galleryBlock.series
CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Main).launch {
val cache = thumbnail.await()
if (!File(cache).exists())
return@launch
val bitmap = BitmapFactory.decodeFile(thumbnail.await())
launch(Dispatchers.Main) {
galleryblock_thumbnail.setImageBitmap(bitmap)
}
Glide.with(holder.view)
.load(cache)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.error(R.drawable.image_broken_variant)
.into(galleryblock_thumbnail)
}
//Check cache
@@ -81,10 +97,10 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
if (readerCache.invoke().exists()) {
val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.invoke().readText())
.parse(Reader.serializer(), readerCache.invoke().readText())
with(galleryblock_progressbar) {
max = reader.size
max = reader.readerItems.size
progress = imageCache.invoke().list()?.size ?: 0
visibility = View.VISIBLE
@@ -108,8 +124,8 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
} else {
if (visibility == View.GONE) {
val reader = Json(JsonConfiguration.Stable)
.parse(ReaderItem.serializer().list, readerCache.invoke().readText())
max = reader.size
.parse(Reader.serializer(), readerCache.invoke().readText())
max = reader.readerItems.size
visibility = View.VISIBLE
}
@@ -334,7 +350,7 @@ class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferre
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
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) {

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
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
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
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) {
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
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
}
val progressDrawable = CircularProgressDrawable(holder.view.context).apply {
strokeWidth = 10f
centerRadius = 100f
start()
}
return inSampleSize
}
with(holder.view as ImageView) {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(images[position], options)
val (reqWidth, reqHeight) = context.resources.displayMetrics.let {
Pair(it.widthPixels, it.heightPixels)
}
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inPreferredConfig = Bitmap.Config.RGB_565
options.inJustDecodeBounds = false
val image = BitmapFactory.decodeFile(images[position], options)
setImageBitmap(image)
}
Glide.with(holder.view)
.load(images[position])
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.placeholder(progressDrawable)
.error(R.drawable.image_broken_variant)
.into(holder.view as ImageView)
}
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
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
@@ -5,7 +23,7 @@ import kotlinx.android.parcel.Parcelize
import xyz.quaver.hitomi.Suggestion
@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)
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
import kotlinx.serialization.Serializable
@@ -90,8 +108,8 @@ class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
}
}
fun removeByArea(area: String) {
filter { it.area == area }.forEach {
fun removeByArea(area: String, isNegative: Boolean? = null) {
filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
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
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
import android.Manifest
import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.*
import android.text.style.AlignmentSpan
import android.view.*
@@ -19,6 +42,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat
import androidx.preference.PreferenceManager
@@ -41,7 +65,6 @@ import kotlinx.serialization.list
import kotlinx.serialization.stringify
import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.*
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
@@ -55,6 +78,8 @@ import java.net.URL
import java.util.*
import javax.net.ssl.HttpsURLConnection
import kotlin.collections.ArrayList
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.min
import kotlin.math.roundToInt
@@ -67,16 +92,24 @@ class MainActivity : AppCompatActivity() {
FAVORITE
}
enum class SortMode {
NEWEST,
POPULAR
}
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
private var query = ""
set(value) {
field = value
findViewById<SearchInputView>(R.id.search_bar_text)
.setText(query, TextView.BufferType.EDITABLE)
with(findViewById<SearchInputView>(R.id.search_bar_text)) {
if (text.toString() != value)
setText(query, TextView.BufferType.EDITABLE)
}
}
private var mode = Mode.SEARCH
private var sortMode = SortMode.NEWEST
private val REQUEST_SETTINGS = 45162
private val REQUEST_LOCK = 561
@@ -132,7 +165,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
else -> super.onBackPressed()
@@ -155,7 +188,7 @@ class MainActivity : AppCompatActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
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) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
@@ -165,7 +198,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
@@ -179,7 +212,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
@@ -197,7 +230,7 @@ class MainActivity : AppCompatActivity() {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
@@ -254,14 +287,36 @@ class MainActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Default).launch {
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 {
setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault().language)
setMessage(Markwon.create(context).toMarkdown(msg))
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) { _, _ ->}
}
@@ -284,6 +339,13 @@ class MainActivity : AppCompatActivity() {
main_searchview.translationY = p1.toFloat()
main_recyclerview.scrollBy(0, prevP1 - p1)
with(main_fab) {
if (prevP1 > p1)
hideMenuButton(true)
else if (prevP1 < p1)
showMenuButton(true)
}
prevP1 = p1
}
)
@@ -300,7 +362,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0
query = ""
mode = Mode.SEARCH
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_history -> {
@@ -309,7 +371,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0
query = ""
mode = Mode.HISTORY
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_downloads -> {
@@ -318,7 +380,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0
query = ""
mode = Mode.DOWNLOAD
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_favorite -> {
@@ -327,7 +389,7 @@ class MainActivity : AppCompatActivity() {
currentPage = 0
query = ""
mode = Mode.FAVORITE
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
R.id.main_drawer_help -> {
@@ -351,9 +413,67 @@ class MainActivity : AppCompatActivity() {
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()
setupRecyclerView()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
@@ -367,7 +487,7 @@ class MainActivity : AppCompatActivity() {
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
@@ -380,9 +500,9 @@ class MainActivity : AppCompatActivity() {
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
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)
histories.add(gallery.id)
@@ -391,7 +511,7 @@ class MainActivity : AppCompatActivity() {
if (v !is CardView)
return@setOnItemLongClickListener true
val galleryBlock = galleries[position].first
val gallery = galleries[position].first
val view = LayoutInflater.from(this@MainActivity)
.inflate(R.layout.dialog_galleryblock, recyclerView, false)
@@ -400,15 +520,15 @@ class MainActivity : AppCompatActivity() {
}.create()
with(view.main_dialog_download) {
text = when(GalleryDownloader.get(galleryBlock.id)) {
text = when(GalleryDownloader.get(gallery.id)) {
null -> getString(R.string.reader_fab_download)
else -> getString(R.string.reader_fab_download_cancel)
}
isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(galleryBlock.id, false)
isEnabled = !(adapter as GalleryBlockAdapter).completeFlag.get(gallery.id, false)
setOnClickListener {
val downloader = GalleryDownloader.get(galleryBlock.id)
val downloader = GalleryDownloader.get(gallery.id)
if (downloader == null)
GalleryDownloader(context, galleryBlock, true).start()
GalleryDownloader(context, gallery.id, true).start()
else {
downloader.cancel()
downloader.clearNotification()
@@ -420,27 +540,27 @@ class MainActivity : AppCompatActivity() {
view.main_dialog_delete.setOnClickListener {
CoroutineScope(Dispatchers.Default).launch {
with(GalleryDownloader[galleryBlock.id]) {
with(GalleryDownloader[gallery.id]) {
this?.cancelAndJoin()
this?.clearNotification()
}
val cache = File(cacheDir, "imageCache/${galleryBlock.id}")
val data = getCachedGallery(context, galleryBlock.id)
val cache = File(cacheDir, "imageCache/${gallery.id}")
val data = getCachedGallery(context, gallery.id)
cache.deleteRecursively()
data.deleteRecursively()
downloads.remove(galleryBlock.id)
downloads.remove(gallery.id)
if (mode == Mode.DOWNLOAD) {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
(adapter as GalleryBlockAdapter).completeFlag.put(galleryBlock.id, false)
(adapter as GalleryBlockAdapter).completeFlag.put(gallery.id, false)
}
dialog.dismiss()
}
@@ -503,7 +623,6 @@ class MainActivity : AppCompatActivity() {
runOnUiThread {
cancelFetch()
clearGalleries()
fetchGalleries(query)
loadBlocks()
}
@@ -583,7 +702,7 @@ class MainActivity : AppCompatActivity() {
//BOTTOM
//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) {
if(!showNext) {
showNext = true
@@ -595,7 +714,7 @@ class MainActivity : AppCompatActivity() {
getChildAt(childCount-1)
}
val absDist = Math.abs(dist)
val absDist = abs(dist)
if (next is LinearLayout) {
val icon = next.findViewById<ImageView>(R.id.icon_next)
@@ -690,72 +809,52 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener {
when(it.itemId) {
R.id.main_menu_settings -> startActivityForResult(Intent(this@MainActivity, SettingsActivity::class.java), REQUEST_SETTINGS)
R.id.main_menu_jump -> {
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,
Math.ceil(totalItems / perPage.toDouble()).roundToInt()
))
setPositiveButton(android.R.string.ok) { _, _ ->
currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1
R.id.main_menu_sort_newest -> {
sortMode = SortMode.NEWEST
it.isChecked = true
runOnUiThread {
currentPage = 0
cancelFetch()
clearGalleries()
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}.show()
}
R.id.main_menu_id -> {
val editText = EditText(context)
R.id.main_menu_sort_popular -> {
sortMode = SortMode.POPULAR
it.isChecked = true
AlertDialog.Builder(context).apply {
setView(editText)
setTitle(R.string.main_open_gallery_by_id)
runOnUiThread {
currentPage = 0
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(
"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()
cancelFetch()
clearGalleries()
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}
}.show()
}
}
}
setOnQueryChangeListener { _, query ->
clearSuggestions()
if (query.isEmpty() or query.endsWith(' '))
return@setOnQueryChangeListener
val currentQuery = query.split(" ").last().replace('_', ' ')
this@MainActivity.query = query
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 {
val suggestions = ArrayList(getSuggestionsForQuery(currentQuery).map { TagSuggestion(it) })
@@ -774,13 +873,14 @@ class MainActivity : AppCompatActivity() {
}
setOnBindSuggestionCallback { suggestionView, leftIcon, textView, item, _ ->
val suggestion = item as TagSuggestion
val tag = "${suggestion.n}:${suggestion.s.replace(Regex("\\s"), "_")}"
item as TagSuggestion
val tag = "${item.n}:${item.s.replace(Regex("\\s"), "_")}"
leftIcon.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
when(suggestion.n) {
when(item.n) {
"female" -> R.drawable.ic_gender_female
"male" -> R.drawable.ic_gender_male
"language" -> R.drawable.ic_translate
@@ -800,7 +900,6 @@ class MainActivity : AppCompatActivity() {
else
setImageResource(R.drawable.ic_star_empty)
visibility = View.VISIBLE
rotation = 0f
isEnabled = true
@@ -827,13 +926,13 @@ class MainActivity : AppCompatActivity() {
}
}
if (suggestion.t == -1) {
textView.text = suggestion.s
if (item.t == -1) {
textView.text = item.s
} else {
val text = "${suggestion.s}\n ${suggestion.t}"
val text = "${item.s}\n ${item.t}"
val len = text.length
val left = suggestion.s.length
val left = item.s.length
textView.text = SpannableString(text).apply {
val s = AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)
@@ -846,14 +945,13 @@ class MainActivity : AppCompatActivity() {
setOnSearchListener(object : FloatingSearchView.OnSearchListener {
override fun onSuggestionClicked(searchSuggestion: SearchSuggestion?) {
val suggestion = searchSuggestion as TagSuggestion
if (searchSuggestion !is TagSuggestion)
return
with(searchInputView.text) {
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?) {
@@ -863,7 +961,7 @@ class MainActivity : AppCompatActivity() {
setOnFocusChangeListener(object: FloatingSearchView.OnFocusChangeListener {
override fun onFocus() {
if (searchInputView.text.isEmpty())
if (query.isEmpty() or query.endsWith(' '))
swapSuggestions(json.parse(serializer, favoritesFile.readText()).map {
TagSuggestion(it.tag, -1, "", it.area ?: "tag")
})
@@ -872,20 +970,14 @@ class MainActivity : AppCompatActivity() {
override fun onFocusCleared() {
suggestionJob?.cancel()
val query = searchInputView.text.toString()
if (query != this@MainActivity.query) {
this@MainActivity.query = query
runOnUiThread {
cancelFetch()
clearGalleries()
currentPage = 0
fetchGalleries(query)
fetchGalleries(query, sortMode)
loadBlocks()
}
}
}
})
attachNavigationDrawerToMenuButton(main_drawer_layout)
@@ -912,9 +1004,8 @@ class MainActivity : AppCompatActivity() {
main_progressbar.show()
}
private fun fetchGalleries(query: String) {
private fun fetchGalleries(query: String, sortMode: SortMode) {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
val defaultQuery = preference.getString("default_query", "")!!
galleryIDs = null
@@ -927,12 +1018,14 @@ class MainActivity : AppCompatActivity() {
Mode.SEARCH -> {
when {
query.isEmpty() and defaultQuery.isEmpty() -> {
fetchNozomi(start = currentPage*perPage, count = perPage).let {
totalItems = it.second
it.first
when(sortMode) {
SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all")
else -> getGalleryIDsFromNozomi(null, "index", "all")
}.apply {
totalItems = size
}
}
else -> doSearch("$defaultQuery $query").apply {
else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply {
totalItems = size
}
}
@@ -985,7 +1078,6 @@ class MainActivity : AppCompatActivity() {
private fun loadBlocks() {
val preference = PreferenceManager.getDefaultSharedPreferences(this)
val perPage = preference.getString("per_page", "25")?.toInt() ?: 25
val defaultQuery = preference.getString("default_query", "")!!
loadingJob = CoroutineScope(Dispatchers.IO).launch {
val galleryIDs = galleryIDs?.await()
@@ -999,12 +1091,7 @@ class MainActivity : AppCompatActivity() {
return@launch
}
when {
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 ->
galleryIDs.slice(currentPage*perPage until min(currentPage*perPage+perPage, galleryIDs.size)).chunked(5).let { chunks ->
for (chunk in chunks)
chunk.map { galleryID ->
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
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
import android.content.Intent
@@ -21,13 +39,7 @@ import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.io.IOException
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.R
import xyz.quaver.pupil.adapters.ReaderAdapter
@@ -37,8 +49,8 @@ import xyz.quaver.pupil.util.ItemClickSupport
class ReaderActivity : AppCompatActivity() {
private var galleryID = 0
private val images = ArrayList<String>()
private lateinit var galleryBlock: GalleryBlock
private var gallerySize = 0
private var currentPage = 0
@@ -66,6 +78,9 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getString(R.string.reader_loading)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
favorites = (application as Pupil).favorites
window.setFlags(
@@ -76,16 +91,13 @@ class ReaderActivity : AppCompatActivity() {
handleIntent(intent)
Crashlytics.setInt("GalleryID", galleryBlock.id)
Crashlytics.setInt("GalleryID", galleryID)
if (!::galleryBlock.isInitialized) {
if (galleryID == 0) {
onBackPressed()
return
}
supportActionBar?.title = galleryBlock.title
supportActionBar?.setDisplayHomeAsUpEnabled(false)
initDownloader()
initView()
@@ -106,25 +118,16 @@ class ReaderActivity : AppCompatActivity() {
if (uri != null && lastPathSegment != null) {
val nonNumber = Regex("[^-?0-9]+")
val galleryID = when (uri.host) {
galleryID = when (uri.host) {
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
"히요비.asia" -> lastPathSegment.toInt()
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
"e-hentai.org" -> uri.pathSegments[1].toInt()
else -> return
}
runBlocking {
CoroutineScope(Dispatchers.IO).launch {
galleryBlock = getGalleryBlock(galleryID) ?: return@launch
}.join()
}
}
} else {
galleryBlock = Json(JsonConfiguration.Stable).parse(
GalleryBlock.serializer(),
intent.getStringExtra("galleryblock")!!
)
galleryID = intent.getIntExtra("galleryID", 0)
}
}
@@ -148,7 +151,7 @@ class ReaderActivity : AppCompatActivity() {
with(menu?.findItem(R.id.reader_menu_favorite)) {
this ?: return@with
if (favorites.contains(galleryBlock.id))
if (favorites.contains(galleryID))
(icon as Animatable).start()
}
@@ -176,7 +179,7 @@ class ReaderActivity : AppCompatActivity() {
dialog.show()
}
R.id.reader_menu_favorite -> {
val id = galleryBlock.id
val id = galleryID
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
if (favorites.contains(id)) {
@@ -215,32 +218,26 @@ class ReaderActivity : AppCompatActivity() {
}
private fun initDownloader() {
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id)
var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
if (d == null) {
try {
d = GalleryDownloader(this, galleryBlock)
} catch (e: IOException) {
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
finish()
return
}
}
if (d == null)
d = GalleryDownloader(this, galleryID)
downloader = d.apply {
onReaderLoadedHandler = {
CoroutineScope(Dispatchers.Main).launch {
title = it.title
with(reader_download_progressbar) {
max = it.size
max = it.readerItems.size
progress = 0
}
with(reader_progressbar) {
max = it.size
max = it.readerItems.size
progress = 0
}
gallerySize = it.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}"
gallerySize = it.readerItems.size
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.readerItems.size}"
}
}
onProgressHandler = {
@@ -262,8 +259,7 @@ class ReaderActivity : AppCompatActivity() {
}
}
onErrorHandler = {
if (it is IOException)
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
Snackbar.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE).show()
downloader.download = false
}
onCompleteHandler = {
@@ -317,6 +313,11 @@ class ReaderActivity : AppCompatActivity() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
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
if (layoutManager.findFirstVisibleItemPosition() == -1)
@@ -341,14 +342,9 @@ class ReaderActivity : AppCompatActivity() {
}
}
reader_fab_fullscreen.setOnClickListener {
isFullscreen = true
fullscreen(isFullscreen)
reader_fab.close(true)
}
reader_fab_download.setOnClickListener {
with(reader_fab_download) {
setImageResource(R.drawable.ic_download)
setOnClickListener {
downloader.download = !downloader.download
if (!downloader.download)
@@ -356,6 +352,17 @@ class ReaderActivity : AppCompatActivity() {
}
}
with(reader_fab_fullscreen) {
setImageResource(R.drawable.ic_fullscreen)
setOnClickListener {
isFullscreen = true
fullscreen(isFullscreen)
this@ReaderActivity.reader_fab.close(true)
}
}
}
private fun fullscreen(isFullscreen: Boolean) {
with(window.attributes) {
if (isFullscreen) {

View File

@@ -1,3 +1,21 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui
import android.app.Activity
@@ -222,14 +240,14 @@ class SettingsActivity : AppCompatActivity() {
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]
if (tag != null) {
setSelection(
@Suppress("UNCHECKED_CAST")
(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
import android.app.PendingIntent
@@ -13,12 +31,13 @@ import kotlinx.coroutines.*
import kotlinx.io.IOException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import xyz.quaver.hitomi.*
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReader
import xyz.quaver.hitomi.getReferer
import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.R
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.FileOutputStream
@@ -30,7 +49,7 @@ import kotlin.concurrent.schedule
class GalleryDownloader(
base: Context,
private val galleryBlock: GalleryBlock,
private val galleryID: Int,
_notify: Boolean = false
) : ContextWrapper(base) {
@@ -41,10 +60,10 @@ class GalleryDownloader(
set(value) {
if (value) {
field = true
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
notificationManager.notify(galleryID, notificationBuilder.build())
val data = getCachedGallery(this, galleryBlock.id)
val cache = File(cacheDir, "imageCache/${galleryBlock.id}")
val data = getCachedGallery(this, galleryID)
val cache = File(cacheDir, "imageCache/$galleryID")
if (File(cache, "images").exists() && !data.exists()) {
cache.copyRecursively(data, true)
@@ -54,7 +73,7 @@ class GalleryDownloader(
if (reader?.isActive == false && downloadJob?.isActive != true)
field = false
downloads.add(galleryBlock.id)
downloads.add(galleryID)
} else {
field = false
}
@@ -78,24 +97,25 @@ class GalleryDownloader(
companion object : SparseArray<GalleryDownloader>()
init {
put(galleryBlock.id, this)
put(galleryID, this)
initNotification()
reader = CoroutineScope(Dispatchers.IO).async {
try {
download = _notify
val json = Json(JsonConfiguration.Stable)
val serializer = ReaderItem.serializer().list
val serializer = Reader.serializer()
//Check cache
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "reader.json")
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
if (cache.exists()) {
val cached = json.parse(serializer, cache.readText())
if (cached.isNotEmpty()) {
if (cached.readerItems.isNotEmpty()) {
useHiyobi = when {
cached.first().url.contains("hitomi.la") -> false
cached.readerItems[0].url.contains("hitomi.la") -> false
else -> true
}
@@ -108,22 +128,22 @@ class GalleryDownloader(
//Cache doesn't exist. Load from internet
val reader = when {
useHiyobi -> {
xyz.quaver.hiyobi.getReader(galleryBlock.id).let {
xyz.quaver.hiyobi.getReader(galleryID).let {
when {
it.isEmpty() -> {
it.readerItems.isEmpty() -> {
useHiyobi = false
getReader(galleryBlock.id)
getReader(galleryID)
}
else -> it
}
}
}
else -> {
getReader(galleryBlock.id)
getReader(galleryID)
}
}
if (reader.isNotEmpty()) {
if (reader.readerItems.isNotEmpty()) {
//Save cache
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
@@ -132,6 +152,9 @@ class GalleryDownloader(
}
reader
} catch (e: Exception) {
Reader("", listOf())
}
}
}
@@ -141,37 +164,30 @@ class GalleryDownloader(
downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await()
if (reader.isEmpty())
onErrorHandler?.invoke(IOException("Couldn't retrieve Reader"))
if (reader.readerItems.isEmpty()) {
onErrorHandler?.invoke(IOException(getString(R.string.unable_to_connect)))
return@launch
}
val list = ArrayList<String>()
onReaderLoadedHandler?.invoke(reader)
notificationBuilder
.setProgress(reader.size, 0, false)
.setContentText("0/${reader.size}")
.setProgress(reader.readerItems.size, 0, false)
.setContentText("0/${reader.readerItems.size}")
reader.chunked(4).forEachIndexed { chunkIndex, chunked ->
reader.readerItems.chunked(4).forEachIndexed { chunkIndex, chunked ->
chunked.mapIndexed { i, it ->
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) {
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
val name = "$index".padStart(4, '0')
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())
try {
@@ -180,7 +196,7 @@ class GalleryDownloader(
setRequestProperty("User-Agent", user_agent)
setRequestProperty("Cookie", cookie)
} else
setRequestProperty("Referer", getReferer(galleryBlock.id))
setRequestProperty("Referer", getReferer(galleryID))
if (cache.parentFile?.exists() == false)
cache.parentFile!!.mkdirs()
@@ -193,31 +209,43 @@ class GalleryDownloader(
onErrorHandler?.invoke(e)
notificationBuilder
.setContentTitle(galleryBlock.title)
.setContentTitle(reader.title)
.setContentText(getString(R.string.reader_notification_error))
.setProgress(0, 0, false)
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
notificationManager.notify(galleryID, notificationBuilder.build())
}
cache.absolutePath
}
}.forEach {
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)
}
}
Timer(false).schedule(1000) {
notificationBuilder
.setContentTitle(galleryBlock.title)
.setContentTitle(reader.title)
.setContentText(getString(R.string.reader_notification_complete))
.setProgress(0, 0, false)
if (download) {
File(cacheDir, "imageCache/${galleryBlock.id}").let {
File(cacheDir, "imageCache/${galleryID}").let {
if (it.exists()) {
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryBlock.id.toString())
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
if (!target.exists())
target.mkdirs()
@@ -227,7 +255,7 @@ class GalleryDownloader(
}
}
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
notificationManager.notify(galleryID, notificationBuilder.build())
download = false
}
@@ -235,20 +263,20 @@ class GalleryDownloader(
onCompleteHandler?.invoke()
}
remove(galleryBlock.id)
remove(galleryID)
}
}
fun cancel() {
downloadJob?.cancel()
remove(galleryBlock.id)
remove(galleryID)
}
suspend fun cancelAndJoin() {
downloadJob?.cancelAndJoin()
remove(galleryBlock.id)
remove(galleryID)
}
fun invokeOnReaderLoaded() {
@@ -258,7 +286,7 @@ class GalleryDownloader(
}
fun clearNotification() {
notificationManager.cancel(galleryBlock.id)
notificationManager.cancel(galleryID)
}
fun invokeOnNotifyChanged() {
@@ -267,22 +295,28 @@ class GalleryDownloader(
private fun initNotification() {
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 {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
notificationManager = NotificationManagerCompat.from(this)
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
setContentTitle(galleryBlock.title)
setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_download)
setContentIntent(pendingIntent)
setProgress(0, 0, true)
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
import android.content.Context
@@ -16,6 +34,7 @@ fun getCachedGallery(context: Context, galleryID: Int): File {
}
}
@Suppress("DEPRECATION")
fun getDownloadDirectory(context: Context): File? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
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
import kotlinx.serialization.ImplicitReflectionSerializer
@@ -11,7 +29,7 @@ class Histories(private val file: File) : ArrayList<Int>() {
init {
if (!file.exists())
file.parentFile.mkdirs()
file.parentFile?.mkdirs()
try {
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
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
import android.util.Log
import kotlinx.serialization.json.*
import xyz.quaver.pupil.BuildConfig
import java.net.URL
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)
if (releases.isEmpty())
return null
val latestVersion = releases[0].jsonObject["tag_name"]?.content
return when {
currentVersion.split('-').size == 1 -> {
when {
currentVersion != latestVersion -> releases[0].jsonObject
else -> null
}
}
else -> {
when {
(currentVersion.split('-')[0] == latestVersion) -> releases[0].jsonObject
else -> null
}
}
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
}
fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
releases["assets"]?.jsonArray?.forEach {
if (Regex("Pupil-v(\\d+\\.)+\\d+\\.apk").matches(it.jsonObject["name"]?.content ?: ""))
return Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
}
return null
}

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#fff"
android:viewportHeight="24.0" android:viewportWidth="24.0"
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>

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: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" />
<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>

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"
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>
<com.arlib.floatingsearchview.FloatingSearchView

View File

@@ -39,6 +39,7 @@
android:layout_width="match_parent"
android:layout_height="4dp"
android:progressTint="@color/material_green_a700"
tools:ignore="UnusedAttribute"
android:visibility="gone"/>
</LinearLayout>
@@ -55,7 +56,6 @@
android:id="@+id/reader_fab_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_downloading"
app:fab_label="@string/reader_fab_download"
app:fab_size="mini"/>
@@ -63,7 +63,6 @@
android:id="@+id/reader_fab_fullscreen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_fullscreen"
app:fab_label="@string/reader_fab_fullscreen"
app:fab_size="mini"/>

View File

@@ -3,6 +3,7 @@
android:contentDescription="@string/reader_imageview_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:paddingBottom="8dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>

View File

@@ -2,15 +2,19 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/main_menu_jump"
android:icon="@drawable/ic_jump"
android:title="@string/main_jump_title"
app:showAsAction="ifRoom"/>
<item android:id="@+id/main_menu_id"
android:icon="@drawable/ic_numeric"
android:title="@string/main_open_gallery_by_id"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/main_menu_sort"
android:title="@string/main_menu_sort">
<menu>
<group android:checkableBehavior="single">
<item android:id="@+id/main_menu_sort_newest"
android:title="@string/main_menu_sort_newest"
android:checked="true"/>
<item android:id="@+id/main_menu_sort_popular"
android:title="@string/main_menu_sort_popular"/>
</group>
</menu>
</item>
<item
android:id="@+id/main_menu_settings"

View File

@@ -79,4 +79,8 @@
<string name="settings_lock_wrong_confirm">ロックが一致しません。やり直してください。</string>
<string name="settings_lock_none">なし</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>

View File

@@ -54,7 +54,7 @@
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
<string name="main_move">%1$d 페이지로 이동</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_export_complete">내보내기 완료</string>
<string name="main_export_open_folder">폴더 열기</string>
@@ -79,4 +79,8 @@
<string name="settings_lock_wrong_confirm">잠금이 일치하지 않습니다. 다시 시도하세요.</string>
<string name="settings_lock_none">없음</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>

View File

@@ -1,7 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<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="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_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_message">Current page: %1$d\nMaximum page: %2$d</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_language">Language: %1$s</string>
<string name="reader_loading">Loading</string>
<string name="reader_go_to_page">Go to page</string>
<string name="reader_fab_fullscreen">Fullscreen</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
import kotlinx.serialization.ImplicitReflectionSerializer
import org.junit.Test
/**
@@ -13,7 +32,10 @@ class ExampleUnitTest {
@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.
buildscript {
ext.kotlin_version = '1.3.31'
ext.kotlin_version = '1.3.41'
repositories {
google()
jcenter()
@@ -14,7 +14,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$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
// in the individual module build.gradle files
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
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
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
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
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import java.util.*
import java.util.concurrent.Executors
val searchDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
fun doSearch(query: String) : List<Int> {
fun doSearch(query: String, sortByPopularity: Boolean = false) : List<Int> {
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
@@ -27,7 +42,20 @@ fun doSearch(query: String) : List<Int> {
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 {
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
else -> getGalleryIDsForQuery(positiveTerms.poll())
}
@@ -42,25 +70,19 @@ fun doSearch(query: String) : List<Int> {
}
//positive results
positiveTerms.map {
launch(searchDispatcher) {
val newResults = getGalleryIDsForQuery(it)
filterPositive(newResults.sorted())
}
}.forEach {
it.join()
positiveResults.forEach {
val result = it.await()
filterPositive(result.sorted())
}
//negative results
negativeTerms.map {
launch(searchDispatcher) {
filterNegative(getGalleryIDsForQuery(it).sorted())
}
}.forEach {
it.join()
negativeResults.forEach {
val result = it.await()
filterNegative(result.sorted())
}
}
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
import java.io.ByteArrayOutputStream
import java.net.URL
import java.nio.ByteBuffer
import java.nio.ByteOrder
@@ -163,8 +180,10 @@ fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List
val nozomi = ArrayList<Int>()
val bytes = inputStream.readBytes()
val arrayBuffer = ByteBuffer
.wrap(inputStream.readBytes())
.wrap(bytes)
.order(ByteOrder.BIG_ENDIAN)
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
import kotlinx.io.IOException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.content
import org.jsoup.Jsoup
import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.ReaderItem
import java.net.URL
@@ -12,13 +28,15 @@ import javax.net.ssl.HttpsURLConnection
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"
class HiyobiReader(title: String, readerItems: List<ReaderItem>) : Reader(title, readerItems)
var cookie: String = ""
get() {
get() {
if (field.isEmpty())
field = renewCookie()
return field
}
}
fun renewCookie() : String {
val url = "https://$hiyobi/"
@@ -35,10 +53,12 @@ fun renewCookie() : String {
}
}
fun getReader(galleryId: Int) : Reader {
val url = "https://$hiyobi/data/json/${galleryId}_list.json"
fun getReader(galleryID: Int) : Reader {
val reader = "https://$hiyobi/reader/$galleryID"
val url = "https://$hiyobi/data/json/${galleryID}_list.json"
val title = Jsoup.connect(reader).get().title()
try {
val json = Json(JsonConfiguration.Stable).parseJson(
with(URL(url).openConnection() as HttpsURLConnection) {
setRequestProperty("User-Agent", user_agent)
@@ -50,11 +70,8 @@ fun getReader(galleryId: Int) : Reader {
}
)
return json.jsonArray.map {
return Reader(title, json.jsonArray.map {
val name = it.jsonObject["name"]!!.content
ReaderItem("https://$hiyobi/data/$galleryId/$name", null)
}
} catch (e: Exception) {
return emptyList()
}
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
import org.junit.Test
import java.net.InetAddress
import java.net.UnknownHostException
class UnitTest {
@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
fun test_nozomi() {
val nozomi = fetchNozomi(start = 0, count = 5)
val nozomi = getGalleryIDsFromNozomi(null, "popular", "all")
nozomi.first
print(nozomi.size)
}
@Test
@@ -44,7 +49,7 @@ class UnitTest {
@Test
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)
}
@@ -72,8 +77,6 @@ class UnitTest {
@Test
fun test_hiyobi() {
xyz.quaver.hiyobi.getReader(1415416).forEach {
println(it.url)
}
}
}