Compare commits
21 Commits
4.3-hotfix
...
5.3-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
384e6c61b0 | ||
|
|
d49c9cec20 | ||
|
|
4b27f1aba1 | ||
|
|
a0a989c785 | ||
|
|
ecaecc1b91 | ||
|
|
938156aa71 | ||
|
|
d30c51bb3a | ||
|
|
874606bff9 | ||
|
|
07643e4b4c | ||
|
|
48f90faf4e | ||
|
|
615b52c4fa | ||
|
|
01a653835e | ||
|
|
9d80857a38 | ||
|
|
8a9ab6b36c | ||
|
|
4edc87c197 | ||
|
|
10712e6e62 | ||
|
|
d73dc19d3d | ||
|
|
c204353220 | ||
|
|
37123a2cd5 | ||
|
|
a39484b6ea | ||
|
|
9ea55664b6 |
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="RIGHT_MARGIN" value="120" />
|
||||||
<AndroidXmlCodeStyleSettings>
|
<AndroidXmlCodeStyleSettings>
|
||||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||||
</AndroidXmlCodeStyleSettings>
|
</AndroidXmlCodeStyleSettings>
|
||||||
|
|||||||
7
.idea/kotlinCodeInsightSettings.xml
generated
Normal file
7
.idea/kotlinCodeInsightSettings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinCodeInsightWorkspaceSettings">
|
||||||
|
<option name="addUnambiguousImportsOnTheFly" value="true" />
|
||||||
|
<option name="optimizeImportsOnTheFly" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Kotlin2JvmCompilerArguments">
|
||||||
|
<option name="jvmTarget" value="1.8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,6 +2,5 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/gh-pages" vcs="Git" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 32
|
versionCode 33
|
||||||
versionName "4.3-hotfix1"
|
versionName "5.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@@ -41,6 +41,9 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
buildToolsVersion = '29.0.2'
|
buildToolsVersion = '29.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +73,11 @@ dependencies {
|
|||||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||||
implementation 'com.github.clans:fab:1.6.4'
|
implementation 'com.github.clans:fab:1.6.4'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
||||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") {
|
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
|
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
|
||||||
|
implementation 'com.gu:option:1.3'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":32,"versionName":"4.3-hotfix1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":33,"versionName":"5.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||||
@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
@@ -37,6 +38,8 @@ import xyz.quaver.hiyobi.createImgList
|
|||||||
import xyz.quaver.hiyobi.getReader
|
import xyz.quaver.hiyobi.getReader
|
||||||
import xyz.quaver.hiyobi.user_agent
|
import xyz.quaver.hiyobi.user_agent
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
import xyz.quaver.pupil.ui.LockActivity
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
import xyz.quaver.pupil.util.updateOldReaderGalleries
|
import xyz.quaver.pupil.util.updateOldReaderGalleries
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -118,4 +121,37 @@ class ExampleInstrumentedTest {
|
|||||||
|
|
||||||
updateOldReaderGalleries(context)
|
updateOldReaderGalleries(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_downloadWorker() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
val galleryID = 515515
|
||||||
|
|
||||||
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
|
||||||
|
worker.queue.add(galleryID)
|
||||||
|
|
||||||
|
while(worker.progress.indexOfKey(galleryID) < 0 || worker.progress[galleryID] != null) {
|
||||||
|
Log.i("PUPILD", worker.progress[galleryID]?.joinToString(" ") ?: "null")
|
||||||
|
|
||||||
|
if (worker.progress[galleryID]?.all { !it.isFinite() } == true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("PUPILD", "DONE!!")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_getReaderOrNull() {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|
||||||
|
val galleryID = 1561552
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
Log.i("PUPILD", Cache(context).getReader(galleryID)?.title ?: "null")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("PUPILD", Cache(context).getReaderOrNull(galleryID)?.title ?: "null")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="21" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Pupil"
|
android:name=".Pupil"
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -22,6 +22,7 @@ import android.app.Notification
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -30,17 +31,12 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import xyz.quaver.pupil.util.updateOldReaderGalleries
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class Pupil : MultiDexApplication() {
|
class Pupil : MultiDexApplication() {
|
||||||
|
|
||||||
lateinit var histories: Histories
|
lateinit var histories: Histories
|
||||||
lateinit var downloads: Histories
|
|
||||||
lateinit var favorites: Histories
|
lateinit var favorites: Histories
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -51,9 +47,15 @@ class Pupil : MultiDexApplication() {
|
|||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||||
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
|
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||||
|
|
||||||
|
val download = preference.getString("dl_location", null)
|
||||||
|
|
||||||
|
if (download == null) {
|
||||||
|
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
|
||||||
|
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
} catch (e: GooglePlayServicesRepairableException) {
|
} catch (e: GooglePlayServicesRepairableException) {
|
||||||
@@ -64,7 +66,7 @@ class Pupil : MultiDexApplication() {
|
|||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_MIN).apply {
|
||||||
description = getString(R.string.channel_download_description)
|
description = getString(R.string.channel_download_description)
|
||||||
enableLights(false)
|
enableLights(false)
|
||||||
enableVibration(false)
|
enableVibration(false)
|
||||||
@@ -73,15 +75,12 @@ class Pupil : MultiDexApplication() {
|
|||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
|
||||||
true -> AppCompatDelegate.MODE_NIGHT_YES
|
true -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
false -> AppCompatDelegate.MODE_NIGHT_NO
|
false -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
})
|
})
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
updateOldReaderGalleries(this@Pupil)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Base64
|
||||||
import android.util.SparseBooleanArray
|
import android.util.SparseBooleanArray
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -27,9 +29,10 @@ import android.widget.LinearLayout
|
|||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.daimajia.swipe.SwipeLayout
|
import com.daimajia.swipe.SwipeLayout
|
||||||
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
import com.daimajia.swipe.adapters.RecyclerSwipeAdapter
|
||||||
@@ -37,28 +40,22 @@ import com.daimajia.swipe.interfaces.SwipeAdapterInterface
|
|||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.util.GalleryDownloader
|
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import xyz.quaver.pupil.util.getCachedGallery
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class GalleryBlockAdapter(private val glide: RequestManager, private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryBlock>) : RecyclerSwipeAdapter<RecyclerView.ViewHolder>(), SwipeAdapterInterface {
|
||||||
|
|
||||||
enum class ViewType {
|
enum class ViewType {
|
||||||
NEXT,
|
NEXT,
|
||||||
@@ -66,10 +63,56 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
PREV
|
PREV
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val glide = Glide.with(context)
|
||||||
private lateinit var favorites: Histories
|
private lateinit var favorites: Histories
|
||||||
|
|
||||||
|
val timer = Timer()
|
||||||
|
|
||||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
|
var timerTask: TimerTask? = null
|
||||||
|
|
||||||
|
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
val cache = Cache(context).getCachedGallery(galleryID)
|
||||||
|
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||||
|
|
||||||
|
if (reader == null) {
|
||||||
|
view.galleryblock_progressbar.visibility = View.GONE
|
||||||
|
view.galleryblock_progress_complete.visibility = View.GONE
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
with(view.galleryblock_progressbar) {
|
||||||
|
|
||||||
|
progress = cache?.listFiles()?.count { file ->
|
||||||
|
Regex("^[0-9]+.+\$").matches(file.name!!)
|
||||||
|
} ?: 0
|
||||||
|
|
||||||
|
if (visibility == View.GONE) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
max = reader.galleryInfo.size
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress == max) {
|
||||||
|
if (completeFlag.get(galleryID, false)) {
|
||||||
|
with(view.galleryblock_progress_complete) {
|
||||||
|
setImageResource(R.drawable.ic_progressbar)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
with(view.galleryblock_progress_complete) {
|
||||||
|
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
||||||
|
this?.start()
|
||||||
|
})
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
completeFlag.put(galleryID, true)
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(galleryBlock: GalleryBlock) {
|
||||||
with(view) {
|
with(view) {
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
val languages = resources.getStringArray(R.array.languages).map {
|
val languages = resources.getStringArray(R.array.languages).map {
|
||||||
@@ -78,16 +121,18 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}.toMap()
|
}.toMap()
|
||||||
|
|
||||||
val (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
|
|
||||||
|
|
||||||
val artists = galleryBlock.artists
|
val artists = galleryBlock.artists
|
||||||
val series = galleryBlock.series
|
val series = galleryBlock.series
|
||||||
|
|
||||||
|
galleryblock_thumbnail.setImageDrawable(CircularProgressDrawable(context).also {
|
||||||
|
it.start()
|
||||||
|
})
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
val cache = thumbnail.await()
|
val thumbnail = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT)
|
||||||
|
|
||||||
glide
|
glide
|
||||||
.load(cache)
|
.load(thumbnail)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.error(R.drawable.image_broken_variant)
|
.error(R.drawable.image_broken_variant)
|
||||||
@@ -99,75 +144,28 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Check cache
|
//Check cache
|
||||||
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") }
|
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||||
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") }
|
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||||
|
|
||||||
try {
|
if (cache != null && reader != null) {
|
||||||
Json(JsonConfiguration.Stable)
|
val count = cache.listFiles().count {
|
||||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
Regex("^[0-9]+.+\$").matches(it.name!!)
|
||||||
} catch(e: Exception) {
|
}
|
||||||
readerCache.invoke().delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readerCache.invoke().exists()) {
|
|
||||||
val reader = Json(JsonConfiguration.Stable)
|
|
||||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
|
||||||
|
|
||||||
with(galleryblock_progressbar) {
|
with(galleryblock_progressbar) {
|
||||||
max = reader.galleryInfo.size
|
max = reader.galleryInfo.size
|
||||||
progress = imageCache.invoke().list()?.size ?: 0
|
progress = count
|
||||||
|
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else
|
||||||
galleryblock_progressbar.visibility = View.GONE
|
galleryblock_progressbar.visibility = View.GONE
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshTasks[this@GalleryViewHolder] == null) {
|
if (timerTask == null)
|
||||||
val refresh = Timer(false).schedule(0, 1000) {
|
timerTask = timer.schedule(0, 1000) {
|
||||||
post {
|
updateProgress(context, galleryBlock.id)
|
||||||
with(view.galleryblock_progressbar) {
|
|
||||||
progress = imageCache.invoke().list()?.size ?: 0
|
|
||||||
|
|
||||||
if (!readerCache.invoke().exists()) {
|
|
||||||
visibility = View.GONE
|
|
||||||
max = 0
|
|
||||||
progress = 0
|
|
||||||
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
|
||||||
} else {
|
|
||||||
if (visibility == View.GONE) {
|
|
||||||
val reader = Json(JsonConfiguration.Stable)
|
|
||||||
.parse(Reader.serializer(), readerCache.invoke().readText())
|
|
||||||
max = reader.galleryInfo.size
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress == max) {
|
|
||||||
if (completeFlag.get(galleryBlock.id, false)) {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
|
||||||
setImageResource(R.drawable.ic_progressbar)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
|
||||||
this?.start()
|
|
||||||
})
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
completeFlag.put(galleryBlock.id, true)
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTasks[this@GalleryViewHolder] = refresh
|
|
||||||
}
|
|
||||||
|
|
||||||
galleryblock_title.text = galleryBlock.title
|
galleryblock_title.text = galleryBlock.title
|
||||||
with(galleryblock_artist) {
|
with(galleryblock_artist) {
|
||||||
text = artists.joinToString(", ") { it.wordCapitalize() }
|
text = artists.joinToString(", ") { it.wordCapitalize() }
|
||||||
@@ -277,7 +275,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
|
|
||||||
val completeFlag = SparseBooleanArray()
|
val completeFlag = SparseBooleanArray()
|
||||||
|
|
||||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||||
@@ -336,10 +333,11 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
override fun onStartOpen(layout: SwipeLayout?) {
|
override fun onStartOpen(layout: SwipeLayout?) {
|
||||||
mItemManger.closeAllExcept(layout)
|
mItemManger.closeAllExcept(layout)
|
||||||
|
|
||||||
holder.view.galleryblock_download.text = when(GalleryDownloader.get(gallery.first.id)) {
|
holder.view.galleryblock_download.text =
|
||||||
null -> holder.view.context.getString(R.string.main_download)
|
if (DownloadWorker.getInstance(holder.view.context).progress.indexOfKey(gallery.id) < 0)
|
||||||
else -> holder.view.context.getString(android.R.string.cancel)
|
holder.view.context.getString(R.string.main_download)
|
||||||
}
|
else
|
||||||
|
holder.view.context.getString(android.R.string.cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClose(layout: SwipeLayout?) {}
|
override fun onClose(layout: SwipeLayout?) {}
|
||||||
@@ -355,10 +353,8 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
|
|||||||
super.onViewDetachedFromWindow(holder)
|
super.onViewDetachedFromWindow(holder)
|
||||||
|
|
||||||
if (holder is GalleryViewHolder) {
|
if (holder is GalleryViewHolder) {
|
||||||
val task = refreshTasks[holder] ?: return
|
holder.timerTask?.cancel()
|
||||||
|
holder.timerTask = null
|
||||||
task.cancel()
|
|
||||||
refreshTasks.remove(holder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
85
app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
Normal file
85
app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.item_mirrors.view.*
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MirrorAdapter(context: Context) : RecyclerView.Adapter<MirrorAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
val mirrors = context.resources.getStringArray(R.array.mirrors).map {
|
||||||
|
it.split('|').let { split ->
|
||||||
|
Pair(split.first(), split.last())
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
val list = mirrors.keys.toMutableList().apply {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString("mirrors", "")!!
|
||||||
|
.split(">")
|
||||||
|
.reversed()
|
||||||
|
.forEach {
|
||||||
|
if (this.contains(it)) {
|
||||||
|
this.remove(it)
|
||||||
|
this.add(0, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onItemMove : ((Int, Int) -> Unit) = { from, to ->
|
||||||
|
Collections.swap(list, from, to)
|
||||||
|
notifyItemMoved(from, to)
|
||||||
|
onItemMoved?.invoke(list)
|
||||||
|
}
|
||||||
|
var onStartDrag : ((ViewHolder) -> Unit)? = null
|
||||||
|
var onItemMoved : ((List<String>) -> (Unit))? = null
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
with(holder.view) {
|
||||||
|
mirror_name.text = mirrors[list.elementAt(position)]
|
||||||
|
mirror_button.setOnTouchListener { _, event ->
|
||||||
|
if (event.action == MotionEvent.ACTION_DOWN)
|
||||||
|
onStartDrag?.invoke(holder)
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return LayoutInflater.from(parent.context).inflate(
|
||||||
|
R.layout.item_mirrors, parent, false
|
||||||
|
).let {
|
||||||
|
ViewHolder(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = mirrors.size
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,24 +18,48 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.adapters
|
package xyz.quaver.pupil.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.getCachedGallery
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import java.io.File
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class ReaderAdapter(private val glide: RequestManager,
|
class ReaderAdapter(private val context: Context,
|
||||||
private val galleryID: Int,
|
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
var isFullScreen = false
|
var isFullScreen = false
|
||||||
|
|
||||||
|
var reader: Reader? = null
|
||||||
|
private val glide = Glide.with(context)
|
||||||
|
val timer = Timer()
|
||||||
|
|
||||||
|
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
reader = Cache(context).getReader(galleryID)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@@ -47,26 +71,75 @@ class ReaderAdapter(private val glide: RequestManager,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
holder.view as ImageView
|
holder.view as ConstraintLayout
|
||||||
|
|
||||||
if (isFullScreen)
|
if (isFullScreen)
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
|
||||||
else
|
else
|
||||||
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
|
||||||
|
|
||||||
glide
|
holder.view.image.setOnPhotoTapListener { _, _, _ ->
|
||||||
.load(File(getCachedGallery(holder.view.context, galleryID), images[position]))
|
onItemClickListener?.invoke(position)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
}
|
||||||
.skipMemoryCache(true)
|
|
||||||
.error(R.drawable.image_broken_variant)
|
holder.view.container.setOnClickListener {
|
||||||
.dontTransform()
|
onItemClickListener?.invoke(position)
|
||||||
.apply {
|
}
|
||||||
if (BuildConfig.CENSOR)
|
|
||||||
override(5, 8)
|
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||||
|
.dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}"
|
||||||
|
|
||||||
|
holder.view.reader_index.text = (position+1).toString()
|
||||||
|
|
||||||
|
val images = Cache(context).getImages(galleryID)
|
||||||
|
|
||||||
|
if (images?.get(position) != null) {
|
||||||
|
glide
|
||||||
|
.load(images[position]?.uri)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.error(R.drawable.image_broken_variant)
|
||||||
|
.apply {
|
||||||
|
if (BuildConfig.CENSOR)
|
||||||
|
override(5, 8)
|
||||||
|
}
|
||||||
|
.into(holder.view.image)
|
||||||
|
} else {
|
||||||
|
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||||
|
|
||||||
|
if (progress?.isNaN() == true) {
|
||||||
|
glide
|
||||||
|
.load(R.drawable.image_broken_variant)
|
||||||
|
.into(holder.view.image)
|
||||||
|
Snackbar
|
||||||
|
.make(
|
||||||
|
holder.view,
|
||||||
|
DownloadWorker.getInstance(context).exception[galleryID]!![position]?.message
|
||||||
|
?: context.getText(R.string.default_error_msg),
|
||||||
|
Snackbar.LENGTH_INDEFINITE
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
.into(holder.view)
|
|
||||||
|
holder.view.reader_item_progressbar.progress =
|
||||||
|
if (progress?.isInfinite() == true)
|
||||||
|
100
|
||||||
|
else
|
||||||
|
progress?.roundToInt() ?: 0
|
||||||
|
|
||||||
|
holder.view.image.setImageDrawable(null)
|
||||||
|
|
||||||
|
|
||||||
|
timer.schedule(1000) {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = images.size
|
override fun getItemCount() = reader?.galleryInfo?.size ?: 0
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||||||
import com.arlib.floatingsearchview.FloatingSearchView
|
import com.arlib.floatingsearchview.FloatingSearchView
|
||||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
import com.arlib.floatingsearchview.util.view.SearchInputView
|
import com.arlib.floatingsearchview.util.view.SearchInputView
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.android.synthetic.main.activity_main_content.*
|
import kotlinx.android.synthetic.main.activity_main_content.*
|
||||||
@@ -55,7 +54,10 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.json.JsonConfiguration
|
import kotlinx.serialization.json.JsonConfiguration
|
||||||
import kotlinx.serialization.list
|
import kotlinx.serialization.list
|
||||||
import kotlinx.serialization.stringify
|
import kotlinx.serialization.stringify
|
||||||
import xyz.quaver.hitomi.*
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.hitomi.doSearch
|
||||||
|
import xyz.quaver.hitomi.getGalleryIDsFromNozomi
|
||||||
|
import xyz.quaver.hitomi.getSuggestionsForQuery
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
|
||||||
@@ -64,11 +66,10 @@ import xyz.quaver.pupil.types.TagSuggestion
|
|||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.types.Tags
|
||||||
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
import xyz.quaver.pupil.ui.dialog.GalleryDialog
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
@@ -89,7 +90,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
POPULAR
|
POPULAR
|
||||||
}
|
}
|
||||||
|
|
||||||
private val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
|
private val galleries = ArrayList<GalleryBlock>()
|
||||||
|
|
||||||
private var query = ""
|
private var query = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -112,7 +113,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
private lateinit var histories: Histories
|
private lateinit var histories: Histories
|
||||||
private lateinit var downloads: Histories
|
|
||||||
private lateinit var favorites: Histories
|
private lateinit var favorites: Histories
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -128,6 +128,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +151,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
with(application as Pupil) {
|
with(application as Pupil) {
|
||||||
this@MainActivity.histories = histories
|
this@MainActivity.histories = histories
|
||||||
this@MainActivity.downloads = downloads
|
|
||||||
this@MainActivity.favorites = favorites
|
this@MainActivity.favorites = favorites
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +176,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
(main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
@@ -386,7 +392,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
with(main_recyclerview) {
|
with(main_recyclerview) {
|
||||||
adapter = GalleryBlockAdapter(Glide.with(this@MainActivity), galleries).apply {
|
adapter = GalleryBlockAdapter(this@MainActivity, galleries).apply {
|
||||||
onChipClickedHandler.add {
|
onChipClickedHandler.add {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
query = it.toQuery()
|
query = it.toQuery()
|
||||||
@@ -399,16 +405,18 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDownloadClickedHandler = { position ->
|
onDownloadClickedHandler = { position ->
|
||||||
val galleryID = galleries[position].first.id
|
val galleryID = galleries[position].id
|
||||||
|
|
||||||
if (!completeFlag.get(galleryID, false)) {
|
if (!completeFlag.get(galleryID, false)) {
|
||||||
val downloader = GalleryDownloader.get(galleryID)
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
|
||||||
if (downloader == null)
|
if (worker.progress.indexOfKey(galleryID) >= 0) //download in progress
|
||||||
GalleryDownloader(context, galleryID, true).start()
|
worker.cancel(galleryID)
|
||||||
else {
|
else {
|
||||||
downloader.cancel()
|
Cache(context).setDownloading(galleryID, true)
|
||||||
downloader.clearNotification()
|
|
||||||
|
if (!worker.queue.contains(galleryID))
|
||||||
|
worker.queue.add(galleryID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,39 +424,27 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDeleteClickedHandler = { position ->
|
onDeleteClickedHandler = { position ->
|
||||||
val galleryID = galleries[position].first.id
|
val galleryID = galleries[position].id
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
with(GalleryDownloader[galleryID]) {
|
DownloadWorker.getInstance(context).cancel(galleryID)
|
||||||
this?.cancelAndJoin()
|
|
||||||
this?.clearNotification()
|
|
||||||
}
|
|
||||||
val cache = File(cacheDir, "imageCache/${galleryID}")
|
|
||||||
val data = getCachedGallery(context, galleryID)
|
|
||||||
cache.deleteRecursively()
|
|
||||||
data.deleteRecursively()
|
|
||||||
|
|
||||||
downloads.remove(galleryID)
|
var cache = Cache(context).getCachedGallery(galleryID)
|
||||||
|
|
||||||
if (this@MainActivity.mode == Mode.DOWNLOAD) {
|
while (cache != null) {
|
||||||
runOnUiThread {
|
cache.deleteRecursively()
|
||||||
cancelFetch()
|
cache = Cache(context).getCachedGallery(galleryID)
|
||||||
clearGalleries()
|
|
||||||
fetchGalleries(query, sortMode)
|
|
||||||
loadBlocks()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
histories.remove(galleryID)
|
histories.remove(galleryID)
|
||||||
|
|
||||||
if (this@MainActivity.mode == Mode.HISTORY) {
|
if (this@MainActivity.mode != Mode.SEARCH)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
cancelFetch()
|
cancelFetch()
|
||||||
clearGalleries()
|
clearGalleries()
|
||||||
fetchGalleries(query, sortMode)
|
fetchGalleries(query, sortMode)
|
||||||
loadBlocks()
|
loadBlocks()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
completeFlag.put(galleryID, false)
|
completeFlag.put(galleryID, false)
|
||||||
}
|
}
|
||||||
@@ -462,7 +458,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return@setOnItemClickListener
|
return@setOnItemClickListener
|
||||||
|
|
||||||
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
val intent = Intent(this@MainActivity, ReaderActivity::class.java)
|
||||||
val gallery = galleries[position].first
|
val gallery = galleries[position]
|
||||||
intent.putExtra("galleryID", gallery.id)
|
intent.putExtra("galleryID", gallery.id)
|
||||||
|
|
||||||
//TODO: Maybe sprinkling some transitions will be nice :D
|
//TODO: Maybe sprinkling some transitions will be nice :D
|
||||||
@@ -474,7 +470,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (v !is CardView)
|
if (v !is CardView)
|
||||||
return@setOnItemLongClickListener true
|
return@setOnItemLongClickListener true
|
||||||
|
|
||||||
val galleryID = galleries[position].first.id
|
val galleryID = galleries[position].id
|
||||||
|
|
||||||
GalleryDialog(
|
GalleryDialog(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
@@ -973,8 +969,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.DOWNLOAD -> {
|
Mode.DOWNLOAD -> {
|
||||||
|
val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file ->
|
||||||
|
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
|
||||||
|
}?.map {
|
||||||
|
it.name!!.toInt()
|
||||||
|
}?: listOf()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> downloads.toList().apply {
|
query.isEmpty() -> downloads.apply {
|
||||||
totalItems = size
|
totalItems = size
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -1022,48 +1024,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
for (chunk in chunks)
|
for (chunk in chunks)
|
||||||
chunk.map { galleryID ->
|
chunk.map { galleryID ->
|
||||||
async {
|
async {
|
||||||
try {
|
Cache(this@MainActivity).getGalleryBlock(galleryID)
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = GalleryBlock.serializer()
|
|
||||||
|
|
||||||
val galleryBlock =
|
|
||||||
File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
|
|
||||||
when {
|
|
||||||
cache.exists() -> json.parse(serializer, cache.readText())
|
|
||||||
else -> {
|
|
||||||
getGalleryBlock(galleryID).apply {
|
|
||||||
this ?: return@apply
|
|
||||||
|
|
||||||
if (cache.parentFile?.exists() == false)
|
|
||||||
cache.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
cache.writeText(json.stringify(serializer, this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return@async null
|
|
||||||
|
|
||||||
val thumbnail = async {
|
|
||||||
val ext = galleryBlock.thumbnails[0].split('.').last()
|
|
||||||
File(getCachedGallery(this@MainActivity, galleryBlock.id), "thumbnail.$ext").apply {
|
|
||||||
if (!exists())
|
|
||||||
try {
|
|
||||||
with(URL(galleryBlock.thumbnails[0]).openConnection() as HttpsURLConnection) {
|
|
||||||
if (this@apply.parentFile?.exists() == false)
|
|
||||||
this@apply.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
inputStream.copyTo(FileOutputStream(this@apply))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
delete()
|
|
||||||
}
|
|
||||||
}.absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair(galleryBlock, thumbnail)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.forEach {
|
}.forEach {
|
||||||
val galleryBlock = it.await()
|
val galleryBlock = it.await()
|
||||||
|
|||||||
@@ -21,40 +21,37 @@ package xyz.quaver.pupil.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.fabric.sdk.android.Fabric
|
import io.fabric.sdk.android.Fabric
|
||||||
import kotlinx.android.synthetic.main.activity_reader.*
|
import kotlinx.android.synthetic.main.activity_reader.*
|
||||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||||
import xyz.quaver.pupil.util.GalleryDownloader
|
|
||||||
import xyz.quaver.pupil.util.Histories
|
import xyz.quaver.pupil.util.Histories
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
class ReaderActivity : AppCompatActivity() {
|
class ReaderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var galleryID = 0
|
private var galleryID = 0
|
||||||
private val images = ArrayList<String>()
|
|
||||||
private var gallerySize = 0
|
|
||||||
private var currentPage = 0
|
private var currentPage = 0
|
||||||
|
|
||||||
private var isScroll = true
|
private var isScroll = true
|
||||||
@@ -70,7 +67,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var downloader: GalleryDownloader
|
private val timer = Timer()
|
||||||
|
|
||||||
private val snapHelper = PagerSnapHelper()
|
private val snapHelper = PagerSnapHelper()
|
||||||
|
|
||||||
@@ -102,12 +99,8 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
initDownloader()
|
|
||||||
|
|
||||||
initView()
|
initView()
|
||||||
|
initDownloader()
|
||||||
if (!downloader.download)
|
|
||||||
downloader.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
@@ -169,7 +162,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
||||||
with(view.dialog_number_picker) {
|
with(view.dialog_number_picker) {
|
||||||
minValue=1
|
minValue=1
|
||||||
maxValue=gallerySize
|
maxValue=reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
value=currentPage
|
value=currentPage
|
||||||
}
|
}
|
||||||
val dialog = AlertDialog.Builder(this).apply {
|
val dialog = AlertDialog.Builder(this).apply {
|
||||||
@@ -202,8 +195,11 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
if (::downloader.isInitialized && !downloader.download)
|
timer.cancel()
|
||||||
downloader.cancel()
|
(reader_recyclerview.adapter as ReaderAdapter).timer.cancel()
|
||||||
|
|
||||||
|
if (!Cache(this).isDownloading(galleryID))
|
||||||
|
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
@@ -239,101 +235,66 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initDownloader() {
|
private fun initDownloader() {
|
||||||
var d: GalleryDownloader? = GalleryDownloader.get(galleryID)
|
val worker = DownloadWorker.getInstance(this).apply {
|
||||||
|
queue.add(galleryID)
|
||||||
if (d == null)
|
|
||||||
d = GalleryDownloader(this, galleryID)
|
|
||||||
|
|
||||||
downloader = d.apply {
|
|
||||||
onReaderLoadedHandler = {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
title = it.title
|
|
||||||
with(reader_download_progressbar) {
|
|
||||||
max = it.galleryInfo.size
|
|
||||||
progress = 0
|
|
||||||
}
|
|
||||||
with(reader_progressbar) {
|
|
||||||
max = it.galleryInfo.size
|
|
||||||
progress = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
gallerySize = it.galleryInfo.size
|
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.galleryInfo.size}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onProgressHandler = {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
reader_download_progressbar.progress = it
|
|
||||||
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDownloadedHandler = {
|
|
||||||
val item = it.toList()
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
if (images.isEmpty()) {
|
|
||||||
images.addAll(item)
|
|
||||||
reader_recyclerview.adapter?.notifyDataSetChanged()
|
|
||||||
} else {
|
|
||||||
images.add(item.last())
|
|
||||||
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onErrorHandler = {
|
|
||||||
Snackbar
|
|
||||||
.make(reader_layout, it.message ?: it.javaClass.name, Snackbar.LENGTH_INDEFINITE)
|
|
||||||
.setAction(R.string.reader_help) {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.error_help))))
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
downloader.download = false
|
|
||||||
}
|
|
||||||
onCompleteHandler = {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
reader_download_progressbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onNotifyChangedHandler = { notify ->
|
|
||||||
val fab = reader_fab_download
|
|
||||||
|
|
||||||
runOnUiThread {
|
|
||||||
if (notify) {
|
|
||||||
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
|
|
||||||
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
|
||||||
if (downloader.download)
|
|
||||||
fab.post {
|
|
||||||
icon.start()
|
|
||||||
fab.labelText = getString(R.string.reader_fab_download_cancel)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
fab.post {
|
|
||||||
fab.setImageResource(R.drawable.ic_download)
|
|
||||||
fab.labelText = getString(R.string.reader_fab_download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fab.setImageDrawable(icon)
|
|
||||||
icon?.start()
|
|
||||||
} else {
|
|
||||||
runOnUiThread {
|
|
||||||
fab.setImageResource(R.drawable.ic_download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloader.download) {
|
timer.schedule(0, 1000) {
|
||||||
downloader.invokeOnReaderLoaded()
|
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||||
downloader.invokeOnNotifyChanged()
|
return@schedule
|
||||||
|
|
||||||
|
if (worker.progress[galleryID] == null) { //Gallery not found
|
||||||
|
timer.cancel()
|
||||||
|
Snackbar
|
||||||
|
.make(reader_layout, R.string.reader_failed_to_find_gallery, Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
|
reader_download_progressbar.progress = worker.progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||||
|
reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
|
||||||
|
|
||||||
|
if (title == getString(R.string.reader_loading)) {
|
||||||
|
val reader = (reader_recyclerview.adapter as ReaderAdapter).reader
|
||||||
|
|
||||||
|
if (reader != null) {
|
||||||
|
title = reader.title
|
||||||
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}"
|
||||||
|
|
||||||
|
menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity,
|
||||||
|
when (reader.code) {
|
||||||
|
Code.HITOMI -> R.drawable.hitomi
|
||||||
|
Code.HIYOBI -> R.drawable.ic_hiyobi
|
||||||
|
else -> android.R.color.transparent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) { //Download finished
|
||||||
|
reader_download_progressbar.visibility = View.GONE
|
||||||
|
|
||||||
|
animateDownloadFAB(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
with(reader_recyclerview) {
|
with(reader_recyclerview) {
|
||||||
adapter = ReaderAdapter(Glide.with(this@ReaderActivity), galleryID, images)
|
adapter = ReaderAdapter(this@ReaderActivity, galleryID).apply {
|
||||||
|
onItemClickListener = {
|
||||||
|
if (isScroll) {
|
||||||
|
isScroll = false
|
||||||
|
isFullscreen = true
|
||||||
|
|
||||||
|
scrollMode(false)
|
||||||
|
fullscreen(true)
|
||||||
|
} else {
|
||||||
|
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
@@ -349,32 +310,24 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
||||||
return
|
return
|
||||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
||||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
|
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${recyclerView.adapter!!.itemCount}"
|
||||||
this@ReaderActivity.reader_progressbar.progress = currentPage
|
this@ReaderActivity.reader_progressbar.progress = currentPage
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ItemClickSupport.addTo(this)
|
|
||||||
.setOnItemClickListener { _, _, _ ->
|
|
||||||
if (isScroll) {
|
|
||||||
isScroll = false
|
|
||||||
isFullscreen = true
|
|
||||||
|
|
||||||
scrollMode(false)
|
|
||||||
fullscreen(true)
|
|
||||||
} else {
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage) //Moves to next page because currentPage is 1-based indexing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with(reader_fab_download) {
|
with(reader_fab_download) {
|
||||||
setImageResource(R.drawable.ic_download)
|
animateDownloadFAB(Cache(context).isDownloading(galleryID)) //If download in progress, animate button
|
||||||
setOnClickListener {
|
|
||||||
downloader.download = !downloader.download
|
|
||||||
|
|
||||||
if (!downloader.download)
|
setOnClickListener {
|
||||||
downloader.clearNotification()
|
if (Cache(context).isDownloading(galleryID)) {
|
||||||
|
Cache(context).setDownloading(galleryID, false)
|
||||||
|
|
||||||
|
animateDownloadFAB(false)
|
||||||
|
} else {
|
||||||
|
Cache(context).setDownloading(galleryID, true)
|
||||||
|
animateDownloadFAB(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,4 +369,34 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun animateDownloadFAB(animate: Boolean) {
|
||||||
|
with(reader_fab_download) {
|
||||||
|
if (animate) {
|
||||||
|
val icon = AnimatedVectorDrawableCompat.create(context, R.drawable.ic_downloading)
|
||||||
|
|
||||||
|
icon?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
|
||||||
|
override fun onAnimationEnd(drawable: Drawable?) {
|
||||||
|
val worker = DownloadWorker.getInstance(context)
|
||||||
|
if (worker.progress[galleryID]?.all { !it.isFinite() } == true) // If download is finished, stop animating
|
||||||
|
post {
|
||||||
|
setImageResource(R.drawable.ic_download)
|
||||||
|
labelText = getString(R.string.reader_fab_download)
|
||||||
|
}
|
||||||
|
else // Or continue animate
|
||||||
|
post {
|
||||||
|
icon.start()
|
||||||
|
labelText = getString(R.string.reader_fab_download_cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setImageDrawable(icon)
|
||||||
|
icon?.start()
|
||||||
|
} else {
|
||||||
|
setImageResource(R.drawable.ic_download)
|
||||||
|
labelText = getString(R.string.reader_fab_download)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,26 +20,33 @@ package xyz.quaver.pupil.ui
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.android.synthetic.main.settings_activity.*
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.parseList
|
import kotlinx.serialization.parseList
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.ui.fragment.LockFragment
|
import xyz.quaver.pupil.ui.fragment.LockFragment
|
||||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||||
|
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
||||||
|
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
||||||
|
import xyz.quaver.pupil.util.REQUEST_LOCK
|
||||||
|
import xyz.quaver.pupil.util.REQUEST_RESTORE
|
||||||
|
import java.io.File
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val REQUEST_LOCK = 38238
|
|
||||||
val REQUEST_RESTORE = 16546
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -114,6 +121,35 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
REQUEST_DOWNLOAD_FOLDER -> {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
data?.data?.also { uri ->
|
||||||
|
val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||||
|
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
|
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
|
||||||
|
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
|
||||||
|
else
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
|
.putString("dl_location", uri.toString())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUEST_DOWNLOAD_FOLDER_OLD -> {
|
||||||
|
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
||||||
|
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||||
|
|
||||||
|
if (!File(directory).canWrite())
|
||||||
|
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
|
||||||
|
else
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
|
.putString("dl_location", Uri.fromFile(File(directory)).toString())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
@@ -28,6 +29,7 @@ import android.view.View
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.android.synthetic.main.dialog_default_query.*
|
||||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.types.Tags
|
import xyz.quaver.pupil.types.Tags
|
||||||
@@ -50,21 +52,41 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
initDialog()
|
||||||
|
|
||||||
|
setTitle(R.string.default_query_dialog_title)
|
||||||
|
setView(dialogView)
|
||||||
|
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ ->
|
||||||
|
val newTags = Tags.parse(default_query_dialog_edittext.text.toString())
|
||||||
|
|
||||||
|
with(default_query_dialog_language_selector) {
|
||||||
|
if (selectedItemPosition != 0)
|
||||||
|
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (default_query_dialog_BL_checkbox.isChecked)
|
||||||
|
newTags.add(excludeBL)
|
||||||
|
|
||||||
|
if (default_query_dialog_guro_checkbox.isChecked)
|
||||||
|
excludeGuro.forEach { tag ->
|
||||||
|
newTags.add(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositiveButtonClickListener?.invoke(newTags)
|
||||||
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
|
||||||
|
|
||||||
initView()
|
|
||||||
|
|
||||||
setContentView(dialogView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
@SuppressLint("InflateParams")
|
||||||
|
private fun initDialog() {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val tags = Tags.parse(
|
val tags = Tags.parse(
|
||||||
preferences.getString("default_query", "") ?: ""
|
preferences.getString("default_query", "") ?: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null)
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_language_selector) {
|
with(dialogView.default_query_dialog_language_selector) {
|
||||||
adapter =
|
adapter =
|
||||||
ArrayAdapter(
|
ArrayAdapter(
|
||||||
@@ -105,7 +127,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
with(dialogView.default_query_dialog_edittext) {
|
with(dialogView.default_query_dialog_edittext) {
|
||||||
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE)
|
||||||
addTextChangedListener(object : TextWatcher {
|
addTextChangedListener(object : TextWatcher {
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(
|
||||||
|
s: CharSequence?,
|
||||||
|
start: Int,
|
||||||
|
count: Int,
|
||||||
|
after: Int
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
|
||||||
@@ -113,29 +141,14 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) {
|
|||||||
s ?: return
|
s ?: return
|
||||||
|
|
||||||
if (s.any { it.isUpperCase() })
|
if (s.any { it.isUpperCase() })
|
||||||
s.replace(0, s.length, s.toString().toLowerCase(java.util.Locale.getDefault()))
|
s.replace(
|
||||||
|
0,
|
||||||
|
s.length,
|
||||||
|
s.toString().toLowerCase(java.util.Locale.getDefault())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogView.default_query_dialog_ok.setOnClickListener {
|
|
||||||
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
|
|
||||||
|
|
||||||
with(dialogView.default_query_dialog_language_selector) {
|
|
||||||
if (selectedItemPosition != 0)
|
|
||||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
|
|
||||||
newTags.add(excludeBL)
|
|
||||||
|
|
||||||
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
|
|
||||||
excludeGuro.forEach { tag ->
|
|
||||||
newTags.add(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositiveButtonClickListener?.invoke(newTags)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,28 +19,37 @@
|
|||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RadioButton
|
import android.widget.RadioButton
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserActivity
|
||||||
|
import net.rdrei.android.dirchooser.DirectoryChooserConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
||||||
|
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
||||||
import xyz.quaver.pupil.util.byteToString
|
import xyz.quaver.pupil.util.byteToString
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
class DownloadLocationDialog(context: Context) : AlertDialog(context) {
|
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||||
|
|
||||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val buttons = mutableListOf<RadioButton>()
|
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
|
||||||
var onDownloadLocationChangedListener : ((Int) -> (Unit))? = null
|
|
||||||
|
|
||||||
init {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
||||||
|
|
||||||
ContextCompat.getExternalFilesDirs(context, null).forEachIndexed { index, dir ->
|
val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
|
||||||
|
|
||||||
|
externalFilesDirs.forEachIndexed { index, dir ->
|
||||||
|
|
||||||
dir ?: return@forEachIndexed
|
dir ?: return@forEachIndexed
|
||||||
|
|
||||||
@@ -54,17 +63,58 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
|
|||||||
byteToString(dir.freeSpace)
|
byteToString(dir.freeSpace)
|
||||||
)
|
)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
buttons.forEach { button ->
|
buttons.forEach { pair ->
|
||||||
button.isChecked = false
|
pair.first.isChecked = false
|
||||||
}
|
}
|
||||||
button.performClick()
|
button.performClick()
|
||||||
onDownloadLocationChangedListener?.invoke(index)
|
preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
|
||||||
}
|
}
|
||||||
buttons.add(button)
|
buttons.add(button to Uri.fromFile(dir))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons[preference.getInt("dl_location", 0)].isChecked = true
|
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
|
||||||
|
location_type.text = context.getString(R.string.settings_dl_location_custom)
|
||||||
|
setOnClickListener {
|
||||||
|
buttons.forEach { pair ->
|
||||||
|
pair.first.isChecked = false
|
||||||
|
}
|
||||||
|
button.performClick()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
|
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
} else { // Can't use SAF on old Androids!
|
||||||
|
val config = DirectoryChooserConfig.builder()
|
||||||
|
.newDirectoryName("Pupil")
|
||||||
|
.allowNewDirectoryNameModification(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
|
||||||
|
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buttons.add(button to null)
|
||||||
|
})
|
||||||
|
|
||||||
|
val pref = Uri.parse(preference.getString("dl_location", null))
|
||||||
|
val index = externalFilesDirs.indexOfFirst {
|
||||||
|
Uri.fromFile(it).toString() == pref.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0)
|
||||||
|
buttons.last().first.isChecked = true
|
||||||
|
else
|
||||||
|
buttons[index].first.isChecked = true
|
||||||
|
|
||||||
setTitle(R.string.settings_dl_location)
|
setTitle(R.string.settings_dl_location)
|
||||||
|
|
||||||
@@ -73,6 +123,8 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
|
|||||||
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
|
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -32,14 +32,16 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.dialog_galleryblock.*
|
import kotlinx.android.synthetic.main.dialog_gallery.*
|
||||||
import kotlinx.android.synthetic.main.gallery_details.view.*
|
import kotlinx.android.synthetic.main.gallery_details.view.*
|
||||||
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
import kotlinx.android.synthetic.main.item_gallery_details.view.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import xyz.quaver.hitomi.Gallery
|
import xyz.quaver.hitomi.Gallery
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.getGallery
|
import xyz.quaver.hitomi.getGallery
|
||||||
import xyz.quaver.hitomi.getGalleryBlock
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.Pupil
|
import xyz.quaver.pupil.Pupil
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
@@ -48,6 +50,7 @@ import xyz.quaver.pupil.adapters.ThumbnailAdapter
|
|||||||
import xyz.quaver.pupil.types.Tag
|
import xyz.quaver.pupil.types.Tag
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
import xyz.quaver.pupil.util.ItemClickSupport
|
import xyz.quaver.pupil.util.ItemClickSupport
|
||||||
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.wordCapitalize
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
|
||||||
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
|
class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(context) {
|
||||||
@@ -64,7 +67,7 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.dialog_galleryblock)
|
setContentView(R.layout.dialog_gallery)
|
||||||
|
|
||||||
window?.attributes.apply {
|
window?.attributes.apply {
|
||||||
this ?: return@apply
|
this ?: return@apply
|
||||||
@@ -183,6 +186,8 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
else -> tag.tag.wordCapitalize()
|
else -> tag.tag.wordCapitalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEnsureMinTouchTargetSize(false)
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
onChipClickedHandler.forEach { handler ->
|
onChipClickedHandler.forEach { handler ->
|
||||||
handler.invoke(tag)
|
handler.invoke(tag)
|
||||||
@@ -219,9 +224,9 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
|
|
||||||
private fun addRelated(gallery: Gallery) {
|
private fun addRelated(gallery: Gallery) {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
val galleries = ArrayList<Pair<GalleryBlock, Deferred<String>>>()
|
val galleries = ArrayList<GalleryBlock>()
|
||||||
|
|
||||||
val adapter = GalleryBlockAdapter(glide, galleries).apply {
|
val adapter = GalleryBlockAdapter(context, galleries).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
this@GalleryDialog.onChipClickedHandler.forEach { handler ->
|
||||||
handler.invoke(tag)
|
handler.invoke(tag)
|
||||||
@@ -232,11 +237,11 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
gallery.related.forEachIndexed { i, galleryID ->
|
gallery.related.forEachIndexed { i, galleryID ->
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
getGalleryBlock(galleryID)
|
Cache(context).getGalleryBlock(galleryID)
|
||||||
}.let {
|
}.let {
|
||||||
val galleryBlock = it.await() ?: return@let
|
val galleryBlock = it.await() ?: return@let
|
||||||
|
|
||||||
galleries.add(Pair(galleryBlock, GlobalScope.async { galleryBlock.thumbnails.first() }))
|
galleries.add(galleryBlock)
|
||||||
adapter.notifyItemInserted(i)
|
adapter.notifyItemInserted(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,14 +257,14 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
|
|||||||
ItemClickSupport.addTo(this)
|
ItemClickSupport.addTo(this)
|
||||||
.setOnItemClickListener { _, position, _ ->
|
.setOnItemClickListener { _, position, _ ->
|
||||||
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
|
||||||
putExtra("galleryID", galleries[position].first.id)
|
putExtra("galleryID", galleries[position].id)
|
||||||
})
|
})
|
||||||
(context.applicationContext as Pupil).histories.add(galleries[position].first.id)
|
(context.applicationContext as Pupil).histories.add(galleries[position].id)
|
||||||
}
|
}
|
||||||
.setOnItemLongClickListener { _, position, _ ->
|
.setOnItemLongClickListener { _, position, _ ->
|
||||||
GalleryDialog(
|
GalleryDialog(
|
||||||
context,
|
context,
|
||||||
galleries[position].first.id
|
galleries[position].id
|
||||||
).apply {
|
).apply {
|
||||||
onChipClickedHandler.add { tag ->
|
onChipClickedHandler.add { tag ->
|
||||||
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
|
||||||
|
|||||||
97
app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
Normal file
97
app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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.dialog
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.adapters.MirrorAdapter
|
||||||
|
|
||||||
|
class MirrorDialog(context: Context) : AlertDialog(context) {
|
||||||
|
|
||||||
|
class ItemTouchHelperCallback : ItemTouchHelper.Callback() {
|
||||||
|
|
||||||
|
var onMoveItem : ((Int, Int) -> (Unit))? = null
|
||||||
|
|
||||||
|
override fun getMovementFlags(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder
|
||||||
|
) = makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
onMoveItem?.invoke(viewHolder.adapterPosition, target.adapterPosition)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var recyclerView: RecyclerView
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
initDialog()
|
||||||
|
|
||||||
|
setTitle(R.string.settings_mirror_title)
|
||||||
|
setView(recyclerView)
|
||||||
|
setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> }
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDialog() {
|
||||||
|
recyclerView = RecyclerView(context).apply recyclerview@{
|
||||||
|
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = MirrorAdapter(context).apply adapter@{
|
||||||
|
val itemTouchHelper = ItemTouchHelper(ItemTouchHelperCallback().apply {
|
||||||
|
onMoveItem = this@adapter.onItemMove
|
||||||
|
}).apply {
|
||||||
|
attachToRecyclerView(this@recyclerview)
|
||||||
|
}
|
||||||
|
|
||||||
|
onStartDrag = {
|
||||||
|
itemTouchHelper.startDrag(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemMoved = {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString("mirrors", it.joinToString(">"))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,10 +19,12 @@
|
|||||||
package xyz.quaver.pupil.ui.fragment
|
package xyz.quaver.pupil.ui.fragment
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
@@ -34,6 +36,7 @@ import xyz.quaver.pupil.ui.LockActivity
|
|||||||
import xyz.quaver.pupil.ui.SettingsActivity
|
import xyz.quaver.pupil.ui.SettingsActivity
|
||||||
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog
|
||||||
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
|
||||||
|
import xyz.quaver.pupil.ui.dialog.MirrorDialog
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -41,7 +44,14 @@ import java.io.File
|
|||||||
class SettingsFragment :
|
class SettingsFragment :
|
||||||
PreferenceFragmentCompat(),
|
PreferenceFragmentCompat(),
|
||||||
Preference.OnPreferenceClickListener,
|
Preference.OnPreferenceClickListener,
|
||||||
Preference.OnPreferenceChangeListener {
|
Preference.OnPreferenceChangeListener,
|
||||||
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
@@ -61,7 +71,7 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDirSize(dir: File) : String {
|
private fun getDirSize(dir: DocumentFile) : String {
|
||||||
val size = dir.walk().map { it.length() }.sum()
|
val size = dir.walk().map { it.length() }.sum()
|
||||||
|
|
||||||
return getString(R.string.settings_clear_summary, byteToString(size))
|
return getString(R.string.settings_clear_summary, byteToString(size))
|
||||||
@@ -76,7 +86,7 @@ class SettingsFragment :
|
|||||||
checkUpdate(activity as SettingsActivity, true)
|
checkUpdate(activity as SettingsActivity, true)
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = File(context.cacheDir, "imageCache")
|
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
@@ -91,7 +101,7 @@ class SettingsFragment :
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
"delete_downloads" -> {
|
"delete_downloads" -> {
|
||||||
val dir = getDownloadDirectory(context)
|
val dir = getDownloadDirectory(context)!!
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
@@ -100,10 +110,6 @@ class SettingsFragment :
|
|||||||
if (dir.exists())
|
if (dir.exists())
|
||||||
dir.deleteRecursively()
|
dir.deleteRecursively()
|
||||||
|
|
||||||
val downloads = (activity!!.application as Pupil).downloads
|
|
||||||
|
|
||||||
downloads.clear()
|
|
||||||
|
|
||||||
summary = getDirSize(dir)
|
summary = getDirSize(dir)
|
||||||
}
|
}
|
||||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||||
@@ -123,33 +129,28 @@ class SettingsFragment :
|
|||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
"dl_location" -> {
|
"dl_location" -> {
|
||||||
DownloadLocationDialog(context).apply {
|
DownloadLocationDialog(activity!!).show()
|
||||||
onDownloadLocationChangedListener = { value ->
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit()
|
|
||||||
.putInt(key, value)
|
|
||||||
.apply()
|
|
||||||
summary = getDownloadDirectory(context).absolutePath
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
"default_query" -> {
|
"default_query" -> {
|
||||||
DefaultQueryDialog(context).apply {
|
DefaultQueryDialog(context).apply {
|
||||||
onPositiveButtonClickListener = { newTags ->
|
onPositiveButtonClickListener = { newTags ->
|
||||||
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||||
summary = newTags.toString()
|
summary = newTags.toString()
|
||||||
dismiss() //This sucks
|
|
||||||
// TODO: make dialog dissmiss itself :P
|
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
"app_lock" -> {
|
"app_lock" -> {
|
||||||
val intent = Intent(context, LockActivity::class.java)
|
val intent = Intent(context, LockActivity::class.java)
|
||||||
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
|
activity?.startActivityForResult(intent, REQUEST_LOCK)
|
||||||
|
}
|
||||||
|
"mirrors" -> {
|
||||||
|
MirrorDialog(context)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
"backup" -> {
|
"backup" -> {
|
||||||
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
||||||
File(getDownloadDirectory(context), "favorites.json"),
|
context,
|
||||||
true
|
getDownloadDirectory(context)?.createFile("null", "favorites.json")!!
|
||||||
)
|
)
|
||||||
|
|
||||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||||
@@ -161,7 +162,7 @@ class SettingsFragment :
|
|||||||
type = "*/*"
|
type = "*/*"
|
||||||
}
|
}
|
||||||
|
|
||||||
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_RESTORE)
|
activity?.startActivityForResult(intent, REQUEST_RESTORE)
|
||||||
}
|
}
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
@@ -188,6 +189,15 @@ class SettingsFragment :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
|
when (key) {
|
||||||
|
"dl_location" -> {
|
||||||
|
findPreference<Preference>(key)?.summary =
|
||||||
|
FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||||
|
|
||||||
@@ -214,13 +224,13 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = File(context.cacheDir, "imageCache")
|
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
||||||
summary = getDirSize(dir)
|
summary = getDirSize(dir)
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"delete_downloads" -> {
|
"delete_downloads" -> {
|
||||||
val dir = getDownloadDirectory(context)
|
val dir = getDownloadDirectory(context)!!
|
||||||
summary = getDirSize(dir)
|
summary = getDirSize(dir)
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
@@ -232,7 +242,7 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"dl_location" -> {
|
"dl_location" -> {
|
||||||
summary = getDownloadDirectory(context).absolutePath
|
summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri)
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
@@ -259,6 +269,9 @@ class SettingsFragment :
|
|||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
|
"mirrors" -> {
|
||||||
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
}
|
||||||
"dark_mode" -> {
|
"dark_mode" -> {
|
||||||
onPreferenceChangeListener = this@SettingsFragment
|
onPreferenceChangeListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
|
|||||||
24
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
24
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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
|
||||||
|
|
||||||
|
const val REQUEST_LOCK = 38238
|
||||||
|
const val REQUEST_RESTORE = 16546
|
||||||
|
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||||
|
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||||
178
app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
Normal file
178
app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2007-2008 OpenIntents.org
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @version 2009-07-03
|
||||||
|
* @author Peli
|
||||||
|
* @version 2013-12-11
|
||||||
|
* @author paulburke (ipaulpro)
|
||||||
|
*/
|
||||||
|
public class FileUtils {
|
||||||
|
/**
|
||||||
|
* Get a file path from a Uri. This will get the the path for Storage Access
|
||||||
|
* Framework Documents, as well as the _data field for the MediaStore and
|
||||||
|
* other file-based ContentProviders.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param uri The Uri to query.
|
||||||
|
* @author paulburke
|
||||||
|
*/
|
||||||
|
public static String getPath(final Context context, final Uri uri) {
|
||||||
|
|
||||||
|
// DocumentProvider
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
|
||||||
|
// ExternalStorageProvider
|
||||||
|
if (isExternalStorageDocument(uri)) {
|
||||||
|
final String docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
final String[] split = docId.split(":");
|
||||||
|
final String type = split[0];
|
||||||
|
|
||||||
|
if ("primary".equalsIgnoreCase(type)) {
|
||||||
|
return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO handle non-primary volumes
|
||||||
|
}
|
||||||
|
// DownloadsProvider
|
||||||
|
else if (isDownloadsDocument(uri)) {
|
||||||
|
|
||||||
|
final String id = DocumentsContract.getDocumentId(uri);
|
||||||
|
final Uri contentUri = ContentUris.withAppendedId(
|
||||||
|
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||||
|
|
||||||
|
return getDataColumn(context, contentUri, null, null);
|
||||||
|
}
|
||||||
|
// MediaProvider
|
||||||
|
else if (isMediaDocument(uri)) {
|
||||||
|
final String docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
final String[] split = docId.split(":");
|
||||||
|
final String type = split[0];
|
||||||
|
|
||||||
|
Uri contentUri = null;
|
||||||
|
if ("image".equals(type)) {
|
||||||
|
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
} else if ("video".equals(type)) {
|
||||||
|
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
} else if ("audio".equals(type)) {
|
||||||
|
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String selection = "_id=?";
|
||||||
|
final String[] selectionArgs = new String[] {
|
||||||
|
split[1]
|
||||||
|
};
|
||||||
|
|
||||||
|
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// MediaStore (and general)
|
||||||
|
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
return getDataColumn(context, uri, null, null);
|
||||||
|
}
|
||||||
|
// File
|
||||||
|
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
return uri.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of the data column for this Uri. This is useful for
|
||||||
|
* MediaStore Uris, and other file-based ContentProviders.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param uri The Uri to query.
|
||||||
|
* @param selection (Optional) Filter used in the query.
|
||||||
|
* @param selectionArgs (Optional) Selection arguments used in the query.
|
||||||
|
* @return The value of the _data column, which is typically a file path.
|
||||||
|
*/
|
||||||
|
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||||
|
String[] selectionArgs) {
|
||||||
|
|
||||||
|
Cursor cursor = null;
|
||||||
|
final String column = "_data";
|
||||||
|
final String[] projection = {
|
||||||
|
column
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||||
|
null);
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
final int column_index = cursor.getColumnIndexOrThrow(column);
|
||||||
|
return cursor.getString(column_index);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (cursor != null)
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param uri The Uri to check.
|
||||||
|
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||||
|
*/
|
||||||
|
public static boolean isExternalStorageDocument(Uri uri) {
|
||||||
|
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param uri The Uri to check.
|
||||||
|
* @return Whether the Uri authority is DownloadsProvider.
|
||||||
|
*/
|
||||||
|
public static boolean isDownloadsDocument(Uri uri) {
|
||||||
|
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param uri The Uri to check.
|
||||||
|
* @return Whether the Uri authority is MediaProvider.
|
||||||
|
*/
|
||||||
|
public static boolean isMediaDocument(Uri uri) {
|
||||||
|
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.app.TaskStackBuilder
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.crashlytics.android.Crashlytics
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.getReader
|
|
||||||
import xyz.quaver.hitomi.getReferer
|
|
||||||
import xyz.quaver.hitomi.urlFromUrlFromHash
|
|
||||||
import xyz.quaver.hiyobi.cookie
|
|
||||||
import xyz.quaver.hiyobi.createImgList
|
|
||||||
import xyz.quaver.hiyobi.user_agent
|
|
||||||
import xyz.quaver.pupil.Pupil
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.ui.ReaderActivity
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class GalleryDownloader(
|
|
||||||
base: Context,
|
|
||||||
private val galleryID: Int,
|
|
||||||
_notify: Boolean = false
|
|
||||||
) : ContextWrapper(base) {
|
|
||||||
|
|
||||||
private val downloads = (applicationContext as Pupil).downloads
|
|
||||||
var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
|
|
||||||
|
|
||||||
var download: Boolean = false
|
|
||||||
set(value) {
|
|
||||||
if (value) {
|
|
||||||
field = true
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
|
|
||||||
if (reader?.isActive == false && downloadJob?.isActive != true) {
|
|
||||||
val data = File(getDownloadDirectory(this), galleryID.toString())
|
|
||||||
val cache = File(cacheDir, "imageCache/$galleryID")
|
|
||||||
|
|
||||||
if (File(cache, "images").exists() && !data.exists()) {
|
|
||||||
cache.copyRecursively(data, true)
|
|
||||||
cache.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
field = false
|
|
||||||
}
|
|
||||||
|
|
||||||
downloads.add(galleryID)
|
|
||||||
} else {
|
|
||||||
field = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onNotifyChangedHandler?.invoke(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val reader: Deferred<Reader?>?
|
|
||||||
private var downloadJob: Job? = null
|
|
||||||
|
|
||||||
private lateinit var notificationBuilder: NotificationCompat.Builder
|
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
|
||||||
|
|
||||||
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
|
|
||||||
var onProgressHandler: ((Int) -> Unit)? = null
|
|
||||||
var onDownloadedHandler: ((List<String>) -> Unit)? = null
|
|
||||||
var onErrorHandler: ((Exception) -> Unit)? = null
|
|
||||||
var onCompleteHandler: (() -> Unit)? = null
|
|
||||||
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
|
|
||||||
|
|
||||||
companion object : SparseArray<GalleryDownloader>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
put(galleryID, this)
|
|
||||||
|
|
||||||
initNotification()
|
|
||||||
|
|
||||||
reader = CoroutineScope(Dispatchers.IO).async {
|
|
||||||
try {
|
|
||||||
download = _notify
|
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
val serializer = Reader.serializer()
|
|
||||||
|
|
||||||
//Check cache
|
|
||||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "reader.json")
|
|
||||||
|
|
||||||
try {
|
|
||||||
json.parse(serializer, cache.readText())
|
|
||||||
} catch(e: Exception) {
|
|
||||||
cache.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.exists()) {
|
|
||||||
val cached = json.parse(serializer, cache.readText())
|
|
||||||
|
|
||||||
if (cached.galleryInfo.isNotEmpty()) {
|
|
||||||
useHiyobi = cached.code == Reader.Code.HIYOBI
|
|
||||||
|
|
||||||
onReaderLoadedHandler?.invoke(cached)
|
|
||||||
|
|
||||||
return@async cached
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Cache doesn't exist. Load from internet
|
|
||||||
val reader = when {
|
|
||||||
useHiyobi -> {
|
|
||||||
try {
|
|
||||||
xyz.quaver.hiyobi.getReader(galleryID)
|
|
||||||
} catch(e: Exception) {
|
|
||||||
useHiyobi = false
|
|
||||||
getReader(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
getReader(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.galleryInfo.isNotEmpty()) {
|
|
||||||
//Save cache
|
|
||||||
if (cache.parentFile?.exists() == false)
|
|
||||||
cache.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
cache.writeText(json.stringify(serializer, reader))
|
|
||||||
}
|
|
||||||
|
|
||||||
reader
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Crashlytics.logException(e)
|
|
||||||
onErrorHandler?.invoke(e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
downloadJob = CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
val reader = reader!!.await() ?: return@launch
|
|
||||||
val lowQuality = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader)
|
|
||||||
.getBoolean("low_quality", false)
|
|
||||||
|
|
||||||
notificationBuilder.setContentTitle(reader.title)
|
|
||||||
|
|
||||||
val list = ArrayList<String>()
|
|
||||||
|
|
||||||
onReaderLoadedHandler?.invoke(reader)
|
|
||||||
|
|
||||||
notificationBuilder
|
|
||||||
.setProgress(reader.galleryInfo.size, 0, false)
|
|
||||||
.setContentText("0/${reader.galleryInfo.size}")
|
|
||||||
|
|
||||||
reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunk ->
|
|
||||||
chunk.mapIndexed { i, galleryInfo ->
|
|
||||||
val index = chunkIndex*4+i
|
|
||||||
|
|
||||||
async(Dispatchers.IO) {
|
|
||||||
val url = when(useHiyobi) {
|
|
||||||
true -> createImgList(galleryID, reader, lowQuality)[index].path
|
|
||||||
false -> when {
|
|
||||||
(!galleryInfo.hash.isNullOrBlank()) && (galleryInfo.haswebp == 1) && lowQuality ->
|
|
||||||
urlFromUrlFromHash(galleryID, galleryInfo, "webp")
|
|
||||||
else ->
|
|
||||||
urlFromUrlFromHash(galleryID, galleryInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val name = "$index".padStart(4, '0')
|
|
||||||
val ext = url.split('.').last()
|
|
||||||
|
|
||||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryID), "images/$name.$ext")
|
|
||||||
|
|
||||||
if (!cache.exists())
|
|
||||||
try {
|
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
|
||||||
if (useHiyobi) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
|
||||||
setRequestProperty("Cookie", cookie)
|
|
||||||
} else
|
|
||||||
setRequestProperty("Referer", getReferer(galleryID))
|
|
||||||
|
|
||||||
if (cache.parentFile?.exists() == false)
|
|
||||||
cache.parentFile!!.mkdirs()
|
|
||||||
|
|
||||||
inputStream.copyTo(FileOutputStream(cache))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
cache.delete()
|
|
||||||
|
|
||||||
onErrorHandler?.invoke(e)
|
|
||||||
|
|
||||||
notificationBuilder
|
|
||||||
.setContentTitle(reader.title)
|
|
||||||
.setContentText(getString(R.string.reader_notification_error))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
"images/$name.$ext"
|
|
||||||
}
|
|
||||||
}.forEach {
|
|
||||||
list.add(it.await())
|
|
||||||
|
|
||||||
val index = list.size
|
|
||||||
|
|
||||||
onProgressHandler?.invoke(index)
|
|
||||||
|
|
||||||
notificationBuilder
|
|
||||||
.setProgress(reader.galleryInfo.size, index, false)
|
|
||||||
.setContentText("$index/${reader.galleryInfo.size}")
|
|
||||||
|
|
||||||
if (download)
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
|
|
||||||
onDownloadedHandler?.invoke(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer(false).schedule(1000) {
|
|
||||||
notificationBuilder
|
|
||||||
.setContentTitle(reader.title)
|
|
||||||
.setContentText(getString(R.string.reader_notification_complete))
|
|
||||||
.setProgress(0, 0, false)
|
|
||||||
|
|
||||||
if (download) {
|
|
||||||
File(cacheDir, "imageCache/${galleryID}").let {
|
|
||||||
if (it.exists()) {
|
|
||||||
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryID.toString())
|
|
||||||
|
|
||||||
if (!target.exists())
|
|
||||||
target.mkdirs()
|
|
||||||
|
|
||||||
it.copyRecursively(target, true)
|
|
||||||
it.deleteRecursively()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(galleryID, notificationBuilder.build())
|
|
||||||
|
|
||||||
download = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onCompleteHandler?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancel() {
|
|
||||||
downloadJob?.cancel()
|
|
||||||
|
|
||||||
remove(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun cancelAndJoin() {
|
|
||||||
downloadJob?.cancelAndJoin()
|
|
||||||
|
|
||||||
remove(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeOnReaderLoaded() {
|
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
|
||||||
onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearNotification() {
|
|
||||||
notificationManager.cancel(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invokeOnNotifyChanged() {
|
|
||||||
onNotifyChangedHandler?.invoke(download)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNotification() {
|
|
||||||
val intent = Intent(this, ReaderActivity::class.java).apply {
|
|
||||||
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(getString(R.string.reader_loading))
|
|
||||||
setContentText(getString(R.string.reader_notification_text))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
220
app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
Normal file
220
app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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.download
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.parse
|
||||||
|
import kotlinx.serialization.stringify
|
||||||
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
import xyz.quaver.pupil.util.*
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class Cache(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
|
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
// Search in this order
|
||||||
|
// Download -> Cache
|
||||||
|
fun getCachedGallery(galleryID: Int) : DocumentFile? {
|
||||||
|
var file = getDownloadDirectory(this)?.findFile(galleryID.toString())
|
||||||
|
|
||||||
|
if (file?.exists() == true)
|
||||||
|
return file
|
||||||
|
|
||||||
|
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
|
||||||
|
|
||||||
|
return if (file.exists())
|
||||||
|
file
|
||||||
|
else
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||||
|
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
||||||
|
val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
|
||||||
|
|
||||||
|
if (file?.exists() != true)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Json.parse(file.readText(this))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
//File corrupted
|
||||||
|
file.delete()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||||
|
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||||
|
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
|
||||||
|
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
||||||
|
if (!it.exists())
|
||||||
|
it.mkdirs()
|
||||||
|
}).createFile("null", ".metadata") ?: return
|
||||||
|
|
||||||
|
file.writeText(this, Json.stringify(metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getThumbnail(galleryID: Int): String? {
|
||||||
|
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||||
|
|
||||||
|
val thumbnail = if (metadata?.thumbnail == null)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
||||||
|
try {
|
||||||
|
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
metadata.thumbnail
|
||||||
|
|
||||||
|
setCachedMetadata(
|
||||||
|
galleryID,
|
||||||
|
Metadata(Cache(this).getCachedMetadata(galleryID), thumbnail = thumbnail)
|
||||||
|
)
|
||||||
|
|
||||||
|
return thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||||
|
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||||
|
|
||||||
|
val galleryBlock = if (metadata?.galleryBlock == null)
|
||||||
|
listOf(
|
||||||
|
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||||
|
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||||
|
).map {
|
||||||
|
CoroutineScope(Dispatchers.IO).async {
|
||||||
|
kotlin.runCatching {
|
||||||
|
it.invoke()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
else
|
||||||
|
metadata.galleryBlock
|
||||||
|
|
||||||
|
setCachedMetadata(
|
||||||
|
galleryID,
|
||||||
|
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||||
|
|
||||||
|
return galleryBlock.firstOrNull {
|
||||||
|
mirrors.contains(it.code.name)
|
||||||
|
} ?: galleryBlock.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getReaderOrNull(galleryID: Int): Reader? {
|
||||||
|
val metadata = getCachedMetadata(galleryID)
|
||||||
|
|
||||||
|
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||||
|
|
||||||
|
return metadata?.readers?.firstOrNull {
|
||||||
|
mirrors.contains(it.code.name)
|
||||||
|
} ?: metadata?.readers?.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getReader(galleryID: Int): Reader? {
|
||||||
|
val metadata = getCachedMetadata(galleryID)
|
||||||
|
|
||||||
|
val readers = if (metadata?.readers == null) {
|
||||||
|
listOf(
|
||||||
|
{ xyz.quaver.hitomi.getReader(galleryID) },
|
||||||
|
{ xyz.quaver.hiyobi.getReader(galleryID) }
|
||||||
|
).map {
|
||||||
|
CoroutineScope(Dispatchers.IO).async {
|
||||||
|
kotlin.runCatching {
|
||||||
|
it.invoke()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
} else {
|
||||||
|
metadata.readers
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readers.isNotEmpty())
|
||||||
|
setCachedMetadata(
|
||||||
|
galleryID,
|
||||||
|
Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers)
|
||||||
|
)
|
||||||
|
|
||||||
|
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||||
|
|
||||||
|
return readers.firstOrNull {
|
||||||
|
mirrors.contains(it.code.name)
|
||||||
|
} ?: readers.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getImages(galleryID: Int): List<DocumentFile?>? {
|
||||||
|
val gallery = getCachedGallery(galleryID) ?: return null
|
||||||
|
val reader = getReaderOrNull(galleryID) ?: return null
|
||||||
|
val images = gallery.listFiles()
|
||||||
|
|
||||||
|
return reader.galleryInfo.indices.map { index ->
|
||||||
|
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putImage(galleryID: Int, name: String, data: ByteArray) {
|
||||||
|
val cache = getCachedGallery(galleryID) ?:
|
||||||
|
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
||||||
|
if (!it.exists())
|
||||||
|
it.mkdirs()
|
||||||
|
}) ?: return
|
||||||
|
|
||||||
|
if (!Regex("""^[0-9]+.+$""").matches(name))
|
||||||
|
throw IllegalArgumentException("File name is not a number")
|
||||||
|
|
||||||
|
cache.createFile("null", name)?.writeBytes(this, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveToDownload(galleryID: Int) {
|
||||||
|
val cache = getCachedGallery(galleryID)
|
||||||
|
|
||||||
|
if (cache != null) {
|
||||||
|
val download = getDownloadDirectory(this)!!
|
||||||
|
|
||||||
|
if (!download.isParentOf(cache)) {
|
||||||
|
cache.copyRecursively(this, download)
|
||||||
|
cache.deleteRecursively()
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
getDownloadDirectory(this)?.createDirectory(galleryID.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
||||||
|
|
||||||
|
fun setDownloading(galleryID: Int, isDownloading: Boolean) {
|
||||||
|
setCachedMetadata(galleryID, Metadata(getCachedMetadata(galleryID), isDownloading = isDownloading))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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.download
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.TaskStackBuilder
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.crashlytics.android.Crashlytics
|
||||||
|
import io.fabric.sdk.android.Fabric
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import okhttp3.*
|
||||||
|
import okio.*
|
||||||
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
import xyz.quaver.hitomi.getReferer
|
||||||
|
import xyz.quaver.hitomi.urlFromUrlFromHash
|
||||||
|
import xyz.quaver.hiyobi.cookie
|
||||||
|
import xyz.quaver.hiyobi.createImgList
|
||||||
|
import xyz.quaver.hiyobi.user_agent
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.ui.ReaderActivity
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
|
||||||
|
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||||
|
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||||
|
|
||||||
|
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
//region ProgressListener
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private val progressListener = object: ProgressListener {
|
||||||
|
override fun update(tag: Any?, bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
|
val (galleryID, index) = (tag as? Pair<Int, Int>) ?: return
|
||||||
|
|
||||||
|
if (!done && progress[galleryID]?.get(index)?.isFinite() == true)
|
||||||
|
progress[galleryID]?.set(index, bytesRead * 100F / contentLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressListener {
|
||||||
|
fun update(tag: Any?, bytesRead : Long, contentLength: Long, done: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProgressResponseBody(
|
||||||
|
val tag: Any?,
|
||||||
|
val responseBody: ResponseBody,
|
||||||
|
val progressListener : ProgressListener
|
||||||
|
) : ResponseBody() {
|
||||||
|
private var bufferedSource : BufferedSource? = null
|
||||||
|
|
||||||
|
override fun contentLength() = responseBody.contentLength()
|
||||||
|
override fun contentType() = responseBody.contentType() ?: null
|
||||||
|
|
||||||
|
override fun source(): BufferedSource {
|
||||||
|
if (bufferedSource == null)
|
||||||
|
bufferedSource = Okio.buffer(source(responseBody.source()))
|
||||||
|
|
||||||
|
return bufferedSource!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun source(source: Source) = object: ForwardingSource(source) {
|
||||||
|
|
||||||
|
var totalBytesRead = 0L
|
||||||
|
|
||||||
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
|
val bytesRead = super.read(sink, byteCount)
|
||||||
|
|
||||||
|
totalBytesRead += if (bytesRead == -1L) 0L else bytesRead
|
||||||
|
progressListener.update(tag, totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||||
|
|
||||||
|
return bytesRead
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Singleton
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Volatile private var instance: DownloadWorker? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context) =
|
||||||
|
instance ?: synchronized(this) {
|
||||||
|
instance ?: DownloadWorker(context).also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
val queue = LinkedBlockingQueue<Int>()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* KEY
|
||||||
|
* primary galleryID
|
||||||
|
* secondary index
|
||||||
|
* PRIMARY VALUE
|
||||||
|
* MutableList -> Download in progress
|
||||||
|
* null -> Loading / Gallery doesn't exist
|
||||||
|
* SECONDARY VALUE
|
||||||
|
* 0 <= value < 100 -> Download in progress
|
||||||
|
* Float.POSITIVE_INFINITY -> Download completed
|
||||||
|
* Float.NaN -> Exception
|
||||||
|
*/
|
||||||
|
val progress = SparseArray<MutableList<Float>?>()
|
||||||
|
/*
|
||||||
|
* KEY
|
||||||
|
* primary galleryID
|
||||||
|
* secondary index
|
||||||
|
* PRIMARY VALUE
|
||||||
|
* MutableList -> Download in progress / Loading
|
||||||
|
* null -> Gallery doesn't exist
|
||||||
|
* SECONDARY VALUE
|
||||||
|
* Throwable -> Exception
|
||||||
|
* null -> Download in progress / Loading
|
||||||
|
*/
|
||||||
|
val exception = SparseArray<MutableList<Throwable?>?>()
|
||||||
|
val notification = SparseArray<NotificationCompat.Builder>()
|
||||||
|
|
||||||
|
private val loop = loop()
|
||||||
|
private val worker = SparseArray<Job?>()
|
||||||
|
@Volatile var nRunners = 0
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request = chain.request()
|
||||||
|
var response = chain.proceed(request)
|
||||||
|
|
||||||
|
var retry = preferences.getInt("retry", 3)
|
||||||
|
while (!response.isSuccessful && retry > 0) {
|
||||||
|
response = chain.proceed(request)
|
||||||
|
retry--
|
||||||
|
}
|
||||||
|
|
||||||
|
response.newBuilder()
|
||||||
|
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor()))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
queue.clear()
|
||||||
|
|
||||||
|
loop.cancel()
|
||||||
|
for (i in 0..worker.size()) {
|
||||||
|
val galleryID = worker.keyAt(i)
|
||||||
|
|
||||||
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
|
worker[galleryID]?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.dispatcher().cancelAll()
|
||||||
|
|
||||||
|
progress.clear()
|
||||||
|
exception.clear()
|
||||||
|
notification.clear()
|
||||||
|
notificationManager.cancelAll()
|
||||||
|
|
||||||
|
nRunners = 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(galleryID: Int) {
|
||||||
|
queue.remove(galleryID)
|
||||||
|
worker[galleryID]?.cancel()
|
||||||
|
|
||||||
|
client.dispatcher().queuedCalls()
|
||||||
|
.filter {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(it.request().tag() as? Pair<Int, Int>)?.first == galleryID
|
||||||
|
}
|
||||||
|
.forEach {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.remove(galleryID)
|
||||||
|
exception.remove(galleryID)
|
||||||
|
notification.remove(galleryID)
|
||||||
|
notificationManager.cancel(galleryID)
|
||||||
|
|
||||||
|
if (progress.indexOfKey(galleryID) >= 0) {
|
||||||
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
|
nRunners--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
||||||
|
|
||||||
|
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
||||||
|
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||||
|
val lowQuality = preferences.getBoolean("low_quality", false)
|
||||||
|
|
||||||
|
//Cache exists :P
|
||||||
|
cache?.get(index)?.let {
|
||||||
|
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
with(Cache(this@DownloadWorker)) {
|
||||||
|
if (isDownloading(galleryID)) {
|
||||||
|
moveToDownload(galleryID)
|
||||||
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nRunners--
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = Request.Builder().apply {
|
||||||
|
when (reader.code) {
|
||||||
|
Code.HITOMI -> {
|
||||||
|
url(
|
||||||
|
urlFromUrlFromHash(
|
||||||
|
galleryID,
|
||||||
|
reader.galleryInfo[index],
|
||||||
|
if (lowQuality) "webp" else null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addHeader("Referer", getReferer(galleryID))
|
||||||
|
}
|
||||||
|
Code.HIYOBI -> {
|
||||||
|
url(createImgList(galleryID, reader, lowQuality)[index].path)
|
||||||
|
addHeader("User-Agent", user_agent)
|
||||||
|
addHeader("Cookie", cookie)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
//shouldn't be called anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag(galleryID to index)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
client.newCall(request).enqueue(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val reader = Cache(this@DownloadWorker).getReader(galleryID)
|
||||||
|
|
||||||
|
//gallery doesn't exist
|
||||||
|
if (reader == null) {
|
||||||
|
progress.put(galleryID, null)
|
||||||
|
exception.put(galleryID, null)
|
||||||
|
|
||||||
|
Cache(this@DownloadWorker).setDownloading(galleryID, false)
|
||||||
|
nRunners--
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
|
||||||
|
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
|
||||||
|
|
||||||
|
notification[galleryID].setContentTitle(reader.title)
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
for (i in reader.galleryInfo.indices) {
|
||||||
|
val callback = object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (Fabric.isInitialized())
|
||||||
|
Crashlytics.logException(e)
|
||||||
|
|
||||||
|
progress[galleryID]?.set(i, Float.NaN)
|
||||||
|
exception[galleryID]?.set(i, e)
|
||||||
|
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
val cache = Cache(this@DownloadWorker)
|
||||||
|
if (cache.isDownloading(galleryID)) {
|
||||||
|
cache.moveToDownload(galleryID)
|
||||||
|
cache.setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
nRunners--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.body().use {
|
||||||
|
val res = it.bytes()
|
||||||
|
val ext =
|
||||||
|
call.request().url().encodedPath().split('.').last()
|
||||||
|
|
||||||
|
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
|
||||||
|
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(galleryID)
|
||||||
|
|
||||||
|
if (isCompleted(galleryID)) {
|
||||||
|
val cache = Cache(this@DownloadWorker)
|
||||||
|
if (cache.isDownloading(galleryID)) {
|
||||||
|
cache.moveToDownload(galleryID)
|
||||||
|
cache.setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
|
nRunners--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueDownload(galleryID, reader, i, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(galleryID: Int) {
|
||||||
|
val max = progress[galleryID]?.size ?: 0
|
||||||
|
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||||
|
|
||||||
|
if (isCompleted(galleryID))
|
||||||
|
notification[galleryID]
|
||||||
|
?.setContentText(getString(R.string.reader_notification_complete))
|
||||||
|
?.setProgress(0, 0, false)
|
||||||
|
else
|
||||||
|
notification[galleryID]
|
||||||
|
?.setProgress(max, progress, false)
|
||||||
|
?.setContentText("$progress/$max")
|
||||||
|
|
||||||
|
if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
|
||||||
|
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||||
|
else
|
||||||
|
notificationManager.cancel(galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotification(galleryID: Int) {
|
||||||
|
val intent = Intent(this, ReaderActivity::class.java).apply {
|
||||||
|
putExtra("galleryID", galleryID)
|
||||||
|
}
|
||||||
|
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||||
|
addNextIntentWithParentStack(intent)
|
||||||
|
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||||
|
setContentTitle(getString(R.string.reader_loading))
|
||||||
|
setContentText(getString(R.string.reader_notification_text))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P
|
||||||
|
setContentIntent(pendingIntent)
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (true) {
|
||||||
|
if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4))
|
||||||
|
continue
|
||||||
|
|
||||||
|
val galleryID = queue.poll() ?: continue
|
||||||
|
|
||||||
|
if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||||
|
continue
|
||||||
|
|
||||||
|
initNotification(galleryID)
|
||||||
|
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||||
|
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||||
|
worker.put(galleryID, download(galleryID))
|
||||||
|
nRunners++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
Normal file
44
app/src/main/java/xyz/quaver/pupil/util/download/Metadata.kt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2020 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.download
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.hitomi.Reader
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Metadata(
|
||||||
|
val thumbnail: String? = null,
|
||||||
|
val galleryBlock: List<GalleryBlock>? = null,
|
||||||
|
val readers: List<Reader>? = null,
|
||||||
|
val isDownloading: Boolean? = null
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
metadata: Metadata?,
|
||||||
|
thumbnail: String? = null,
|
||||||
|
galleryBlock: List<GalleryBlock>? = null,
|
||||||
|
readers: List<Reader>? = null,
|
||||||
|
isDownloading: Boolean? = null
|
||||||
|
) : this(
|
||||||
|
thumbnail ?: metadata?.thumbnail,
|
||||||
|
galleryBlock ?: metadata?.galleryBlock,
|
||||||
|
readers ?: metadata?.readers,
|
||||||
|
isDownloading ?: metadata?.isDownloading
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,28 +19,32 @@
|
|||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.ContextCompat
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
fun getCachedGallery(context: Context, galleryID: Int): File {
|
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||||
return File(getDownloadDirectory(context), galleryID.toString()).let {
|
getDownloadDirectory(context)?.findFile(galleryID.toString()) ?:
|
||||||
when {
|
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
|
||||||
it.exists() -> it
|
|
||||||
else -> File(context.cacheDir, "imageCache/$galleryID")
|
fun getDownloadDirectory(context: Context) : DocumentFile? {
|
||||||
}
|
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||||
|
Uri.parse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return if (uri.toString().startsWith("file"))
|
||||||
|
DocumentFile.fromFile(File(uri.path!!))
|
||||||
|
else
|
||||||
|
DocumentFile.fromTreeUri(context, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadDirectory(context: Context): File {
|
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||||
val dlLocation = PreferenceManager.getDefaultSharedPreferences(context).getInt("dl_location", 0)
|
context.contentResolver.openOutputStream(to.uri).use { out ->
|
||||||
|
out!!
|
||||||
return ContextCompat.getExternalFilesDirs(context, null)[dlLocation]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
|
||||||
to.outputStream().use { out ->
|
|
||||||
|
|
||||||
with(openConnection()) {
|
with(openConnection()) {
|
||||||
val fileSize = contentLength.toLong()
|
val fileSize = contentLength.toLong()
|
||||||
@@ -49,6 +53,7 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
|||||||
|
|
||||||
var bytesCopied: Long = 0
|
var bytesCopied: Long = 0
|
||||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
|
||||||
var bytes = it.read(buffer)
|
var bytes = it.read(buffer)
|
||||||
while (bytes >= 0) {
|
while (bytes >= 0) {
|
||||||
out.write(buffer, 0, bytes)
|
out.write(buffer, 0, bytes)
|
||||||
@@ -61,4 +66,71 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
|
||||||
|
var parent = file?.parentFile
|
||||||
|
while (parent != null) {
|
||||||
|
if (this.uri.path == parent.uri.path)
|
||||||
|
return true
|
||||||
|
|
||||||
|
parent = parent.parentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
|
||||||
|
fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
|
||||||
|
fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
|
||||||
|
|
||||||
|
fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
|
||||||
|
fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
|
||||||
|
|
||||||
|
fun DocumentFile.copyRecursively(
|
||||||
|
context: Context,
|
||||||
|
target: DocumentFile
|
||||||
|
) {
|
||||||
|
if (!exists())
|
||||||
|
throw Exception("The source file doesn't exist.")
|
||||||
|
|
||||||
|
if (this.isFile)
|
||||||
|
target.createFile("null", name!!)!!.writeBytes(
|
||||||
|
context,
|
||||||
|
readBytes(context)
|
||||||
|
)
|
||||||
|
else if (this.isDirectory) {
|
||||||
|
target.createDirectory(name!!).also { newTarget ->
|
||||||
|
listFiles().forEach { child ->
|
||||||
|
child.copyRecursively(context, newTarget!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DocumentFile.deleteRecursively() {
|
||||||
|
|
||||||
|
if (this.isDirectory)
|
||||||
|
listFiles().forEach {
|
||||||
|
it.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
|
||||||
|
if (state.isEmpty())
|
||||||
|
state.push(this)
|
||||||
|
|
||||||
|
listFiles().forEach {
|
||||||
|
state.push(it)
|
||||||
|
|
||||||
|
if (it.isDirectory) {
|
||||||
|
it.walk(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
|
||||||
@@ -21,27 +21,20 @@ package xyz.quaver.pupil.util
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.InternalSerializationApi
|
|
||||||
import kotlinx.serialization.internal.EnumSerializer
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.availableInHiyobi
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -153,10 +146,10 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch io@{
|
CoroutineScope(Dispatchers.IO).launch io@{
|
||||||
val target = File(getDownloadDirectory(context), "Pupil.apk")
|
val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
URL(url).download(target) { progress, fileSize ->
|
URL(url).download(context, target) { progress, fileSize ->
|
||||||
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
@@ -175,15 +168,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
|
|
||||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
setDataAndType(FileProvider.getUriForFile(
|
setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
context,
|
|
||||||
context.applicationContext.packageName + ".fileprovider",
|
|
||||||
target
|
|
||||||
), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
|
||||||
|
|
||||||
if (resolveActivity(context.packageManager) == null)
|
|
||||||
setDataAndType(Uri.fromFile(target),
|
|
||||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.apply {
|
builder.apply {
|
||||||
@@ -214,59 +199,4 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getOldReaderGalleries(context: Context) : List<File> {
|
|
||||||
val oldGallery = mutableListOf<File>()
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
getDownloadDirectory(context),
|
|
||||||
File(context.cacheDir, "imageCache")
|
|
||||||
).forEach { root ->
|
|
||||||
root.listFiles()?.forEach { gallery ->
|
|
||||||
File(gallery, "reader.json").let { readerFile ->
|
|
||||||
if (!readerFile.exists())
|
|
||||||
return@let
|
|
||||||
|
|
||||||
try {
|
|
||||||
Json(JsonConfiguration.Stable).parseJson(readerFile.readText())
|
|
||||||
.jsonObject.let { reader ->
|
|
||||||
if (!reader.contains("code"))
|
|
||||||
oldGallery.add(gallery)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldGallery
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseExperimental(InternalSerializationApi::class)
|
|
||||||
fun updateOldReaderGalleries(context: Context) {
|
|
||||||
|
|
||||||
val json = Json(JsonConfiguration.Stable)
|
|
||||||
|
|
||||||
getOldReaderGalleries(context).forEach { gallery ->
|
|
||||||
val reader = json.parseJson(File(gallery, "reader.json").apply {
|
|
||||||
if (!exists())
|
|
||||||
return@forEach
|
|
||||||
}.readText())
|
|
||||||
.jsonObject.toMutableMap()
|
|
||||||
|
|
||||||
val codeSerializer = EnumSerializer(Reader.Code::class)
|
|
||||||
|
|
||||||
reader["code"] = when {
|
|
||||||
(File(gallery, "images").list()?.
|
|
||||||
all { !it.endsWith("webp") } ?: return@forEach) &&
|
|
||||||
availableInHiyobi(gallery.name.toIntOrNull() ?: return@forEach)
|
|
||||||
-> json.toJson(codeSerializer, Reader.Code.HIYOBI)
|
|
||||||
else -> json.toJson(codeSerializer, Reader.Code.HITOMI)
|
|
||||||
}
|
|
||||||
|
|
||||||
File(gallery, "reader.json").writeText(JsonObject(reader).toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
BIN
app/src/main/res/drawable/hitomi.png
Normal file
BIN
app/src/main/res/drawable/hitomi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<vector android:height="24dp" android:tint="#fff"
|
<vector android:height="24dp"
|
||||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
<path android:fillColor="#fff" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||||
|
|||||||
8
app/src/main/res/drawable/menu.xml
Normal file
8
app/src/main/res/drawable/menu.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- drawable/menu.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,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
|
||||||
|
</vector>
|
||||||
34
app/src/main/res/drawable/reader_item_boundary.xml
Normal file
34
app/src/main/res/drawable/reader_item_boundary.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2020 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/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:bottom="1dp"
|
||||||
|
android:left="1dp"
|
||||||
|
android:right="1dp"
|
||||||
|
android:top="1dp">
|
||||||
|
<shape android:shape="rectangle" >
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#555555" />
|
||||||
|
|
||||||
|
<solid android:color="@color/transparent" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -18,21 +18,12 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/default_query_dialog_title"
|
|
||||||
style="@style/TextAppearance.AppCompat.Large"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/default_query_dialog_title"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"/>
|
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
tools:ignore="Autofill"
|
tools:ignore="Autofill"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
@@ -40,7 +31,7 @@
|
|||||||
android:id="@+id/default_query_dialog_edittext"
|
android:id="@+id/default_query_dialog_edittext"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_title"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
@@ -116,14 +107,4 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/default_query_dialog_ok"
|
|
||||||
style="?borderlessButtonStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/default_query_dialog_guro_layout"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:text="@android:string/ok"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -35,6 +35,6 @@
|
|||||||
android:id="@+id/gallery_details_tags"
|
android:id="@+id/gallery_details_tags"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:chipSpacingVertical="8dp"/>
|
app:chipSpacingVertical="4dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
app:chipSpacing="2dp"
|
app:chipSpacing="4dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
|
app:layout_constraintTop_toBottomOf="@id/galleryblock_padding"
|
||||||
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/galleryblock_thumbnail"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|||||||
49
app/src/main/res/layout/item_mirrors.xml
Normal file
49
app/src/main/res/layout/item_mirrors.xml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Pupil, Hitomi.la viewer for Android
|
||||||
|
~ Copyright (C) 2020 tom5079
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:paddingStart="32dp"
|
||||||
|
android:paddingLeft="32dp"
|
||||||
|
android:paddingEnd="32dp"
|
||||||
|
android:paddingRight="32dp"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/mirror_name"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Headline6"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/mirror_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/menu"
|
||||||
|
app:tint="?attr/colorControlNormal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
||||||
android:tint="@color/colorAccent"
|
app:tint="@color/colorAccent"
|
||||||
android:rotation="180"/>
|
android:rotation="180"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
app:srcCompat="@drawable/ic_navigate_next_black_24dp"
|
||||||
android:tint="@color/colorAccent"
|
app:tint="@color/colorAccent"
|
||||||
android:rotation="180"/>
|
android:rotation="180"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -17,9 +17,55 @@
|
|||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<com.github.chrisbanes.photoview.PhotoView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:contentDescription="@string/reader_imageview_description"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="100dp"
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
android:paddingBottom="8dp"/>
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:background="@drawable/reader_item_boundary">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/reader_item_progressbar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:progressBarStyleHorizontal"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:progress="0"
|
||||||
|
android:max="100"
|
||||||
|
android:visibility="visible"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reader_index"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/TextAppearance.AppCompat.Caption"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.github.chrisbanes.photoview.PhotoView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:contentDescription="@string/reader_imageview_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="8dp"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -25,11 +25,9 @@
|
|||||||
android:icon="@drawable/avd_star"
|
android:icon="@drawable/avd_star"
|
||||||
app:showAsAction="always"/>
|
app:showAsAction="always"/>
|
||||||
|
|
||||||
<item android:id="@+id/reader_menu_use_hiyobi"
|
<item android:id="@+id/reader_type"
|
||||||
android:title=""
|
android:title=""
|
||||||
android:icon="@drawable/ic_hiyobi"
|
app:showAsAction="ifRoom"/>
|
||||||
app:showAsAction="ifRoom"
|
|
||||||
android:visible="false"/>
|
|
||||||
|
|
||||||
<item android:id="@+id/reader_menu_page_indicator"
|
<item android:id="@+id/reader_menu_page_indicator"
|
||||||
android:title="@string/page_indicator_placeholder"
|
android:title="@string/page_indicator_placeholder"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<string name="update_title">新しいアップデートがあります</string>
|
<string name="update_title">新しいアップデートがあります</string>
|
||||||
<string name="warning">注意</string>
|
<string name="warning">注意</string>
|
||||||
<string name="settings_miscellaneous_title">その他</string>
|
<string name="settings_miscellaneous_title">その他</string>
|
||||||
<string name="settings_use_hiyobi_title">hiyobi.meからロード</string>
|
<string name="settings_mirror_title">ミラーサーバー</string>
|
||||||
<string name="settings_clear_history">履歴を削除</string>
|
<string name="settings_clear_history">履歴を削除</string>
|
||||||
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
<string name="settings_clear_history_alert_message">履歴を削除しますか?</string>
|
||||||
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
<string name="settings_clear_history_summary">履歴数: %1$d</string>
|
||||||
@@ -61,10 +61,10 @@
|
|||||||
<string name="main_export_error">エクスポートエラーが発生しました</string>
|
<string name="main_export_error">エクスポートエラーが発生しました</string>
|
||||||
<string name="settings_clear_downloads">ダウンロード削除</string>
|
<string name="settings_clear_downloads">ダウンロード削除</string>
|
||||||
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
<string name="settings_clear_downloads_alert_message">ダウンロードしたギャラリーを全て削除します。\n実行しますか?</string>
|
||||||
<string name="settings_use_hiyobi_summary">ロード速度を向上させるため可能であればhiyobi.meからイメージロード</string>
|
<string name="settings_mirror_summary">ミラーサーバからイメージをロード</string>
|
||||||
<string name="main_drawer_favorite">お気に入り</string>
|
<string name="main_drawer_favorite">お気に入り</string>
|
||||||
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
|
<string name="main_open_gallery_by_id">ギャラリー番号で見る</string>
|
||||||
<string name="main_open_gallery_by_id_error">エラーが発生しました</string>
|
<string name="reader_failed_to_find_gallery">エラーが発生しました</string>
|
||||||
<string name="settings_storage">ストレージ</string>
|
<string name="settings_storage">ストレージ</string>
|
||||||
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
|
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
|
||||||
<string name="settings_app_lock">アプリロック</string>
|
<string name="settings_app_lock">アプリロック</string>
|
||||||
@@ -120,4 +120,6 @@
|
|||||||
<string name="settings_app_version_description">v%s</string>
|
<string name="settings_app_version_description">v%s</string>
|
||||||
<string name="settings_low_quality">低解像度イメージ</string>
|
<string name="settings_low_quality">低解像度イメージ</string>
|
||||||
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
|
<string name="settings_low_quality_summary">ロード速度とデータ使用料を改善するため低解像度イメージをロード</string>
|
||||||
|
<string name="settings_dl_location_custom">手動で設定</string>
|
||||||
|
<string name="settings_dl_location_not_writable">このフォルダにアクセスできません。他のフォルダを選択してください。</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
<string name="main_no_result">결과 없음</string>
|
<string name="main_no_result">결과 없음</string>
|
||||||
<string name="main_search">검색</string>
|
<string name="main_search">검색</string>
|
||||||
<string name="settings_miscellaneous_title">기타</string>
|
<string name="settings_miscellaneous_title">기타</string>
|
||||||
<string name="settings_use_hiyobi_title">hiyobi.me 사용</string>
|
|
||||||
<string name="settings_clear_history">기록 삭제</string>
|
<string name="settings_clear_history">기록 삭제</string>
|
||||||
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
<string name="settings_clear_history_alert_message">기록을 삭제하시겠습니까?</string>
|
||||||
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
<string name="settings_clear_history_summary">기록 %1$d개 저장됨</string>
|
||||||
@@ -61,10 +60,9 @@
|
|||||||
<string name="main_export_error">내보내기 오류가 발생했습니다</string>
|
<string name="main_export_error">내보내기 오류가 발생했습니다</string>
|
||||||
<string name="settings_clear_downloads">다운로드 삭제</string>
|
<string name="settings_clear_downloads">다운로드 삭제</string>
|
||||||
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
<string name="settings_clear_downloads_alert_message">다운로드 된 만화를 모두 삭제합니다.\n계속하시겠습니까?</string>
|
||||||
<string name="settings_use_hiyobi_summary">속도 향상을 위해 가능하면 hiyobi.me에서 이미지 로드</string>
|
|
||||||
<string name="main_drawer_favorite">즐겨찾기</string>
|
<string name="main_drawer_favorite">즐겨찾기</string>
|
||||||
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
|
<string name="main_open_gallery_by_id">갤러리 번호로 열기</string>
|
||||||
<string name="main_open_gallery_by_id_error">갤러리를 찾지 못했습니다</string>
|
<string name="reader_failed_to_find_gallery">갤러리를 찾지 못했습니다</string>
|
||||||
<string name="settings_storage">저장 공간</string>
|
<string name="settings_storage">저장 공간</string>
|
||||||
<string name="main_drawer_grouop_contact_discord">디스코드</string>
|
<string name="main_drawer_grouop_contact_discord">디스코드</string>
|
||||||
<string name="settings_app_lock">앱 잠금</string>
|
<string name="settings_app_lock">앱 잠금</string>
|
||||||
@@ -120,4 +118,8 @@
|
|||||||
<string name="settings_app_version_description">v%s</string>
|
<string name="settings_app_version_description">v%s</string>
|
||||||
<string name="settings_low_quality">저해상도 이미지</string>
|
<string name="settings_low_quality">저해상도 이미지</string>
|
||||||
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
|
<string name="settings_low_quality_summary">로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드</string>
|
||||||
|
<string name="settings_mirror_summary">미러 서버에서 이미지 로드</string>
|
||||||
|
<string name="settings_mirror_title">미러 설정</string>
|
||||||
|
<string name="settings_dl_location_custom">직접 설정</string>
|
||||||
|
<string name="settings_dl_location_not_writable">이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -57,4 +57,9 @@
|
|||||||
<item>japanese|日本語</item>
|
<item>japanese|日本語</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="mirrors">
|
||||||
|
<item>HITOMI|hitomi.la</item>
|
||||||
|
<item>HIYOBI|hiyobi.me</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<string name="main_jump_title">Jump to page</string>
|
<string name="main_jump_title">Jump to page</string>
|
||||||
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
|
<string name="main_jump_message">Current page: %1$d\nMaximum page: %2$d</string>
|
||||||
<string name="main_open_gallery_by_id">Open Gallery by ID</string>
|
<string name="main_open_gallery_by_id">Open Gallery by ID</string>
|
||||||
<string name="main_open_gallery_by_id_error">Failed to open gallery</string>
|
<string name="reader_failed_to_find_gallery">Failed to open gallery</string>
|
||||||
|
|
||||||
<string name="main_move">Move to page %1$d</string>
|
<string name="main_move">Move to page %1$d</string>
|
||||||
|
|
||||||
@@ -140,6 +140,8 @@
|
|||||||
<string name="settings_dl_location_removable">Removable Storage</string>
|
<string name="settings_dl_location_removable">Removable Storage</string>
|
||||||
<string name="settings_dl_location_internal">Internal Storage</string>
|
<string name="settings_dl_location_internal">Internal Storage</string>
|
||||||
<string name="settings_dl_location_available">%s available</string>
|
<string name="settings_dl_location_available">%s available</string>
|
||||||
|
<string name="settings_dl_location_custom">Custom Location</string>
|
||||||
|
<string name="settings_dl_location_not_writable">This folder is not writable. Please select another folder.</string>
|
||||||
<string name="settings_low_quality">Low quality images</string>
|
<string name="settings_low_quality">Low quality images</string>
|
||||||
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
|
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
|
||||||
|
|
||||||
@@ -151,8 +153,7 @@
|
|||||||
<!-- SETTINGS/MISCELLANEOUS -->
|
<!-- SETTINGS/MISCELLANEOUS -->
|
||||||
|
|
||||||
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
<string name="settings_miscellaneous_title">Miscellaneous</string>
|
||||||
<string name="settings_use_hiyobi_title">Use hiyobi.me</string>
|
<string name="settings_mirror_summary">Load images from mirrors</string>
|
||||||
<string name="settings_use_hiyobi_summary">Load images from hiyobi.me to improve loading speed (if available)</string>
|
|
||||||
<string name="settings_security_mode_title">Enable security mode</string>
|
<string name="settings_security_mode_title">Enable security mode</string>
|
||||||
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
|
<string name="settings_security_mode_summary">Enable security mode to make the screen invisible on recent app window</string>
|
||||||
<string name="settings_dark_mode_title">Dark mode</string>
|
<string name="settings_dark_mode_title">Dark mode</string>
|
||||||
@@ -186,5 +187,6 @@
|
|||||||
<string name="default_query_dialog_filter_BL">Filter BL</string>
|
<string name="default_query_dialog_filter_BL">Filter BL</string>
|
||||||
<string name="default_query_dialog_filter_guro">Filter Guro</string>
|
<string name="default_query_dialog_filter_guro">Filter Guro</string>
|
||||||
<string name="default_query_dialog_language_selector_none">Any</string>
|
<string name="default_query_dialog_language_selector_none">Any</string>
|
||||||
|
<string name="settings_mirror_title">Mirrors</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -66,10 +66,10 @@
|
|||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
app:title="@string/settings_miscellaneous_title">
|
app:title="@string/settings_miscellaneous_title">
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<Preference
|
||||||
app:key="use_hiyobi"
|
app:key="mirrors"
|
||||||
app:title="@string/settings_use_hiyobi_title"
|
app:title="@string/settings_mirror_title"
|
||||||
app:summary="@string/settings_use_hiyobi_summary"/>
|
app:summary="@string/settings_mirror_summary"/>
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="security_mode"
|
app:key="security_mode"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("UNUSED_VARIABLE")
|
@file:Suppress("UNUSED_VARIABLE", "IncorrectScope")
|
||||||
|
|
||||||
package xyz.quaver.pupil
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
@@ -26,20 +26,16 @@ package xyz.quaver.pupil
|
|||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import android.util.SparseArray
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import xyz.quaver.pupil.util.download
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class ExampleUnitTest {
|
class ExampleUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
URL("https://github.om/tom5079/Pupil/releases/download/4.2-beta2-hotfix2/Pupil-v4.2-beta2-hotfix2.apk").download(
|
val arr = SparseArray<Float>()
|
||||||
File(System.getenv("USERPROFILE"), "Pupil.apk")
|
|
||||||
) { downloaded, fileSize ->
|
print(arr.indexOfKey(34))
|
||||||
println("%.1f%%".format(downloaded*100.0/fileSize))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ buildscript {
|
|||||||
classpath 'com.google.gms:google-services:4.3.3'
|
classpath 'com.google.gms:google-services:4.3.3'
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
classpath 'io.fabric.tools:gradle:1.29.0'
|
classpath 'io.fabric.tools:gradle:1.31.0'
|
||||||
classpath 'com.google.firebase:perf-plugin:1.3.1'
|
classpath 'com.google.firebase:perf-plugin:1.3.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ allprojects {
|
|||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
|
maven { url 'http://guardian.github.com/maven/repo-releases' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
# Project-wide Gradle settings.
|
## For more details on how to configure your build environment visit
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
#
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx1536m
|
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||||
|
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
#
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
#Thu Jan 30 12:29:48 KST 2020
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
android.enableJetifier=true
|
||||||
|
org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1024M"
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
|
||||||
@@ -5,10 +5,10 @@ apply plugin: 'kotlinx-serialization'
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
|
||||||
implementation 'org.jsoup:jsoup:1.12.1'
|
implementation 'org.jsoup:jsoup:1.12.1'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = "7"
|
sourceCompatibility = "7"
|
||||||
|
|||||||
24
libpupil/src/main/java/xyz/quaver/Code.kt
Normal file
24
libpupil/src/main/java/xyz/quaver/Code.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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
|
||||||
|
|
||||||
|
|
||||||
|
enum class Code {
|
||||||
|
HITOMI,
|
||||||
|
HIYOBI,
|
||||||
|
SORALA
|
||||||
|
}
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
|
|
||||||
package xyz.quaver.hitomi
|
package xyz.quaver.hitomi
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class Gallery(
|
data class Gallery(
|
||||||
val related: List<Int>,
|
val related: List<Int>,
|
||||||
val langList: List<Pair<String, String>>,
|
val langList: List<Pair<String, String>>,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package xyz.quaver.hitomi
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.Code
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
@@ -67,6 +68,7 @@ fun fetchNozomi(area: String? = null, tag: String = "index", language: String =
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GalleryBlock(
|
data class GalleryBlock(
|
||||||
|
val code: Code,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val galleryUrl: String,
|
val galleryUrl: String,
|
||||||
val thumbnails: List<String>,
|
val thumbnails: List<String>,
|
||||||
@@ -102,7 +104,7 @@ fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
|||||||
href.slice(5 until href.indexOf("-all"))
|
href.slice(5 until href.indexOf("-all"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package xyz.quaver.hitomi
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.Code
|
||||||
|
|
||||||
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
||||||
|
|
||||||
@@ -31,13 +32,7 @@ data class GalleryInfo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>) {
|
data class Reader(val code: Code, val title: String, val galleryInfo: List<GalleryInfo>)
|
||||||
enum class Code {
|
|
||||||
HITOMI,
|
|
||||||
HIYOBI,
|
|
||||||
SORALA
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Set header `Referer` to reader url to avoid 403 error
|
//Set header `Referer` to reader url to avoid 403 error
|
||||||
fun getReader(galleryID: Int) : Reader {
|
fun getReader(galleryID: Int) : Reader {
|
||||||
@@ -45,5 +40,5 @@ fun getReader(galleryID: Int) : Reader {
|
|||||||
|
|
||||||
val doc = Jsoup.connect(readerUrl).get()
|
val doc = Jsoup.connect(readerUrl).get()
|
||||||
|
|
||||||
return Reader(Reader.Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
|
return Reader(Code.HITOMI, doc.title(), getGalleryInfo(galleryID))
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ fun doSearch(query: String, sortByPopularity: Boolean = false) : List<Int> {
|
|||||||
val terms = query
|
val terms = query
|
||||||
.trim()
|
.trim()
|
||||||
.replace(Regex("""^\?"""), "")
|
.replace(Regex("""^\?"""), "")
|
||||||
.toLowerCase()
|
.toLowerCase(Locale.US)
|
||||||
.split(Regex("\\s+"))
|
.split(Regex("\\s+"))
|
||||||
.map {
|
.map {
|
||||||
it.replace('_', ' ')
|
it.replace('_', ' ')
|
||||||
|
|||||||
49
libpupil/src/main/java/xyz/quaver/hiyobi/galleryblock.kt
Normal file
49
libpupil/src/main/java/xyz/quaver/hiyobi/galleryblock.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 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 org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.Code
|
||||||
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
|
import xyz.quaver.hitomi.protocol
|
||||||
|
|
||||||
|
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
||||||
|
val url = "$protocol//$hiyobi/search/$galleryID"
|
||||||
|
|
||||||
|
try {
|
||||||
|
val doc = Jsoup.connect(url).get()
|
||||||
|
|
||||||
|
val galleryBlock = doc.selectFirst(".gallery-content")
|
||||||
|
|
||||||
|
val galleryUrl = galleryBlock.selectFirst("a").attr("href")
|
||||||
|
|
||||||
|
val thumbnails = listOf(galleryBlock.selectFirst("img").attr("abs:src"))
|
||||||
|
|
||||||
|
val title = galleryBlock.selectFirst("b").text()
|
||||||
|
val artists = galleryBlock.select("tr:matches(작가) a[href~=artist]").map { it.text() }
|
||||||
|
val series = galleryBlock.select("tr:matches(원작) a").map { it.attr("href").substringAfter("series:").replace('_', ' ') }
|
||||||
|
val type = galleryBlock.selectFirst("tr:matches(종류) a").attr("href").substringAfter("type:").replace('_', ' ')
|
||||||
|
|
||||||
|
val language = "korean"
|
||||||
|
|
||||||
|
val relatedTags = galleryBlock.select("tr:matches(태그) a").map { it.attr("href").substringAfterLast('/').replace('_', ' ') }
|
||||||
|
|
||||||
|
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ package xyz.quaver.hiyobi
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.list
|
import kotlinx.serialization.list
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
import xyz.quaver.hitomi.GalleryInfo
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.hitomi.protocol
|
import xyz.quaver.hitomi.protocol
|
||||||
@@ -76,7 +77,7 @@ fun getReader(galleryID: Int) : Reader {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return Reader(Reader.Code.HIYOBI, title, galleryInfo)
|
return Reader(Code.HIYOBI, title, galleryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
|
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
|
||||||
|
|||||||
@@ -102,4 +102,11 @@ class UnitTest {
|
|||||||
|
|
||||||
print(result)
|
print(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_hiyobi_galleryBlock() {
|
||||||
|
val galleryBlock = xyz.quaver.hiyobi.getGalleryBlock(10000027)
|
||||||
|
|
||||||
|
print(galleryBlock)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user