diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 00000000..0aab68d2
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
deleted file mode 100644
index 06dee2c4..00000000
--- a/app/src/main/java/xyz/quaver/pupil/adapters/MirrorAdapter.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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 .
- */
-
-package xyz.quaver.pupil.adapters
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.view.LayoutInflater
-import android.view.MotionEvent
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.databinding.MirrorsItemBinding
-import xyz.quaver.pupil.util.Preferences
-import java.util.*
-
-class MirrorAdapter(context: Context) : RecyclerView.Adapter() {
-
- @SuppressLint("ClickableViewAccessibility")
- inner class ViewHolder(val binding: MirrorsItemBinding) : RecyclerView.ViewHolder(binding.root) {
- init {
- binding.mirrorButton.setOnTouchListener { _, event ->
- if (event.action == MotionEvent.ACTION_DOWN)
- onStartDrag?.invoke(this)
-
- true
- }
- }
- fun bind(mirror: String) {
- binding.mirrorName.text = mirror
- }
- }
-
- 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 {
- Preferences.get("mirrors")
- .split(">")
- .asReversed()
- .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) -> (Unit))? = null
-
- @SuppressLint("ClickableViewAccessibility")
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bind(mirrors[list.elementAt(position)] ?: error(""))
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(MirrorsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
-
- override fun getItemCount() = mirrors.size
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/SourceAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/SourceAdapter.kt
deleted file mode 100644
index 94cf748c..00000000
--- a/app/src/main/java/xyz/quaver/pupil/adapters/SourceAdapter.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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 .
- */
-
-package xyz.quaver.pupil.adapters
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
-import xyz.quaver.pupil.databinding.SourceSelectDialogItemBinding
-import xyz.quaver.pupil.sources.ItemInfo
-import xyz.quaver.pupil.sources.Source
-import xyz.quaver.pupil.sources.SourceEntries
-
-class SourceAdapter(sources: SourceEntries) : RecyclerView.Adapter() {
-
- var onSourceSelectedListener: ((String) -> Unit)? = null
- var onSourceSettingsSelectedListener: ((String) -> Unit)? = null
-
- private val sources = sources.toList()
-
- inner class ViewHolder(private val binding: SourceSelectDialogItemBinding) : RecyclerView.ViewHolder(binding.root) {
- lateinit var source: Source
-
- init {
- binding.go.setOnClickListener {
- onSourceSelectedListener?.invoke(source.name)
- }
- binding.settings.setOnClickListener {
- onSourceSettingsSelectedListener?.invoke(source.name)
- }
- }
-
- fun bind(source: Source) {
- this.source = source
-
- // TODO: save image somewhere else
- binding.icon.setImageResource(source.iconResID)
- binding.name.text = source.name
- }
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(SourceSelectDialogItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bind(sources[position].second)
- }
-
- override fun getItemCount(): Int = sources.size
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
similarity index 100%
rename from app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
rename to app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
index 316b4b7c..89c5ea65 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
@@ -87,7 +87,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
val imageHeights = remember { mutableStateListOf() }
val states = remember { mutableStateListOf() }
- LaunchedEffect(model.progressList.sum()) {
+ LaunchedEffect(model.imageList.count { it != null }) {
if (imageSources.isEmpty() && model.imageList.isNotEmpty())
imageSources.addAll(List(model.imageList.size) { null })
@@ -97,16 +97,22 @@ class ReaderActivity : ComponentActivity(), DIAware {
if (imageHeights.isEmpty() && model.imageList.isNotEmpty())
imageHeights.addAll(List(model.imageList.size) { null })
+ logger.info {
+ "${model.imageList.count { it == null }} nulls"
+ }
+
model.imageList.forEachIndexed { i, image ->
if (imageSources[i] == null && image != null)
- CoroutineScope(Dispatchers.Default).launch {
- imageSources[i] = kotlin.runCatching {
- FileXImageSource(FileX(this@ReaderActivity, image))
- }.onFailure {
- logger.warning(it)
- model.error(i)
- }.getOrNull()
- }
+ imageSources[i] = kotlin.runCatching {
+ FileXImageSource(FileX(this@ReaderActivity, image))
+ }.onFailure {
+ logger.warning(it)
+ model.error(i)
+ }.getOrNull()
+ }
+
+ logger.info {
+ "${imageSources.count { it == null }} nulls"
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt
index d51c2a90..1cdf2747 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/SourceSelectDialog.kt
@@ -20,15 +20,9 @@ package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.os.Bundle
-import android.view.ViewGroup.LayoutParams
-import android.view.Window
import androidx.fragment.app.DialogFragment
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import org.kodein.di.*
+import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
-import xyz.quaver.pupil.adapters.SourceAdapter
-import xyz.quaver.pupil.sources.*
class SourceSelectDialog : DialogFragment(), DIAware {
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
index 676f40b4..e80c1135 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
@@ -76,6 +76,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
var imageCount by mutableStateOf(0)
private set
+ private var images: List? = null
val imageList = mutableStateListOf()
val progressList = mutableStateListOf()
@@ -140,6 +141,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
withContext(Dispatchers.IO) {
source.images(itemID)
}.let { images ->
+ this@ReaderViewModel.images = images
+
imageCount = images.size
progressList.addAll(List(imageCount) { 0f })
@@ -151,13 +154,11 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
images.forEachIndexed { index, image ->
when (val scheme = image.takeWhile { it != ':' }) {
"http", "https" -> {
- val file = cache.load {
+ val (channel, file) = cache.load {
url(image)
headers(source.getHeadersBuilderForImage(itemID, image))
}
- val channel = cache.channels[image] ?: error("Channel is null")
-
if (channel.isClosedForReceive) {
imageList[index] = Uri.fromFile(file)
totalProgressMutex.withLock {
@@ -187,6 +188,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
}
}
"content" -> {
+ imageList[index] = Uri.parse(image)
progressList[index] = 1f
}
else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
@@ -211,4 +213,9 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
}
}
+ override fun onCleared() {
+ cache.cleanup()
+ images?.let { cache.free(it) }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt b/app/src/main/java/xyz/quaver/pupil/util/NetworkCache.kt
similarity index 68%
rename from app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
rename to app/src/main/java/xyz/quaver/pupil/util/NetworkCache.kt
index 37f7df85..7c15bc0d 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/NetworkCache.kt
@@ -26,6 +26,7 @@ import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
+import io.ktor.util.collections.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
@@ -38,39 +39,64 @@ import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
import xyz.quaver.hitomi.sha256
import java.io.File
+import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import kotlin.text.toByteArray
+private const val CACHE_LIMIT = 100*1024*1024 // 100M
+
class NetworkCache(context: Context) : DIAware {
override val di by closestDI(context)
+ private val logger = newLogger(LoggerFactory.default)
+
private val client: HttpClient by instance()
+ private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher())
private val cacheDir = context.cacheDir
- private val _channels = ConcurrentHashMap>()
- val channels = _channels as Map>
+ private val channel = ConcurrentHashMap>()
+ private val requests = ConcurrentHashMap()
+ private val activeFiles = Collections.newSetFromMap(ConcurrentHashMap())
- private val requests = mutableMapOf()
+ private fun urlToFilename(url: String): String {
+ val hash = sha256(url.toByteArray()).joinToString("") { "%02x".format(it) }
+ return "$hash.${url.takeLastWhile { it != '.' }}"
+ }
- private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher())
+ fun cleanup() = CoroutineScope(Dispatchers.IO).launch {
+ if (cacheDir.size() > CACHE_LIMIT)
+ cacheDir.listFiles { file -> file.name !in activeFiles }?.forEach { it.delete() }
+ }
- private val logger = newLogger(LoggerFactory.default)
+ fun free(urls: List) = urls.forEach {
+ requests[it]?.cancel()
+ channel.remove(it)
+ activeFiles.remove(urlToFilename(it))
+ }
+
+ fun clear() = CoroutineScope(Dispatchers.IO).launch {
+ requests.values.forEach { it.cancel() }
+ channel.clear()
+ activeFiles.clear()
+ cacheDir.listFiles()?.forEach { it.delete() }
+ }
@OptIn(ExperimentalCoroutinesApi::class)
- suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File = coroutineScope {
+ suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): Pair, File> = coroutineScope {
val request = HttpRequestBuilder().apply(requestBuilder)
val url = request.url.buildString()
- val hash = sha256(url.toByteArray()).joinToString("") { "%02x".format(it) }
- val file = File(cacheDir, "$hash.${url.takeLastWhile { it != '.' }}")
+ val fileName = urlToFilename(url)
+ val file = File(cacheDir, fileName)
+ activeFiles.add(fileName)
- val progressChannel = if (_channels[url]?.isClosedForSend == false)
- _channels[url]!!
+ val progressChannel = if (channel[url]?.isClosedForSend == false)
+ channel[url]!!
else
- Channel(1, BufferOverflow.DROP_OLDEST).also { _channels[url] = it }
+ Channel(1, BufferOverflow.DROP_OLDEST).also { channel[url] = it }
if (file.exists())
progressChannel.close()
@@ -86,8 +112,18 @@ class NetworkCache(context: Context) : DIAware {
file.outputStream().use { outputStream ->
while (!responseChannel.isClosedForRead) {
+ if (!isActive) {
+ file.delete()
+ break
+ }
+
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
while (!packet.isEmpty) {
+ if (!isActive) {
+ file.delete()
+ break
+ }
+
val bytes = packet.readBytes()
outputStream.write(bytes)
@@ -106,6 +142,6 @@ class NetworkCache(context: Context) : DIAware {
}
}
- return@coroutineScope file
+ return@coroutineScope progressChannel to file
}
}
\ No newline at end of file