WIP
This commit is contained in:
@@ -31,6 +31,7 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
|
||||
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class ImageCache(context: Context) : DIAware {
|
||||
override val di by closestDI(context)
|
||||
|
||||
private val applicationContext: Pupil by instance()
|
||||
private val client: HttpClient by instance()
|
||||
|
||||
val cacheFolder = File(context.cacheDir, "imageCache")
|
||||
val cache = SavedMap(File(cacheFolder, ".cache"), "", "")
|
||||
|
||||
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
|
||||
val channels = _channels as Map<String, Channel<Float>>
|
||||
|
||||
private val requests = mutableMapOf<String, Job>()
|
||||
|
||||
@Synchronized
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun cleanup() = coroutineScope {
|
||||
val LIMIT = 100*1024*1024
|
||||
|
||||
cacheFolder.listFiles { it -> it.canonicalPath !in cache.values || it.name == ".cache" }?.forEach { it.delete() }
|
||||
|
||||
if (cacheFolder.size() > LIMIT)
|
||||
do {
|
||||
cache.entries.firstOrNull { !channels.containsKey(it.key) }?.let {
|
||||
File(it.value).delete()
|
||||
cache.remove(it.key)
|
||||
}
|
||||
} while (cacheFolder.size() > LIMIT / 2)
|
||||
}
|
||||
|
||||
fun free(images: List<String>) {
|
||||
images.forEach {
|
||||
requests[it]?.cancel()
|
||||
}
|
||||
|
||||
images.forEach { _channels.remove(it) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
suspend fun clear() = coroutineScope {
|
||||
requests.values.forEach { it.cancel() }
|
||||
cacheFolder.listFiles()?.forEach { it.delete() }
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File {
|
||||
val request = HttpRequestBuilder().apply(requestBuilder)
|
||||
|
||||
val key = request.url.buildString()
|
||||
|
||||
val progressChannel = if (_channels[key]?.isClosedForSend == false)
|
||||
_channels[key]!!
|
||||
else
|
||||
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it }
|
||||
|
||||
return cache[key]?.let {
|
||||
progressChannel.close()
|
||||
File(it)
|
||||
} ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file ->
|
||||
if (!file.exists())
|
||||
file.createNewFile()
|
||||
|
||||
cache[key] = file.canonicalPath
|
||||
|
||||
requests[key] = CoroutineScope(Dispatchers.IO).launch {
|
||||
kotlin.runCatching {
|
||||
client.get<HttpStatement>(request).execute { httpResponse ->
|
||||
val responseChannel: ByteReadChannel = httpResponse.receive()
|
||||
val contentLength = httpResponse.contentLength() ?: -1
|
||||
var readBytes = 0F
|
||||
|
||||
while (!responseChannel.isClosedForRead) {
|
||||
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||
while (!packet.isEmpty) {
|
||||
val bytes = packet.readBytes()
|
||||
file.appendBytes(bytes)
|
||||
readBytes += bytes.size
|
||||
progressChannel.trySend(readBytes / contentLength)
|
||||
}
|
||||
}
|
||||
progressChannel.close()
|
||||
}
|
||||
}.onFailure {
|
||||
file.delete()
|
||||
cache.remove(key)
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
progressChannel.close(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.Json.Default.decodeFromString
|
||||
import kotlinx.serialization.serializer
|
||||
import java.io.File
|
||||
|
||||
class SavedMap <K: Any, V: Any> (private val file: File, anyKey: K, anyValue: V, private val map: MutableMap<K, V> = mutableMapOf()) : MutableMap<K, V> by map {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val serializer: KSerializer<Map<K, V>> = MapSerializer(serializer(anyKey::class.java) as KSerializer<K>, serializer(anyValue::class.java) as KSerializer<V>)
|
||||
|
||||
init {
|
||||
if (!file.exists()) {
|
||||
save()
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun load() {
|
||||
map.clear()
|
||||
kotlin.runCatching {
|
||||
decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
map.putAll(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save() {
|
||||
file.parentFile?.mkdirs()
|
||||
if (!file.exists())
|
||||
file.createNewFile()
|
||||
|
||||
file.writeText(Json.encodeToString(serializer, map))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun put(key: K, value: V): V? {
|
||||
map.remove(key)
|
||||
|
||||
return map.put(key, value).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun putAll(from: Map<out K, V>) {
|
||||
for (key in from.keys) {
|
||||
map.remove(key)
|
||||
}
|
||||
|
||||
map.putAll(from)
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun remove(key: K): V? {
|
||||
return map.remove(key).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@RequiresApi(24)
|
||||
override fun remove(key: K, value: V): Boolean {
|
||||
return map.remove(key, value).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun clear() {
|
||||
map.clear()
|
||||
save()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SavedSourceSet(private val file: File) {
|
||||
private val _map = mutableMapOf<String, MutableList<String>>()
|
||||
val map: Map<String, List<String>> = _map
|
||||
|
||||
private val serializer = MapSerializer(String.serializer(), ListSerializer(String.serializer()))
|
||||
|
||||
@Synchronized
|
||||
fun load() {
|
||||
_map.clear()
|
||||
kotlin.runCatching {
|
||||
decodeFromString(serializer, file.readText())
|
||||
}.onSuccess {
|
||||
it.forEach { (k, v) ->
|
||||
_map[k] = v.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save() {
|
||||
file.parentFile?.mkdirs()
|
||||
if (!file.exists())
|
||||
file.createNewFile()
|
||||
|
||||
file.writeText(Json.encodeToString(serializer, _map))
|
||||
}
|
||||
|
||||
operator fun get(key: String) = _map[key]
|
||||
|
||||
@Synchronized
|
||||
fun add(source: String, value: String) {
|
||||
_map[source]?.remove(value)
|
||||
|
||||
if (!_map.containsKey(source))
|
||||
_map[source] = mutableListOf(value)
|
||||
else
|
||||
_map[source]!!.add(value)
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addAll(from: Map<String, Set<String>>) {
|
||||
for (source in from.keys) {
|
||||
if (_map.containsKey(source)) {
|
||||
_map[source]!!.removeAll(from[source]!!)
|
||||
_map[source]!!.addAll(from[source]!!)
|
||||
} else {
|
||||
_map[source] = from[source]!!.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun remove(source: String, value: String): Boolean {
|
||||
return (_map[source]?.remove(value) ?: false).also {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
_map.clear()
|
||||
save()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,7 +43,7 @@ fun hashWithSalt(password: String): Pair<String, String> {
|
||||
return Pair(hash(password+salt), salt)
|
||||
}
|
||||
|
||||
const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
private const val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
@Serializable
|
||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.DirectDIAware
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
import java.io.InputStream
|
||||
@@ -40,7 +41,7 @@ fun String.wordCapitalize() : String {
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
for (word in this.split(" "))
|
||||
result.add(word.capitalize(Locale.US))
|
||||
result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() })
|
||||
|
||||
return result.joinToString(" ")
|
||||
}
|
||||
@@ -73,9 +74,8 @@ fun byteToString(byte: Long, precision : Int = 1) : String {
|
||||
fun Int.normalizeID() = this.and(0xFFFF)
|
||||
|
||||
val formatMap = mapOf<String, ItemInfo.() -> (String)>(
|
||||
"-id-" to { id },
|
||||
"-id-" to { itemID },
|
||||
"-title-" to { title },
|
||||
"-artist-" to { artists }
|
||||
// TODO
|
||||
)
|
||||
/**
|
||||
@@ -103,8 +103,8 @@ operator fun JsonElement.get(tag: String) =
|
||||
val JsonElement.content
|
||||
get() = this.jsonPrimitive.contentOrNull
|
||||
|
||||
fun List<MenuItem>.findMenu(itemID: Int): MenuItem {
|
||||
return first { it.itemId == itemID }
|
||||
fun List<MenuItem>.findMenu(itemID: Int): MenuItem? {
|
||||
return firstOrNull { it.itemId == itemID }
|
||||
}
|
||||
|
||||
fun <E> MutableLiveData<MutableList<E>>.notify() {
|
||||
@@ -127,6 +127,9 @@ fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytes
|
||||
fun DIAware.source(source: String) = lazy { direct.source(source) }
|
||||
fun DirectDIAware.source(source: String) = instance<SourceEntries>().toMap()[source]!!
|
||||
|
||||
fun DIAware.database() = lazy { direct.database() }
|
||||
fun DirectDIAware.database() = instance<AppDatabase>()
|
||||
|
||||
fun View.hide() {
|
||||
visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user