Compare commits

..

9 Commits

Author SHA1 Message Date
Koitharu
b7e4c6b8c0 New cbz write utility 2020-09-26 17:51:47 +03:00
Koitharu
df599e9d50 Fix MangaLib parser 2020-09-25 19:44:18 +03:00
Koitharu
6009f089e7 Update coil version 2020-09-25 19:23:57 +03:00
Koitharu
0a4f2f848e UI fixes 2020-09-20 17:31:33 +03:00
Koitharu
85fc3a024c Fix henchan 2020-09-20 17:08:42 +03:00
Koitharu
eeb536b1ac Fix crash on import 2020-09-19 17:32:22 +03:00
Koitharu
5b8e8d76c0 Update target sdk 2020-09-19 17:27:45 +03:00
Koitharu
73cf2964b2 Option to manually track manga updates 2020-09-19 15:22:18 +03:00
Koitharu
8372f9b5de Update dependencies 2020-09-19 14:21:32 +03:00
24 changed files with 222 additions and 110 deletions

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="1.8" />
</component>
</project>

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -5,9 +5,9 @@ jdk:
android:
components:
- tools
- platform-tools-29.0.6
- build-tools-29.0.3
- android-29
- platform-tools-30.0.3
- build-tools-30.0.2
- android-30
licenses:
- android-sdk-preview-license-.+
- android-sdk-license-.+

View File

@@ -8,15 +8,15 @@ plugins {
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
android {
compileSdkVersion 29
buildToolsVersion '29.0.3'
compileSdkVersion 30
buildToolsVersion '30.0.2'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
versionCode gitCommits
versionName '0.5.1'
versionName '0.5.3'
kapt {
arguments {
@@ -63,7 +63,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0-alpha02'
implementation 'androidx.core:core-ktx:1.5.0-alpha03'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07'
@@ -87,12 +87,12 @@ dependencies {
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.8.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.8.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.koin:koin-android:2.2.0-beta-1'
implementation 'io.coil-kt:coil:1.0.0-rc2'
implementation 'io.coil-kt:coil:1.0.0-rc3'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'

View File

@@ -75,7 +75,6 @@
<service
android:name=".ui.download.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" />
<service
android:name=".ui.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />

View File

@@ -0,0 +1,93 @@
package org.koitharu.kotatsu.core.local
import androidx.annotation.CheckResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class WritableCbzFile(private val file: File) {
private val dir = File(file.parentFile, file.nameWithoutExtension)
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun prepare() = withContext(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty"
}
if (!dir.exists()) {
dir.mkdir()
}
ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val target = File(dir.path + File.separator + entry.name)
target.parentFile?.mkdirs()
target.outputStream().use { out ->
zip.copyTo(out)
}
zip.closeEntry()
entry = zip.nextEntry
}
}
}
suspend fun cleanup() = withContext(Dispatchers.IO) {
dir.deleteRecursively()
}
@CheckResult
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun flush() = withContext(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) {
tempFile.delete()
}
try {
ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
dir.listFiles()?.forEach {
zipFile(it, it.name, zip)
}
zip.flush()
}
tempFile.renameTo(file)
} finally {
if (tempFile.exists()) {
tempFile.delete()
}
}
}
operator fun get(name: String) = File(dir, name)
operator fun set(name: String, file: File) {
file.copyTo(this[name], overwrite = true)
}
companion object {
private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
if (fileToZip.isDirectory) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(ZipEntry(fileName))
} else {
zipOut.putNextEntry(ZipEntry("$fileName/"))
}
zipOut.closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
} else {
FileInputStream(fileToZip).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
}
}
}
}

View File

@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml
@@ -12,9 +9,25 @@ import org.koitharu.kotatsu.utils.ext.withDomain
class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {
override val defaultDomain = "henchan.pro"
override val defaultDomain = "hentaichan.pro"
override val source = MangaSource.HENCHAN
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> {
return super.getList(offset, query, sortOrder, tag).map {
val cover = it.coverUrl
if (cover.contains("_blur")) {
it.copy(coverUrl = cover.replace("_blur", ""))
} else {
it
}
}
}
override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(defaultDomain)
val doc = loaderContext.httpGet(manga.url).parseHtml()

View File

@@ -95,7 +95,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
append("/c")
append(item.getString("chapter_number"))
append('/')
append(item.getJSONArray("teams").getJSONObject(0).getString("slug"))
append(item.getJSONArray("teams").getJSONObject(0).optString("slug"))
}
var name = item.getString("chapter_name")
if (name.isNullOrBlank() || name == "null") {
@@ -142,12 +142,18 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = loaderContext.httpGet(chapter.url).parseHtml()
val scripts = doc.head().select("script")
val pg = doc.body().getElementById("pg").html().substringAfter('=').substringBeforeLast(';')
val pg = doc.body().getElementById("pg").html()
.substringAfter('=')
.substringBeforeLast(';')
val pages = JSONArray(pg)
for (script in scripts) {
val raw = script.html().trim()
if (raw.startsWith("window.__info")) {
val json = JSONObject(raw.substringAfter('=').substringBeforeLast(';'))
if (raw.contains("window.__info")) {
val json = JSONObject(
raw.substringAfter("window.__info")
.substringAfter('=')
.substringBeforeLast(';')
)
val domain = json.getJSONObject("servers").run {
getStringOrNull("main") ?: getString(
json.getJSONObject("img").getString("server")

View File

@@ -1,63 +1,35 @@
package org.koitharu.kotatsu.domain.local
import androidx.annotation.CheckResult
import androidx.annotation.WorkerThread
import org.koitharu.kotatsu.core.local.WritableCbzFile
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.utils.ext.sub
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import org.koitharu.kotatsu.utils.ext.toFileNameSafe
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@WorkerThread
class MangaZip(val file: File) {
private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() }
?: throw RuntimeException("Cannot create temporary directory")
private val writableCbz = WritableCbzFile(file)
private var index = MangaIndex(null)
fun prepare(manga: Manga) {
extract()
index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText())
suspend fun prepare(manga: Manga) {
writableCbz.prepare()
index = MangaIndex(writableCbz[INDEX_ENTRY].takeIfReadable()?.readText())
index.setMangaInfo(manga, append = true)
}
fun cleanup() {
dir.deleteRecursively()
suspend fun cleanup() {
writableCbz.cleanup()
}
fun compress() {
dir.sub(INDEX_ENTRY).writeText(index.toString())
ZipOutputStream(file.outputStream()).use { out ->
for (file in dir.listFiles().orEmpty()) {
val entry = ZipEntry(file.name)
out.putNextEntry(entry)
file.inputStream().use { stream ->
stream.copyTo(out)
}
out.closeEntry()
}
}
}
private fun extract() {
if (!file.exists()) {
return
}
ZipInputStream(file.inputStream()).use { input ->
while (true) {
val entry = input.nextEntry ?: return
if (!entry.isDirectory) {
dir.sub(entry.name).outputStream().use { out ->
input.copyTo(out)
}
}
input.closeEntry()
}
}
@CheckResult
suspend fun compress(): Boolean {
writableCbz[INDEX_ENTRY].writeText(index.toString())
return writableCbz.flush()
}
fun addCover(file: File, ext: String) {
@@ -68,7 +40,7 @@ class MangaZip(val file: File) {
append(ext)
}
}
file.copyTo(dir.sub(name), overwrite = true)
writableCbz[name] = file
index.setCoverEntry(name)
}
@@ -80,7 +52,7 @@ class MangaZip(val file: File) {
append(ext)
}
}
file.copyTo(dir.sub(name), overwrite = true)
writableCbz[name] = file
index.addChapter(chapter)
}

View File

@@ -27,7 +27,7 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), Ko
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false

View File

@@ -142,7 +142,9 @@ class DownloadService : BaseService() {
notification.setCancelId(0)
notification.setPostProcessing()
notification.update()
output.compress()
if (!output.compress()) {
throw RuntimeException("Cannot create target file")
}
val result = MangaProviderFactory.createLocal().getFromFile(output.file)
notification.setDone(result)
notification.dismiss()

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.ui.list.feed
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
@@ -16,6 +17,7 @@ import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems
@@ -53,6 +55,15 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), FeedView,
inflater.inflate(R.menu.opt_feed, menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
R.id.action_update -> {
TrackWorker.startNow(requireContext())
Snackbar.make(recyclerView, R.string.feed_will_update_soon, Snackbar.LENGTH_LONG).show()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onDestroyView() {
adapter = null
super.onDestroyView()

View File

@@ -21,6 +21,10 @@ import java.io.File
class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri> {
private val presenter by moxyPresenter(factory = ::LocalListPresenter)
private val importCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(),
this
)
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(offset)
@@ -35,8 +39,7 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
return when (item.itemId) {
R.id.action_import -> {
try {
registerForActivityResult(ActivityResultContracts.OpenDocument(), this)
.launch(arrayOf("*/*"))
importCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()

View File

@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.ui.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.transition.Slide
import android.view.Gravity
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.preference.Preference
@@ -23,9 +21,7 @@ class SettingsActivity : BaseActivity(),
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
supportFragmentManager.commit {
replace(R.id.container, MainSettingsFragment().also {
it.exitTransition = Slide(Gravity.START)
})
replace(R.id.container, MainSettingsFragment())
}
}
}
@@ -49,10 +45,9 @@ class SettingsActivity : BaseActivity(),
}
private fun openFragment(fragment: Fragment) {
fragment.enterTransition = Slide(Gravity.END)
fragment.exitTransition = Slide(Gravity.START)
supportFragmentManager.commit {
replace(R.id.container, fragment)
setReorderingAllowed(true)
addToBackStack(null)
}
}

View File

@@ -222,5 +222,17 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
}
fun startNow(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<TrackWorker>()
.setConstraints(constraints)
.addTag(TAG)
.build()
WorkManager.getInstance(context)
.enqueue(request)
}
}
}

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import coil.Coil
import coil.executeBlocking
import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
@@ -37,13 +38,11 @@ class RecentListFactory(private val context: Context) : RemoteViewsService.Remot
val views = RemoteViews(context.packageName, R.layout.item_recent)
val item = dataSet[position]
try {
val cover = runBlocking {
Coil.execute(
ImageRequest.Builder(context)
.data(item.coverUrl)
.build()
).requireBitmap()
}
val cover = Coil.imageLoader(context).executeBlocking(
ImageRequest.Builder(context)
.data(item.coverUrl)
.build()
).requireBitmap()
views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) {
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import coil.Coil
import coil.executeBlocking
import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
@@ -48,13 +49,11 @@ class ShelfListFactory(private val context: Context, widgetId: Int) : RemoteView
val item = dataSet[position]
views.setTextViewText(R.id.textView_title, item.title)
try {
val cover = runBlocking {
Coil.execute(
ImageRequest.Builder(context)
.data(item.coverUrl)
.build()
).requireBitmap()
}
val cover = Coil.imageLoader(context).executeBlocking(
ImageRequest.Builder(context)
.data(item.coverUrl)
.build()
).requireBitmap()
views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) {
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)

View File

@@ -5,10 +5,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?android:windowBackground"
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:strokeColor="?android:colorControlNormal"
app:strokeColor="?colorOnSurface"
app:strokeWidth="1px">
<LinearLayout
@@ -30,7 +29,7 @@
android:gravity="center_vertical|start"
android:lines="2"
android:padding="6dp"
android:text="?android:textColorPrimary"
android:textColor="?android:textColorPrimary"
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@@ -5,10 +5,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/manga_list_details_item_height"
app:cardBackgroundColor="?android:windowBackground"
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:strokeColor="?android:colorControlNormal"
app:strokeColor="?colorOnSurface"
app:strokeWidth="1px">
<LinearLayout

View File

@@ -1,2 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu />
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_update"
android:orderInCategory="50"
android:title="@string/update"
app:showAsAction="never" />
</menu>

View File

@@ -143,4 +143,6 @@
<string name="clear_updates_feed">Очистить ленту обновлений</string>
<string name="updates_feed_cleared">Лента обновлений очищена</string>
<string name="rotate_screen">Повернуть экран</string>
<string name="update">Обновить</string>
<string name="feed_will_update_soon">Обновление скоро начнётся</string>
</resources>

View File

@@ -144,4 +144,6 @@
<string name="clear_updates_feed">Clear updates feed</string>
<string name="updates_feed_cleared">Updates feed cleared</string>
<string name="rotate_screen">Rotate screen</string>
<string name="update">Update</string>
<string name="feed_will_update_soon">Feed update will start soon</string>
</resources>

View File

@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.0'
ext.kotlin_version = '1.4.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0-alpha09'
classpath 'com.android.tools.build:gradle:4.1.0-rc03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -1,22 +1,18 @@
# Project-wide Gradle settings.
# 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
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# 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.
# 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
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
#Sat Sep 19 17:19:33 EEST 2020
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
android.nonTransitiveRClass=true
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M"