Compare commits

...

33 Commits

Author SHA1 Message Date
Pupil
63e07f56e0 Merge pull request #51 from tom5079/development
Version 4.3
2020-01-13 20:35:43 +09:00
Pupil
ee87122bb2 Fixed Checking permission despite requiring no permission 2020-01-13 20:32:50 +09:00
tom5079
290dda9018 Added log to indicate firebase status 2020-01-13 15:25:44 +09:00
Pupil
1d3d78b936 Merge pull request #50 from tom5079/development
Version 4.2
2020-01-13 15:07:00 +09:00
Pupil
a947bc6415 Merge pull request #49 from tom5079/Pupil-28
Created beta channel update feature
2020-01-13 15:05:58 +09:00
tom5079
9ca891b2f5 Changed SwitchPreference to SwitchPreferenceCompat for better integration 2020-01-13 15:02:00 +09:00
tom5079
48e0ebc8ae Pupil-28 Add option to select update channels 2020-01-13 14:43:55 +09:00
tom5079
b323353006 Merge remote-tracking branch 'origin/development' into development 2020-01-13 14:11:07 +09:00
Pupil
c85d3ebe81 Merge pull request #48 from tom5079/issue-39
Added Changing Download directory feature
2020-01-13 14:09:41 +09:00
tom5079
ce843abec8 Changed logic to update app from utilizing DownloadManager to manual download 2020-01-13 14:08:31 +09:00
tom5079
6b43faa70e Fixed crash when built without google-services.json 2020-01-13 14:08:31 +09:00
tom5079
2d0c997b2e Updated build.gradle 2020-01-13 14:08:31 +09:00
tom5079
1db5118377 Updated .gitignore 2020-01-13 14:08:31 +09:00
tom5079
26b53ed7ac Fixed crash when built without google-services.json 2020-01-12 19:12:47 +09:00
tom5079
2c85ea6443 Removes Permission check for downloading updates
TODO: write logic for downloading update file instead of using DownloadManager(Permission problem)
2020-01-11 18:51:15 +09:00
tom5079
cbc2b30f47 resolves #39 2020-01-11 06:51:51 +09:00
tom5079
0b58deb92c Updated build.gradle 2020-01-04 14:01:54 +09:00
tom5079
ed1cf23c91 Updated .gitignore 2020-01-04 14:00:31 +09:00
tom5079
6fbb644e4b Added download directory entry on preferences
Changed download folder
2020-01-04 13:16:39 +09:00
Pupil
774867502d Merge pull request #47 from tom5079/issue-42
Fixed #42
2020-01-02 10:31:39 +09:00
tom5079
c8b1439aeb Fixed #42 2020-01-02 10:30:55 +09:00
tom5079
38c16adffe Fixed to be able to build without google-services.json 2020-01-02 10:18:39 +09:00
Pupil
18aede2701 Merge pull request #45 from tom5079/issue-44
issue-44
2019-12-29 14:46:13 +09:00
tom5079
c59d08a0a1 Fixes #44 2019-12-29 14:42:28 +09:00
tom5079
66ae29eb5b Fixes #44 2019-12-29 14:24:20 +09:00
tom5079
7d9cb3e150 Dependency update 2019-12-29 13:34:45 +09:00
Pupil
9922a9f82a Merge pull request #41 from tom5079/hotfix-40
Fixes #40
2019-12-19 09:37:23 +09:00
tom5079
445b9b4673 Fixes #40 2019-12-19 09:36:51 +09:00
tom5079
0ef7b358e0 Fixes #40 2019-12-19 09:33:10 +09:00
tom5079
2d3fb75576 Fixes wierd crash 2019-12-14 17:04:43 +09:00
tom5079
d55ff6d68e Fixes wierd crash 2019-12-14 17:04:04 +09:00
Pupil
079654a9c7 Merge pull request #36 from tom5079/Pupil-35
fixes #35
2019-12-14 16:56:58 +09:00
tom5079
30263c6260 fixes #35
warning: this can cause OOM
2019-12-14 16:54:59 +09:00
29 changed files with 534 additions and 287 deletions

2
.gitignore vendored
View File

@@ -16,4 +16,4 @@
/gh-pages
#Private files
/app/google-services.json
**/google-services.json

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/gh-pages" vcs="Git" />
</component>
</project>

View File

@@ -3,9 +3,15 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
if (file("google-services.json").exists()) {
logger.lifecycle("Firebase Enabled")
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
apply plugin: 'com.google.firebase.firebase-perf'
} else {
logger.lifecycle("Firebase Disabled")
}
android {
compileSdkVersion 29
@@ -13,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
versionCode 31
versionName "4.2-beta1"
versionCode 32
versionName "4.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -25,7 +31,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
buildTypes.each {
it.buildConfigField('boolean', 'PRERELEASE', 'false')
it.buildConfigField('boolean', 'CENSOR', 'false')
}
}
@@ -36,6 +41,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion = '29.0.2'
}
dependencies {
@@ -55,12 +61,12 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.0"
implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation 'com.google.android.material:material:1.2.0-alpha02'
implementation 'com.google.android.material:material:1.2.0-alpha03'
implementation 'com.google.firebase:firebase-core:17.2.1'
implementation 'com.google.firebase:firebase-perf:19.0.3'
implementation 'com.google.firebase:firebase-perf:19.0.4'
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":31,"versionName":"4.2-beta1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":31,"versionName":"4.2","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -20,8 +20,8 @@
package xyz.quaver.pupil
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
@@ -64,7 +64,9 @@ class ExampleInstrumentedTest {
val activityTestRule = ActivityTestRule(LockActivity::class.java)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
activityTestRule.launchActivity(Intent())
ContextCompat.getExternalFilesDirs(appContext, null).forEachIndexed { index, file ->
Log.i("PUPILD", "$index: ${file?.absolutePath}")
}
}
@Test

View File

@@ -4,8 +4,6 @@
package="xyz.quaver.pupil">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
@@ -55,18 +53,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="히요비.asia"
android:pathPrefix="/reader"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="xn--9w3b15m8vo.asia"
android:host="hiyobi.me"
android:pathPrefix="/reader"
android:scheme="https" />
</intent-filter>
@@ -99,20 +86,9 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="히요비.asia"
android:pathPrefix="/reader"
android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="xn--9w3b15m8vo.asia"
android:pathPrefix="/reader"
android:scheme="http" />
android:host="hiyobi.me"
android:scheme="http"
android:pathPrefix="/reader" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View File

@@ -62,7 +62,6 @@ class Pupil : MultiDexApplication() {
e.printStackTrace()
}
if (!preference.getBoolean("channel_created", false)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
@@ -74,9 +73,6 @@ class Pupil : MultiDexApplication() {
manager.createNotificationChannel(channel)
}
preference.edit().putBoolean("channel_created", true).apply()
}
AppCompatDelegate.setDefaultNightMode(when (preference.getBoolean("dark_mode", false)) {
true -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO

View File

@@ -95,7 +95,6 @@ class GalleryBlockAdapter(private val glide: RequestManager, private val galleri
if (BuildConfig.CENSOR)
override(5, 8)
}
.fitCenter()
.into(galleryblock_thumbnail)
}

View File

@@ -49,17 +49,21 @@ class ReaderAdapter(private val glide: RequestManager,
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.view as ImageView
if (isFullScreen)
holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT
else
holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT
glide
.load(File(getCachedGallery(holder.view.context, galleryID), images[position]))
.dontTransform()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
.dontTransform()
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.fitCenter()
.into(holder.view)
}

View File

@@ -39,7 +39,6 @@ class ThumbnailAdapter(private val glide: RequestManager, private val thumbnails
if (BuildConfig.CENSOR)
override(5, 8)
}
.fitCenter()
.into(holder.view)
}

View File

@@ -18,17 +18,11 @@
package xyz.quaver.pupil.ui
import android.Manifest
import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.*
import android.text.style.AlignmentSpan
import android.view.KeyEvent
@@ -42,7 +36,6 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat
@@ -60,11 +53,8 @@ import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.content
import kotlinx.serialization.list
import kotlinx.serialization.stringify
import ru.noties.markwon.Markwon
import xyz.quaver.hitomi.*
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
@@ -72,6 +62,7 @@ import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.types.TagSuggestion
import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.ui.dialog.GalleryDialog
import xyz.quaver.pupil.util.*
import java.io.File
import java.io.FileOutputStream
@@ -143,8 +134,6 @@ class MainActivity : AppCompatActivity() {
if (lockManager.isNotEmpty())
startActivityForResult(Intent(this, LockActivity::class.java), REQUEST_LOCK)
checkPermissions()
val preference = PreferenceManager.getDefaultSharedPreferences(this)
if (Locale.getDefault().language == "ko") {
@@ -167,7 +156,7 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main)
checkUpdate()
checkUpdate(this)
initView()
}
@@ -256,125 +245,6 @@ class MainActivity : AppCompatActivity() {
}
}
private fun checkUpdate() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
if (ignoreUpdateUntil > System.currentTimeMillis())
return
fun extractReleaseNote(update: JsonObject, locale: String) : String {
val markdown = update["body"]!!.content
val target = when(locale) {
"ko" -> "한국어"
"ja" -> "日本語"
else -> "English"
}
val releaseNote = Regex("^# Release Note.+$")
val language = Regex("^## $target$")
val end = Regex("^#.+$")
var releaseNoteFlag = false
var languageFlag = false
val result = StringBuilder()
for(line in markdown.lines()) {
if (releaseNote.matches(line)) {
releaseNoteFlag = true
continue
}
if (releaseNoteFlag) {
if (language.matches(line)) {
languageFlag = true
continue
}
}
if (languageFlag) {
if (end.matches(line))
break
result.append(line+"\n")
}
}
return getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
}
CoroutineScope(Dispatchers.Default).launch {
val update =
checkUpdate(getString(R.string.release_url)) ?: return@launch
val (url, fileName) = getApkUrl(update) ?: return@launch
fileName ?: return@launch
val dialog = AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault().language)
setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ ->
if (!this@MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.warning)
setMessage(R.string.update_no_permission)
setPositiveButton(android.R.string.ok) { _, _ -> }
}.show()
return@setPositiveButton
}
val request = DownloadManager.Request(Uri.parse(url)).apply {
setDescription(getString(R.string.update_notification_description))
setTitle(getString(R.string.app_name))
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
}
val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val id = manager.enqueue(request)
registerReceiver(object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
try {
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(manager.getUriForDownloadedFile(id), manager.getMimeTypeForDownloadedFile(id))
}
startActivity(install)
unregisterReceiver(this)
} catch (e: Exception) {
AlertDialog.Builder(this@MainActivity).apply {
setTitle(R.string.update_failed)
setMessage(R.string.update_failed_message)
setPositiveButton(android.R.string.ok) { _, _ -> }
}.show()
}
}
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
setNegativeButton(R.string.ignore_update) { _, _ ->
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
.apply()
}
}
launch(Dispatchers.Main) {
dialog.show()
}
}
}
private fun checkPermissions() {
if (!hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE))
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 13489)
}
private fun initView() {
var prevP1 = 0
main_appbar_layout.addOnOffsetChangedListener(
@@ -613,7 +483,10 @@ class MainActivity : AppCompatActivity() {
val galleryID = galleries[position].first.id
GalleryDialog(this@MainActivity, galleryID).apply {
GalleryDialog(
this@MainActivity,
galleryID
).apply {
onChipClickedHandler.add {
runOnUiThread {
query = it.toQuery()

View File

@@ -18,7 +18,6 @@
package xyz.quaver.pupil.ui
import android.Manifest
import android.content.Intent
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
@@ -36,6 +35,7 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.bumptech.glide.Glide
import com.crashlytics.android.Crashlytics
import com.google.android.material.snackbar.Snackbar
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.android.synthetic.main.activity_reader.view.*
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
@@ -49,7 +49,6 @@ import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.util.GalleryDownloader
import xyz.quaver.pupil.util.Histories
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.hasPermission
class ReaderActivity : AppCompatActivity() {
@@ -95,6 +94,7 @@ class ReaderActivity : AppCompatActivity() {
handleIntent(intent)
if (Fabric.isInitialized())
Crashlytics.setInt("GalleryID", galleryID)
if (galleryID == 0) {
@@ -371,17 +371,6 @@ class ReaderActivity : AppCompatActivity() {
with(reader_fab_download) {
setImageResource(R.drawable.ic_download)
setOnClickListener {
if (!this@ReaderActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
AlertDialog.Builder(this@ReaderActivity).apply {
setTitle(R.string.warning)
setMessage(R.string.update_no_permission)
setPositiveButton(android.R.string.ok) { _, _ -> }
}.show()
return@setOnClickListener
}
downloader.download = !downloader.download
if (!downloader.download)

View File

@@ -44,9 +44,8 @@ import kotlinx.serialization.parseList
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.types.Tags
import xyz.quaver.pupil.util.Lock
import xyz.quaver.pupil.util.LockManager
import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog
import xyz.quaver.pupil.util.*
import java.io.File
import java.nio.charset.Charset
import java.util.*
@@ -85,14 +84,6 @@ class SettingsActivity : AppCompatActivity() {
class SettingsFragment : PreferenceFragmentCompat() {
private val suffix = listOf(
"B",
"kB",
"MB",
"GB",
"TB" //really?
)
override fun onResume() {
super.onResume()
@@ -112,15 +103,9 @@ class SettingsActivity : AppCompatActivity() {
}
private fun getDirSize(dir: File) : String {
var size = dir.walk().map { it.length() }.sum()
var suffixIndex = 0
val size = dir.walk().map { it.length() }.sum()
while (size >= 1024) {
size /= 1024
suffixIndex++
}
return getString(R.string.settings_clear_summary, size, suffix[suffixIndex])
return getString(R.string.settings_clear_summary, byteToString(size))
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -132,7 +117,13 @@ class SettingsActivity : AppCompatActivity() {
val manager = context.packageManager
val info = manager.getPackageInfo(context.packageName, 0)
summary = info.versionName
summary = context.getString(R.string.settings_app_version_description, info.versionName)
setOnPreferenceClickListener {
checkUpdate(activity as SettingsActivity, true)
true
}
}
with(findPreference<Preference>("delete_cache")) {
@@ -208,6 +199,25 @@ class SettingsActivity : AppCompatActivity() {
}
}
with(findPreference<Preference>("dl_location")) {
this!!
summary = getDownloadDirectory(context).absolutePath
onPreferenceClickListener = Preference.OnPreferenceClickListener {
DownloadLocationDialog(context).apply {
onDownloadLocationChangedListener = { value ->
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(key, value)
.apply()
summary = getDownloadDirectory(context).absolutePath
}
}.show()
true
}
}
with(findPreference<Preference>("default_query")) {
this!!

View File

@@ -0,0 +1,78 @@
/*
* 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.widget.LinearLayout
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.byteToString
@SuppressLint("InflateParams")
class DownloadLocationDialog(context: Context) : AlertDialog(context) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
private val buttons = mutableListOf<RadioButton>()
var onDownloadLocationChangedListener : ((Int) -> (Unit))? = null
init {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
ContextCompat.getExternalFilesDirs(context, null).forEachIndexed { index, dir ->
dir ?: return@forEachIndexed
view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
location_type.text = context.getString(when (index) {
0 -> R.string.settings_dl_location_internal
else -> R.string.settings_dl_location_removable
})
location_available.text = context.getString(
R.string.settings_dl_location_available,
byteToString(dir.freeSpace)
)
setOnClickListener {
buttons.forEach { button ->
button.isChecked = false
}
button.performClick()
onDownloadLocationChangedListener?.invoke(index)
}
buttons.add(button)
})
}
buttons[preference.getInt("dl_location", 0)].isChecked = true
setTitle(R.string.settings_dl_location)
setView(view)
setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ ->
dismiss()
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* Pupil, Hitomi.la viewer for Android
* Copyright (C) 2019 tom5079
* 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
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package xyz.quaver.pupil.ui
package xyz.quaver.pupil.ui.dialog
import android.app.Dialog
import android.content.Context
@@ -46,6 +46,7 @@ import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
import xyz.quaver.pupil.adapters.ThumbnailAdapter
import xyz.quaver.pupil.types.Tag
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.ItemClickSupport
import xyz.quaver.pupil.util.wordCapitalize
@@ -256,7 +257,10 @@ class GalleryDialog(context: Context, private val galleryID: Int) : Dialog(conte
(context.applicationContext as Pupil).histories.add(galleries[position].first.id)
}
.setOnItemLongClickListener { _, position, _ ->
GalleryDialog(context, galleries[position].first.id).apply {
GalleryDialog(
context,
galleries[position].first.id
).apply {
onChipClickedHandler.add { tag ->
this@GalleryDialog.onChipClickedHandler.forEach { it.invoke(tag) }
}

View File

@@ -163,8 +163,6 @@ class GalleryDownloader(
}
}
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
fun start() {
downloadJob = CoroutineScope(Dispatchers.Default).launch {
val reader = reader!!.await() ?: return@launch

View File

@@ -19,9 +19,10 @@
package xyz.quaver.pupil.util
import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import java.io.File
import java.net.URL
fun getCachedGallery(context: Context, galleryID: Int): File {
return File(getDownloadDirectory(context), galleryID.toString()).let {
@@ -32,10 +33,32 @@ fun getCachedGallery(context: Context, galleryID: Int): File {
}
}
@Suppress("DEPRECATION")
fun getDownloadDirectory(context: Context): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
context.getExternalFilesDir("Pupil")!!
else
File(Environment.getExternalStorageDirectory(), "Pupil")
val dlLocation = PreferenceManager.getDefaultSharedPreferences(context).getInt("dl_location", 0)
return ContextCompat.getExternalFilesDirs(context, null)[dlLocation]
}
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
to.outputStream().use { out ->
with(openConnection()) {
val fileSize = contentLength.toLong()
getInputStream().use {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = it.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
onDownloadProgress?.invoke(bytesCopied, fileSize)
bytes = it.read(buffer)
}
}
}
}
}

View File

@@ -18,18 +18,45 @@
package xyz.quaver.pupil.util
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
fun Context.hasPermission(permission: String) =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.round
@UseExperimental(ExperimentalStdlibApi::class)
fun String.wordCapitalize() : String {
val result = ArrayList<String>()
for (word in this.split(" "))
result.add(word.capitalize())
result.add(word.capitalize(Locale.US))
return result.joinToString(" ")
}
//https://discuss.kotlinlang.org/t/how-do-you-round-a-number-to-n-decimal-places/8843(fvasco)
fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return round(this * multiplier) / multiplier
}
fun byteToString(byte: Long, precision : Int = 1) : String {
val suffix = listOf(
"B",
"kB",
"MB",
"GB",
"TB" //really?
)
var size = byte.toDouble()
var suffixIndex = 0
while (size >= 1024) {
size /= 1024
suffixIndex++
}
return "${size.round(precision)} ${suffix[suffixIndex]}"
}

View File

@@ -18,15 +18,31 @@
package xyz.quaver.pupil.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.internal.EnumSerializer
import kotlinx.serialization.json.*
import ru.noties.markwon.Markwon
import xyz.quaver.availableInHiyobi
import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
import java.io.File
import java.net.URL
import java.util.*
fun getReleases(url: String) : JsonArray {
return try {
@@ -38,18 +54,17 @@ fun getReleases(url: String) : JsonArray {
}
}
fun checkUpdate(url: String) : JsonObject? {
fun checkUpdate(context: Context, url: String) : JsonObject? {
val releases = getReleases(url)
if (releases.isEmpty())
return null
return releases.firstOrNull {
if (BuildConfig.PRERELEASE) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("beta", false))
true
} else {
else
it.jsonObject["prerelease"]?.boolean == false
}
}?.let {
if (it.jsonObject["tag_name"]?.content == BuildConfig.VERSION_NAME)
null
@@ -58,14 +73,128 @@ fun checkUpdate(url: String) : JsonObject? {
}
}
fun getApkUrl(releases: JsonObject) : Pair<String?, String?>? {
fun getApkUrl(releases: JsonObject) : String? {
return releases["assets"]?.jsonArray?.firstOrNull {
Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.content ?: "")
}.let {
if (it == null)
null
it?.jsonObject?.get("browser_download_url")?.content
}
}
const val UPDATE_NOTIFICATION_ID = 384823
fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0)
if (!force && ignoreUpdateUntil > System.currentTimeMillis())
return
fun extractReleaseNote(update: JsonObject, locale: Locale) : String {
val markdown = update["body"]!!.content
val target = when(locale) {
Locale.KOREAN -> "한국어"
Locale.JAPANESE -> "日本語"
else -> "English"
}
val releaseNote = Regex("^# Release Note.+$")
val language = Regex("^## $target$")
val end = Regex("^#.+$")
var releaseNoteFlag = false
var languageFlag = false
val result = StringBuilder()
for(line in markdown.lines()) {
if (releaseNote.matches(line)) {
releaseNoteFlag = true
continue
}
if (releaseNoteFlag) {
if (language.matches(line)) {
languageFlag = true
continue
}
}
if (languageFlag) {
if (end.matches(line))
break
result.append(line+"\n")
}
}
return context.getString(R.string.update_release_note, update["tag_name"]?.content, result.toString())
}
CoroutineScope(Dispatchers.Default).launch {
val update =
checkUpdate(context, context.getString(R.string.release_url)) ?: return@launch
val url = getApkUrl(update) ?: return@launch
val dialog = AlertDialog.Builder(context).apply {
setTitle(R.string.update_title)
val msg = extractReleaseNote(update, Locale.getDefault())
setMessage(Markwon.create(context).toMarkdown(msg))
setPositiveButton(android.R.string.yes) { _, _ ->
val notificationManager = NotificationManagerCompat.from(context)
val builder = NotificationCompat.Builder(context, "download").apply {
setContentTitle(context.getString(R.string.update_notification_description))
setSmallIcon(android.R.drawable.stat_sys_download)
priority = NotificationCompat.PRIORITY_LOW
}
CoroutineScope(Dispatchers.IO).launch {
val target = File(getDownloadDirectory(context), "Pupil.apk")
URL(url).download(target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
target
), MimeTypeMap.getSingleton().getExtensionFromMimeType(".apk"))
}
builder.apply {
setContentIntent(PendingIntent.getActivity(context, 0, install, 0))
setProgress(0, 0, false)
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentTitle(context.getString(R.string.update_download_completed))
setContentText(context.getString(R.string.update_download_completed_description))
}
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
if (context.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
context.startActivity(install)
else
Pair(it.jsonObject["browser_download_url"]?.content, it.jsonObject["name"]?.content)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
}
setNegativeButton(if (force) android.R.string.no else R.string.ignore_update) { _, _ ->
if (!force)
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)
.apply()
}
}
launch(Dispatchers.Main) {
dialog.show()
}
}
}
@@ -81,10 +210,15 @@ fun getOldReaderGalleries(context: Context) : List<File> {
if (!readerFile.exists())
return@let
Json(JsonConfiguration.Stable).parseJson(readerFile.readText()).jsonObject.let { reader ->
try {
Json(JsonConfiguration.Stable).parseJson(readerFile.readText())
.jsonObject.let { reader ->
if (!reader.contains("code"))
oldGallery.add(gallery)
}
} catch (e: Exception) {
// do nothing
}
}
}
}
@@ -109,7 +243,8 @@ fun updateOldReaderGalleries(context: Context) {
reader["code"] = when {
(File(gallery, "images").list()?.
all { !it.endsWith("webp") } ?: return@forEach) &&
availableInHiyobi(gallery.name.toInt()) -> json.toJson(codeSerializer, Reader.Code.HIYOBI)
availableInHiyobi(gallery.name.toIntOrNull() ?: return@forEach)
-> json.toJson(codeSerializer, Reader.Code.HIYOBI)
else -> json.toJson(codeSerializer, Reader.Code.HITOMI)
}

View File

@@ -26,19 +26,12 @@
android:background="@color/dark_gray"
tools:context=".ui.ReaderActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/reader_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -0,0 +1,25 @@
<?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/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"/>

View File

@@ -0,0 +1,51 @@
<?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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true">
<RadioButton
android:id="@+id/button"
android:clickable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/location_type"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/location_available"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

View File

@@ -22,6 +22,4 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:paddingBottom="8dp"
android:scaleType="fitCenter"
android:adjustViewBounds="true"/>
android:paddingBottom="8dp"/>

View File

@@ -9,7 +9,7 @@
<string name="search_hint_with_page">ギャラリー検索</string>
<string name="settings_clear_cache">キャッシュクリア</string>
<string name="settings_clear_cache_alert_message">キャッシュをクリアするとイメージのロード速度に影響を与えます。実行しますか?</string>
<string name="settings_clear_summary">サイズ: %1$d%2$s</string>
<string name="settings_clear_summary">サイズ: %s</string>
<string name="settings_default_query">デフォルトキーワード</string>
<string name="settings_galleries_per_page">一回にロードするギャラリー数</string>
<string name="settings_search_title">検索設定</string>
@@ -69,7 +69,7 @@
<string name="main_drawer_grouop_contact_discord">ディスコード</string>
<string name="settings_app_lock">アプリロック</string>
<string name="settings_app_lock_type">アップロックの種類</string>
<string name="settings_app_version_title">バージョン</string>
<string name="settings_app_version_title">バージョン(アップデート確認)</string>
<string name="settings_lock_biometrics">生体認識</string>
<string name="settings_lock_confirm">ロック確認のためもう一回入力してください。</string>
<string name="settings_lock_enabled">有効</string>
@@ -110,4 +110,12 @@
<string name="settings_backup_checkout">確認</string>
<string name="settings_restore_failed">復元に失敗しました</string>
<string name="settings_restore_successful">%1$d項目を復元しました</string>
<string name="settings_dl_location">ダウンロード場所</string>
<string name="settings_dl_location_internal">内部ストレージ</string>
<string name="settings_dl_location_removable">外部SDカード</string>
<string name="settings_dl_location_available">%s 使用可能</string>
<string name="update_download_completed">ダウンロードが完了しました</string>
<string name="update_download_completed_description">ここをクリックしてアップデートを行えます</string>
<string name="settings_beta">ベータチャンネルでアップデートを受信</string>
<string name="settings_app_version_description">v%s</string>
</resources>

View File

@@ -8,7 +8,7 @@
<string name="settings_default_query">기본 검색어</string>
<string name="settings_clear_cache">캐시 정리하기</string>
<string name="settings_clear_cache_alert_message">캐시를 정리하면 이미지 로딩속도가 느려질 수 있습니다. 계속하시겠습니까?</string>
<string name="settings_clear_summary">사용량: %1$d%2$s</string>
<string name="settings_clear_summary">사용량: %s</string>
<string name="settings_galleries_per_page">한 번에 로드할 갤러리 수</string>
<string name="settings_search_title">검색 설정</string>
<string name="settings_title">설정</string>
@@ -69,7 +69,7 @@
<string name="main_drawer_grouop_contact_discord">디스코드</string>
<string name="settings_app_lock">앱 잠금</string>
<string name="settings_app_lock_type">앱 잠금 종류</string>
<string name="settings_app_version_title">앱 버전</string>
<string name="settings_app_version_title">앱 버전(업데이트 확인)</string>
<string name="settings_lock_biometrics">생체 인식</string>
<string name="settings_lock_confirm">잠금 확인을 위해 한번 더 입력해주세요</string>
<string name="settings_lock_enabled">사용 중</string>
@@ -110,4 +110,12 @@
<string name="settings_backup_checkout">확인</string>
<string name="settings_restore_failed">복원에 실패했습니다</string>
<string name="settings_restore_successful">%1$d개 항목을 복원했습니다</string>
<string name="settings_dl_location">다운로드 위치</string>
<string name="settings_dl_location_internal">내부 저장공간</string>
<string name="settings_dl_location_removable">외부 SD카드</string>
<string name="settings_dl_location_available">%s 사용 가능</string>
<string name="update_download_completed">다운로드가 완료되었습니다</string>
<string name="update_download_completed_description">여기를 클릭해서 업데이트를 진행할 수 있습니다</string>
<string name="settings_beta">베타 채널에서 업데이트</string>
<string name="settings_app_version_description">v%s</string>
</resources>

View File

@@ -76,7 +76,9 @@
<string name="update_title">Update available</string>
<string name="update_download_started">Download started</string>
<string name="update_notification_description">Downloading apk&#8230;</string>
<string name="update_download_completed">Download Completed</string>
<string name="update_download_completed_description">Click here to update</string>
<string name="update_notification_description">Downloading update&#8230;</string>
<string name="update_release_note"># Release Note(v%1$s)\n%2$s</string>
<string name="search_hint">Search galleries</string>
@@ -96,6 +98,8 @@
<string name="galleryblock_type">Type: %1$s</string>
<string name="galleryblock_language">Language: %1$s</string>
<!-- READER -->
<string name="reader_loading">Loading</string>
<string name="reader_go_to_page">Go to page</string>
<string name="reader_fab_fullscreen">Fullscreen</string>
@@ -107,22 +111,43 @@
<string name="reader_help">Help</string>
<!-- SETTINGS -->
<string name="settings_title">Settings</string>
<string name="settings_app_version_title">App version</string>
<string name="settings_app_version_title">App version(Click to check update)</string>
<string name="settings_app_version_description">v%s</string>
<string name="settings_beta">Update from beta channel</string>
<!-- SEARCH -->
<string name="settings_search_title">Search Settings</string>
<string name="settings_galleries_per_page">Galleries per page</string>
<string name="settings_default_query">Default query</string>
<!-- SETTINGS/STORAGE -->
<string name="settings_storage">Storage</string>
<string name="settings_clear_cache">Clear cache</string>
<string name="settings_clear_cache_alert_message">Deleting cache can affect image loading speed. Do you want to continue?</string>
<string name="settings_clear_summary">Currently using %1$d%2$s</string>
<string name="settings_clear_summary">Currently using %s</string>
<string name="settings_clear_downloads">Clear downloads</string>
<string name="settings_clear_downloads_alert_message">Delete all downloaded galleries.\nDo you want to continue?</string>
<string name="settings_clear_history">Clear history</string>
<string name="settings_clear_history_alert_message">Do you want to clear histories?</string>
<string name="settings_clear_history_summary">%1$d histories saved</string>
<string name="settings_dl_location">Download directory</string>
<string name="settings_dl_location_removable">Removable Storage</string>
<string name="settings_dl_location_internal">Internal Storage</string>
<string name="settings_dl_location_available">%s available</string>
<!-- SETTINGS/APP LOCK -->
<string name="settings_app_lock">App lock</string>
<string name="settings_app_lock_type">App lock type</string>
<!-- SETTINGS/MISCELLANEOUS -->
<string name="settings_miscellaneous_title">Miscellaneous</string>
<string name="settings_use_hiyobi_title">Use hiyobi.me</string>
<string name="settings_use_hiyobi_summary">Load images from hiyobi.me to improve loading speed (if available)</string>
@@ -139,6 +164,8 @@
<string name="settings_restore_failed">Restore failed</string>
<string name="settings_restore_successful">%1$d entries restored</string>
<!-- SETTINGS/APP LOCK ACTIVITY -->
<string name="settings_lock_none">None</string>
<string name="settings_lock_pattern">Pattern</string>
<string name="settings_lock_pin" translatable="false">PIN</string>
@@ -150,6 +177,8 @@
<string name="settings_lock_remove_message">Do you want to remove lock?</string>
<string name="settings_lock_wrong_confirm">Lock is different from last one. Please try again.</string>
<!-- SETTINGS/DEFAULT QUERY DIALOG -->
<string name="default_query_dialog_title">Set default query</string>
<string name="default_query_dialog_language">Language: </string>
<string name="default_query_dialog_filter_BL">Filter BL</string>

View File

@@ -6,6 +6,10 @@
app:title="@string/settings_app_version_title"
app:key="app_version"/>
<SwitchPreferenceCompat
app:title="@string/settings_beta"
app:key="beta"/>
<PreferenceCategory
app:title="@string/settings_search_title">
@@ -39,6 +43,10 @@
app:title="@string/settings_clear_history"
app:key="clear_history"/>
<Preference
app:title="@string/settings_dl_location"
app:key="dl_location"/>
</PreferenceCategory>
<PreferenceCategory
@@ -53,23 +61,23 @@
<PreferenceCategory
app:title="@string/settings_miscellaneous_title">
<SwitchPreference
<SwitchPreferenceCompat
app:key="use_hiyobi"
app:title="@string/settings_use_hiyobi_title"
app:summary="@string/settings_use_hiyobi_summary"/>
<SwitchPreference
<SwitchPreferenceCompat
app:key="security_mode"
app:title="@string/settings_security_mode_title"
app:summary="@string/settings_security_mode_summary"
app:defaultValue="true"/>
<SwitchPreference
<SwitchPreferenceCompat
app:key="dark_mode"
app:title="@string/settings_dark_mode_title"
app:summary="@string/settings_dark_mode_summary"/>
<SwitchPreference
<SwitchPreferenceCompat
app:key="nomedia"
app:title="@string/settings_nomedia_title"
app:summary="@string/settings_nomedia_title"/>

View File

@@ -27,12 +27,19 @@ package xyz.quaver.pupil
*/
import org.junit.Test
import xyz.quaver.pupil.util.download
import java.io.File
import java.net.URL
class ExampleUnitTest {
@Test
fun test() {
URL("https://github.om/tom5079/Pupil/releases/download/4.2-beta2-hotfix2/Pupil-v4.2-beta2-hotfix2.apk").download(
File(System.getenv("USERPROFILE"), "Pupil.apk")
) { downloaded, fileSize ->
println("%.1f%%".format(downloaded*100.0/fileSize))
}
}
}

View File

@@ -7,7 +7,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'org.jsoup:jsoup:1.11.3'
implementation 'org.jsoup:jsoup:1.12.1'
testImplementation 'junit:junit:4.12'
}