Compare commits

...

17 Commits
v0.1 ... v0.1.2

Author SHA1 Message Date
Koitharu
7ee486e4f2 Fix version code 2020-03-29 10:58:24 +03:00
Koitharu
1314c601b2 Simplify settings 2020-03-28 19:40:43 +02:00
Koitharu
c5970c5606 Confirm large manga downloading 2020-03-28 19:01:50 +02:00
Koitharu
85b18d118b Store page scroll position in history 2020-03-28 18:49:01 +02:00
Koitharu
e7a150bd9a Replace history with remote manga when delete local 2020-03-28 12:46:57 +02:00
Koitharu
2c66edda68 Update database schema: foreign keys and indices 2020-03-28 12:28:03 +02:00
Koitharu
1a93cc228d Update dependencies 2020-03-23 18:14:27 +02:00
Koitharu
798ae6aeb7 Fix pages max scale 2020-03-22 17:35:42 +02:00
Koitharu
418d0247f5 Option to hide manga source 2020-03-19 16:52:16 +02:00
Koitharu
db0ee268f9 Browser activity 2020-03-19 14:31:25 +02:00
Koitharu
032d671c38 Add Chucker for debug 2020-03-19 13:39:19 +02:00
Koitharu
127978d3d7 Decrease app update checking interval 2020-03-19 12:53:14 +02:00
Koitharu
fddc3e41cf Remove shortcut if local manga removed 2020-03-19 12:47:36 +02:00
Koitharu
e0e6f0dab4 DesuMe parser 2020-03-18 20:33:57 +02:00
Koitharu
beaa825a9f Add UserAgent 2020-03-18 19:22:10 +02:00
Koitharu
cae27dda05 Fix all parsers issues 2020-03-18 19:12:27 +02:00
Koitharu
0d041e9a0a Fix appending saved chapters 2020-03-17 18:15:15 +02:00
95 changed files with 1363 additions and 480 deletions

120
.idea/codeStyles generated
View File

@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleConfiguration">
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
</project>

186
.idea/codeStyles/Project.xml generated Executable file
View File

@@ -0,0 +1,186 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="USE_TAB_CHARACTER" value="true" />
</value>
</option>
<AndroidXmlCodeStyleSettings>
<option name="LAYOUT_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
</value>
</option>
<option name="MANIFEST_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
</value>
</option>
<option name="OTHER_SETTINGS">
<value>
<option name="INSERT_LINE_BREAK_BEFORE_NAMESPACE_DECLARATION" value="true" />
</value>
</option>
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Groovy">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Shell Script">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Executable file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,6 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="admin">
<words>
<w>chucker</w>
<w>desu</w>
<w>koin</w>
<w>kotatsu</w>
<w>manga</w>

View File

@@ -21,5 +21,15 @@
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://dl.bintray.com/kotlin/kotlin-eap" />
</remote-repository>
</component>
</project>

View File

@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def gitCommits = 'git rev-list --all --count'.execute([], rootDir).text.trim().toInteger()
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
def gitBranch = 'git branch --show-current'.execute([], rootDir).text.trim()
android {
@@ -15,7 +15,7 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode gitCommits
versionName '0.1'
versionName '0.1.2'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
@@ -62,7 +62,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'androidx.core:core-ktx:1.3.0-alpha02'
implementation 'androidx.fragment:fragment-ktx:1.2.2'
implementation 'androidx.fragment:fragment-ktx:1.2.3'
implementation 'androidx.appcompat:appcompat:1.2.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
@@ -70,26 +70,29 @@ dependencies {
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha05'
implementation 'androidx.room:room-runtime:2.2.4'
implementation 'androidx.room:room-ktx:2.2.4'
kapt 'androidx.room:room-compiler:2.2.4'
implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
implementation 'com.github.moxy-community:moxy:2.1.1'
implementation 'com.github.moxy-community:moxy-androidx:2.1.1'
implementation 'com.github.moxy-community:moxy-material:2.1.1'
implementation 'com.github.moxy-community:moxy-ktx:2.1.1'
kapt 'com.github.moxy-community:moxy-compiler:2.1.1'
implementation 'com.github.moxy-community:moxy:2.1.2'
implementation 'com.github.moxy-community:moxy-androidx:2.1.2'
implementation 'com.github.moxy-community:moxy-material:2.1.2'
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.4.0'
implementation 'com.squareup.okio:okio:2.4.3'
implementation 'org.jsoup:jsoup:1.12.2'
implementation 'com.squareup.okhttp3:okhttp:4.4.1'
implementation 'com.squareup.okio:okio:2.5.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.koin:koin-android:2.1.3'
implementation 'org.koin:koin-android:2.1.4'
implementation 'io.coil-kt:coil:0.9.5'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
debugImplementation 'com.github.ChuckerTeam.Chucker:library:3.1.2'
releaseImplementation 'com.github.ChuckerTeam.Chucker:library-no-op:3.1.2'
testImplementation 'junit:junit:4.13'
testImplementation 'org.json:json:20190722'
}

1
app/libs/.gitkeep Normal file
View File

@@ -0,0 +1 @@

Binary file not shown.

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.koitharu.kotatsu">
<uses-permission android:name="android.permission.INTERNET" />
@@ -18,7 +19,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<activity android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -43,6 +45,9 @@
<activity
android:name=".ui.reader.SimpleSettingsActivity"
android:label="@string/settings" />
<activity
android:name=".ui.browser.BrowserActivity"
android:launchMode="singleInstance" />
<service
android:name=".ui.download.DownloadService"
@@ -51,7 +56,9 @@
<provider
android:name=".ui.search.MangaSuggestionsProvider"
android:authorities="${applicationId}.MangaSuggestionsProvider" />
android:authorities="${applicationId}.MangaSuggestionsProvider"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.files"

View File

@@ -6,18 +6,22 @@ import androidx.room.Room
import coil.Coil
import coil.ImageLoader
import coil.util.CoilUtils
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.CacheUtils
@@ -29,10 +33,17 @@ class KotatsuApp : Application() {
PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(applicationContext))
}
private val chuckerCollector by lazy(LazyThreadSafetyMode.NONE) {
ChuckerCollector(applicationContext)
}
override fun onCreate() {
super.onCreate()
initKoin()
initCoil()
if (BuildConfig.DEBUG) {
initErrorHandler()
}
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
}
@@ -77,13 +88,22 @@ class KotatsuApp : Application() {
})
}
private fun initErrorHandler() {
val exceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
chuckerCollector.onError("CRASH", e)
exceptionHandler?.uncaughtException(t, e)
}
}
private fun okHttp() = OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
addInterceptor(UserAgentInterceptor)
if (BuildConfig.DEBUG) {
addInterceptor(OkHttpProfilerInterceptor())
addInterceptor(ChuckerInterceptor(applicationContext, collector = chuckerCollector))
}
}
@@ -91,5 +111,5 @@ class KotatsuApp : Application() {
applicationContext,
MangaDatabase::class.java,
"kotatsu-db"
)
).addMigrations(Migration1To2, Migration2To3)
}

View File

@@ -24,13 +24,13 @@ abstract class HistoryDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: HistoryEntity): Long
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, updated_at = :updatedAt WHERE manga_id = :mangaId")
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, updatedAt: Long): Int
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, updated_at = :updatedAt WHERE manga_id = :mangaId")
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, scroll: Float, updatedAt: Long): Int
@Query("DELETE FROM history WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.updatedAt)
suspend fun update(entity: HistoryEntity) = update(entity.mangaId, entity.page, entity.chapterId, entity.scroll, entity.updatedAt)
@Transaction
open suspend fun upsert(entity: HistoryEntity) {

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.*
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class
], version = 1
], version = 3
)
abstract class MangaDatabase : RoomDatabase() {

View File

@@ -2,10 +2,26 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(tableName = "favourites", primaryKeys = ["manga_id", "category_id"])
@Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = FavouriteCategoryEntity::class,
parentColumns = ["category_id"],
childColumns = ["category_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class FavouriteEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "category_id") val categoryId: Long,
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long
)

View File

@@ -2,24 +2,36 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.MangaHistory
import java.util.*
@Entity(tableName = "history")
@Entity(
tableName = "history", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class HistoryEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int,
@ColumnInfo(name = "scroll") val scroll: Float
) {
fun toMangaHistory() = MangaHistory(
createdAt = Date(createdAt),
updatedAt = Date(updatedAt),
chapterId = chapterId,
page = page
)
fun toMangaHistory() = MangaHistory(
createdAt = Date(createdAt),
updatedAt = Date(updatedAt),
chapterId = chapterId,
page = page,
scroll = scroll
)
}

View File

@@ -2,9 +2,18 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(tableName = "preferences")
@Entity(
tableName = "preferences", foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)]
)
data class MangaPrefsEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,

View File

@@ -2,9 +2,25 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"])
@Entity(
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = TagEntity::class,
parentColumns = ["tag_id"],
childColumns = ["tag_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class MangaTagsEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "tag_id") val tagId: Long
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
)

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration1To2 : Migration(1, 2) {
/**
* Adding foreign keys
*/
override fun migrate(database: SupportSQLiteDatabase) {
/* manga_tags */
database.execSQL(
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id, tag_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
database.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
database.execSQL("DROP TABLE manga_tags")
database.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
/* favourites */
database.execSQL(
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id, category_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
database.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
database.execSQL("DROP TABLE favourites")
database.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
/* history */
database.execSQL(
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
database.execSQL("DROP TABLE history")
database.execSQL("ALTER TABLE history_tmp RENAME TO history")
/* preferences */
database.execSQL(
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
" PRIMARY KEY(manga_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
database.execSQL("DROP TABLE preferences")
database.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration2To3 : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
}
}

View File

@@ -15,6 +15,7 @@ import java.util.zip.ZipFile
class CbzFetcher : Fetcher<Uri> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch(
pool: BitmapPool,
data: Uri,

View File

@@ -37,7 +37,7 @@ class SetCookieCache : CookieCache {
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
private inner class SetCookieCacheIterator internal constructor() : MutableIterator<Cookie> {
private inner class SetCookieCacheIterator() : MutableIterator<Cookie> {
private val iterator = cookies.iterator()

View File

@@ -41,27 +41,24 @@ class SharedPrefsCookiePersistor(private val sharedPreferences: SharedPreference
return cookies
}
@SuppressLint("ApplySharedPref")
override fun saveAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit()
for (cookie in cookies) {
editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie))
}
editor.commit()
editor.apply()
}
@SuppressLint("ApplySharedPref")
override fun removeAll(cookies: Collection<Cookie>) {
val editor = sharedPreferences.edit()
for (cookie in cookies) {
editor.remove(createCookieKey(cookie))
}
editor.commit()
editor.apply()
}
@SuppressLint("ApplySharedPref")
override fun clear() {
sharedPreferences.edit().clear().commit()
sharedPreferences.edit().clear().apply()
}
private companion object {

View File

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

View File

@@ -18,6 +18,7 @@ enum class MangaSource(
MINTMANGA("MintManga", "ru", MintMangaRepository::class.java),
SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java),
MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java),
DESUME("Desu.me", "ru", DesuMeRepository::class.java),
HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java),
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java)
}

View File

@@ -38,6 +38,7 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
getFromFile(Uri.parse(manga.url).toFile())
} else manga
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val file = Uri.parse(chapter.url).toFile()
val zip = ZipFile(file)
@@ -104,6 +105,16 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
}
}
fun getRemoteManga(localManga: Manga): Manga? {
val file = safe {
Uri.parse(localManga.url).toFile()
} ?: return null
val zip = ZipFile(file)
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null
return index.getMangaInfo()
}
private fun zipUri(file: File, entryName: String) =
Uri.fromParts("cbz", file.path, entryName).toString()

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint
import android.os.Build
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import java.util.*
@SuppressLint("ConstantLocale")
object UserAgentInterceptor : Interceptor {
private val userAgent = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language
)
override fun intercept(chain: Interceptor.Chain) = chain.proceed(
chain.request().newBuilder()
.header("User-Agent", userAgent)
.build()
)
}

View File

@@ -93,7 +93,7 @@ abstract class ChanRepository : RemoteMangaRepository() {
val json = data.substring(pos).substringAfter('[').substringBefore(';')
.substringBeforeLast(']')
return json.split(",").mapNotNull {
it.trim().removeSurrounding('"').takeUnless(String::isBlank)
it.trim().removeSurrounding('"','\'').takeUnless(String::isBlank)
}.map { url ->
MangaPage(
id = url.longHashCode(),

View File

@@ -0,0 +1,137 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.utils.ext.*
class DesuMeRepository : RemoteMangaRepository() {
override val source = MangaSource.DESUME
override val sortOrders = setOf(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL
)
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> {
val domain = conf.getDomain(DOMAIN)
val url = buildString {
append("https://")
append(domain)
append("/manga/api/?limit=20&order=")
append(getSortKey(sortOrder))
append("&page=")
append((offset / 20) + 1)
if (tag != null) {
append("&genres=")
append(tag.key)
}
if (query != null) {
append("&search=")
append(query)
}
}
val json = loaderContext.httpGet(url).parseJson().getJSONArray("response")
?: throw ParseException("Invalid response")
val total = json.length()
val list = ArrayList<Manga>(total)
for (i in 0 until total) {
val jo = json.getJSONObject(i)
val cover = jo.getJSONObject("image")
list += Manga(
url = jo.getString("url"),
source = MangaSource.DESUME,
title = jo.getString("russian"),
altTitle = jo.getString("name"),
coverUrl = cover.getString("preview"),
largeCoverUrl = cover.getString("original"),
state = when {
jo.getInt("ongoing") == 1 -> MangaState.ONGOING
else -> null
},
rating = jo.getDouble("score").toFloat().coerceIn(0f, 1f),
id = ID_MASK + jo.getLong("id"),
description = jo.getString("description")
)
}
return list
}
override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(DOMAIN)
val url = "https://$domain/manga/api/${manga.id - ID_MASK}"
val json = loaderContext.httpGet(url).parseJson().getJSONObject("response")
?: throw ParseException("Invalid response")
return manga.copy(
tags = json.getJSONArray("genres").map {
MangaTag(
key = it.getString("text"),
title = it.getString("russian"),
source = manga.source
)
}.toSet(),
description = json.getString("description"),
chapters = json.getJSONObject("chapters").getJSONArray("list").mapIndexed { i, it ->
val chid = it.getLong("id")
MangaChapter(
id = ID_MASK + chid,
source = manga.source,
url = "$url/chapter/$chid",
name = it.optString("title", "${manga.title} #${it.getDouble("ch")}"),
number = i + 1
)
}
)
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val json = loaderContext.httpGet(chapter.url).parseJson().getJSONObject("response")
?: throw ParseException("Invalid response")
return json.getJSONObject("pages").getJSONArray("list").map {
MangaPage(
id = it.getLong("id"),
source = chapter.source,
url = it.getString("img")
)
}
}
override suspend fun getTags(): Set<MangaTag> {
val domain = conf.getDomain(DOMAIN)
val doc = loaderContext.httpGet("https://$domain/manga/").parseHtml()
val root = doc.body().getElementById("animeFilter").selectFirst(".catalog-genres")
return root.select("li").map {
MangaTag(
source = source,
key = it.selectFirst("input").attr("data-genre"),
title = it.selectFirst("label").text()
)
}.toSet()
}
override fun onCreatePreferences() = setOf(R.string.key_parser_domain)
private fun getSortKey(sortOrder: SortOrder?) =
when (sortOrder) {
SortOrder.ALPHABETICAL -> "name"
SortOrder.POPULARITY -> "popular"
SortOrder.UPDATED -> "updated"
SortOrder.NEWEST -> "id"
else -> "updated"
}
private companion object {
private const val ID_MASK = 1000
private const val DOMAIN = "desu.me"
}
}

View File

@@ -4,6 +4,7 @@ import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml
import org.koitharu.kotatsu.utils.ext.withDomain
@@ -18,22 +19,27 @@ class HenChanRepository : ChanRepository() {
val doc = loaderContext.httpGet(manga.url).parseHtml()
val root =
doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root")
val readLink = manga.url.replace("manga", "online")
return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.attr("src")?.withDomain(domain),
chapters = root.getElementById("right").select("table.table_cha").flatMap { table ->
table.select("div.manga2")
}.mapNotNull { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a ->
val href = a.attr("href")
?.withDomain(domain) ?: return@mapIndexedNotNull null
MangaChapter(
id = href.longHashCode(),
name = a.text().trim(),
number = i + 1,
url = href,
tags = root.selectFirst("div.sidetags")?.select("li.sidetag")?.map {
val a = it.children().last()
MangaTag(
title = a.text(),
key = a.attr("href").substringAfterLast('/'),
source = source
)
}
}?.toSet() ?: manga.tags,
chapters = listOf(
MangaChapter(
id = readLink.longHashCode(),
url = readLink,
source = source,
number = 1,
name = manga.title
)
)
)
}
}

View File

@@ -1,9 +1,39 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml
import org.koitharu.kotatsu.utils.ext.withDomain
class YaoiChanRepository : ChanRepository() {
override val source = MangaSource.YAOICHAN
override val defaultDomain = "yaoi-chan.me"
override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(defaultDomain)
val doc = loaderContext.httpGet(manga.url).parseHtml()
val root =
doc.body().getElementById("dle-content") ?: throw ParseException("Cannot find root")
return manga.copy(
description = root.getElementById("description")?.html()?.substringBeforeLast("<div"),
largeCoverUrl = root.getElementById("cover")?.attr("src")?.withDomain(domain),
chapters = root.select("table.table_cha").flatMap { table ->
table.select("div.manga")
}.mapNotNull { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a ->
val href = a.attr("href")
?.withDomain(domain) ?: return@mapIndexedNotNull null
MangaChapter(
id = href.longHashCode(),
name = a.text().trim(),
number = i + 1,
url = href,
source = source
)
}
)
}
}

View File

@@ -60,6 +60,8 @@ class AppSettings private constructor(resources: Resources, private val prefs: S
sourcesOrderStr = value.joinToString("|")
}
var hiddenSources by StringSetPreferenceDelegate(resources.getString(R.string.key_sources_hidden))
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}

View File

@@ -9,15 +9,23 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
object MangaProviderFactory : KoinComponent {
val sources: List<MangaSource>
get() {
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = get<AppSettings>().sourcesOrder
return list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
fun getSources(includeHidden: Boolean): List<MangaSource> {
val settings = get<AppSettings>()
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = settings.sourcesOrder
val hidden = settings.hiddenSources
val sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if(includeHidden) {
sorted
} else {
sorted.filterNot { x ->
x.name in hidden
}
}
}
fun createLocal() = LocalMangaRepository()

View File

@@ -21,7 +21,7 @@ class HistoryRepository : KoinComponent {
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
}
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int) {
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Float) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.tagsDao().upsert(tags)
db.mangaDao().upsert(MangaEntity.from(manga), tags)
@@ -31,7 +31,8 @@ class HistoryRepository : KoinComponent {
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
chapterId = chapterId,
page = page
page = page,
scroll = scroll
)
)
notifyHistoryChanged()
@@ -43,7 +44,8 @@ class HistoryRepository : KoinComponent {
createdAt = Date(it.createdAt),
updatedAt = Date(it.updatedAt),
chapterId = it.chapterId,
page = it.page
page = it.page,
scroll = it.scroll
)
}
}
@@ -58,6 +60,17 @@ class HistoryRepository : KoinComponent {
notifyHistoryChanged()
}
/**
* Try to replace one manga with another one
* Useful for replacing saved manga on deleting it with remove source
*/
suspend fun deleteOrSwap(manga: Manga, alternative: Manga?) {
if (alternative == null || db.mangaDao().update(MangaEntity.from(alternative)) <= 0) {
db.historyDao().delete(manga.id)
notifyHistoryChanged()
}
}
companion object {
private val listeners = HashSet<OnHistoryChangeListener>()

View File

@@ -14,7 +14,7 @@ class MangaIndex(source: String?) {
private val json: JSONObject = source?.let(::JSONObject) ?: JSONObject()
fun setMangaInfo(manga: Manga) {
fun setMangaInfo(manga: Manga, append: Boolean) {
json.put("id", manga.id)
json.put("title", manga.title)
json.put("title_alt", manga.altTitle)
@@ -32,7 +32,9 @@ class MangaIndex(source: String?) {
a.put(jo)
}
})
json.put("chapters", JSONObject())
if (!append || !json.has("chapters")) {
json.put("chapters", JSONObject())
}
json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE)
}

View File

@@ -17,11 +17,12 @@ class MangaZip(val file: File) {
private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() }
?: throw RuntimeException("Cannot create temporary directory")
private val index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText())
private var index = MangaIndex(null)
fun prepare(manga: Manga) {
extract()
index.setMangaInfo(manga)
index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText())
index.setMangaInfo(manga, append = true)
}
fun cleanup() {

View File

@@ -0,0 +1,94 @@
package org.koitharu.kotatsu.ui.browser
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.activity_browser.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity(), BrowserCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_browser)
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_cross)
}
with(webView.settings) {
javaScriptEnabled = true
}
webView.webViewClient = BrowserClient(this)
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finish()
} else {
webView.loadUrl(url)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_browser, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
webView.stopLoading()
finish()
true
}
R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(webView.url)
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
}
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onPause() {
webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
webView.onResume()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
progressBar.isVisible = isLoading
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
this.title = title
supportActionBar?.subtitle = subtitle
}
companion object {
@JvmStatic
fun newIntent(context: Context, url: String) = Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url))
}
}

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.ui.browser
interface BrowserCallback {
fun onLoadingStateChanged(isLoading: Boolean)
fun onTitleChanged(title: CharSequence, subtitle: CharSequence?)
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.ui.browser
import android.graphics.Bitmap
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.utils.ext.safe
class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), KoinComponent {
private val okHttp by inject<OkHttpClient>()
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
callback.onLoadingStateChanged(isLoading = false)
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
callback.onLoadingStateChanged(isLoading = true)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title, url)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false
override fun shouldOverrideUrlLoading(view: WebView, url: String) = false
override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? {
return url?.let(::doRequest)
}
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
return request?.url?.toString()?.let(::doRequest)
}
private fun doRequest(url: String): WebResourceResponse? = safe {
val request = Request.Builder()
.url(url)
.build()
val response = okHttp.newCall(request).execute()
val ct = response.body?.contentType()
WebResourceResponse(
"${ct?.type}/${ct?.subtype}",
ct?.charset()?.name() ?: "utf-8",
response.body?.byteStream()
)
}
}

View File

@@ -0,0 +1,67 @@
package org.koitharu.kotatsu.ui.common.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.Checkable
import androidx.appcompat.widget.AppCompatImageView
class CheckableImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr), Checkable {
private var isCheckedInternal = false
private var isBroadcasting = false
var onCheckedChangeListener: OnCheckedChangeListener? = null
init {
setOnClickListener {
toggle()
}
}
fun setOnCheckedChangeListener(listener: (Boolean) -> Unit) {
onCheckedChangeListener = object : OnCheckedChangeListener {
override fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean) {
listener(isChecked)
}
}
}
override fun isChecked() = isCheckedInternal
override fun toggle() {
isChecked = !isCheckedInternal
}
override fun setChecked(checked: Boolean) {
if (checked != isCheckedInternal) {
isCheckedInternal = checked
refreshDrawableState()
if (!isBroadcasting) {
isBroadcasting = true
onCheckedChangeListener?.onCheckedChanged(this, checked)
isBroadcasting = false
}
}
}
override fun onCreateDrawableState(extraSpace: Int): IntArray {
val state = super.onCreateDrawableState(extraSpace + 1)
if (isCheckedInternal) {
mergeDrawableStates(state, CHECKED_STATE_SET)
}
return state
}
interface OnCheckedChangeListener {
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)
}
private companion object {
@JvmStatic
private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
}
}

View File

@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope
@@ -21,6 +22,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.browser.BrowserActivity
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.download.DownloadService
import org.koitharu.kotatsu.utils.ShareHelper
@@ -119,7 +121,24 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
}
R.id.action_save -> {
manga?.let {
DownloadService.start(this, it)
val chaptersCount = it.chapters?.size ?: 0
if (chaptersCount > 5) {
AlertDialog.Builder(this)
.setTitle(R.string.save_manga)
.setMessage(getString(R.string.large_manga_save_confirm, chaptersCount))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(this, it)
}.show()
} else {
DownloadService.start(this, it)
}
}
true
}
R.id.action_browser -> {
manga?.let {
startActivity(BrowserActivity.newIntent(this, it.url))
}
true
}

View File

@@ -94,9 +94,10 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
withContext(Dispatchers.IO) {
val repository =
MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
safe {
HistoryRepository().delete(manga)
HistoryRepository().deleteOrSwap(manga, original)
}
}
viewState.onMangaRemoved(manga)

View File

@@ -3,7 +3,10 @@ package org.koitharu.kotatsu.ui.download
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.os.PowerManager
import android.os.WorkSource
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.ContextCompat
import coil.Coil
import coil.api.get
@@ -27,11 +30,13 @@ import org.koitharu.kotatsu.utils.ext.retryUntilSuccess
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
class DownloadService : BaseService() {
private lateinit var notification: DownloadNotification
private lateinit var wakeLock: PowerManager.WakeLock
private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>()
@@ -41,6 +46,8 @@ class DownloadService : BaseService() {
override fun onCreate() {
super.onCreate()
notification = DownloadNotification(this)
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -50,6 +57,7 @@ class DownloadService : BaseService() {
val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet()
if (manga != null) {
jobs[startId] = downloadManga(manga, chapters, startId)
Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
} else {
stopSelf(startId)
}
@@ -67,6 +75,7 @@ class DownloadService : BaseService() {
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
return launch(Dispatchers.IO) {
mutex.lock()
wakeLock.acquire(TimeUnit.MINUTES.toMillis(20))
withContext(Dispatchers.Main) {
notification.fillFrom(manga)
notification.setCancelId(startId)
@@ -154,6 +163,7 @@ class DownloadService : BaseService() {
notification.dismiss()
stopSelf(startId)
}
wakeLock.release()
mutex.unlock()
}
}

View File

@@ -28,8 +28,8 @@ import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.settings.AppUpdateService
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.resolveDp
@@ -66,7 +66,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setCheckedItem(R.id.nav_history)
setPrimaryFragment(HistoryListFragment.newInstance())
}
drawer.postDelayed(4000) {
drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext)
}
}
@@ -79,7 +79,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
drawerToggle.syncState()
initSideMenu(MangaProviderFactory.sources)
initSideMenu(MangaProviderFactory.getSources(includeHidden = false))
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -148,7 +148,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
getString(R.string.key_sources_order) -> initSideMenu(MangaProviderFactory.sources)
getString(R.string.key_sources_hidden),
getString(R.string.key_sources_order) -> {
initSideMenu(MangaProviderFactory.getSources(includeHidden = false))
}
}
}

View File

@@ -26,7 +26,7 @@ class MainPresenter : BasePresenter<MainView>() {
val history = repo.getOne(manga) ?: throw EmptyHistoryException()
ReaderState(
MangaProviderFactory.create(manga.source).getDetails(manga),
history.chapterId, history.page
history.chapterId, history.page, history.scroll
)
}
viewState.onOpenReader(state)

View File

@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_categories),
class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories),
FavouriteCategoriesView,
OnCategoryCheckListener {

View File

@@ -2,12 +2,14 @@ package org.koitharu.kotatsu.ui.main.list.local
import android.content.Context
import android.net.Uri
import android.os.Build
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import moxy.presenterScope
import org.koin.core.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.model.Manga
@@ -18,6 +20,7 @@ import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView
import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.ShortcutUtils
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import java.io.File
@@ -88,11 +91,15 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
presenterScope.launch {
try {
withContext(Dispatchers.IO) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
safe {
HistoryRepository().delete(manga)
HistoryRepository().deleteOrSwap(manga, original)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutUtils.removeAppShortcut(get(), manga)
}
viewState.onItemRemoved(manga)
} catch (e: CancellationException) {
} catch (e: Throwable) {

View File

@@ -23,6 +23,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun loadFile(url: String, force: Boolean): File {
if (!force) {
cache[url]?.let {

View File

@@ -195,8 +195,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
else -> super.onOptionsItemSelected(item)
}
override fun saveState(chapterId: Long, page: Int) {
state = state.copy(chapterId = chapterId, page = page)
override fun saveState(chapterId: Long, page: Int, scroll: Float) {
state = state.copy(chapterId = chapterId, page = page, scroll = scroll)
ReaderPresenter.saveState(state)
}
@@ -293,7 +293,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
override fun onChapterChanged(chapter: MangaChapter) {
state = state.copy(
chapterId = chapter.id,
page = 0
page = 0,
scroll = 0f
)
reader?.updateState(chapterId = chapter.id)
}
@@ -371,7 +372,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
manga = manga,
chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id
?: -1 else chapterId,
page = 0
page = 0,
scroll = 0f
)
)
@@ -383,7 +385,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
context, ReaderState(
manga = manga,
chapterId = history.chapterId,
page = history.page
page = history.page,
scroll = history.scroll
)
)
}

View File

@@ -7,5 +7,5 @@ interface ReaderListener : BaseMvpView {
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
fun saveState(chapterId: Long, page: Int)
fun saveState(chapterId: Long, page: Int, scroll: Float)
}

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.ui.reader
import android.content.ContentResolver
import android.util.Log
import android.webkit.URLUtil
import kotlinx.coroutines.*
import moxy.InjectViewState
@@ -9,7 +8,6 @@ import moxy.presenterScope
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.ReaderMode
@@ -103,7 +101,8 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
HistoryRepository().addOrUpdate(
manga = state.manga,
chapterId = state.chapterId,
page = state.page
page = state.page,
scroll = state.scroll
)
}
}

View File

@@ -10,7 +10,8 @@ import org.koitharu.kotatsu.core.model.MangaChapter
data class ReaderState(
val manga: Manga,
val chapterId: Long,
val page: Int
val page: Int,
val scroll: Float
) : Parcelable {
@IgnoredOnParcel

View File

@@ -6,8 +6,8 @@ import android.os.Bundle
import androidx.fragment.app.commit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.settings.MainSettingsFragment
import org.koitharu.kotatsu.ui.settings.ReaderSettingsFragment
import org.koitharu.kotatsu.ui.settings.SettingsHeadersFragment
class SimpleSettingsActivity : BaseActivity() {
@@ -19,7 +19,7 @@ class SimpleSettingsActivity : BaseActivity() {
supportFragmentManager.commit {
replace(R.id.container, when(section) {
SECTION_READER -> ReaderSettingsFragment()
else -> SettingsHeadersFragment()
else -> MainSettingsFragment()
})
}
}

View File

@@ -58,6 +58,9 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
pages.addLast(state.chapterId, it)
adapter?.notifyDataSetChanged()
setCurrentItem(state.page, false)
if (state.scroll != 0f) {
restorePageScroll(state.page, state.scroll)
}
}
}
@@ -67,7 +70,8 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
ARG_STATE, ReaderState(
manga = manga,
chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return,
page = pages.getRelativeIndex(getCurrentItem())
page = pages.getRelativeIndex(getCurrentItem()),
scroll = getCurrentPageScroll()
)
)
}
@@ -174,7 +178,7 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
val page = pages.getRelativeIndex(getCurrentItem())
if (page != -1) {
readerListener?.saveState(chapterId, page)
readerListener?.saveState(chapterId, page, getCurrentPageScroll())
}
Log.i(TAG, "saveState(chapterId=$chapterId, page=$page)")
}
@@ -217,6 +221,10 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
protected abstract fun getCurrentItem(): Int
protected abstract fun getCurrentPageScroll(): Float
protected abstract fun restorePageScroll(position: Int, scroll: Float)
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
protected abstract fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter

View File

@@ -55,7 +55,7 @@ class GroupedList<K, T> {
lruGroup = entry.second
lruGroupKey = entry.first
lruGroupFirstIndex = lastIndex - entry.second.size
return entry.second.get(index - lruGroupFirstIndex)
return entry.second[index - lruGroupFirstIndex]
}
lastIndex -= entry.second.size
}

View File

@@ -49,7 +49,10 @@ class PageHolder(parent: ViewGroup, private val loader: PageLoader) :
}
}
override fun onReady() = Unit
override fun onReady() {
ssiv.maxScale = 2f * maxOf(ssiv.width / ssiv.sWidth.toFloat(), ssiv.height / ssiv.sHeight.toFloat())
ssiv.resetScaleAndCenter()
}
override fun onImageLoadError(e: Exception) = onError(e)

View File

@@ -5,14 +5,14 @@ import android.view.View
import kotlinx.android.synthetic.main.fragment_reader_standard.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.withArgs
class PagerReaderFragment() : AbstractReader(R.layout.fragment_reader_standard) {
class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard) {
private var paginationListener: PagerPaginationListener? = null
@@ -43,6 +43,10 @@ class PagerReaderFragment() : AbstractReader(R.layout.fragment_reader_standard)
pager.setCurrentItem(position, isSmooth)
}
override fun getCurrentPageScroll() = 0f
override fun restorePageScroll(position: Int, scroll: Float) = Unit
companion object {
fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) {

View File

@@ -20,6 +20,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
private var job: Job? = null
private var scrollToRestore = 0f
init {
ssiv.setOnImageEventListener(this)
@@ -34,6 +35,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel()
scrollToRestore = 0f
job = launch {
layout_error.isVisible = false
progressBar.isVisible = true
@@ -51,17 +53,34 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
}
}
fun getScrollY() = ssiv.center?.y ?: 0f
fun restoreScroll(scroll: Float) {
if (ssiv.isReady) {
ssiv.setScaleAndCenter(
ssiv.scale,
PointF(
ssiv.sWidth / 2f,
scroll
)
)
} else {
scrollToRestore = scroll
}
}
override fun onReady() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.setScaleAndCenter(
ssiv.minScale,
PointF(
ssiv.sWidth / 2f,
if (itemView.top < 0) {
ssiv.sHeight.toFloat()
} else {
0f
when {
scrollToRestore != 0f -> scrollToRestore
itemView.top < 0 -> ssiv.sHeight.toFloat()
else -> 0f
}
)
)

View File

@@ -11,7 +11,6 @@ import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList
import org.koitharu.kotatsu.ui.reader.standard.PagerReaderFragment
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
import org.koitharu.kotatsu.utils.ext.findMiddleVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.firstItem
@@ -20,7 +19,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
private val scrollInterpolator = AccelerateDecelerateInterpolator()
protected var paginationListener: ListPaginationListener? = null
private var paginationListener: ListPaginationListener? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -56,7 +55,23 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
}
override fun switchPageBy(delta: Int) {
recyclerView.smoothScrollBy(0, (recyclerView.height * 0.9).toInt() * delta, scrollInterpolator)
recyclerView.smoothScrollBy(
0,
(recyclerView.height * 0.9).toInt() * delta,
scrollInterpolator
)
}
override fun getCurrentPageScroll(): Float {
return (recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder)
?.getScrollY() ?: 0f
}
override fun restorePageScroll(position: Int, scroll: Float) {
recyclerView.post {
val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: return@post
(holder as WebtoonHolder).restoreScroll(scroll)
}
}
companion object {

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.ui.settings
import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
class AboutSettingsFragment : BasePreferenceFragment(R.string.about_app) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_about)
}
}

View File

@@ -90,7 +90,7 @@ class AppUpdateService : BaseService() {
private const val NOTIFICATION_ID = 202
private const val CHANNEL_ID = "update"
private val PERIOD = TimeUnit.HOURS.toMillis(10)
private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun start(context: Context) =
context.startService(Intent(context, AppUpdateService::class.java))

View File

@@ -5,21 +5,24 @@ import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arrayMapOf
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import androidx.preference.SeekBarPreference
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
import org.koitharu.kotatsu.ui.main.list.ListModeSelectDialog
import org.koitharu.kotatsu.ui.settings.utils.MultiSummaryProvider
class AppearanceSettingsFragment : BasePreferenceFragment(R.string.appearance),
class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_appearance)
addPreferencesFromResource(R.xml.pref_main)
findPreference<Preference>(R.string.key_list_mode)?.summary =
listModes[settings.listMode]?.let(::getString)
LIST_MODES[settings.listMode]?.let(::getString)
findPreference<SeekBarPreference>(R.string.key_grid_size)?.run {
summary = "%d%%".format(value)
setOnPreferenceChangeListener { preference, newValue ->
@@ -27,6 +30,24 @@ class AppearanceSettingsFragment : BasePreferenceFragment(R.string.appearance),
true
}
}
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
MultiSummaryProvider(R.string.gestures_only)
findPreference<PreferenceScreen>(R.string.key_remote_sources)?.run {
val total = MangaSource.values().size - 1
summary = getString(
R.string.enabled_d_from_d, total - settings.hiddenSources.size, total
)
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
getString(R.string.key_list_mode) -> findPreference<Preference>(R.string.key_list_mode)?.summary =
LIST_MODES[settings.listMode]?.let(::getString)
getString(R.string.key_theme) -> {
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -49,19 +70,9 @@ class AppearanceSettingsFragment : BasePreferenceFragment(R.string.appearance),
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
getString(R.string.key_list_mode) -> findPreference<Preference>(R.string.key_list_mode)?.summary =
listModes[settings.listMode]?.let(::getString)
getString(R.string.key_theme) -> {
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
}
}
private companion object {
val listModes = arrayMapOf(
val LIST_MODES = arrayMapOf(
ListMode.DETAILED_LIST to R.string.detailed_list,
ListMode.GRID to R.string.grid,
ListMode.LIST to R.string.list

View File

@@ -3,11 +3,9 @@ package org.koitharu.kotatsu.ui.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import kotlinx.android.synthetic.main.activity_settings.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.common.BaseActivity
@@ -20,48 +18,25 @@ class SettingsActivity : BaseActivity(),
setContentView(R.layout.activity_settings)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val isTablet = container_side != null
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
supportFragmentManager.commit {
if (isTablet) {
replace(R.id.container_side, SettingsHeadersFragment())
replace(R.id.container, AppearanceSettingsFragment())
} else {
replace(R.id.container, SettingsHeadersFragment())
}
replace(R.id.container, MainSettingsFragment())
}
}
}
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
val fm = supportFragmentManager
if (container_side != null && caller is SettingsHeadersFragment) {
fm.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment)
fragment.arguments = pref.extras
fragment.setTargetFragment(caller, 0)
fm.commit {
replace(R.id.container, fragment)
if (container_side == null || caller !is SettingsHeadersFragment) {
addToBackStack(null)
}
addToBackStack(null)
}
return true
}
override fun onTitleChanged(title: CharSequence?, color: Int) {
if (container_side == null) {
super.onTitleChanged(title, color)
} else {
if (supportFragmentManager.backStackEntryCount == 0) {
supportActionBar?.subtitle = null
} else {
supportActionBar?.subtitle = title
}
}
}
fun openMangaSourceSettings(mangaSource: MangaSource) {
supportFragmentManager.commit {
replace(R.id.container, SourceSettingsFragment.newInstance(mangaSource))

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.ui.settings
import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
class SettingsHeadersFragment : BasePreferenceFragment(R.string.settings) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_headers)
}
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.ui.settings.sources
import android.view.ViewGroup
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class SourceDividerHolder(parent: ViewGroup) :
BaseViewHolder<Unit, Unit>(parent, R.layout.item_sources_pref_divider) {
override fun onBind(data: Unit, extra: Unit) = Unit
}

View File

@@ -7,9 +7,10 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class SourceViewHolder(parent: ViewGroup) :
BaseViewHolder<MangaSource, Unit>(parent, R.layout.item_source_config) {
BaseViewHolder<MangaSource, Boolean>(parent, R.layout.item_source_config) {
override fun onBind(data: MangaSource, extra: Unit) {
override fun onBind(data: MangaSource, extra: Boolean) {
textView_title.text = data.title
imageView_hidden.isChecked = extra
}
}

View File

@@ -6,32 +6,46 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_source_config.*
import org.koin.core.KoinComponent
import org.koin.core.get
import org.koin.core.inject
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.utils.ext.safe
class SourcesAdapter(private val onItemClickListener: OnRecyclerItemClickListener<MangaSource>) :
RecyclerView.Adapter<BaseViewHolder<*, Unit>>(), KoinComponent {
RecyclerView.Adapter<SourceViewHolder>(), KoinComponent {
private val dataSet = MangaProviderFactory.sources.toMutableList()
private val dataSet = MangaProviderFactory.getSources(includeHidden = true).toMutableList()
private val settings by inject<AppSettings>()
private val hiddenItems = settings.hiddenSources.mapNotNull {
safe {
MangaSource.valueOf(it)
}
}.toMutableSet()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
ITEM_SOURCE -> SourceViewHolder(parent).also(::onViewHolderCreated)
ITEM_DIVIDER -> SourceDividerHolder(parent)
else -> throw IllegalArgumentException("Unsupported viewType $viewType")
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = SourceViewHolder(parent).also(::onViewHolderCreated)
override fun getItemCount() = dataSet.size
override fun onBindViewHolder(holder: BaseViewHolder<*, Unit>, position: Int) {
(holder as? SourceViewHolder)?.bind(dataSet[position], Unit)
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
val item = dataSet[position]
holder.bind(item, !hiddenItems.contains(item))
}
@SuppressLint("ClickableViewAccessibility")
private fun onViewHolderCreated(holder: SourceViewHolder) {
holder.imageView_hidden.setOnCheckedChangeListener {
if (it) {
hiddenItems.remove(holder.requireData())
} else {
hiddenItems.add(holder.requireData())
}
settings.hiddenSources = hiddenItems.map { x -> x.name }.toSet()
}
holder.imageView_config.setOnClickListener { v ->
onItemClickListener.onItemClick(holder.requireData(), holder.adapterPosition, v)
}
@@ -48,12 +62,6 @@ class SourcesAdapter(private val onItemClickListener: OnRecyclerItemClickListene
val item = dataSet.removeAt(oldPos)
dataSet.add(newPos, item)
notifyItemMoved(oldPos, newPos)
get<AppSettings>().sourcesOrder = dataSet.map { it.ordinal }
}
companion object {
const val ITEM_SOURCE = 0
const val ITEM_DIVIDER = 1
settings.sourcesOrder = dataSet.map { it.ordinal }
}
}

View File

@@ -5,7 +5,7 @@ import androidx.core.content.edit
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class StringSetPreferenceDelegate(private val key: String, private val defValue: Set<String>) :
class StringSetPreferenceDelegate(private val key: String, private val defValue: Set<String> = emptySet()) :
ReadWriteProperty<SharedPreferences, Set<String>> {
override fun getValue(thisRef: SharedPreferences, property: KProperty<*>): Set<String> {

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
import android.content.Context
import androidx.appcompat.app.AlertDialog
@Deprecated("Useless")
fun Context.showDialog(block: AlertDialog.Builder.() -> Unit): AlertDialog {
return AlertDialog.Builder(this)
.apply(block)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
import android.annotation.SuppressLint
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
@SuppressLint("SimpleDateFormat")
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)

View File

@@ -11,4 +11,14 @@ fun <T> JSONArray.map(block: (JSONObject) -> T): List<T> {
result.add(block(jo))
}
return result
}
fun <T> JSONArray.mapIndexed(block: (Int, JSONObject) -> T): List<T> {
val len = length()
val result = ArrayList<T>(len)
for(i in 0 until len) {
val jo = getJSONObject(i)
result.add(block(i, jo))
}
return result
}

View File

@@ -1,8 +1,12 @@
package org.koitharu.kotatsu.utils.ext
import android.annotation.SuppressLint
import androidx.core.app.NotificationCompat
@SuppressLint("RestrictedApi")
fun NotificationCompat.Builder.clearActions(): NotificationCompat.Builder {
mActions.clear()
safe {
mActions.clear()
}
return this
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z" />
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="@android:integer/config_mediumAnimTime"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:drawable="@drawable/ic_eye" android:state_checked="true" />
<item android:drawable="@drawable/ic_eye_off" android:state_checked="false" />
</selector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:fillAlpha="0.3"
android:pathData="M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27M12,9A3,3 0 0,1 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9M12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z" />
</vector>

View File

@@ -1,19 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorButtonNormal"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:background="?colorPrimary"
android:theme="@style/AppToolbarTheme">
@@ -26,23 +25,16 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_side"
android:layout_width="340dp"
android:layout_height="match_parent"
android:layout_below="@id/appbar"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true" />
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/appbar"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:layout_toEndOf="@id/container_side"
app:cardBackgroundColor="?android:windowBackground">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintWidth_percent="0.6"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:layout_width="0dp"
android:layout_height="0dp">
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
@@ -51,4 +43,4 @@
</com.google.android.material.card.MaterialCardView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -16,6 +16,7 @@
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:scaleType="centerInside"
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -119,7 +120,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/chips_tags"
tools:text="@tools:sample/lorem/random" />
tools:text="@tools:sample/lorem/random"
tools:ignore="UnusedAttribute" />
<ProgressBar
android:id="@+id/progressBar"

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:fitsSystemWindows="true"
android:theme="@style/AppToolbarTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppPopupTheme" />
</com.google.android.material.appbar.AppBarLayout>
<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
<ProgressBar
android:id="@+id/progressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/appbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -16,6 +16,7 @@
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@@ -131,7 +132,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chips_tags"
tools:text="@tools:sample/lorem/random" />
tools:text="@tools:sample/lorem/random"
tools:ignore="UnusedAttribute" />
<ProgressBar
android:id="@+id/progressBar"

View File

@@ -10,6 +10,7 @@
android:id="@+id/imageView_cover"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:orientation="vertical" />
<LinearLayout

View File

@@ -20,6 +20,7 @@
android:id="@+id/imageView_cover"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:orientation="vertical" />
<LinearLayout

View File

@@ -28,6 +28,15 @@
android:textColor="?android:attr/textColorPrimary"
tools:text="@tools:sample/lorem[1]" />
<org.koitharu.kotatsu.ui.common.widgets.CheckableImageView
android:id="@+id/imageView_hidden"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:padding="?listPreferredItemPaddingEnd"
android:scaleType="center"
android:src="@drawable/ic_eye_checkable" />
<ImageView
android:id="@+id/imageView_config"
android:layout_width="wrap_content"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_browser"
android:icon="@drawable/ic_open_external"
android:title="@string/open_in_browser"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -20,6 +20,11 @@
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/action_browser"
android:title="@string/open_in_browser"
app:showAsAction="never" />
<item
android:id="@+id/action_shortcut"
android:title="@string/create_shortcut"

View File

@@ -15,7 +15,7 @@
<string name="settings">Настройки</string>
<string name="remote_sources">Онлайн каталоги</string>
<string name="loading_">Загрузка…</string>
<string name="chapter_d_of_d">Глава %d из %d</string>
<string name="chapter_d_of_d">Глава %1$d из %2$d</string>
<string name="close">Закрыть</string>
<string name="try_again">Повторить</string>
<string name="clear_history">Очистить историю</string>
@@ -108,4 +108,9 @@
<string name="app_update_available">Доступно обновление приложения</string>
<string name="about_app">О программе</string>
<string name="show_notification_app_update">Показывать уведомление при наличии новой версии</string>
<string name="open_in_browser">Открыть в браузере</string>
<string name="large_manga_save_confirm">В этой манге %d глав. Вы уверены, что хотите сохранить их все?</string>
<string name="save_manga">Сохранить мангу</string>
<string name="notifications">Уведомления</string>
<string name="enabled_d_from_d">Включено %1$d из %2$d</string>
</resources>

View File

@@ -3,12 +3,14 @@
<string name="key_list_mode">list_mode</string>
<string name="key_theme">theme</string>
<string name="key_sources_order">sources_order</string>
<string name="key_sources_hidden">sources_hidden</string>
<string name="key_traffic_warning">traffic_warning</string>
<string name="key_pages_cache_clear">pages_cache_clear</string>
<string name="key_thumbs_cache_clear">thumbs_cache_clear</string>
<string name="key_search_history_clear">search_history_clear</string>
<string name="key_reading_history_clear">reading_history_clear</string>
<string name="key_grid_size">grid_size</string>
<string name="key_remote_sources">remote_sources</string>
<string name="key_reader_switchers">reader_switchers</string>
<string name="key_app_update">app_update</string>
<string name="key_app_update_auto">app_update_auto</string>

View File

@@ -16,7 +16,7 @@
<string name="settings">Settings</string>
<string name="remote_sources">Remote sources</string>
<string name="loading_">Loading…</string>
<string name="chapter_d_of_d">Chapter %d of %d</string>
<string name="chapter_d_of_d">Chapter %1$d of %2$d</string>
<string name="close">Close</string>
<string name="try_again">Try again</string>
<string name="clear_history">Clear history</string>
@@ -109,4 +109,9 @@
<string name="app_update_available">Application update is available</string>
<string name="about_app">About</string>
<string name="show_notification_app_update">Show notification if update is available</string>
<string name="open_in_browser">Open in browser</string>
<string name="large_manga_save_confirm">This manga has %d chapters. Are you want to save all of it?</string>
<string name="save_manga">Save manga</string>
<string name="notifications">Notifications</string>
<string name="enabled_d_from_d">Enabled %1$d from %2$d</string>
</resources>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreference
android:defaultValue="true"
android:key="@string/key_app_update_auto"
android:summary="@string/show_notification_app_update"
android:title="@string/application_update"
app:allowDividerBelow="true"
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:defaultValue="-1"
android:entries="@array/themes"
android:entryValues="@array/values_theme"
android:key="@string/key_theme"
android:title="@string/theme"
app:useSimpleSummaryProvider="true"
app:iconSpaceReserved="false" />
<Preference
android:key="@string/key_list_mode"
android:persistent="false"
android:title="@string/list_mode"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
<SeekBarPreference
android:key="@string/key_grid_size"
android:title="@string/grid_size"
app:iconSpaceReserved="false"
app:defaultValue="100"
app:min="50"
app:showSeekBarValue="false"
app:updatesContinuously="true"
app:seekBarIncrement="10"
app:allowDividerBelow="true"
android:max="150" />
</PreferenceScreen>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.AppearanceSettingsFragment"
android:icon="@drawable/ic_palette"
android:title="@string/appearance" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.sources.SourcesSettingsFragment"
android:icon="@drawable/ic_web"
android:title="@string/remote_sources" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.ReaderSettingsFragment"
android:icon="@drawable/ic_book"
android:title="@string/reader_settings" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.HistorySettingsFragment"
android:icon="@drawable/ic_history"
android:title="@string/history_and_cache" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.AboutSettingsFragment"
android:icon="@drawable/ic_information"
android:title="@string/about_app" />
</PreferenceScreen>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:defaultValue="-1"
android:entries="@array/themes"
android:entryValues="@array/values_theme"
android:key="@string/key_theme"
android:title="@string/theme"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<Preference
android:key="@string/key_list_mode"
android:persistent="false"
android:title="@string/list_mode"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
<SeekBarPreference
android:key="@string/key_grid_size"
android:max="150"
android:title="@string/grid_size"
app:defaultValue="100"
app:iconSpaceReserved="false"
app:min="50"
app:seekBarIncrement="10"
app:showSeekBarValue="false"
app:updatesContinuously="true" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.sources.SourcesSettingsFragment"
android:title="@string/remote_sources"
app:allowDividerAbove="true"
android:key="@string/key_remote_sources"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.ui.settings.HistorySettingsFragment"
android:title="@string/history_and_cache"
app:iconSpaceReserved="false" />
<MultiSelectListPreference
android:defaultValue="@array/values_reader_switchers_default"
android:entries="@array/reader_switchers"
android:entryValues="@array/values_reader_switchers"
android:key="@string/key_reader_switchers"
android:title="@string/switch_pages"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
<PreferenceCategory
android:title="@string/notifications"
app:allowDividerAbove="true"
app:iconSpaceReserved="false">
<SwitchPreference
android:defaultValue="true"
android:key="@string/key_app_update_auto"
android:summary="@string/show_notification_app_update"
android:title="@string/application_update"
app:allowDividerBelow="true"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.parsers
interface MangaParserTest {
fun testMangaList()
fun testMangaDetails()
fun testMangaPages()
fun testTags()
}

View File

@@ -26,8 +26,8 @@ class RemoteRepositoryTest(source: MangaSource) {
val list = runBlocking { repo.getList(60) }
Assert.assertFalse(list.isEmpty())
val item = list.random()
AssertX.assertContentType(item.coverUrl, "image")
AssertX.assertContentType(item.url, "text", "html")
AssertX.assertContentType(item.coverUrl, "image/*")
AssertX.assertContentType(item.url, "text/html", "application/json")
Assert.assertFalse(item.title.isBlank())
}
@@ -36,8 +36,8 @@ class RemoteRepositoryTest(source: MangaSource) {
val list = runBlocking { repo.getList(0, query = "tail") }
Assert.assertFalse(list.isEmpty())
val item = list.random()
AssertX.assertContentType(item.coverUrl, "image")
AssertX.assertContentType(item.url, "text", "html")
AssertX.assertContentType(item.coverUrl, "image/*")
AssertX.assertContentType(item.url, "text/html", "application/json")
Assert.assertFalse(item.title.isBlank())
}
@@ -51,8 +51,8 @@ class RemoteRepositoryTest(source: MangaSource) {
val list = runBlocking { repo.getList(0, tag = tag) }
Assert.assertFalse(list.isEmpty())
val item = list.random()
AssertX.assertContentType(item.coverUrl, "image")
AssertX.assertContentType(item.url, "text", "html")
AssertX.assertContentType(item.coverUrl, "image/*")
AssertX.assertContentType(item.url, "text/html", "application/json")
Assert.assertFalse(item.title.isBlank())
}
@@ -64,7 +64,7 @@ class RemoteRepositoryTest(source: MangaSource) {
Assert.assertFalse(details.description.isNullOrEmpty())
val chapter = details.chapters!!.random()
Assert.assertFalse(chapter.name.isBlank())
AssertX.assertContentType(chapter.url, "text", "html")
AssertX.assertContentType(chapter.url, "text/html", "application/json")
}
@Test
@@ -75,7 +75,7 @@ class RemoteRepositoryTest(source: MangaSource) {
Assert.assertFalse(pages.isEmpty())
val page = pages.random()
val fullUrl = runBlocking { repo.getPageFullUrl(page) }
AssertX.assertContentType(fullUrl, "image")
AssertX.assertContentType(fullUrl, "image/*")
}
companion object {

View File

@@ -6,23 +6,25 @@ import java.net.URL
object AssertX {
fun assertContentType(url: String, type: String, subtype: String? = null) {
Assert.assertFalse("URL is empty", url.isEmpty())
val cn = URL(url).openConnection() as HttpURLConnection
cn.requestMethod = "HEAD"
cn.connect()
when (val code = cn.responseCode) {
HttpURLConnection.HTTP_MOVED_PERM,
HttpURLConnection.HTTP_MOVED_TEMP -> assertContentType(cn.getHeaderField("Location"), type, subtype)
HttpURLConnection.HTTP_OK -> {
val ct = cn.contentType.substringBeforeLast(';').split("/")
Assert.assertEquals(type, ct.first())
if (subtype != null) {
Assert.assertEquals(subtype, ct.last())
}
}
else -> Assert.fail("Invalid response code $code")
}
}
fun assertContentType(url: String, vararg types: String) {
Assert.assertFalse("URL is empty", url.isEmpty())
val cn = URL(url).openConnection() as HttpURLConnection
cn.requestMethod = "HEAD"
cn.connect()
when (val code = cn.responseCode) {
HttpURLConnection.HTTP_MOVED_PERM,
HttpURLConnection.HTTP_MOVED_TEMP -> {
assertContentType(cn.getHeaderField("Location"), *types)
}
HttpURLConnection.HTTP_OK -> {
val ct = cn.contentType.substringBeforeLast(';').split("/")
Assert.assertTrue(types.any {
val x = it.split('/')
x[0] == ct[0] && (x[1] == "*" || x[1] == ct[1])
})
}
else -> Assert.fail("Invalid response code $code at $url")
}
}
}

View File

@@ -1,12 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.70"
ext.kotlin_version = "1.4-M1"
repositories {
google()
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/kotlin-eap'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0-beta02'
classpath 'com.android.tools.build:gradle:4.0.0-beta03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@@ -18,6 +21,12 @@ allprojects {
repositories {
google()
jcenter()
maven {
url 'https://jitpack.io'
}
maven {
url 'https://dl.bintray.com/kotlin/kotlin-eap'
}
}
}

View File

@@ -1,6 +1,6 @@
#Sun Mar 08 18:35:17 EET 2020
#Sat Mar 28 11:01:15 EET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip