diff --git a/.idea/copyright/Apache.xml b/.idea/copyright/Apache.xml new file mode 100644 index 00000000..3c72b283 --- /dev/null +++ b/.idea/copyright/Apache.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/GPL.xml b/.idea/copyright/GPL.xml new file mode 100644 index 00000000..aff4b88e --- /dev/null +++ b/.idea/copyright/GPL.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..b1a28ee4 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 3e352b83..219eea45 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,7 +13,6 @@ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 146ab09b..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 84da703c..37a75096 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,9 @@ - - + + + + + \ No newline at end of file diff --git a/.idea/scopes/Pupil.xml b/.idea/scopes/Pupil.xml new file mode 100644 index 00000000..f607cbcf --- /dev/null +++ b/.idea/scopes/Pupil.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/scopes/libpupil.xml b/.idea/scopes/libpupil.xml new file mode 100644 index 00000000..9ad573e4 --- /dev/null +++ b/.idea/scopes/libpupil.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7f..35eb1ddf 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/LICENSE b/LICENSE index 01c46b58..ba08fc0c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ License is specified in following module separately app/ -libpupil/ +libpupil/ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 989ed597..ef3a25cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 "2.12" 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', 'true') + } } 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' diff --git a/app/release/output.json b/app/release/output.json index de58b687..e36241c5 100644 --- a/app/release/output.json +++ b/app/release/output.json @@ -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":{}}] \ No newline at end of file +[{"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":{}}] \ No newline at end of file diff --git a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt index f7decdb4..b283f4c0 100644 --- a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt @@ -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 . + */ + +@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::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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89e70b1e..08cc55a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,9 @@ package="xyz.quaver.pupil"> - + + + . + */ + 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 diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt index 68b23485..efbc0851 100644 --- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt +++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt @@ -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 . + */ + 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>) { + inner class GalleryViewHolder(val view: CardView) : RecyclerView.ViewHolder(view) { + fun bind(holder: GalleryViewHolder, item: Pair>) { with(view) { val resources = context.resources val languages = resources.getStringArray(R.array.languages).map { @@ -62,17 +80,15 @@ class GalleryBlockAdapter(private val galleries: List. + */ + 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) : RecyclerView.Adapter() { @@ -25,47 +43,19 @@ class ReaderAdapter(private val images: List) : RecyclerView.Adapter 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 + val progressDrawable = CircularProgressDrawable(holder.view.context).apply { + strokeWidth = 10f + centerRadius = 100f + start() } - 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 diff --git a/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt b/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt index 2b39db9f..f3795283 100644 --- a/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt +++ b/app/src/main/java/xyz/quaver/pupil/types/TagSuggestion.kt @@ -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 . + */ + 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 { diff --git a/app/src/main/java/xyz/quaver/pupil/types/Tags.kt b/app/src/main/java/xyz/quaver/pupil/types/Tags.kt index 70d0a119..8bf8011f 100644 --- a/app/src/main/java/xyz/quaver/pupil/types/Tags.kt +++ b/app/src/main/java/xyz/quaver/pupil/types/Tags.kt @@ -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 . + */ + package xyz.quaver.pupil.types import kotlinx.serialization.Serializable @@ -90,8 +108,8 @@ class Tags(tag: List?) : ArrayList() { } } - 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) } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/LockActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/LockActivity.kt index 57f9ed9a..81f7622d 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/LockActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/LockActivity.kt @@ -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 . + */ + package xyz.quaver.pupil.ui import android.app.Activity diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt index a19254a9..c4708b98 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -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 . + */ + 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 @@ -66,17 +91,25 @@ class MainActivity : AppCompatActivity() { DOWNLOAD, FAVORITE } + + enum class SortMode { + NEWEST, + POPULAR + } private val galleries = ArrayList>>() private var query = "" set(value) { field = value - findViewById(R.id.search_bar_text) - .setText(query, TextView.BufferType.EDITABLE) + with(findViewById(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(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) + R.id.main_menu_sort_newest -> { + sortMode = SortMode.NEWEST + it.isChecked = true - 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() - )) + runOnUiThread { + currentPage = 0 - setPositiveButton(android.R.string.ok) { _, _ -> - currentPage = (editText.text.toString().toIntOrNull() ?: return@setPositiveButton)-1 - - runOnUiThread { - cancelFetch() - clearGalleries() - fetchGalleries(query) - loadBlocks() - } - } - }.show() + cancelFetch() + clearGalleries() + fetchGalleries(query, sortMode) + loadBlocks() + } } - 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() - } - } - } - }.show() + cancelFetch() + clearGalleries() + fetchGalleries(query, sortMode) + loadBlocks() + } } } } 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,18 +970,12 @@ 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) - loadBlocks() - } + runOnUiThread { + cancelFetch() + clearGalleries() + currentPage = 0 + fetchGalleries(query, sortMode) + loadBlocks() } } }) @@ -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 { diff --git a/app/src/main/java/xyz/quaver/pupil/ui/PatternLockFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/PatternLockFragment.kt index 11d8a53b..f542a019 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/PatternLockFragment.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/PatternLockFragment.kt @@ -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 . + */ + package xyz.quaver.pupil.ui import android.os.Bundle diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt index e29b3111..3d3270ac 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt @@ -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 . + */ + 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() - 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,18 +342,24 @@ class ReaderActivity : AppCompatActivity() { } } - reader_fab_fullscreen.setOnClickListener { - isFullscreen = true - fullscreen(isFullscreen) + with(reader_fab_download) { + setImageResource(R.drawable.ic_download) + setOnClickListener { + downloader.download = !downloader.download - reader_fab.close(true) + if (!downloader.download) + downloader.clearNotification() + } } - reader_fab_download.setOnClickListener { - downloader.download = !downloader.download + with(reader_fab_fullscreen) { + setImageResource(R.drawable.ic_fullscreen) + setOnClickListener { + isFullscreen = true + fullscreen(isFullscreen) - if (!downloader.download) - downloader.clearNotification() + this@ReaderActivity.reader_fab.close(true) + } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt index e4229365..0d0bc35c 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt @@ -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 . + */ + 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).getPosition(tag) ) - tags.removeByArea("language") + tags.removeByArea("language", false) } } } diff --git a/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt b/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt index 9be00515..d78a565f 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt @@ -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 . + */ + 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,60 +97,64 @@ class GalleryDownloader( companion object : SparseArray() init { - put(galleryBlock.id, this) + put(galleryID, this) initNotification() reader = CoroutineScope(Dispatchers.IO).async { - download = _notify - val json = Json(JsonConfiguration.Stable) - val serializer = ReaderItem.serializer().list + try { + download = _notify + val json = Json(JsonConfiguration.Stable) + val serializer = Reader.serializer() - //Check cache - val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "reader.json") + //Check cache + val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json") - if (cache.exists()) { - val cached = json.parse(serializer, cache.readText()) + if (cache.exists()) { + val cached = json.parse(serializer, cache.readText()) - if (cached.isNotEmpty()) { - useHiyobi = when { - cached.first().url.contains("hitomi.la") -> false - else -> true + if (cached.readerItems.isNotEmpty()) { + useHiyobi = when { + cached.readerItems[0].url.contains("hitomi.la") -> false + else -> true + } + + onReaderLoadedHandler?.invoke(cached) + + return@async cached } - - onReaderLoadedHandler?.invoke(cached) - - return@async cached } - } - //Cache doesn't exist. Load from internet - val reader = when { - useHiyobi -> { - xyz.quaver.hiyobi.getReader(galleryBlock.id).let { - when { - it.isEmpty() -> { - useHiyobi = false - getReader(galleryBlock.id) + //Cache doesn't exist. Load from internet + val reader = when { + useHiyobi -> { + xyz.quaver.hiyobi.getReader(galleryID).let { + when { + it.readerItems.isEmpty() -> { + useHiyobi = false + 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 { 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() 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/file.kt b/app/src/main/java/xyz/quaver/pupil/util/file.kt index a1823442..07032896 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/file.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/file.kt @@ -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 . + */ + 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") diff --git a/app/src/main/java/xyz/quaver/pupil/util/history.kt b/app/src/main/java/xyz/quaver/pupil/util/history.kt index 1815b58a..665ccdfe 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/history.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/history.kt @@ -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 . + */ + package xyz.quaver.pupil.util import kotlinx.serialization.ImplicitReflectionSerializer @@ -11,7 +29,7 @@ class Histories(private val file: File) : ArrayList() { init { if (!file.exists()) - file.parentFile.mkdirs() + file.parentFile?.mkdirs() try { load() diff --git a/app/src/main/java/xyz/quaver/pupil/util/lock.kt b/app/src/main/java/xyz/quaver/pupil/util/lock.kt index 13f87003..15970b02 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/lock.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/lock.kt @@ -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 . + */ + package xyz.quaver.pupil.util import android.content.Context diff --git a/app/src/main/java/xyz/quaver/pupil/util/update.kt b/app/src/main/java/xyz/quaver/pupil/util/update.kt index 1461f75c..fec736d6 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -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 . + */ + 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 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 { - currentVersion.split('-').size == 1 -> { - when { - currentVersion != latestVersion -> releases[0].jsonObject - else -> null - } - } - else -> { - when { - (currentVersion.split('-')[0] == latestVersion) -> releases[0].jsonObject - else -> null - } - } +fun getApkUrl(releases: JsonObject) : Pair? { + 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 } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml index c95797c1..aa88433b 100644 --- a/app/src/main/res/drawable/ic_download.xml +++ b/app/src/main/res/drawable/ic_download.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_numeric.xml b/app/src/main/res/drawable/ic_numeric.xml deleted file mode 100644 index fc54c34a..00000000 --- a/app/src/main/res/drawable/ic_numeric.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/image_broken_variant.xml b/app/src/main/res/drawable/image_broken_variant.xml new file mode 100644 index 00000000..c4aa1d54 --- /dev/null +++ b/app/src/main/res/drawable/image_broken_variant.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/numeric.xml b/app/src/main/res/drawable/numeric.xml index fc54c34a..db07bdc4 100644 --- a/app/src/main/res/drawable/numeric.xml +++ b/app/src/main/res/drawable/numeric.xml @@ -4,5 +4,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/sort_variant.xml b/app/src/main/res/drawable/sort_variant.xml new file mode 100644 index 00000000..fdea2e49 --- /dev/null +++ b/app/src/main/res/drawable/sort_variant.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_content.xml b/app/src/main/res/layout/activity_main_content.xml index bde8ddf4..38049787 100644 --- a/app/src/main/res/layout/activity_main_content.xml +++ b/app/src/main/res/layout/activity_main_content.xml @@ -56,6 +56,30 @@ android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> + + + + + + + + @@ -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"/> diff --git a/app/src/main/res/layout/item_reader.xml b/app/src/main/res/layout/item_reader.xml index 9f5abe22..eb9909bd 100644 --- a/app/src/main/res/layout/item_reader.xml +++ b/app/src/main/res/layout/item_reader.xml @@ -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"/> \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 82a54109..b94c8378 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -2,15 +2,19 @@ - - - + + + + + + + + ロックが一致しません。やり直してください。 なし ロックを無効にしますか? + ロード中 + ソート + 投稿日時順 + 人気順 \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a1fe7f96..f8839501 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -54,7 +54,7 @@ hitomi.la에 연결할 수 없습니다 %1$d 페이지로 이동 접속 불가 현상 안내 - 최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다\n이 경우 플레이스토어에서 SNIper앱을 이용하시면 정상이용이 가능합니다. + 최근 https 차단으로 접속이 안 되는 경우가 발생하고 있습니다 이 경우 플레이스토어에서 Intra앱을 이용하시면 정상이용이 가능합니다. 갤러리 내보내기 내보내기 완료 폴더 열기 @@ -79,4 +79,8 @@ 잠금이 일치하지 않습니다. 다시 시도하세요. 없음 잠금을 해제할까요? + 로딩중 + 정렬 + 인기순 + 시간순 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a2738d3..76341623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Pupil - https://api.github.com/repos/tom5079/Pupil-issue/releases + https://api.github.com/repos/tom5079/Pupil/releases Pupil-v(\\d+\\.)+\\d+\\.apk http://bit.ly/2EZDClw @@ -45,6 +45,10 @@ Email me! Kakaotalk + Sort + Newest + Popular + Jump to page Current page: %1$d\nMaximum page: %2$d Open Gallery by ID @@ -71,6 +75,7 @@ Type: %1$s Language: %1$s + Loading Go to page Fullscreen Background download diff --git a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt index c4a08536..fae6b731 100644 --- a/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt +++ b/app/src/test/java/xyz/quaver/pupil/ExampleUnitTest.kt @@ -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 . + */ + +@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) } } diff --git a/build.gradle b/build.gradle index 8695ee94..548c220f 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/common.kt b/libpupil/src/main/java/xyz/quaver/hitomi/common.kt index bc3c181b..9e35af1d 100644 --- a/libpupil/src/main/java/xyz/quaver/hitomi/common.kt +++ b/libpupil/src/main/java/xyz/quaver/hitomi/common.kt @@ -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:" diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt b/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt index ffbe2a98..3f292dca 100644 --- a/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt +++ b/libpupil/src/main/java/xyz/quaver/hitomi/galleries.kt @@ -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 diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt b/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt index 3139cfa7..4196d98e 100644 --- a/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt +++ b/libpupil/src/main/java/xyz/quaver/hitomi/galleryblock.kt @@ -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 diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/reader.kt b/libpupil/src/main/java/xyz/quaver/hitomi/reader.kt new file mode 100644 index 00000000..1b1d6b08 --- /dev/null +++ b/libpupil/src/main/java/xyz/quaver/hitomi/reader.kt @@ -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) + +//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.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) + }) +} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/readers.kt b/libpupil/src/main/java/xyz/quaver/hitomi/readers.kt deleted file mode 100644 index 0be173e9..00000000 --- a/libpupil/src/main/java/xyz/quaver/hitomi/readers.kt +++ /dev/null @@ -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 -//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.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() - } -} \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/results.kt b/libpupil/src/main/java/xyz/quaver/hitomi/results.kt index 0142dd2d..ee51d067 100644 --- a/libpupil/src/main/java/xyz/quaver/hitomi/results.kt +++ b/libpupil/src/main/java/xyz/quaver/hitomi/results.kt @@ -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 { +fun doSearch(query: String, sortByPopularity: Boolean = false) : List { val terms = query .trim() .replace(Regex("""^\?"""), "") @@ -27,7 +42,20 @@ fun doSearch(query: String) : List { 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 { } //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 - } \ No newline at end of file diff --git a/libpupil/src/main/java/xyz/quaver/hitomi/search.kt b/libpupil/src/main/java/xyz/quaver/hitomi/search.kt index 833c826c..5b08b1df 100644 --- a/libpupil/src/main/java/xyz/quaver/hitomi/search.kt +++ b/libpupil/src/main/java/xyz/quaver/hitomi/search.kt @@ -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() + val bytes = inputStream.readBytes() + val arrayBuffer = ByteBuffer - .wrap(inputStream.readBytes()) + .wrap(bytes) .order(ByteOrder.BIG_ENDIAN) while (arrayBuffer.hasRemaining()) diff --git a/libpupil/src/main/java/xyz/quaver/hiyobi/reader.kt b/libpupil/src/main/java/xyz/quaver/hiyobi/reader.kt index a20de850..fb9a5459 100644 --- a/libpupil/src/main/java/xyz/quaver/hiyobi/reader.kt +++ b/libpupil/src/main/java/xyz/quaver/hiyobi/reader.kt @@ -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" -var cookie: String = "" -get() { - if (field.isEmpty()) - field = renewCookie() +class HiyobiReader(title: String, readerItems: List) : Reader(title, readerItems) - return field -} +var cookie: String = "" + get() { + if (field.isEmpty()) + field = renewCookie() + + return field + } fun renewCookie() : String { val url = "https://$hiyobi/" @@ -35,26 +53,25 @@ 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" - try { - val json = Json(JsonConfiguration.Stable).parseJson( - with(URL(url).openConnection() as HttpsURLConnection) { - setRequestProperty("User-Agent", user_agent) - setRequestProperty("Cookie", cookie) - connectTimeout = 2000 - connect() + val title = Jsoup.connect(reader).get().title() - 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 { - val name = it.jsonObject["name"]!!.content - ReaderItem("https://$hiyobi/data/$galleryId/$name", null) + inputStream.bufferedReader().use { it.readText() } } - } catch (e: Exception) { - return emptyList() - } + ) + + return Reader(title, json.jsonArray.map { + val name = it.jsonObject["name"]!!.content + ReaderItem("https://$hiyobi/data/$galleryID/$name", null) + }) } \ No newline at end of file diff --git a/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt b/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt index 1b864d91..851ce929 100644 --- a/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt +++ b/libpupil/src/test/java/xyz/quaver/hitomi/UnitTest.kt @@ -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) - } + } } \ No newline at end of file