Compare commits

...

24 Commits

Author SHA1 Message Date
8f2cf8141a Mark HenChan manga as nsfw 2021-12-10 18:10:12 +02:00
eefd1129f7 Update dependencies, setup FragmentStrictMode 2021-12-04 12:09:13 +02:00
5ed0f8b5a6 Check if category name is not empty 2021-12-03 21:11:50 +02:00
9b4aa4fd64 Migrate AniBel parser to graphql 2021-12-03 20:30:20 +02:00
bbb226791b Merge branch 'hotifx/2.0.1' into devel 2021-11-22 08:48:11 +02:00
30ac4435d4 Update version 2021-11-22 08:41:35 +02:00
1b9dfe1901 Temporary change anibel domain 2021-11-21 17:41:32 +02:00
808a6efd8f [Source] [MangaOwl] Fix not loading chapter list 2021-11-21 17:13:39 +02:00
66ed19ed5a [Source] [MangaOwl] Fix not loading chapter list 2021-11-21 17:12:04 +02:00
527a3cbd09 Option to exclude NSFW content from history 2021-11-20 16:49:30 +02:00
f22963b315 Use DownloadManager for pages saving 2021-11-18 20:06:44 +02:00
2ce5cb524f Increase version 2021-11-18 18:43:30 +02:00
4cbc6392fb Update AdapterDelegates library 2021-11-17 19:23:08 +02:00
049f9fa625 Fix cover image in lists 2021-11-17 19:08:13 +02:00
c853fae820 Fix browser activity insets 2021-11-12 20:46:25 +02:00
dd1d84a4fe Remanga authorization support #73 2021-11-12 20:36:01 +02:00
1569aa5dd5 Cleanup data classes 2021-11-12 20:13:49 +02:00
51cd88eded Update dependencies 2021-11-12 20:13:48 +02:00
Aliaksiej Razumaŭ
bf386deef0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
2021-11-12 20:13:26 +02:00
J. Lavoie
5c80cdee81 Translated using Weblate (Spanish)
Currently translated at 98.7% (241 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2021-11-12 20:13:26 +02:00
J. Lavoie
b29fbb37cd Translated using Weblate (Finnish)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
2021-11-12 20:13:26 +02:00
J. Lavoie
589831beef Translated using Weblate (French)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
2021-11-12 20:13:26 +02:00
J. Lavoie
0f5d153543 Translated using Weblate (Italian)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
2021-11-12 20:13:26 +02:00
J. Lavoie
ab1c99d132 Translated using Weblate (German)
Currently translated at 100.0% (244 of 244 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
2021-11-12 20:13:26 +02:00
101 changed files with 698 additions and 478 deletions

1
.idea/gradle.xml generated
View File

@@ -14,7 +14,6 @@
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@@ -13,8 +13,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 31
versionCode 370 versionCode 374
versionName '2.0-b2' versionName '2.0.2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -59,8 +59,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [ freeCompilerArgs += [
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=kotlinx.coroutines.FlowPreview',
'-Xopt-in=org.koin.core.component.KoinApiExtension'
] ]
} }
} }
@@ -69,23 +67,23 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.3.1' implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation 'androidx.fragment:fragment-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-service:2.3.1' implementation 'androidx.lifecycle:lifecycle-service:2.4.0'
implementation 'androidx.lifecycle:lifecycle-process:2.3.1' implementation 'androidx.lifecycle:lifecycle-process:2.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.7.0' implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.4.0' implementation 'com.google.android.material:material:1.4.0'
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1' kapt 'androidx.lifecycle:lifecycle-compiler:2.4.0'
implementation 'androidx.room:room-runtime:2.3.0' implementation 'androidx.room:room-runtime:2.3.0'
implementation 'androidx.room:room-ktx:2.3.0' implementation 'androidx.room:room-ktx:2.3.0'
@@ -95,10 +93,10 @@ dependencies {
implementation 'com.squareup.okio:okio:2.10.0' implementation 'com.squareup.okio:okio:2.10.0'
implementation 'org.jsoup:jsoup:1.14.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.1'
implementation 'io.insert-koin:koin-android:3.1.2' implementation 'io.insert-koin:koin-android:3.1.4'
implementation 'io.coil-kt:coil-base:1.4.0' implementation 'io.coil-kt:coil-base:1.4.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.3' implementation 'com.github.solkin:disk-lru-cache:1.3'
@@ -109,7 +107,7 @@ dependencies {
testImplementation 'com.google.truth:truth:1.1.3' testImplementation 'com.google.truth:truth:1.1.3'
testImplementation 'org.json:json:20210307' testImplementation 'org.json:json:20210307'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.2' testImplementation 'io.insert-koin:koin-test-junit4:3.1.4'
androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0'

View File

@@ -5,9 +5,7 @@
public static void checkReturnedValueIsNotNull(...); public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...); public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...); public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...);
} }
-keep class org.koitharu.kotatsu.core.db.entity.* { *; } -keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-keepclassmembers public class * extends org.koitharu.kotatsu.core.parser.MangaRepository {
public <init>(...);
}
-dontwarn okhttp3.internal.platform.ConscryptPlatform -dontwarn okhttp3.internal.platform.ConscryptPlatform

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
@@ -65,7 +66,7 @@ class KotatsuApp : Application() {
trackerModule, trackerModule,
settingsModule, settingsModule,
readerModule, readerModule,
appWidgetModule appWidgetModule,
) )
} }
} }
@@ -86,5 +87,13 @@ class KotatsuApp : Application() {
.penaltyLog() .penaltyLog()
.build() .build()
) )
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectTargetFragmentUsage()
.detectSetUserVisibleHint()
.build()
} }
} }

View File

@@ -5,7 +5,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
data class MangaIntent( class MangaIntent(
val manga: Manga?, val manga: Manga?,
val mangaId: Long, val mangaId: Long,
val uri: Uri? val uri: Uri?

View File

@@ -1,15 +1,21 @@
package org.koitharu.kotatsu.base.domain package org.koitharu.kotatsu.base.domain
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koitharu.kotatsu.core.exceptions.GraphQLException
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.parseJson
open class MangaLoaderContext( open class MangaLoaderContext(
private val okHttp: OkHttpClient, private val okHttp: OkHttpClient,
val cookieJar: CookieJar val cookieJar: CookieJar,
) : KoinComponent { ) : KoinComponent {
suspend fun httpGet(url: String, headers: Headers? = null): Response { suspend fun httpGet(url: String, headers: Headers? = null): Response {
@@ -24,7 +30,7 @@ open class MangaLoaderContext(
suspend fun httpPost( suspend fun httpPost(
url: String, url: String,
form: Map<String, String> form: Map<String, String>,
): Response { ): Response {
val body = FormBody.Builder() val body = FormBody.Builder()
form.forEach { (k, v) -> form.forEach { (k, v) ->
@@ -38,7 +44,7 @@ open class MangaLoaderContext(
suspend fun httpPost( suspend fun httpPost(
url: String, url: String,
payload: String payload: String,
): Response { ): Response {
val body = FormBody.Builder() val body = FormBody.Builder()
payload.split('&').forEach { payload.split('&').forEach {
@@ -55,10 +61,24 @@ open class MangaLoaderContext(
return okHttp.newCall(request.build()).await() return okHttp.newCall(request.build()).await()
} }
open fun getSettings(source: MangaSource) = SourceSettings(get(), source) suspend fun graphQLQuery(endpoint: String, query: String): JSONObject {
val body = JSONObject()
private companion object { body.put("operationName", null)
body.put("variables", JSONObject())
private const val SCHEME_HTTP = "http" body.put("query", "{${query}}")
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = body.toString().toRequestBody(mediaType)
val request = Request.Builder()
.post(requestBody)
.url(endpoint)
val json = okHttp.newCall(request.build()).await().parseJson()
json.optJSONArray("errors")?.let {
if (it.length() != 0) {
throw GraphQLException(it)
}
}
return json
} }
open fun getSettings(source: MangaSource) = SourceSettings(get(), source)
} }

View File

@@ -7,7 +7,6 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository

View File

@@ -6,11 +6,10 @@ import android.text.InputFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.databinding.DialogInputBinding import org.koitharu.kotatsu.databinding.DialogInputBinding
class TextInputDialog private constructor( class TextInputDialog private constructor(
private val delegate: AlertDialog private val delegate: AlertDialog,
) : DialogInterface by delegate { ) : DialogInterface by delegate {
fun show() = delegate.show() fun show() = delegate.show()
@@ -33,7 +32,7 @@ class TextInputDialog private constructor(
} }
fun setHint(@StringRes hintResId: Int): Builder { fun setHint(@StringRes hintResId: Int): Builder {
binding.inputLayout.hint = binding.root.context.getString(hintResId) binding.inputEdit.hint = binding.root.context.getString(hintResId)
return this return this
} }
@@ -64,7 +63,7 @@ class TextInputDialog private constructor(
listener: (DialogInterface, String) -> Unit listener: (DialogInterface, String) -> Unit
): Builder { ): Builder {
delegate.setPositiveButton(textId) { dialog, _ -> delegate.setPositiveButton(textId) { dialog, _ ->
listener(dialog, binding.inputEdit.text.toString().orEmpty()) listener(dialog, binding.inputEdit.text?.toString().orEmpty())
} }
return this return this
} }

View File

@@ -1,26 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import java.util.*
@Deprecated("")
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
getId(oldList[oldItemPosition]) == getId(newList[newItemPosition])
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
Objects.equals(oldList[oldItemPosition], newList[newItemPosition])
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
})
operator fun invoke(adapter: RecyclerView.Adapter<*>) {
diff.dispatchUpdatesTo(adapter)
}
}

View File

@@ -96,11 +96,32 @@ class ChipsView @JvmOverloads constructor(
} }
} }
data class ChipModel( class ChipModel(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val title: CharSequence, val title: CharSequence,
val data: Any? = null val data: Any? = null
) ) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ChipModel
if (icon != other.icon) return false
if (title != other.title) return false
if (data != other.data) return false
return true
}
override fun hashCode(): Int {
var result = icon
result = 31 * result + title.hashCode()
result = 31 * result + data.hashCode()
return result
}
}
fun interface OnChipClickListener { fun interface OnChipClickListener {

View File

@@ -6,7 +6,7 @@ import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.resolveAdjustedSize import kotlin.math.roundToInt
class CoverImageView @JvmOverloads constructor( class CoverImageView @JvmOverloads constructor(
@@ -17,47 +17,22 @@ class CoverImageView @JvmOverloads constructor(
init { init {
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) { context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
orientation = getInt(R.styleable.CoverImageView_android_orientation, HORIZONTAL) orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
} }
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val w: Int super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val h: Int val desiredWidth: Int
val desiredHeight: Int
if (orientation == VERTICAL) { if (orientation == VERTICAL) {
val desiredHeight = (drawable?.intrinsicHeight?.coerceAtLeast(0) ?: 0) + desiredHeight = measuredHeight
paddingTop + paddingBottom desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
h = resolveAdjustedSize(
desiredHeight.coerceAtLeast(suggestedMinimumHeight),
maxHeight,
heightMeasureSpec
)
val desiredWidth =
(h * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).toInt() + paddingLeft + paddingRight
w = resolveAdjustedSize(
desiredWidth.coerceAtLeast(suggestedMinimumWidth),
maxWidth,
widthMeasureSpec
)
} else { } else {
val desiredWidth = (drawable?.intrinsicWidth?.coerceAtLeast(0) ?: 0) + desiredWidth = measuredWidth
paddingLeft + paddingRight desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
w = resolveAdjustedSize(
desiredWidth.coerceAtLeast(suggestedMinimumWidth),
maxWidth,
widthMeasureSpec
)
val desiredHeight =
(w * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).toInt() + paddingTop + paddingBottom
h = resolveAdjustedSize(
desiredHeight.coerceAtLeast(suggestedMinimumHeight),
maxHeight,
heightMeasureSpec
)
} }
val widthSize = resolveSizeAndState(w, widthMeasureSpec, 0) setMeasuredDimension(desiredWidth, desiredHeight)
val heightSize = resolveSizeAndState(h, heightMeasureSpec, 0)
setMeasuredDimension(widthSize, heightSize)
} }
companion object { companion object {

View File

@@ -92,8 +92,16 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(top = insets.top) binding.appbar.updatePadding(
binding.webView.updatePadding(bottom = insets.bottom) top = insets.top,
left = insets.left,
right = insets.right,
)
binding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
} }
companion object { companion object {

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.backup
import org.json.JSONArray import org.json.JSONArray
data class BackupEntry( class BackupEntry(
val name: String, val name: String,
val data: JSONArray val data: JSONArray
) { ) {

View File

@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
@Entity(tableName = "manga") @Entity(tableName = "manga")
data class MangaEntity( class MangaEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long, @ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,

View File

@@ -14,7 +14,7 @@ import androidx.room.PrimaryKey
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
)] )]
) )
data class MangaPrefsEntity( class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "mode") val mode: Int @ColumnInfo(name = "mode") val mode: Int

View File

@@ -20,7 +20,7 @@ import androidx.room.ForeignKey
) )
] ]
) )
data class MangaTagsEntity( class MangaTagsEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "tag_id", index = true) val tagId: Long @ColumnInfo(name = "tag_id", index = true) val tagId: Long
) )

View File

@@ -5,7 +5,7 @@ import androidx.room.Junction
import androidx.room.Relation import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
data class MangaWithTags( class MangaWithTags(
@Embedded val manga: MangaEntity, @Embedded val manga: MangaEntity,
@Relation( @Relation(
parentColumn = "manga_id", parentColumn = "manga_id",

View File

@@ -16,7 +16,7 @@ import androidx.room.PrimaryKey
) )
] ]
) )
data class SuggestionEntity( class SuggestionEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "relevance") val relevance: Float, @ColumnInfo(name = "relevance") val relevance: Float,

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
@Entity(tableName = "tags") @Entity(tableName = "tags")
data class TagEntity( class TagEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long, @ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,

View File

@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
) )
] ]
) )
data class TrackEntity( class TrackEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "chapters_total") val totalChapters: Int, @ColumnInfo(name = "chapters_total") val totalChapters: Int,

View File

@@ -15,7 +15,7 @@ import androidx.room.PrimaryKey
) )
] ]
) )
data class TrackLogEntity( class TrackLogEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Long = 0L, @ColumnInfo(name = "id") val id: Long = 0L,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.* import java.util.*
data class TrackLogWithManga( class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity, @Embedded val trackLog: TrackLogEntity,
@Relation( @Relation(
parentColumn = "manga_id", parentColumn = "manga_id",

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.exceptions
import org.json.JSONArray
import org.koitharu.kotatsu.utils.ext.map
class GraphQLException(private val errors: JSONArray) : RuntimeException() {
val messages = errors.map {
it.getString("message")
}
override val message: String
get() = messages.joinToString("\n")
}

View File

@@ -2,12 +2,12 @@ package org.koitharu.kotatsu.core.github
import java.util.* import java.util.*
data class VersionId( class VersionId(
val major: Int, val major: Int,
val minor: Int, val minor: Int,
val build: Int, val build: Int,
val variantType: String, val variantType: String,
val variantNumber: Int val variantNumber: Int,
) : Comparable<VersionId> { ) : Comparable<VersionId> {
override fun compareTo(other: VersionId): Int { override fun compareTo(other: VersionId): Int {
@@ -30,6 +30,30 @@ data class VersionId(
return variantNumber.compareTo(other.variantNumber) return variantNumber.compareTo(other.variantNumber)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as VersionId
if (major != other.major) return false
if (minor != other.minor) return false
if (build != other.build) return false
if (variantType != other.variantType) return false
if (variantNumber != other.variantNumber) return false
return true
}
override fun hashCode(): Int {
var result = major
result = 31 * result + minor
result = 31 * result + build
result = 31 * result + variantType.hashCode()
result = 31 * result + variantNumber
return result
}
companion object { companion object {
private fun variantWeight(variantType: String) = private fun variantWeight(variantType: String) =

View File

@@ -9,8 +9,13 @@ data class MangaChapter(
val name: String, val name: String,
val number: Int, val number: Int,
val url: String, val url: String,
val scanlator: String? = null, val scanlator: String?,
val uploadDate: Long, val uploadDate: Long,
val branch: String? = null, val branch: String?,
val source: MangaSource val source: MangaSource,
) : Parcelable ) : Parcelable, Comparable<MangaChapter> {
override fun compareTo(other: MangaChapter): Int {
return number.compareTo(other.number)
}
}

View File

@@ -10,5 +10,5 @@ data class MangaHistory(
val updatedAt: Date, val updatedAt: Date,
val chapterId: Long, val chapterId: Long,
val page: Int, val page: Int,
val scroll: Int val scroll: Int,
) : Parcelable ) : Parcelable

View File

@@ -8,6 +8,6 @@ data class MangaPage(
val id: Long, val id: Long,
val url: String, val url: String,
val referer: String, val referer: String,
val preview: String? = null, val preview: String?,
val source: MangaSource val source: MangaSource,
) : Parcelable ) : Parcelable

View File

@@ -14,7 +14,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
enum class MangaSource( enum class MangaSource(
val title: String, val title: String,
val locale: String?, val locale: String?,
val cls: Class<out MangaRepository> val cls: Class<out MangaRepository>,
) : Parcelable { ) : Parcelable {
LOCAL("Local", null, LocalMangaRepository::class.java), LOCAL("Local", null, LocalMangaRepository::class.java),
READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java), READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java),

View File

@@ -7,5 +7,5 @@ import kotlinx.parcelize.Parcelize
data class MangaTag( data class MangaTag(
val title: String, val title: String,
val key: String, val key: String,
val source: MangaSource val source: MangaSource,
) : Parcelable ) : Parcelable

View File

@@ -6,4 +6,5 @@ object CommonHeaders {
const val USER_AGENT = "User-Agent" const val USER_AGENT = "User-Agent"
const val ACCEPT = "Accept" const val ACCEPT = "Accept"
const val CONTENT_DISPOSITION = "Content-Disposition" const val CONTENT_DISPOSITION = "Content-Disposition"
const val COOKIE = "Cookie"
} }

View File

@@ -8,6 +8,7 @@ import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
val networkModule val networkModule
@@ -28,4 +29,5 @@ val networkModule
} }
}.build() }.build()
} }
factory { DownloadManagerHelper(get(), get()) }
} }

View File

@@ -1,9 +1,14 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import androidx.collection.ArraySet
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.mapIndexed
import org.koitharu.kotatsu.utils.ext.stringIterator
import java.util.* import java.util.*
class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) {
@@ -20,160 +25,235 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
offset: Int, offset: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
sortOrder: SortOrder? sortOrder: SortOrder?,
): List<Manga> { ): List<Manga> {
if (!query.isNullOrEmpty()) { if (!query.isNullOrEmpty()) {
return if (offset == 0) search(query) else emptyList() return if (offset == 0) {
search(query)
} else {
emptyList()
}
} }
val page = (offset / 12f).toIntUp().inc() val filters = tags?.takeUnless { it.isEmpty() }?.joinToString(
val link = when { separator = ",",
tags.isNullOrEmpty() -> "/manga?page=$page".withDomain() prefix = "genres: [",
else -> tags.joinToString( postfix = "]"
prefix = "/manga?", ) { "\"it.key\"" }.orEmpty()
postfix = "&page=$page", val array = apiCall(
separator = "&", """
) { tag -> "genre[]=${tag.key}" }.withDomain() getMediaList(offset: $offset, limit: 20, mediaType: manga, filters: {$filters}) {
} docs {
val doc = loaderContext.httpGet(link).parseHtml() mediaId
val root = doc.body().select("div.manga-block") ?: parseFailed("Cannot find root") title {
val items = root.select("div.anime-card") be
return items.mapNotNull { card -> alt
val href = card.selectFirst("a")?.attr("href") ?: return@mapNotNull null }
val status = card.select("tr")[2].text() rating
val fullTitle = card.selectFirst("h1.anime-card-title")?.text() poster
?.substringBeforeLast('[') ?: return@mapNotNull null genres
val titleParts = fullTitle.splitTwoParts('/') slug
mediaType
status
}
}
""".trimIndent()
).getJSONObject("getMediaList").getJSONArray("docs")
return array.map { jo ->
val mediaId = jo.getString("mediaId")
val title = jo.getJSONObject("title")
val href = "${jo.getString("mediaType")}/${jo.getString("slug")}"
Manga( Manga(
id = generateUid(href), id = generateUid(mediaId),
title = titleParts?.first?.trim() ?: fullTitle, title = title.getString("be"),
coverUrl = card.selectFirst("img")?.attr("data-src") coverUrl = jo.getString("poster").removePrefix("/cdn")
?.withDomain().orEmpty(), .withDomain("cdn") + "?width=200&height=280",
altTitle = titleParts?.second?.trim(), altTitle = title.getString("alt").takeUnless(String::isEmpty),
author = null, author = null,
rating = Manga.NO_RATING, rating = jo.getDouble("rating").toFloat() / 10f,
url = href, url = href,
publicUrl = href.withDomain(), publicUrl = "https://${getDomain()}/${href}",
tags = card.select("p.tupe.tag").select("a").mapNotNullToSet tags@{ x -> tags = jo.getJSONArray("genres").mapToTags(),
MangaTag( state = when (jo.getString("status")) {
title = x.text(), "ongoing" -> MangaState.ONGOING
key = x.attr("href").ifEmpty { "finished" -> MangaState.FINISHED
return@mapNotNull null
}.substringAfterLast("="),
source = source
)
},
state = when (status) {
"выпускаецца" -> MangaState.ONGOING
"завершанае" -> MangaState.FINISHED
else -> null else -> null
}, },
source = source source = source,
) )
} }
} }
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = loaderContext.httpGet(manga.publicUrl).parseHtml() val (type, slug) = manga.url.split('/')
val root = doc.body().select("div.container") ?: parseFailed("Cannot find root") val details = apiCall(
"""
media(mediaType: $type, slug: "$slug") {
mediaId
title {
be
alt
}
description {
be
}
status
poster
rating
genres
}
""".trimIndent()
).getJSONObject("media")
val title = details.getJSONObject("title")
val poster = details.getString("poster").removePrefix("/cdn")
.withDomain("cdn")
val chapters = apiCall(
"""
chapters(mediaId: "${details.getString("mediaId")}") {
id
chapter
released
}
""".trimIndent()
).getJSONArray("chapters")
return manga.copy( return manga.copy(
description = root.select("div.manga-block.grid-12")[2].select("p").text(), title = title.getString("be"),
chapters = root.select("ul.series").flatMap { table -> altTitle = title.getString("alt"),
table.select("li") coverUrl = "$poster?width=200&height=280",
}.map { it.selectFirst("a") }.mapIndexedNotNull { i, a -> largeCoverUrl = poster,
val href = a?.select("a")?.first()?.attr("href") description = details.getJSONObject("description").getString("be"),
?.toRelativeUrl(getDomain()) ?: return@mapIndexedNotNull null rating = details.getDouble("rating").toFloat() / 10f,
tags = details.getJSONArray("genres").mapToTags(),
state = when (details.getString("status")) {
"ongoing" -> MangaState.ONGOING
"finished" -> MangaState.FINISHED
else -> null
},
chapters = chapters.map { jo ->
val number = jo.getInt("chapter")
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(jo.getString("id")),
name = "Глава " + a.selectFirst("a")?.text().orEmpty(), name = "Глава $number",
number = i + 1, number = number,
url = href, url = "${manga.url}/read/$number",
uploadDate = 0L, scanlator = null,
source = source uploadDate = jo.getLong("released"),
branch = null,
source = source,
) )
} }
) )
} }
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.withDomain() val (_, slug, _, number) = chapter.url.split('/')
val doc = loaderContext.httpGet(fullUrl).parseHtml() val chapterJson = apiCall(
val scripts = doc.select("script") """
for (script in scripts) { chapter(slug: "$slug", chapter: $number) {
val data = script.html() id
val pos = data.indexOf("dataSource") images {
if (pos == -1) { large
continue thumbnail
} }
val json = data.substring(pos).substringAfter('[').substringBefore(']')
val domain = getDomain()
return json.split(",").mapNotNull {
it.trim()
.removeSurrounding('"', '\'')
.toRelativeUrl(domain)
.takeUnless(String::isBlank)
}.map { url ->
MangaPage(
id = generateUid(url),
url = url,
referer = fullUrl,
source = source
)
} }
""".trimIndent()
).getJSONObject("chapter")
val pages = chapterJson.getJSONArray("images")
val chapterUrl = "https://${getDomain()}/${chapter.url}"
return pages.mapIndexed { i, jo ->
MangaPage(
id = generateUid("${chapter.url}/$i"),
url = jo.getString("large"),
referer = chapterUrl,
preview = jo.getString("thumbnail"),
source = source,
)
} }
parseFailed("Pages list not found at ${chapter.url.withDomain()}")
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
val doc = loaderContext.httpGet("https://${getDomain()}/manga").parseHtml() val json = apiCall(
val root = doc.body().select("div#tabs-genres").select("ul#list.ul-three-colums") """
return root.select("p.menu-tags.tupe").mapToSet { p -> getFilters(mediaType: manga) {
val a = p.selectFirst("a") ?: parseFailed("a is null") genres
MangaTag( }
title = a.text().toCamelCase(), """.trimIndent()
key = a.attr("data-name"), )
source = source val array = json.getJSONObject("getFilters").getJSONArray("genres")
) return array.mapToTags()
}
} }
private suspend fun search(query: String): List<Manga> { private suspend fun search(query: String): List<Manga> {
val domain = getDomain() val json = apiCall(
val doc = loaderContext.httpGet("https://$domain/search?q=$query").parseHtml() """
val root = doc.body().select("div.manga-block").select("article.tab-2") ?: parseFailed("Cannot find root") search(query: "$query", limit: 40) {
val items = root.select("div.anime-card") id
return items.mapNotNull { card -> title {
val href = card.select("a").attr("href") be
val status = card.select("tr")[2].text() en
val fullTitle = card.selectFirst("h1.anime-card-title")?.text() }
?.substringBeforeLast('[') ?: return@mapNotNull null poster
val titleParts = fullTitle.splitTwoParts('/') url
type
}
""".trimIndent()
)
val array = json.getJSONArray("search")
return array.map { jo ->
val mediaId = jo.getString("id")
val title = jo.getJSONObject("title")
val href = "${jo.getString("type").lowercase()}/${jo.getString("url")}"
Manga( Manga(
id = generateUid(href), id = generateUid(mediaId),
title = titleParts?.first?.trim() ?: fullTitle, title = title.getString("be"),
coverUrl = card.selectFirst("img")?.attr("src") coverUrl = jo.getString("poster").removePrefix("/cdn")
?.withDomain().orEmpty(), .withDomain("cdn") + "?width=200&height=280",
altTitle = titleParts?.second?.trim(), altTitle = title.getString("en").takeUnless(String::isEmpty),
author = null, author = null,
rating = Manga.NO_RATING, rating = Manga.NO_RATING,
url = href, url = href,
publicUrl = href.withDomain(), publicUrl = "https://${getDomain()}/${href}",
tags = card.select("p.tupe.tag").select("a").mapNotNullToSet tags@{ x -> tags = emptySet(),
MangaTag( state = null,
title = x.text(), source = source,
key = x.attr("href").ifEmpty {
return@mapNotNull null
}.substringAfterLast("="),
source = source
)
},
state = when (status) {
"выпускаецца" -> MangaState.ONGOING
"завершанае" -> MangaState.FINISHED
else -> null
},
source = source
) )
} }
} }
private suspend fun apiCall(request: String): JSONObject {
return loaderContext.graphQLQuery("https://api.${getDomain()}/", request)
.getJSONObject("data")
}
private fun JSONArray.mapToTags(): Set<MangaTag> {
fun toTitle(slug: String): String {
val builder = StringBuilder(slug)
var capitalize = true
for ((i, c) in builder.withIndex()) {
when {
c == '-' -> {
builder.setCharAt(i, ' ')
capitalize = true
}
capitalize -> {
builder.setCharAt(i, c.uppercaseChar())
capitalize = false
}
}
}
return builder.toString()
}
val result = ArraySet<MangaTag>(length())
stringIterator().forEach {
result.add(
MangaTag(
title = toTitle(it),
key = it,
source = source,
)
)
}
return result
}
} }

View File

@@ -88,8 +88,10 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
name = tr.selectFirst("a")?.text().orEmpty(), name = tr.selectFirst("a")?.text().orEmpty(),
number = i + 1, number = i + 1,
url = href, url = href,
scanlator = null,
branch = null,
uploadDate = dateFormat.tryParse(tr.selectFirst("div.date")?.text()), uploadDate = dateFormat.tryParse(tr.selectFirst("div.date")?.text()),
source = source source = source,
) )
} }
) )
@@ -117,8 +119,9 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
preview = null,
referer = fullUrl, referer = fullUrl,
source = source source = source,
) )
} }
} }

View File

@@ -101,7 +101,9 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
url = "$baseChapterUrl$chid", url = "$baseChapterUrl$chid",
uploadDate = it.getLong("date") * 1000, uploadDate = it.getLong("date") * 1000,
name = if (title.isEmpty()) volChap else "$volChap: $title", name = if (title.isEmpty()) volChap else "$volChap: $title",
number = totalChapters - i number = totalChapters - i,
scanlator = null,
branch = null,
) )
}.reversed() }.reversed()
) )
@@ -116,8 +118,9 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
MangaPage( MangaPage(
id = generateUid(jo.getLong("id")), id = generateUid(jo.getLong("id")),
referer = fullUrl, referer = fullUrl,
preview = null,
source = chapter.source, source = chapter.source,
url = jo.getString("img") url = jo.getString("img"),
) )
} }
} }

View File

@@ -143,6 +143,8 @@ class ExHentaiRepository(
url = url, url = url,
uploadDate = 0L, uploadDate = 0L,
source = source, source = source,
scanlator = null,
branch = null,
) )
} }
chapters chapters

View File

@@ -142,7 +142,8 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
url = href, url = href,
uploadDate = dateFormat.tryParse(tr.selectFirst("td.d-none")?.text()), uploadDate = dateFormat.tryParse(tr.selectFirst("td.d-none")?.text()),
scanlator = translators, scanlator = translators,
source = source source = source,
branch = null,
) )
} }
) )
@@ -167,8 +168,9 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
preview = null,
referer = chapter.url, referer = chapter.url,
source = source source = source,
) )
} }
} }

View File

@@ -18,12 +18,10 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
sortOrder: SortOrder? sortOrder: SortOrder?
): List<Manga> { ): List<Manga> {
return super.getList2(offset, query, tags, sortOrder).map { return super.getList2(offset, query, tags, sortOrder).map {
val cover = it.coverUrl it.copy(
if (cover.contains("_blur")) { coverUrl = it.coverUrl.replace("_blur", ""),
it.copy(coverUrl = cover.replace("_blur", "")) isNsfw = true,
} else { )
it
}
} }
} }
@@ -50,7 +48,9 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load
source = source, source = source,
number = 1, number = 1,
uploadDate = 0L, uploadDate = 0L,
name = manga.title name = manga.title,
scanlator = null,
branch = null,
) )
) )
) )

View File

@@ -118,7 +118,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
item.getString("chapter_created_at").substringBefore(" ") item.getString("chapter_created_at").substringBefore(" ")
), ),
scanlator = scanlator, scanlator = scanlator,
name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter" branch = null,
name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter",
) )
) )
} }
@@ -178,8 +179,9 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
MangaPage( MangaPage(
id = generateUid(pageUrl), id = generateUid(pageUrl),
url = pageUrl, url = pageUrl,
preview = null,
referer = fullUrl, referer = fullUrl,
source = source source = source,
) )
} }
} }

View File

@@ -93,7 +93,7 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
}, },
chapters = table.select("div.table.table-chapter-list").select("li.list-group-item.chapter_list").asReversed().mapIndexed { i, li -> chapters = table.select("div.table.table-chapter-list").select("li.list-group-item.chapter_list").asReversed().mapIndexed { i, li ->
val a = li.select("a") val a = li.select("a")
val href = a.attr("href").ifEmpty { val href = a.attr("data-href").ifEmpty {
parseFailed("Link is missing") parseFailed("Link is missing")
} }
MangaChapter( MangaChapter(
@@ -101,8 +101,10 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
name = a.select("label").text(), name = a.select("label").text(),
number = i + 1, number = i + 1,
url = href, url = href,
scanlator = null,
branch = null,
uploadDate = dateFormat.tryParse(li.selectFirst("small:last-of-type")?.text()), uploadDate = dateFormat.tryParse(li.selectFirst("small:last-of-type")?.text()),
source = MangaSource.MANGAOWL source = MangaSource.MANGAOWL,
) )
} }
) )
@@ -117,8 +119,9 @@ class MangaOwlRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposit
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
preview = null,
referer = fullUrl, referer = fullUrl,
source = MangaSource.MANGAOWL source = MangaSource.MANGAOWL,
) )
} }
} }

View File

@@ -124,7 +124,9 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
dateFormat, dateFormat,
li.selectFirst("span.time")?.text() li.selectFirst("span.time")?.text()
), ),
name = name.ifEmpty { "${manga.title} - ${i + 1}" } name = name.ifEmpty { "${manga.title} - ${i + 1}" },
scanlator = null,
branch = null,
) )
} }
) )
@@ -143,8 +145,9 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
MangaPage( MangaPage(
id = generateUid(href), id = generateUid(href),
url = href, url = href,
preview = null,
referer = fullUrl, referer = fullUrl,
source = MangaSource.MANGATOWN source = MangaSource.MANGATOWN,
) )
} ?: parseFailed("Pages list not found") } ?: parseFailed("Pages list not found")
} }

View File

@@ -146,7 +146,9 @@ class MangareadRepository(
dateFormat, dateFormat,
doc2.selectFirst("span.chapter-release-date i")?.text() doc2.selectFirst("span.chapter-release-date i")?.text()
), ),
source = MangaSource.MANGAREAD source = MangaSource.MANGAREAD,
scanlator = null,
branch = null,
) )
} }
) )
@@ -164,8 +166,9 @@ class MangareadRepository(
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
preview = null,
referer = fullUrl, referer = fullUrl,
source = MangaSource.MANGAREAD source = MangaSource.MANGAREAD,
) )
} }
} }

View File

@@ -114,6 +114,8 @@ abstract class NineMangaRepository(
url = href, url = href,
uploadDate = parseChapterDateByLang(li.selectFirst("span")?.text().orEmpty()), uploadDate = parseChapterDateByLang(li.selectFirst("span")?.text().orEmpty()),
source = source, source = source,
scanlator = null,
branch = null,
) )
} }
) )

View File

@@ -6,16 +6,20 @@ import org.json.JSONObject
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext),
MangaRepositoryAuthProvider {
override val source = MangaSource.REMANGA override val source = MangaSource.REMANGA
override val defaultDomain = "remanga.org" override val defaultDomain = "remanga.org"
override val authUrl: String
get() = "https://${getDomain()}/user/login"
override val sortOrders: Set<SortOrder> = EnumSet.of( override val sortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED, SortOrder.UPDATED,
@@ -30,6 +34,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
sortOrder: SortOrder? sortOrder: SortOrder?
): List<Manga> { ): List<Manga> {
copyCookies()
val domain = getDomain() val domain = getDomain()
val urlBuilder = StringBuilder() val urlBuilder = StringBuilder()
.append("https://api.") .append("https://api.")
@@ -78,6 +83,7 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
} }
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
copyCookies()
val domain = getDomain() val domain = getDomain()
val slug = manga.url.find(LAST_URL_PATH_REGEX) val slug = manga.url.find(LAST_URL_PATH_REGEX)
?: throw ParseException("Cannot obtain slug from ${manga.url}") ?: throw ParseException("Cannot obtain slug from ${manga.url}")
@@ -130,7 +136,8 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
}, },
uploadDate = dateFormat.tryParse(jo.getString("upload_date")), uploadDate = dateFormat.tryParse(jo.getString("upload_date")),
scanlator = publishers.optJSONObject(0)?.getStringOrNull("name"), scanlator = publishers.optJSONObject(0)?.getStringOrNull("name"),
source = MangaSource.REMANGA source = MangaSource.REMANGA,
branch = null,
) )
}.asReversed() }.asReversed()
) )
@@ -164,6 +171,17 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
} }
} }
override fun isAuthorized(): Boolean {
return loaderContext.cookieJar.getCookies(getDomain()).any {
it.name == "user"
}
}
private fun copyCookies() {
val domain = getDomain()
loaderContext.cookieJar.copyCookies(domain, "api.$domain")
}
private fun getSortKey(order: SortOrder?) = when (order) { private fun getSortKey(order: SortOrder?) = when (order) {
SortOrder.UPDATED -> "-chapter_date" SortOrder.UPDATED -> "-chapter_date"
SortOrder.POPULARITY -> "-rating" SortOrder.POPULARITY -> "-rating"
@@ -175,8 +193,9 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito
private fun parsePage(jo: JSONObject, referer: String) = MangaPage( private fun parsePage(jo: JSONObject, referer: String) = MangaPage(
id = generateUid(jo.getLong("id")), id = generateUid(jo.getLong("id")),
url = jo.getString("link"), url = jo.getString("link"),
preview = null,
referer = referer, referer = referer,
source = source source = source,
) )
private companion object { private companion object {

View File

@@ -30,7 +30,9 @@ class YaoiChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loa
number = i + 1, number = i + 1,
url = href, url = href,
uploadDate = 0L, uploadDate = 0L,
source = source source = source,
scanlator = null,
branch = null,
) )
} }
) )

View File

@@ -79,6 +79,8 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
var historyGrouping by BoolPreferenceDelegate(KEY_HISTORY_GROUPING, true) var historyGrouping by BoolPreferenceDelegate(KEY_HISTORY_GROUPING, true)
var isHistoryExcludeNsfw by BoolPreferenceDelegate(KEY_HISTORY_EXCLUDE_NSFW, false)
var chaptersReverse by BoolPreferenceDelegate(KEY_REVERSE_CHAPTERS, false) var chaptersReverse by BoolPreferenceDelegate(KEY_REVERSE_CHAPTERS, false)
val zoomMode by EnumPreferenceDelegate( val zoomMode by EnumPreferenceDelegate(
@@ -192,6 +194,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val KEY_RESTORE = "restore" const val KEY_RESTORE = "restore"
const val KEY_HISTORY_GROUPING = "history_grouping" const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -14,16 +14,35 @@ sealed class DateTimeAgo : ListModel {
} }
} }
data class MinutesAgo(val minutes: Int) : DateTimeAgo() { class MinutesAgo(val minutes: Int) : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes) return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MinutesAgo
return minutes == other.minutes
}
override fun hashCode(): Int = minutes
} }
data class HoursAgo(val hours: Int) : DateTimeAgo() { class HoursAgo(val hours: Int) : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.hours_ago, hours, hours) return resources.getQuantityString(R.plurals.hours_ago, hours, hours)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as HoursAgo
return hours == other.hours
}
override fun hashCode(): Int = hours
} }
object Today : DateTimeAgo() { object Today : DateTimeAgo() {
@@ -38,10 +57,19 @@ sealed class DateTimeAgo : ListModel {
} }
} }
data class DaysAgo(val days: Int) : DateTimeAgo() { class DaysAgo(val days: Int) : DateTimeAgo() {
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.days_ago, days, days) return resources.getQuantityString(R.plurals.days_ago, days, days)
} }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DaysAgo
return days == other.days
}
override fun hashCode(): Int = days
} }
object LongAgo : DateTimeAgo() { object LongAgo : DateTimeAgo() {

View File

@@ -44,7 +44,7 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class DetailsActivity : BaseActivity<ActivityDetailsBinding>(), class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
TabLayoutMediator.TabConfigurationStrategy { TabLayoutMediator.TabConfigurationStrategy {
private val viewModel by viewModel<DetailsViewModel>(mode = LazyThreadSafetyMode.NONE) { private val viewModel by viewModel<DetailsViewModel> {
parametersOf(MangaIntent.from(intent)) parametersOf(MangaIntent.from(intent))
} }

View File

@@ -19,10 +19,6 @@ class ChaptersAdapter(
return items[position].chapter.id return items[position].chapter.id
} }
fun setItems(newItems: List<ChapterListItem>, callback: Runnable) {
differ.submitList(newItems, callback)
}
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() { private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean { override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.model.SortOrder
import java.util.* import java.util.*
@Entity(tableName = "favourite_categories") @Entity(tableName = "favourite_categories")
data class FavouriteCategoryEntity( class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int, @ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long, @ColumnInfo(name = "created_at") val createdAt: Long,

View File

@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
) )
] ]
) )
data class FavouriteEntity( class FavouriteEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long, @ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long @ColumnInfo(name = "created_at") val createdAt: Long

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
data class FavouriteManga( class FavouriteManga(
@Embedded val favourite: FavouriteEntity, @Embedded val favourite: FavouriteEntity,
@Relation( @Relation(
parentColumn = "manga_id", parentColumn = "manga_id",

View File

@@ -30,9 +30,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
override val recycledViewPool = RecyclerView.RecycledViewPool() override val recycledViewPool = RecyclerView.RecycledViewPool()
private val viewModel by viewModel<FavouritesCategoriesViewModel>( private val viewModel by viewModel<FavouritesCategoriesViewModel>()
mode = LazyThreadSafetyMode.NONE
)
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) { private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this) CategoriesEditDelegate(requireContext(), this)
} }

View File

@@ -30,9 +30,7 @@ class CategoriesActivity : BaseActivity<ActivityCategoriesBinding>(),
OnListItemClickListener<FavouriteCategory>, OnListItemClickListener<FavouriteCategory>,
View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback { View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
private val viewModel by viewModel<FavouritesCategoriesViewModel>( private val viewModel by viewModel<FavouritesCategoriesViewModel>()
mode = LazyThreadSafetyMode.NONE
)
private lateinit var adapter: CategoriesAdapter private lateinit var adapter: CategoriesAdapter
private lateinit var reorderHelper: ItemTouchHelper private lateinit var reorderHelper: ItemTouchHelper

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context import android.content.Context
import android.text.InputType import android.text.InputType
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
@@ -32,7 +33,12 @@ class CategoriesEditDelegate(
.setNegativeButton(android.R.string.cancel) .setNegativeButton(android.R.string.cancel)
.setMaxLength(MAX_TITLE_LENGTH, false) .setMaxLength(MAX_TITLE_LENGTH, false)
.setPositiveButton(R.string.rename) { _, name -> .setPositiveButton(R.string.rename) { _, name ->
callback.onRenameCategory(category, name) val trimmed = name.trim()
if (trimmed.isEmpty()) {
Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show()
} else {
callback.onRenameCategory(category, name)
}
}.create() }.create()
.show() .show()
} }
@@ -45,7 +51,12 @@ class CategoriesEditDelegate(
.setNegativeButton(android.R.string.cancel) .setNegativeButton(android.R.string.cancel)
.setMaxLength(MAX_TITLE_LENGTH, false) .setMaxLength(MAX_TITLE_LENGTH, false)
.setPositiveButton(R.string.add) { _, name -> .setPositiveButton(R.string.add) { _, name ->
callback.onCreateCategory(name) val trimmed = name.trim()
if (trimmed.isEmpty()) {
Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show()
} else {
callback.onCreateCategory(trimmed)
}
}.create() }.create()
.show() .show()
} }

View File

@@ -25,7 +25,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet<DialogFavoriteCategoriesBindin
OnListItemClickListener<MangaCategoryItem>, CategoriesEditDelegate.CategoriesEditCallback, OnListItemClickListener<MangaCategoryItem>, CategoriesEditDelegate.CategoriesEditCallback,
View.OnClickListener { View.OnClickListener {
private val viewModel by viewModel<MangaCategoriesViewModel>(mode = LazyThreadSafetyMode.NONE) { private val viewModel by viewModel<MangaCategoriesViewModel> {
parametersOf(requireNotNull(arguments?.getParcelable<Manga>(MangaIntent.KEY_MANGA))) parametersOf(requireNotNull(arguments?.getParcelable<Manga>(MangaIntent.KEY_MANGA)))
} }
@@ -36,7 +36,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet<DialogFavoriteCategoriesBindin
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?,
) = DialogFavoriteCategoriesBinding.inflate(inflater, container, false) ) = DialogFavoriteCategoriesBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class FavouritesListFragment : MangaListFragment() { class FavouritesListFragment : MangaListFragment() {
override val viewModel by viewModel<FavouritesListViewModel>(mode = LazyThreadSafetyMode.NONE) { override val viewModel by viewModel<FavouritesListViewModel> {
parametersOf(categoryId) parametersOf(categoryId)
} }

View File

@@ -8,6 +8,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel
val historyModule val historyModule
get() = module { get() = module {
single { HistoryRepository(get(), get()) } single { HistoryRepository(get(), get(), get()) }
viewModel { HistoryListViewModel(get(), get(), get()) } viewModel { HistoryListViewModel(get(), get(), get()) }
} }

View File

@@ -18,14 +18,14 @@ import java.util.*
) )
] ]
) )
data class HistoryEntity( class HistoryEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "updated_at") val updatedAt: Long, @ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long, @ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int, @ColumnInfo(name = "page") val page: Int,
@ColumnInfo(name = "scroll") val scroll: Float @ColumnInfo(name = "scroll") val scroll: Float,
) { ) {
fun toMangaHistory() = MangaHistory( fun toMangaHistory() = MangaHistory(

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
data class HistoryWithManga( class HistoryWithManga(
@Embedded val history: HistoryEntity, @Embedded val history: HistoryEntity,
@Relation( @Relation(
parentColumn = "manga_id", parentColumn = "manga_id",

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.mapItems import org.koitharu.kotatsu.utils.ext.mapItems
@@ -16,6 +17,7 @@ import org.koitharu.kotatsu.utils.ext.mapToSet
class HistoryRepository( class HistoryRepository(
private val db: MangaDatabase, private val db: MangaDatabase,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
) { ) {
suspend fun getList(offset: Int, limit: Int = 20): List<Manga> { suspend fun getList(offset: Int, limit: Int = 20): List<Manga> {
@@ -45,6 +47,9 @@ class HistoryRepository(
} }
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) { suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
if (manga.isNsfw && settings.isHistoryExcludeNsfw) {
return
}
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.tagsDao.upsert(tags)

View File

@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.utils.ext.ellipsize
class HistoryListFragment : MangaListFragment() { class HistoryListFragment : MangaListFragment() {
override val viewModel by viewModel<HistoryListViewModel>(mode = LazyThreadSafetyMode.NONE) override val viewModel by viewModel<HistoryListViewModel>()
override val isSwipeRefreshEnabled = false override val isSwipeRefreshEnabled = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -43,10 +43,6 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick))
} }
fun setItems(list: List<ListModel>, commitCallback: Runnable) {
differ.submitList(list, commitCallback)
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when { override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {

View File

@@ -69,8 +69,9 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
MangaPage( MangaPage(
id = entryUri.longHashCode(), id = entryUri.longHashCode(),
url = entryUri, url = entryUri,
preview = null,
referer = chapter.url, referer = chapter.url,
source = MangaSource.LOCAL source = MangaSource.LOCAL,
) )
} }
} }
@@ -124,7 +125,9 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
number = i + 1, number = i + 1,
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
uploadDate = 0L, uploadDate = 0L,
url = uriBuilder.fragment(s).build().toString() url = uriBuilder.fragment(s).build().toString(),
scanlator = null,
branch = null,
) )
} }
) )

View File

@@ -19,9 +19,9 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.utils.ext.ellipsize import org.koitharu.kotatsu.utils.ext.ellipsize
class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> { class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri?> {
override val viewModel by viewModel<LocalListViewModel>(mode = LazyThreadSafetyMode.NONE) override val viewModel by viewModel<LocalListViewModel>()
private val importCall = registerForActivityResult( private val importCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(), ActivityResultContracts.OpenDocument(),
this this

View File

@@ -16,9 +16,9 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.resolveName
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -74,7 +74,7 @@ class LocalListViewModel(
launchLoadingJob { launchLoadingJob {
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val name = MediaStoreCompat(contentResolver).getName(uri) val name = contentResolver.resolveName(uri)
?: throw IOException("Cannot fetch name from uri: $uri") ?: throw IOException("Cannot fetch name from uri: $uri")
if (!LocalMangaRepository.isFileSupported(name)) { if (!LocalMangaRepository.isFileSupported(name)) {
throw UnsupportedFileException("Unsupported file on $uri") throw UnsupportedFileException("Unsupported file on $uri")

View File

@@ -57,10 +57,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
NavigationView.OnNavigationItemSelectedListener, AppBarOwner, NavigationView.OnNavigationItemSelectedListener, AppBarOwner,
View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener { View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener {
private val viewModel by viewModel<MainViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<MainViewModel>()
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>( private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>()
mode = LazyThreadSafetyMode.NONE
)
private lateinit var navHeaderBinding: NavigationHeaderBinding private lateinit var navHeaderBinding: NavigationHeaderBinding
private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerToggle: ActionBarDrawerToggle

View File

@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEditorActionListener, class ProtectActivity : BaseActivity<ActivityProtectBinding>(), TextView.OnEditorActionListener,
TextWatcher, View.OnClickListener { TextWatcher, View.OnClickListener {
private val viewModel by viewModel<ProtectViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<ProtectViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -13,6 +13,6 @@ val readerModule
single { PagesCache(get()) } single { PagesCache(get()) }
viewModel { params -> viewModel { params ->
ReaderViewModel(params[0], params[1], get(), get(), get(), get()) ReaderViewModel(params[0], params[1], get(), get(), get(), get(), get())
} }
} }

View File

@@ -55,7 +55,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback, GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
ActivityResultCallback<Boolean>, ReaderControlDelegate.OnInteractionListener { ActivityResultCallback<Boolean>, ReaderControlDelegate.OnInteractionListener {
private val viewModel by viewModel<ReaderViewModel>(mode = LazyThreadSafetyMode.NONE) { private val viewModel by viewModel<ReaderViewModel> {
parametersOf(MangaIntent.from(intent), intent?.getParcelableExtra<ReaderState>(EXTRA_STATE)) parametersOf(MangaIntent.from(intent), intent?.getParcelableExtra<ReaderState>(EXTRA_STATE))
} }
@@ -196,7 +196,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
override fun onActivityResult(result: Boolean) { override fun onActivityResult(result: Boolean) {
if (result) { if (result) {
viewModel.saveCurrentState(reader?.getCurrentState()) viewModel.saveCurrentState(reader?.getCurrentState())
viewModel.saveCurrentPage(contentResolver) viewModel.saveCurrentPage()
} }
} }

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.reader.ui
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.util.LongSparseArray import android.util.LongSparseArray
import android.webkit.URLUtil
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -23,10 +22,9 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.DownloadManagerHelper
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.IgnoreErrors import org.koitharu.kotatsu.utils.ext.IgnoreErrors
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -38,7 +36,8 @@ class ReaderViewModel(
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository, private val shortcutsRepository: ShortcutsRepository,
private val settings: AppSettings private val settings: AppSettings,
private val downloadManagerHelper: DownloadManagerHelper,
) : BaseViewModel() { ) : BaseViewModel() {
private var loadingJob: Job? = null private var loadingJob: Job? = null
@@ -150,7 +149,7 @@ class ReaderViewModel(
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() } return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
} }
fun saveCurrentPage(resolver: ContentResolver) { fun saveCurrentPage() {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
try { try {
val state = currentState.value ?: error("Undefined state") val state = currentState.value ?: error("Undefined state")
@@ -159,13 +158,8 @@ class ReaderViewModel(
}?.toMangaPage() ?: error("Page not found") }?.toMangaPage() ?: error("Page not found")
val repo = MangaRepository(page.source) val repo = MangaRepository(page.source)
val pageUrl = repo.getPageUrl(page) val pageUrl = repo.getPageUrl(page)
val file = get<PagesCache>()[pageUrl] ?: error("Page not found in cache") val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
val uri = file.inputStream().use { input -> val uri = downloadManagerHelper.awaitDownload(downloadId)
val fileName = URLUtil.guessFileName(pageUrl, null, null)
MediaStoreCompat(resolver).insertImage(fileName) {
input.copyTo(it)
}
}
onPageSaved.postCall(uri) onPageSaved.postCall(uri)
} catch (e: CancellationException) { } catch (e: CancellationException) {
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -47,10 +47,6 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
viewType: Int viewType: Int
): H = onCreateViewHolder(parent, loader, settings, exceptionResolver) ): H = onCreateViewHolder(parent, loader, settings, exceptionResolver)
fun setItems(items: List<ReaderPage>, callback: Runnable) {
differ.submitList(items, callback)
}
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont -> suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
differ.submitList(items) { differ.submitList(items) {
cont.resume(Unit) cont.resume(Unit)

View File

@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class RemoteListFragment : MangaListFragment() { class RemoteListFragment : MangaListFragment() {
override val viewModel by viewModel<RemoteListViewModel>(mode = LazyThreadSafetyMode.NONE) { override val viewModel by viewModel<RemoteListViewModel> {
parametersOf(source) parametersOf(source)
} }

View File

@@ -18,9 +18,7 @@ import org.koitharu.kotatsu.utils.ext.showKeyboard
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener { class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>( private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>()
mode = LazyThreadSafetyMode.NONE
)
private lateinit var source: MangaSource private lateinit var source: MangaSource
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class SearchFragment : MangaListFragment() { class SearchFragment : MangaListFragment() {
override val viewModel by viewModel<SearchViewModel>(mode = LazyThreadSafetyMode.NONE) { override val viewModel by viewModel<SearchViewModel> {
parametersOf(source, query) parametersOf(source, query)
} }

View File

@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class GlobalSearchFragment : MangaListFragment() { class GlobalSearchFragment : MangaListFragment() {
override val viewModel by viewModel<GlobalSearchViewModel>(mode = LazyThreadSafetyMode.NONE) { override val viewModel by viewModel<GlobalSearchViewModel> {
parametersOf(query) parametersOf(query)
} }

View File

@@ -1,23 +1,21 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.* import androidx.preference.*
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import java.io.File import java.io.File
import java.util.* import java.util.*
@@ -134,53 +132,4 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
settings.setStorageDir(context ?: return, file) settings.setStorageDir(context ?: return, file)
} }
private fun enableAppProtection(preference: SwitchPreference) {
val ctx = preference.context ?: return
val cancelListener =
object : DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
override fun onCancel(dialog: DialogInterface?) {
settings.appPassword = null
preference.isChecked = false
preference.isEnabled = true
}
override fun onClick(dialog: DialogInterface?, which: Int) = onCancel(dialog)
}
preference.isEnabled = false
TextInputDialog.Builder(ctx)
.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
.setHint(R.string.enter_password)
.setNegativeButton(android.R.string.cancel, cancelListener)
.setOnCancelListener(cancelListener)
.setPositiveButton(android.R.string.ok) { d, password ->
if (password.isBlank()) {
cancelListener.onCancel(d)
return@setPositiveButton
}
TextInputDialog.Builder(ctx)
.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
.setHint(R.string.repeat_password)
.setNegativeButton(android.R.string.cancel, cancelListener)
.setOnCancelListener(cancelListener)
.setPositiveButton(android.R.string.ok) { d2, password2 ->
if (password == password2) {
settings.appPassword = password.md5()
preference.isChecked = true
preference.isEnabled = true
} else {
cancelListener.onCancel(d2)
Snackbar.make(
listView,
R.string.passwords_mismatch,
Snackbar.LENGTH_SHORT
).show()
}
}.setTitle(preference.title)
.create()
.show()
}.setTitle(preference.title)
.create()
.show()
}
} }

View File

@@ -21,7 +21,7 @@ import java.io.FileOutputStream
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() { class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private val viewModel by viewModel<BackupViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<BackupViewModel>()
private var backup: File? = null private var backup: File? = null
private val saveFileContract = private val saveFileContract =

View File

@@ -13,7 +13,7 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore), class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
ActivityResultCallback<Uri> { ActivityResultCallback<Uri?> {
private val backupSelectCall = registerForActivityResult( private val backupSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(), ActivityResultContracts.OpenDocument(),

View File

@@ -22,10 +22,10 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?,
) = DialogProgressBinding.inflate(inflater, container, false) ) = DialogProgressBinding.inflate(inflater, container, false)
private val viewModel by viewModel<RestoreViewModel>(mode = LazyThreadSafetyMode.NONE) { private val viewModel by viewModel<RestoreViewModel> {
parametersOf(arguments?.getString(ARG_FILE)?.toUriOrNull()) parametersOf(arguments?.getString(ARG_FILE)?.toUriOrNull())
} }

View File

@@ -21,7 +21,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(), class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(),
OnListItemClickListener<SourceLocale>, DialogInterface.OnClickListener { OnListItemClickListener<SourceLocale>, DialogInterface.OnClickListener {
private val viewModel by viewModel<OnboardViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<OnboardViewModel>()
private var isWelcome: Boolean = false private var isWelcome: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.databinding.ActivitySetupProtectBinding
class ProtectSetupActivity : BaseActivity<ActivitySetupProtectBinding>(), TextWatcher, class ProtectSetupActivity : BaseActivity<ActivitySetupProtectBinding>(), TextWatcher,
View.OnClickListener, TextView.OnEditorActionListener { View.OnClickListener, TextView.OnEditorActionListener {
private val viewModel by viewModel<ProtectSetupViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<ProtectSetupViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -26,7 +26,7 @@ import org.koitharu.kotatsu.utils.progress.Progress
class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListener.Callback, class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListener.Callback,
OnListItemClickListener<Manga> { OnListItemClickListener<Manga> {
private val viewModel by viewModel<FeedViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<FeedViewModel>()
private var feedAdapter: FeedAdapter? = null private var feedAdapter: FeedAdapter? = null
private var updateStatusSnackbar: Snackbar? = null private var updateStatusSnackbar: Snackbar? = null

View File

@@ -0,0 +1,87 @@
package org.koitharu.kotatsu.utils
import android.app.DownloadManager
import android.app.DownloadManager.Request.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.ext.toFileNameSafe
import java.io.File
import kotlin.coroutines.resume
class DownloadManagerHelper(
private val context: Context,
private val cookieJar: CookieJar,
) {
private val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
private val subDir = context.getString(R.string.app_name).toFileNameSafe()
fun downloadPage(page: MangaPage, fullUrl: String): Long {
val uri = fullUrl.toUri()
val cookies = cookieJar.loadForRequest(fullUrl.toHttpUrl())
val dest = subDir + File.separator + uri.lastPathSegment
val request = DownloadManager.Request(uri)
.addRequestHeader(CommonHeaders.REFERER, page.referer)
.addRequestHeader(CommonHeaders.COOKIE, cookieHeader(cookies))
.setAllowedOverMetered(true)
.setAllowedNetworkTypes(NETWORK_WIFI or NETWORK_MOBILE)
.setNotificationVisibility(VISIBILITY_VISIBLE)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, dest)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
request.allowScanningByMediaScanner()
}
return manager.enqueue(request)
}
suspend fun awaitDownload(id: Long): Uri {
getUriForDownloadedFile(id)?.let { return it } // fast path
suspendCancellableCoroutine<Unit> { cont ->
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (
intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE &&
intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L) == id
) {
context.unregisterReceiver(this)
cont.resume(Unit)
}
}
}
context.registerReceiver(
receiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
cont.invokeOnCancellation {
context.unregisterReceiver(receiver)
}
}
return checkNotNull(getUriForDownloadedFile(id))
}
private suspend fun getUriForDownloadedFile(id: Long) = withContext(Dispatchers.IO) {
manager.getUriForDownloadedFile(id)
}
private fun cookieHeader(cookies: List<Cookie>): String = buildString {
cookies.forEachIndexed { index, cookie ->
if (index > 0) append("; ")
append(cookie.name).append('=').append(cookie.value)
}
}
}

View File

@@ -1,66 +0,0 @@
package org.koitharu.kotatsu.utils
import android.content.ContentResolver
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.core.database.getStringOrNull
import org.koitharu.kotatsu.BuildConfig
import java.io.OutputStream
class MediaStoreCompat(private val contentResolver: ContentResolver) {
fun insertImage(
fileName: String,
block: (OutputStream) -> Unit
): Uri? {
val name = fileName.substringBeforeLast('.')
val cv = ContentValues(7)
cv.put(MediaStore.Images.Media.DISPLAY_NAME, name)
cv.put(MediaStore.Images.Media.TITLE, name)
cv.put(
MediaStore.Images.Media.MIME_TYPE,
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName.substringAfterLast('.'))
)
cv.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1_000)
cv.put(MediaStore.Images.Media.DATE_MODIFIED, System.currentTimeMillis())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
cv.put(MediaStore.Images.Media.IS_PENDING, 1)
}
var uri: Uri? = null
try {
uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv)
contentResolver.openOutputStream(uri!!)?.use(block)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
cv.clear()
cv.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(uri, cv, null, null)
}
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
uri?.let {
contentResolver.delete(it, null, null)
}
uri = null
}
return uri
}
fun getName(uri: Uri): String? =
(if (uri.scheme == "content") {
contentResolver.query(uri, null, null, null, null)?.use {
if (it.moveToFirst()) {
it.getStringOrNull(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
} else {
null
}
}
} else {
null
}) ?: uri.path?.substringAfterLast('/')
}

View File

@@ -1,10 +1,13 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.storage.StorageManager import android.os.storage.StorageManager
import android.provider.OpenableColumns
import androidx.core.database.getStringOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -60,4 +63,19 @@ fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null
suspend fun File.deleteAwait() = withContext(Dispatchers.IO) { suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
delete() delete()
}
fun ContentResolver.resolveName(uri: Uri): String? {
val fallback = uri.lastPathSegment
if (uri.scheme != "content") {
return fallback
}
query(uri, null, null, null, null)?.use {
if (it.moveToFirst()) {
it.getStringOrNull(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))?.let { name ->
return name
}
}
}
return fallback
} }

View File

@@ -44,6 +44,8 @@ fun JSONObject.getLongOrDefault(name: String, defaultValue: Long): Long = opt(na
operator fun JSONArray.iterator(): Iterator<JSONObject> = JSONIterator(this) operator fun JSONArray.iterator(): Iterator<JSONObject> = JSONIterator(this)
fun JSONArray.stringIterator(): Iterator<String> = JSONStringIterator(this)
private class JSONIterator(private val array: JSONArray) : Iterator<JSONObject> { private class JSONIterator(private val array: JSONArray) : Iterator<JSONObject> {
private val total = array.length() private val total = array.length()
@@ -52,7 +54,16 @@ private class JSONIterator(private val array: JSONArray) : Iterator<JSONObject>
override fun hasNext() = index < total - 1 override fun hasNext() = index < total - 1
override fun next(): JSONObject = array.getJSONObject(index++) override fun next(): JSONObject = array.getJSONObject(index++)
}
private class JSONStringIterator(private val array: JSONArray) : Iterator<String> {
private val total = array.length()
private var index = 0
override fun hasNext() = index < total - 1
override fun next(): String = array.getString(index++)
} }
fun <T> JSONArray.mapToSet(block: (JSONObject) -> T): Set<T> { fun <T> JSONArray.mapToSet(block: (JSONObject) -> T): Set<T> {

View File

@@ -122,7 +122,7 @@ fun ByteArray.byte2HexFormatted(): String {
if (l > 2) { if (l > 2) {
h = h.substring(l - 2, l) h = h.substring(l - 2, l)
} }
str.append(h.toUpperCase(Locale.ROOT)) str.append(h.uppercase(Locale.ROOT))
if (i < size - 1) { if (i < size - 1) {
str.append(':') str.append(':')
} }

View File

@@ -27,9 +27,10 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
class ShelfConfigActivity : BaseActivity<ActivityCategoriesBinding>(), OnListItemClickListener<CategoryItem> { class ShelfConfigActivity : BaseActivity<ActivityCategoriesBinding>(),
OnListItemClickListener<CategoryItem> {
private val viewModel by viewModel<ShelfConfigViewModel>(mode = LazyThreadSafetyMode.NONE) private val viewModel by viewModel<ShelfConfigViewModel>()
private lateinit var adapter: CategorySelectAdapter private lateinit var adapter: CategorySelectAdapter
private lateinit var config: AppWidgetConfig private lateinit var config: AppWidgetConfig

View File

@@ -11,7 +11,10 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputLayout" android:id="@+id/inputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" app:boxBackgroundMode="filled"
app:boxBackgroundColor="@android:color/transparent"
app:hintEnabled="false"
app:expandedHintEnabled="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@@ -21,7 +24,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:singleLine="true" android:singleLine="true"
tools:text="@tools:sample/lorem[2]" /> tools:hint="@tools:sample/lorem[2]" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View File

@@ -11,15 +11,15 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_margin="8dp" android:layout_margin="8dp"
app:cardCornerRadius="4dp" app:cardCornerRadius="4dp"
app:cardElevation="4dp"> app:cardElevation="4dp">
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView <org.koitharu.kotatsu.base.ui.widgets.CoverImageView
android:id="@+id/imageView_cover" android:id="@+id/imageView_cover"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />

View File

@@ -10,7 +10,7 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_margin="8dp" android:layout_margin="8dp"
app:cardElevation="4dp"> app:cardElevation="4dp">

View File

@@ -242,4 +242,6 @@
<string name="genres">Жанры</string> <string name="genres">Жанры</string>
<string name="state_finished">Завершана</string> <string name="state_finished">Завершана</string>
<string name="state_ongoing">Ангоінг</string> <string name="state_ongoing">Ангоінг</string>
<string name="date_format">Фармат даты</string>
<string name="system_default">Па змаўчанні</string>
</resources> </resources>

View File

@@ -242,4 +242,6 @@
<string name="auth_complete">Autorisierung abgeschlossen</string> <string name="auth_complete">Autorisierung abgeschlossen</string>
<string name="state_finished">Beendet</string> <string name="state_finished">Beendet</string>
<string name="state_ongoing">Fortlaufend</string> <string name="state_ongoing">Fortlaufend</string>
<string name="date_format">Datumsformat</string>
<string name="system_default">Standard</string>
</resources> </resources>

View File

@@ -239,4 +239,6 @@
<string name="chapter_is_missing_text">Este capítulo no aparece en su dispositivo. Descárguelo o léalo en línea.</string> <string name="chapter_is_missing_text">Este capítulo no aparece en su dispositivo. Descárguelo o léalo en línea.</string>
<string name="auth_complete">Autorización completa</string> <string name="auth_complete">Autorización completa</string>
<string name="about_feedback_4pda">Tema sobre 4PDA</string> <string name="about_feedback_4pda">Tema sobre 4PDA</string>
<string name="date_format">Formato de la fecha</string>
<string name="system_default">Por defecto</string>
</resources> </resources>

View File

@@ -242,4 +242,6 @@
<string name="enter_password">Syötä salasana</string> <string name="enter_password">Syötä salasana</string>
<string name="state_finished">Päättynyt</string> <string name="state_finished">Päättynyt</string>
<string name="state_ongoing">Jatkuva</string> <string name="state_ongoing">Jatkuva</string>
<string name="date_format">Päivämäärän muoto</string>
<string name="system_default">Oletus</string>
</resources> </resources>

View File

@@ -242,4 +242,6 @@
<string name="auth_complete">Autorisation complète</string> <string name="auth_complete">Autorisation complète</string>
<string name="state_finished">Terminé</string> <string name="state_finished">Terminé</string>
<string name="state_ongoing">En cours</string> <string name="state_ongoing">En cours</string>
<string name="date_format">Format de la date</string>
<string name="system_default">Par défaut</string>
</resources> </resources>

View File

@@ -242,4 +242,6 @@
<string name="auth_complete">Autorizzazione completa</string> <string name="auth_complete">Autorizzazione completa</string>
<string name="state_finished">Finito</string> <string name="state_finished">Finito</string>
<string name="state_ongoing">In corso</string> <string name="state_ongoing">In corso</string>
<string name="system_default">Predefinito</string>
<string name="date_format">Formato della data</string>
</resources> </resources>

View File

@@ -243,4 +243,6 @@
<string name="state_ongoing">Онгоинг</string> <string name="state_ongoing">Онгоинг</string>
<string name="date_format">Формат даты</string> <string name="date_format">Формат даты</string>
<string name="system_default">По умолчанию</string> <string name="system_default">По умолчанию</string>
<string name="exclude_nsfw_from_history">Исключить NSFW мангу из истории</string>
<string name="error_empty_name">Имя не может быть пустым</string>
</resources> </resources>

View File

@@ -244,4 +244,6 @@
<string name="state_ongoing">Ongoing</string> <string name="state_ongoing">Ongoing</string>
<string name="date_format">Date format</string> <string name="date_format">Date format</string>
<string name="system_default">Default</string> <string name="system_default">Default</string>
<string name="exclude_nsfw_from_history">Exclude NSFW manga from history</string>
<string name="error_empty_name">Name sould not be empty</string>
</resources> </resources>

View File

@@ -15,6 +15,11 @@
android:title="@string/clear_updates_feed" android:title="@string/clear_updates_feed"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="history_exclude_nsfw"
android:title="@string/exclude_nsfw_from_history"
app:iconSpaceReserved="false" />
<PreferenceCategory <PreferenceCategory
android:title="@string/cache" android:title="@string/cache"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">

Some files were not shown because too many files have changed in this diff Show More