Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547b6e8e3b | ||
|
|
d88ac27e72 | ||
|
|
e551a40d08 | ||
|
|
e810abe33a | ||
|
|
6172a73719 | ||
|
|
7455e68a45 | ||
|
|
748495ca64 | ||
|
|
f6d9c7f550 |
@@ -19,8 +19,8 @@ android {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 33
|
||||
versionName "5.0"
|
||||
versionCode 37
|
||||
versionName "5.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"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":{}}]
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":37,"versionName":"5.4","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
@@ -22,7 +22,7 @@
|
||||
tools:replace="android:theme">
|
||||
|
||||
<provider
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
@@ -49,7 +49,11 @@ class Pupil : MultiDexApplication() {
|
||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
|
||||
val download = preference.getString("dl_location", null)
|
||||
val download = try {
|
||||
preference.getString("dl_location", null)
|
||||
} catch (e: Exception) {
|
||||
preference.edit().remove("dl_location").apply()
|
||||
}
|
||||
|
||||
if (download == null) {
|
||||
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
|
||||
|
||||
@@ -129,7 +129,12 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
||||
})
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val thumbnail = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT)
|
||||
val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
|
||||
if (it != null)
|
||||
Base64.decode(it, Base64.DEFAULT)
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
glide
|
||||
.load(thumbnail)
|
||||
|
||||
@@ -26,7 +26,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -108,17 +109,13 @@ class ReaderAdapter(private val context: Context,
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
|
||||
if (progress?.isNaN() == true) {
|
||||
|
||||
if (Fabric.isInitialized())
|
||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -969,11 +969,11 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
Mode.DOWNLOAD -> {
|
||||
val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file ->
|
||||
val downloads = getDownloadDirectory(this@MainActivity).listFiles().filter { file ->
|
||||
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
|
||||
}?.map {
|
||||
}.map {
|
||||
it.name!!.toInt()
|
||||
}?: listOf()
|
||||
}
|
||||
|
||||
when {
|
||||
query.isEmpty() -> downloads.apply {
|
||||
|
||||
@@ -162,7 +162,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
||||
with(view.dialog_number_picker) {
|
||||
minValue=1
|
||||
maxValue=reader_recyclerview.adapter?.itemCount ?: 0
|
||||
maxValue=reader_recyclerview?.adapter?.itemCount ?: 0
|
||||
value=currentPage
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
@@ -196,7 +196,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
super.onDestroy()
|
||||
|
||||
timer.cancel()
|
||||
(reader_recyclerview.adapter as ReaderAdapter).timer.cancel()
|
||||
(reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
|
||||
|
||||
if (!Cache(this).isDownloading(galleryID))
|
||||
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)
|
||||
|
||||
@@ -101,7 +101,7 @@ class SettingsFragment :
|
||||
}.show()
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(context)!!
|
||||
val dir = getDownloadDirectory(context)
|
||||
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
@@ -150,7 +150,12 @@ class SettingsFragment :
|
||||
"backup" -> {
|
||||
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
||||
context,
|
||||
getDownloadDirectory(context)?.createFile("null", "favorites.json")!!
|
||||
getDownloadDirectory(context).let {
|
||||
if (it.findFile("favorites.json") != null)
|
||||
it
|
||||
else
|
||||
it.createFile("null", "favorites.json")!!
|
||||
}
|
||||
)
|
||||
|
||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||
@@ -192,8 +197,7 @@ class SettingsFragment :
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
"dl_location" -> {
|
||||
findPreference<Preference>(key)?.summary =
|
||||
FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
|
||||
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).uri.path
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +234,7 @@ class SettingsFragment :
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"delete_downloads" -> {
|
||||
val dir = getDownloadDirectory(context)!!
|
||||
val dir = getDownloadDirectory(context)
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
@@ -242,7 +246,7 @@ class SettingsFragment :
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
"dl_location" -> {
|
||||
summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri)
|
||||
summary = getDownloadDirectory(context).uri.path
|
||||
|
||||
onPreferenceClickListener = this@SettingsFragment
|
||||
}
|
||||
|
||||
@@ -23,11 +23,15 @@ import android.content.ContextWrapper
|
||||
import android.util.Base64
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.parse
|
||||
import kotlinx.serialization.stringify
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.pupil.util.*
|
||||
@@ -41,7 +45,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
// Search in this order
|
||||
// Download -> Cache
|
||||
fun getCachedGallery(galleryID: Int) : DocumentFile? {
|
||||
var file = getDownloadDirectory(this)?.findFile(galleryID.toString())
|
||||
var file = getDownloadDirectory(this).findFile(galleryID.toString())
|
||||
|
||||
if (file?.exists() == true)
|
||||
return file
|
||||
@@ -107,17 +111,21 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
|
||||
val metadata = Cache(this).getCachedMetadata(galleryID)
|
||||
|
||||
val source = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
)
|
||||
|
||||
val galleryBlock = if (metadata?.galleryBlock == null)
|
||||
listOf(
|
||||
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
|
||||
).map {
|
||||
source.entries.map {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
kotlin.runCatching {
|
||||
it.invoke()
|
||||
it.value.invoke()
|
||||
}.getOrNull()
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}.firstOrNull {
|
||||
it.await() != null
|
||||
}?.await()
|
||||
else
|
||||
metadata.galleryBlock
|
||||
|
||||
@@ -126,52 +134,56 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
|
||||
)
|
||||
|
||||
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||
|
||||
return galleryBlock.firstOrNull {
|
||||
mirrors.contains(it.code.name)
|
||||
} ?: galleryBlock.firstOrNull()
|
||||
return galleryBlock
|
||||
}
|
||||
|
||||
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()
|
||||
return getCachedMetadata(galleryID)?.reader
|
||||
}
|
||||
|
||||
suspend fun getReader(galleryID: Int): Reader? {
|
||||
val metadata = getCachedMetadata(galleryID)
|
||||
val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
|
||||
|
||||
val readers = if (metadata?.readers == null) {
|
||||
listOf(
|
||||
{ xyz.quaver.hitomi.getReader(galleryID) },
|
||||
{ xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).map {
|
||||
val sources = mapOf(
|
||||
Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
|
||||
Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
|
||||
).let {
|
||||
if (mirrors.isNotEmpty())
|
||||
it.toSortedMap(
|
||||
Comparator { o1, o2 ->
|
||||
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
|
||||
}
|
||||
)
|
||||
else
|
||||
it
|
||||
}
|
||||
|
||||
val reader = if (metadata?.reader == null) {
|
||||
CoroutineScope(Dispatchers.IO).async {
|
||||
kotlin.runCatching {
|
||||
it.invoke()
|
||||
var retval: Reader? = null
|
||||
|
||||
for (source in sources) {
|
||||
retval = kotlin.runCatching {
|
||||
source.value.invoke()
|
||||
}.getOrNull()
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
} else {
|
||||
metadata.readers
|
||||
|
||||
if (retval != null)
|
||||
break
|
||||
}
|
||||
|
||||
if (readers.isNotEmpty())
|
||||
retval
|
||||
}.await()
|
||||
} else
|
||||
metadata.reader
|
||||
|
||||
if (reader != null)
|
||||
setCachedMetadata(
|
||||
galleryID,
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers)
|
||||
Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
|
||||
)
|
||||
|
||||
val mirrors = preference.getString("mirrors", "")!!.split('>')
|
||||
|
||||
return readers.firstOrNull {
|
||||
mirrors.contains(it.code.name)
|
||||
} ?: readers.firstOrNull()
|
||||
return reader
|
||||
}
|
||||
|
||||
fun getImages(galleryID: Int): List<DocumentFile?>? {
|
||||
@@ -180,7 +192,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
val images = gallery.listFiles()
|
||||
|
||||
return reader.galleryInfo.indices.map { index ->
|
||||
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true }
|
||||
images.firstOrNull { file -> file.name?.startsWith("%05d".format(index)) == true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,21 +206,26 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
if (!Regex("""^[0-9]+.+$""").matches(name))
|
||||
throw IllegalArgumentException("File name is not a number")
|
||||
|
||||
cache.createFile("null", name)?.writeBytes(this, data)
|
||||
cache.let {
|
||||
if (it.findFile(name) != null)
|
||||
it
|
||||
else
|
||||
it.createFile("null", name)
|
||||
}?.writeBytes(this, data)
|
||||
}
|
||||
|
||||
fun moveToDownload(galleryID: Int) {
|
||||
val cache = getCachedGallery(galleryID)
|
||||
|
||||
if (cache != null) {
|
||||
val download = getDownloadDirectory(this)!!
|
||||
val download = getDownloadDirectory(this)
|
||||
|
||||
if (!download.isParentOf(cache)) {
|
||||
cache.copyRecursively(this, download)
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
} else
|
||||
getDownloadDirectory(this)?.createDirectory(galleryID.toString())
|
||||
getDownloadDirectory(this).createDirectory(galleryID.toString())
|
||||
}
|
||||
|
||||
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
||||
|
||||
@@ -162,7 +162,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
}
|
||||
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor()))
|
||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||
.build()
|
||||
|
||||
fun stop() {
|
||||
@@ -279,6 +279,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
|
||||
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
|
||||
|
||||
if (notification[galleryID] == null)
|
||||
initNotification(galleryID)
|
||||
|
||||
notification[galleryID].setContentTitle(reader.title)
|
||||
notify(galleryID)
|
||||
|
||||
@@ -309,7 +312,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
val ext =
|
||||
call.request().url().encodedPath().split('.').last()
|
||||
|
||||
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res)
|
||||
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
|
||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,20 +25,20 @@ import xyz.quaver.hitomi.Reader
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
val thumbnail: String? = null,
|
||||
val galleryBlock: List<GalleryBlock>? = null,
|
||||
val readers: List<Reader>? = null,
|
||||
val galleryBlock: GalleryBlock? = null,
|
||||
val reader: Reader? = null,
|
||||
val isDownloading: Boolean? = null
|
||||
) {
|
||||
constructor(
|
||||
metadata: Metadata?,
|
||||
thumbnail: String? = null,
|
||||
galleryBlock: List<GalleryBlock>? = null,
|
||||
readers: List<Reader>? = null,
|
||||
galleryBlock: GalleryBlock? = null,
|
||||
readers: Reader? = null,
|
||||
isDownloading: Boolean? = null
|
||||
) : this(
|
||||
thumbnail ?: metadata?.thumbnail,
|
||||
galleryBlock ?: metadata?.galleryBlock,
|
||||
readers ?: metadata?.readers,
|
||||
readers ?: metadata?.reader,
|
||||
isDownloading ?: metadata?.isDownloading
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.io.File
|
||||
@@ -28,20 +29,29 @@ import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||
getDownloadDirectory(context)?.findFile(galleryID.toString()) ?:
|
||||
getDownloadDirectory(context).findFile(galleryID.toString()) ?:
|
||||
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
|
||||
|
||||
fun getDownloadDirectory(context: Context) : DocumentFile? {
|
||||
fun getDownloadDirectory(context: Context) : DocumentFile {
|
||||
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||
if (it != null)
|
||||
Uri.parse(it)
|
||||
else
|
||||
Uri.fromFile(context.getExternalFilesDir(null))
|
||||
}
|
||||
|
||||
return if (uri.toString().startsWith("file"))
|
||||
DocumentFile.fromFile(File(uri.path!!))
|
||||
else
|
||||
DocumentFile.fromTreeUri(context, uri)
|
||||
DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
|
||||
}
|
||||
|
||||
fun convertUpdateUri(context: Context, uri: Uri) : Uri =
|
||||
if (uri.toString().startsWith("file"))
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!.substringAfter("file:///")))
|
||||
else
|
||||
uri
|
||||
|
||||
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||
context.contentResolver.openOutputStream(to.uri).use { out ->
|
||||
out!!
|
||||
@@ -94,12 +104,17 @@ fun DocumentFile.copyRecursively(
|
||||
if (!exists())
|
||||
throw Exception("The source file doesn't exist.")
|
||||
|
||||
if (this.isFile)
|
||||
target.createFile("null", name!!)!!.writeBytes(
|
||||
if (this.isFile) {
|
||||
target.let {
|
||||
if (it.findFile(name!!) != null)
|
||||
it
|
||||
else
|
||||
createFile("null", name!!)!!
|
||||
}.writeBytes(
|
||||
context,
|
||||
readBytes(context)
|
||||
)
|
||||
else if (this.isDirectory) {
|
||||
} else if (this.isDirectory) {
|
||||
target.createDirectory(name!!).also { newTarget ->
|
||||
listFiles().forEach { child ->
|
||||
child.copyRecursively(context, newTarget!!)
|
||||
|
||||
@@ -146,7 +146,12 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch io@{
|
||||
val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
|
||||
val target = getDownloadDirectory(context).let {
|
||||
if (it.findFile("Pupil.apk") != null)
|
||||
it
|
||||
else
|
||||
it.createFile("null", "Pupil.apk")!!
|
||||
}
|
||||
|
||||
try {
|
||||
URL(url).download(context, target) { progress, fileSize ->
|
||||
@@ -168,7 +173,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
||||
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
setDataAndType(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}
|
||||
|
||||
builder.apply {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<paths>
|
||||
<external-path name="external" path="/"/>
|
||||
<external-files-path name="files" path="/"/>
|
||||
</paths>
|
||||
@@ -75,7 +75,7 @@ class UnitTest {
|
||||
|
||||
@Test
|
||||
fun test_getReader() {
|
||||
val reader = getReader(1442740)
|
||||
val reader = getReader(1567569)
|
||||
|
||||
print(reader)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user