Merge branch 'devel' into feature/multiselect

This commit is contained in:
Koitharu
2022-04-09 08:29:57 +03:00
92 changed files with 1037 additions and 1045 deletions

View File

@@ -25,6 +25,8 @@ Download APK from Github Releases:
* Tablet-optimized material design UI
* Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed
* Available in multiple languages
* Password protect access to the app
### Screenshots
@@ -35,6 +37,14 @@ Download APK from Github Releases:
| ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
### Localization
<a href="https://hosted.weblate.org/engage/kotatsu/">
<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status" />
</a>
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, please head over to the Weblate <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a>
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -65,12 +65,12 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation('com.github.nv95:kotatsu-parsers:3ea7e92e64') {
implementation('com.github.nv95:kotatsu-parsers:0ee689cd2f') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.4.0'
@@ -108,7 +108,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
testImplementation 'io.insert-koin:koin-test-junit4:3.1.5'
androidTestImplementation 'androidx.test:runner:1.4.0'

View File

@@ -58,15 +58,6 @@
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:label="@string/settings" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:exported="true"
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"

View File

@@ -9,7 +9,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
class MangaDataRepository(private val db: MangaDatabase) {
@@ -37,7 +37,7 @@ class MangaDataRepository(private val db: MangaDatabase) {
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != 0L -> db.mangaDao.find(intent.mangaId)?.toManga()
intent.mangaId != 0L -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri
}

View File

@@ -14,7 +14,7 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.medianOrNull
import org.koitharu.kotatsu.parsers.util.medianOrNull
import java.io.InputStream
import java.util.zip.ZipFile

View File

@@ -6,14 +6,18 @@ import androidx.annotation.CallSuper
import androidx.annotation.StringRes
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(),
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(),
WindowInsetsDelegate.WindowInsetsListener,
RecyclerViewOwner {
@@ -39,16 +43,20 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : Pre
override fun onResume() {
super.onResume()
if (titleId != 0) {
activity?.setTitle(titleId)
setTitle(getString(titleId))
}
}
@CallSuper
override fun onWindowInsetsChanged(insets: Insets) {
listView.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom
)
}
}
@Suppress("UsePropertyAccessSyntax")
protected fun setTitle(title: CharSequence) {
(parentFragment as? SettingsHeadersFragment)?.setTitle(title)
?: activity?.setTitle(title)
}
}

View File

@@ -12,7 +12,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
GROUP BY manga_tags.tag_id
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
@@ -22,7 +22,7 @@ abstract class TagsDao {
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source
GROUP BY manga_tags.tag_id
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
@@ -32,7 +32,7 @@ abstract class TagsDao {
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source AND title LIKE :query
GROUP BY manga_tags.tag_id
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)
@@ -42,7 +42,7 @@ abstract class TagsDao {
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE title LIKE :query
GROUP BY manga_tags.tag_id
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit"""
)

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
class MangaWithTags(
@Embedded val manga: MangaEntity,
@@ -15,7 +15,5 @@ class MangaWithTags(
val tags: List<TagEntity>
) {
fun toManga() = manga.toManga(tags.mapToSet {
it.toMangaTag()
})
fun toManga() = manga.toManga(tags.mapToSet { it.toMangaTag() })
}

View File

@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.utils.ext.mapToSet
class TrackLogWithManga(
@Embedded val trackLog: TrackLogEntity,

View File

@@ -4,7 +4,7 @@ import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.koitharu.kotatsu.parsers.model.*
fun Manga.writeToParcel(out: Parcel, flags: Int) {
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
out.writeLong(id)
out.writeString(title)
out.writeString(altTitle)
@@ -18,7 +18,11 @@ fun Manga.writeToParcel(out: Parcel, flags: Int) {
out.writeParcelable(ParcelableMangaTags(tags), flags)
out.writeSerializable(state)
out.writeString(author)
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
if (withChapters) {
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
} else {
out.writeString(null)
}
out.writeSerializable(source)
}

View File

@@ -2,24 +2,34 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.parsers.model.Manga
// Limits to avoid TransactionTooLargeException
private const val MAX_SAFE_SIZE = 1024 * 512 // Assume that 512 kb is safe parcel size
private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe
class ParcelableManga(
val manga: Manga,
) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readManga())
init {
if (BuildConfig.DEBUG && manga.chapters != null) {
Log.w("ParcelableManga", "Passing manga with chapters as Parcelable is dangerous!")
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
manga.writeToParcel(parcel, flags)
val chapters = manga.chapters
if (chapters == null || chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
// fast path
manga.writeToParcel(parcel, flags, withChapters = true)
return
}
val tempParcel = Parcel.obtain()
manga.writeToParcel(tempParcel, flags, withChapters = true)
val size = tempParcel.dataSize()
if (size < MAX_SAFE_SIZE) {
parcel.appendFrom(tempParcel, 0, size)
} else {
manga.writeToParcel(parcel, flags, withChapters = false)
}
tempParcel.recycle()
}
override fun describeContents(): Int {

View File

@@ -1,56 +0,0 @@
package org.koitharu.kotatsu.core.network
import android.util.Log
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.nio.charset.StandardCharsets
private const val TAG = "CURL"
class CurlLoggingInterceptor(
private val extraCurlOptions: String? = null,
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
var compressed = false
val curlCmd = StringBuilder("curl")
if (extraCurlOptions != null) {
curlCmd.append(" ").append(extraCurlOptions)
}
curlCmd.append(" -X ").append(request.method)
val headers = request.headers
var i = 0
val count = headers.size
while (i < count) {
val name = headers.name(i)
val value = headers.value(i)
if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value,
ignoreCase = true)
) {
compressed = true
}
curlCmd.append(" -H " + "\"").append(name).append(": ").append(value).append("\"")
i++
}
val requestBody = request.body
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
val contentType = requestBody.contentType()
val charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
curlCmd.append(" --data $'")
.append(buffer.readString(charset).replace("\n", "\\n"))
.append("'")
}
curlCmd.append(if (compressed) " --compressed " else " ").append(request.url)
Log.d(TAG, "╭--- cURL (" + request.url + ")")
Log.d(TAG, curlCmd.toString())
Log.d(TAG, "╰--- (copy and paste the above line to a terminal)")
return chain.proceed(request)
}
}

View File

@@ -5,7 +5,6 @@ import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.MangaLoaderContext
@@ -22,9 +21,6 @@ val networkModule
cache(get<LocalStorageManager>().createHttpCache())
addInterceptor(UserAgentInterceptor())
addInterceptor(CloudFlareInterceptor())
if (BuildConfig.DEBUG) {
addNetworkInterceptor(CurlLoggingInterceptor())
}
}.build()
}
single<MangaLoaderContext> { MangaLoaderContextImpl(get(), get(), get()) }

View File

@@ -165,6 +165,18 @@ class AppSettings(context: Context) {
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun getSuggestionsTagsBlacklistRegex(): Regex? {
val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',')
if (string.isNullOrEmpty()) {
return null
}
val tags = string.split(',')
val regex = tags.joinToString(prefix = "(", separator = "|", postfix = ")") { tag ->
Regex.escape(tag.trim())
}
return Regex(regex, RegexOption.IGNORE_CASE)
}
fun getMangaSources(includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toMutableList()
list.remove(MangaSource.LOCAL)
@@ -247,17 +259,16 @@ class AppSettings(context: Context) {
const val KEY_PAGES_PRELOAD = "pages_preload"
const val KEY_SUGGESTIONS = "suggestions"
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags"
const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source"
// About
const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_UPDATE_AUTO = "app_update_auto"
const val KEY_APP_TRANSLATION = "about_app_translation"
const val KEY_APP_GRATITUDES = "about_gratitudes"
const val KEY_FEEDBACK_4PDA = "about_feedback_4pda"
const val KEY_FEEDBACK_DISCORD = "about_feedback_discord"
const val KEY_FEEDBACK_GITHUB = "about_feedback_github"
const val KEY_SUPPORT_DEVELOPER = "about_support_developer"
private const val NETWORK_NEVER = 0
private const val NETWORK_ALWAYS = 1

View File

@@ -24,11 +24,11 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.iterator
import org.koitharu.kotatsu.utils.ext.mapToSet
import java.io.IOException
class DetailsViewModel(

View File

@@ -6,7 +6,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.replaceWith
import org.koitharu.kotatsu.parsers.util.replaceWith
class BranchesAdapter : BaseAdapter() {

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.progress.ProgressJob

View File

@@ -16,8 +16,8 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.format
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import com.google.android.material.R as materialR

View File

@@ -35,7 +35,6 @@ import org.koitharu.kotatsu.utils.ext.throttle
import org.koitharu.kotatsu.utils.ext.toArraySet
import org.koitharu.kotatsu.utils.progress.ProgressJob
import java.util.concurrent.TimeUnit
import kotlin.collections.set
class DownloadService : BaseService() {
@@ -71,7 +70,6 @@ class DownloadService : BaseService() {
return if (manga != null) {
jobs[startId] = downloadManga(startId, manga, chapters)
jobCount.value = jobs.size
Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
START_REDELIVER_INTENT
} else {
stopSelf(startId)
@@ -185,6 +183,7 @@ class DownloadService : BaseService() {
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
}
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -24,9 +24,10 @@ class ForegroundNotificationSwitcher(
@Synchronized
fun notify(startId: Int, notification: Notification) {
if (notifications.isEmpty()) {
handler.postDelayed(StartForegroundRunnable(startId, notification), DEFAULT_DELAY)
StartForegroundRunnable(startId, notification)
} else {
notificationManager.notify(startId, notification)
}
notificationManager.notify(startId, notification)
notifications[startId] = notification
}

View File

@@ -13,8 +13,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesRepository(private val db: MangaDatabase) {

View File

@@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
abstract class HistoryDao {
@@ -23,8 +22,15 @@ abstract class HistoryDao {
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
abstract suspend fun findAllManga(): List<MangaEntity>
@Query("SELECT * FROM tags WHERE tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_id IN (SELECT manga_id FROM history))")
abstract suspend fun findAllTags(): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
INNER JOIN history ON history.manga_id = manga_tags.manga_id
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_tags.manga_id) DESC
LIMIT :limit"""
)
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@Query("SELECT * FROM history WHERE manga_id = :id")
abstract suspend fun find(id: Long): HistoryEntity?
@@ -60,5 +66,4 @@ abstract class HistoryDao {
true
} else false
}
}

View File

@@ -11,9 +11,9 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class HistoryRepository(
private val db: MangaDatabase,
@@ -99,7 +99,7 @@ class HistoryRepository(
}
}
suspend fun getAllTags(): Set<MangaTag> {
return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() }
suspend fun getPopularTags(limit: Int): List<MangaTag> {
return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() }
}
}

View File

@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ShareHelper
@@ -158,7 +159,7 @@ class ReaderActivity :
)
}
R.id.action_settings -> {
startActivity(SimpleSettingsActivity.newReaderSettingsIntent(this))
startActivity(SettingsActivity.newReaderSettingsIntent(this))
}
R.id.action_chapters -> {
ChaptersBottomSheet.show(

View File

@@ -1,80 +0,0 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.*
class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>(), AppBarOwner {
override val appBar: AppBarLayout
get() = binding.appbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySettingsSimpleBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.commit {
replace(
R.id.container,
when (intent?.action) {
Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment()
ACTION_READER -> ReaderSettingsFragment()
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL
)
else -> MainSettingsFragment()
}
)
}
}
override fun onWindowInsetsChanged(insets: Insets) {
with(binding.toolbar) {
updatePadding(
left = insets.left,
right = insets.right
)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
}
companion object {
private const val ACTION_READER =
"${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
private const val ACTION_SUGGESTIONS =
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
private const val ACTION_SOURCE =
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
private const val EXTRA_SOURCE = "source"
fun newReaderSettingsIntent(context: Context) =
Intent(context, SimpleSettingsActivity::class.java)
.setAction(ACTION_READER)
fun newSuggestionsSettingsIntent(context: Context) =
Intent(context, SimpleSettingsActivity::class.java)
.setAction(ACTION_SUGGESTIONS)
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
Intent(context, SimpleSettingsActivity::class.java)
.setAction(ACTION_SOURCE)
.putExtra(EXTRA_SOURCE, source)
}
}

View File

@@ -10,7 +10,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.utils.ext.serializableArgument
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -35,10 +35,7 @@ class RemoteListFragment : MangaListFragment() {
return when (item.itemId) {
R.id.action_source_settings -> {
startActivity(
SimpleSettingsActivity.newSourceSettingsIntent(
context ?: return false,
source,
)
SettingsActivity.newSourceSettingsIntent(context ?: return false, source)
)
true
}

View File

@@ -84,7 +84,7 @@ class MangaSearchRepository(
return when {
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit)
query.isNotEmpty() -> db.tagsDao.findTags("%$query%", limit)
source != null -> db.tagsDao.findTags(source.name, limit)
source != null -> db.tagsDao.findPopularTags(source.name, limit)
else -> db.tagsDao.findPopularTags(limit)
}.map {
it.toMangaTag()

View File

@@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.areItemsEquals
import org.koitharu.kotatsu.parsers.util.areItemsEquals
sealed interface SearchSuggestionItem {

View File

@@ -0,0 +1,100 @@
package org.koitharu.kotatsu.settings
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.TwoStatePreference
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import java.util.*
class AppearanceSettingsFragment :
BasePreferenceFragment(R.string.appearance),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_appearance)
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.run {
summary = "%d%%".format(value)
setOnPreferenceChangeListener { preference, newValue ->
preference.summary = "%d%%".format(newValue)
true
}
}
preferenceScreen?.findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name)
}
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = AppSettings.isDynamicColorAvailable
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
entryValues = resources.getStringArray(R.array.date_formats)
val now = Date().time
entries = entryValues.map { value ->
val formattedDate = settings.getDateFormat(value.toString()).format(now)
if (value == "") {
"${context.getString(R.string.system_default)} ($formattedDate)"
} else {
formattedDate
}
}.toTypedArray()
setDefaultValueCompat("")
summary = "%s"
}
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settings.subscribe(this)
}
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_THEME -> {
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
AppSettings.KEY_DYNAMIC_THEME -> {
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
}
AppSettings.KEY_THEME_AMOLED -> {
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
}
AppSettings.KEY_APP_PASSWORD -> {
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty()
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_PROTECT_APP -> {
val pref = (preference as? TwoStatePreference ?: return false)
if (pref.isChecked) {
pref.isChecked = false
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
} else {
settings.appPassword = null
}
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
}

View File

@@ -0,0 +1,96 @@
package org.koitharu.kotatsu.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import java.io.File
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
class ContentSettingsFragment :
BasePreferenceFragment(R.string.content),
SharedPreferences.OnSharedPreferenceChangeListener,
StorageSelectDialog.OnStorageSelectListener {
private val storageManager by inject<LocalStorageManager>()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_content)
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
)
bindRemoteSourcesSummary()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
settings.subscribe(this)
}
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_LOCAL_STORAGE -> {
findPreference<Preference>(key)?.bindStorageName()
}
AppSettings.KEY_SUGGESTIONS -> {
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
)
}
AppSettings.KEY_SOURCES_HIDDEN -> {
bindRemoteSourcesSummary()
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_LOCAL_STORAGE -> {
val ctx = context ?: return false
StorageSelectDialog.Builder(ctx, storageManager, this)
.setTitle(preference.title ?: "")
.setNegativeButton(android.R.string.cancel)
.create()
.show()
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
override fun onStorageSelected(file: File) {
settings.mangaStorageDir = file
}
private fun Preference.bindStorageName() {
viewLifecycleScope.launch {
val storage = storageManager.getDefaultWriteableDir()
summary = storage?.getStorageName(context) ?: getString(R.string.not_available)
}
}
private fun bindRemoteSourcesSummary() {
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.run {
val total = MangaSource.values().size - 1
summary = getString(
R.string.enabled_d_of_d, total - settings.hiddenSources.size, total
)
}
}
}

View File

@@ -1,182 +0,0 @@
package org.koitharu.kotatsu.settings
import android.content.ComponentName
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import androidx.preference.TwoStatePreference
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import java.io.File
import java.util.*
class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
SharedPreferences.OnSharedPreferenceChangeListener,
StorageSelectDialog.OnStorageSelectListener {
private val storageManager by inject<LocalStorageManager>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_main)
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.run {
summary = "%d%%".format(value)
setOnPreferenceChangeListener { preference, newValue ->
preference.summary = "%d%%".format(newValue)
true
}
}
preferenceScreen?.findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name)
}
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
AppSettings.isDynamicColorAvailable
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
entryValues = resources.getStringArray(R.array.date_formats)
val now = Date().time
entries = entryValues.map { value ->
val formattedDate = settings.getDateFormat(value.toString()).format(now)
if (value == "") {
"${context.getString(R.string.system_default)} ($formattedDate)"
} else {
formattedDate
}
}.toTypedArray()
setDefaultValueCompat("")
summary = "%s"
}
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
!settings.appPassword.isNullOrEmpty()
settings.subscribe(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_settings, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_leaks -> {
val intent = Intent()
intent.component = ComponentName(requireContext(), "leakcanary.internal.activity.LeakActivity")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
AppSettings.KEY_THEME -> {
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
AppSettings.KEY_DYNAMIC_THEME -> {
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
}
AppSettings.KEY_THEME_AMOLED -> {
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
}
AppSettings.KEY_LOCAL_STORAGE -> {
findPreference<Preference>(key)?.bindStorageName()
}
AppSettings.KEY_APP_PASSWORD -> {
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty()
}
AppSettings.KEY_SUGGESTIONS -> {
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
)
}
}
}
override fun onResume() {
super.onResume()
findPreference<PreferenceScreen>(AppSettings.KEY_REMOTE_SOURCES)?.run {
val total = MangaSource.values().size - 1
summary = getString(
R.string.enabled_d_of_d, total - settings.hiddenSources.size, total
)
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_LOCAL_STORAGE -> {
val ctx = context ?: return false
StorageSelectDialog.Builder(ctx, storageManager, this)
.setTitle(preference.title ?: "")
.setNegativeButton(android.R.string.cancel)
.create()
.show()
true
}
AppSettings.KEY_PROTECT_APP -> {
val pref = (preference as? TwoStatePreference ?: return false)
if (pref.isChecked) {
pref.isChecked = false
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
} else {
settings.appPassword = null
}
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
override fun onStorageSelected(file: File) {
settings.mangaStorageDir = file
}
private fun Preference.bindStorageName() {
viewLifecycleScope.launch {
val storage = storageManager.getDefaultWriteableDir()
summary = storage?.getStorageName(context) ?: getString(R.string.not_available)
}
}
}

View File

@@ -4,9 +4,9 @@ import android.os.Bundle
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
class NetworkSettingsFragment : BasePreferenceFragment(R.string.settings) {
class RootSettingsFragment : BasePreferenceFragment(R.string.settings) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
//TODO https://developer.android.com/training/basics/network-ops/managing
addPreferencesFromResource(R.xml.pref_root)
}
}

View File

@@ -1,9 +1,13 @@
package org.koitharu.kotatsu.settings
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
@@ -11,6 +15,7 @@ import androidx.fragment.app.commit
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.appbar.AppBarLayout
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
@@ -34,9 +39,7 @@ class SettingsActivity :
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
supportFragmentManager.commit {
replace(R.id.container, MainSettingsFragment())
}
openDefaultFragment()
}
}
@@ -55,6 +58,22 @@ class SettingsActivity :
super.onStop()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_settings, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_leaks -> {
val intent = Intent()
intent.component = ComponentName(this, "leakcanary.internal.activity.LeakActivity")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onBackStackChanged() {
val fragment = supportFragmentManager.findFragmentById(R.id.container) as? RecyclerViewOwner ?: return
val recyclerView = fragment.recyclerView
@@ -70,32 +89,66 @@ class SettingsActivity :
val fm = supportFragmentManager
val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false)
fragment.arguments = pref.extras
fragment.setTargetFragment(caller, 0)
// fragment.setTargetFragment(caller, 0)
openFragment(fragment)
return true
}
fun openMangaSourceSettings(mangaSource: MangaSource) {
openFragment(SourceSettingsFragment.newInstance(mangaSource))
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(
left = insets.left,
right = insets.right,
)
binding.container.updatePadding(
left = insets.left,
right = insets.right,
)
}
fun openNotificationSettingsLegacy() {
openFragment(NotificationSettingsLegacyFragment())
}
private fun openFragment(fragment: Fragment) {
fun openFragment(fragment: Fragment) {
supportFragmentManager.commit {
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
replace(R.id.container, fragment)
setReorderingAllowed(true)
replace(R.id.container, fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
addToBackStack(null)
}
}
override fun onWindowInsetsChanged(insets: Insets) = Unit
private fun openDefaultFragment() {
val fragment = when (intent?.action) {
ACTION_READER -> ReaderSettingsFragment()
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL
)
else -> SettingsHeadersFragment()
}
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.container, fragment)
}
}
companion object {
private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
private const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
private const val EXTRA_SOURCE = "source"
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)
fun newReaderSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_READER)
fun newSuggestionsSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SUGGESTIONS)
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SOURCE)
.putExtra(EXTRA_SOURCE, source)
}
}

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceHeaderFragmentCompat
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import org.koitharu.kotatsu.R
class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLayout.PanelSlideListener {
private var currentTitle: CharSequence? = null
override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = RootSettingsFragment()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
slidingPaneLayout.addPanelSlideListener(this)
}
override fun onPanelSlide(panel: View, slideOffset: Float) = Unit
override fun onPanelOpened(panel: View) {
activity?.title = currentTitle ?: getString(R.string.settings)
}
override fun onPanelClosed(panel: View) {
activity?.setTitle(R.string.settings)
}
fun setTitle(title: CharSequence?) {
currentTitle = title
if (slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) {
activity?.title = title
}
}
fun openFragment(fragment: Fragment) {
childFragmentManager.commit {
setReorderingAllowed(true)
replace(androidx.preference.R.id.preferences_detail, fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
addToBackStack(null)
}
}
}

View File

@@ -25,7 +25,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
override fun onResume() {
super.onResume()
activity?.title = source.title
setTitle(source.title)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View File

@@ -4,10 +4,13 @@ import android.content.SharedPreferences
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.utils.MultiAutoCompleteTextViewPreference
import org.koitharu.kotatsu.settings.utils.TagsAutoCompleteProvider
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
@@ -23,6 +26,11 @@ class SuggestionsSettingsFragment : BasePreferenceFragment(R.string.suggestions)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_suggestions)
findPreference<MultiAutoCompleteTextViewPreference>(AppSettings.KEY_SUGGESTIONS_EXCLUDE_TAGS)?.run {
autoCompleteProvider = TagsAutoCompleteProvider(get())
summaryProvider = MultiAutoCompleteTextViewPreference.SimpleSummaryProvider(summary)
}
}
override fun onDestroy() {

View File

@@ -31,7 +31,6 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch
append(getString(R.string.read_more))
}
}
warningPreference
}
}
@@ -43,10 +42,10 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID)
startActivity(intent)
true
} else {
(activity as? SettingsActivity)?.openNotificationSettingsLegacy()
super.onPreferenceTreeClick(preference)
}
true
}
else -> super.onPreferenceTreeClick(preference)
}

View File

@@ -1,13 +1,13 @@
package org.koitharu.kotatsu.settings.about
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.core.net.toUri
import androidx.preference.Preference
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.AppUpdateChecker
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -16,20 +16,16 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_about)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val isUpdateSupported = AppUpdateChecker.isUpdateSupported(requireContext())
findPreference<Preference>(AppSettings.KEY_APP_UPDATE_AUTO)?.run {
isVisible = AppUpdateChecker.isUpdateSupported(context)
isVisible = isUpdateSupported
}
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
title = getString(R.string.app_version, BuildConfig.VERSION_NAME)
isEnabled = AppUpdateChecker.isUpdateSupported(context)
isEnabled = isUpdateSupported
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_APP_VERSION -> {
@@ -37,39 +33,19 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
true
}
AppSettings.KEY_APP_TRANSLATION -> {
startActivity(context?.let { BrowserActivity.newIntent(it,
"https://hosted.weblate.org/engage/kotatsu",
resources.getString(R.string.about_app_translation)) })
openLink(getString(R.string.url_weblate), preference.title)
true
}
AppSettings.KEY_FEEDBACK_4PDA -> {
startActivity(context?.let { BrowserActivity.newIntent(it,
"https://4pda.to/forum/index.php?showtopic=697669",
resources.getString(R.string.about_feedback_4pda)) })
openLink(getString(R.string.url_forpda), preference.title)
true
}
AppSettings.KEY_FEEDBACK_DISCORD -> {
startActivity(context?.let { BrowserActivity.newIntent(it,
"https://discord.gg/NNJ5RgVBC5",
"Discord") })
openLink(getString(R.string.url_discord), preference.title)
true
}
AppSettings.KEY_FEEDBACK_GITHUB -> {
startActivity(context?.let { BrowserActivity.newIntent(it,
"https://github.com/nv95/Kotatsu/issues",
"GitHub") })
true
}
AppSettings.KEY_SUPPORT_DEVELOPER -> {
startActivity(context?.let { BrowserActivity.newIntent(it,
"https://yoomoney.ru/to/410012543938752",
resources.getString(R.string.about_support_developer)) })
true
}
AppSettings.KEY_APP_GRATITUDES -> {
startActivity(context?.let { BrowserActivity.newIntent(it,
"https://github.com/nv95/Kotatsu/graphs/contributors",
resources.getString(R.string.about_gratitudes)) })
openLink(getString(R.string.url_github_issues), preference.title)
true
}
else -> super.onPreferenceTreeClick(preference)
@@ -95,4 +71,16 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
}
}
}
}
private fun openLink(url: String, title: CharSequence?) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url.toUri()
startActivity(
if (title != null) {
Intent.createChooser(intent, title)
} else {
intent
}
)
}
}

View File

@@ -1,41 +0,0 @@
package org.koitharu.kotatsu.settings.about
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.databinding.FragmentCopyrightBinding
class LicenseFragment : BaseFragment<FragmentCopyrightBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.apply {
text =
SpannableStringBuilder(resources.openRawResource(R.raw.copyright).bufferedReader()
.readText()
.parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST))
movementMethod = LinkMovementMethod.getInstance()
}
}
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCopyrightBinding.inflate(inflater, container, false)
override fun onResume() {
super.onResume()
activity?.setTitle(R.string.about_license)
}
override fun onWindowInsetsChanged(insets: Insets) = Unit
}

View File

@@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.map

View File

@@ -15,6 +15,8 @@ import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.SettingsHeadersFragment
import org.koitharu.kotatsu.settings.SourceSettingsFragment
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
@@ -87,7 +89,9 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
}
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) {
(activity as? SettingsActivity)?.openMangaSourceSettings(item.source)
val fragment = SourceSettingsFragment.newInstance(item.source)
(parentFragment as? SettingsHeadersFragment)?.openFragment(fragment)
?: (activity as? SettingsActivity)?.openFragment(fragment)
}
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {

View File

@@ -0,0 +1,118 @@
package org.koitharu.kotatsu.settings.utils
import android.content.Context
import android.util.AttributeSet
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Filter
import android.widget.MultiAutoCompleteTextView
import androidx.annotation.AttrRes
import androidx.annotation.MainThread
import androidx.annotation.StyleRes
import androidx.annotation.WorkerThread
import androidx.preference.EditTextPreference
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.replaceWith
class MultiAutoCompleteTextViewPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = R.attr.multiAutoCompleteTextViewPreferenceStyle,
@StyleRes defStyleRes: Int = R.style.Preference_MultiAutoCompleteTextView,
) : EditTextPreference(context, attrs, defStyleAttr, defStyleRes) {
private val autoCompleteBindListener = AutoCompleteBindListener()
var autoCompleteProvider: AutoCompleteProvider? = null
init {
super.setOnBindEditTextListener(autoCompleteBindListener)
}
override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) {
autoCompleteBindListener.delegate = onBindEditTextListener
}
private inner class AutoCompleteBindListener : OnBindEditTextListener {
var delegate: OnBindEditTextListener? = null
override fun onBindEditText(editText: EditText) {
delegate?.onBindEditText(editText)
if (editText !is MultiAutoCompleteTextView) {
return
}
editText.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer())
editText.setAdapter(
autoCompleteProvider?.let {
CompletionAdapter(editText.context, it, ArrayList())
}
)
editText.threshold = 1
}
}
interface AutoCompleteProvider {
suspend fun getSuggestions(query: String): List<String>
}
class SimpleSummaryProvider(
private val emptySummary: CharSequence?,
) : SummaryProvider<MultiAutoCompleteTextViewPreference> {
override fun provideSummary(preference: MultiAutoCompleteTextViewPreference): CharSequence? {
return if (preference.text.isNullOrEmpty()) {
emptySummary
} else {
preference.text?.trimEnd(' ', ',')
}
}
}
private class CompletionAdapter(
context: Context,
private val completionProvider: AutoCompleteProvider,
private val dataset: MutableList<String>,
) : ArrayAdapter<String>(context, android.R.layout.simple_dropdown_item_1line, dataset) {
override fun getFilter(): Filter {
return CompletionFilter(this, completionProvider)
}
fun publishResults(results: List<String>) {
dataset.replaceWith(results)
notifyDataSetChanged()
}
}
private class CompletionFilter(
private val adapter: CompletionAdapter,
private val provider: AutoCompleteProvider,
) : Filter() {
@WorkerThread
override fun performFiltering(constraint: CharSequence?): FilterResults {
val query = constraint?.toString().orEmpty()
val suggestions = runBlocking { provider.getSuggestions(query) }
return CompletionResults(suggestions)
}
@MainThread
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
val completions = (results as CompletionResults).completions
adapter.publishResults(completions)
}
private class CompletionResults(
val completions: List<String>,
) : FilterResults() {
init {
values = completions
count = completions.size
}
}
}
}

View File

@@ -30,18 +30,22 @@ class SliderPreference @JvmOverloads constructor(
set(value) = setValueInternal(value, notifyChanged = true)
private val sliderListener = Slider.OnChangeListener { _, value, fromUser ->
if (fromUser) {
syncValueInternal(value.toInt())
}
if (fromUser) {
syncValueInternal(value.toInt())
}
}
init {
context.withStyledAttributes(attrs,
context.withStyledAttributes(
attrs,
R.styleable.SliderPreference,
defStyleAttr,
defStyleRes) {
valueFrom = getFloat(R.styleable.SliderPreference_android_valueFrom,
valueFrom.toFloat()).toInt()
defStyleRes
) {
valueFrom = getFloat(
R.styleable.SliderPreference_android_valueFrom,
valueFrom.toFloat()
).toInt()
valueTo =
getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt()
stepSize =

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.settings.utils
import org.koitharu.kotatsu.core.db.MangaDatabase
class TagsAutoCompleteProvider(
private val db: MangaDatabase,
) : MultiAutoCompleteTextViewPreference.AutoCompleteProvider {
override suspend fun getSuggestions(query: String): List<String> {
if (query.isEmpty()) {
return emptyList()
}
val tags = db.tagsDao.findTags(query = "$query%", limit = 6)
val set = HashSet<String>()
val result = ArrayList<String>(tags.size)
for (tag in tags) {
if (set.add(tag.title)) {
result.add(tag.title)
}
}
return result
}
}

View File

@@ -6,9 +6,9 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet
class SuggestionRepository(
private val db: MangaDatabase,

View File

@@ -9,7 +9,7 @@ import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
import org.koitharu.kotatsu.settings.SettingsActivity
class SuggestionsFragment : MangaListFragment() {
@@ -38,7 +38,7 @@ class SuggestionsFragment : MangaListFragment() {
true
}
R.id.action_settings -> {
startActivity(SimpleSettingsActivity.newSuggestionsSettingsIntent(requireContext()))
startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext()))
true
}
else -> super.onOptionsItemSelected(item)

View File

@@ -1,16 +1,29 @@
package org.koitharu.kotatsu.suggestions.ui
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.annotation.FloatRange
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.trySetForeground
import java.util.concurrent.TimeUnit
import kotlin.math.pow
@@ -21,11 +34,41 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
private val historyRepository by inject<HistoryRepository>()
private val appSettings by inject<AppSettings>()
override suspend fun doWork(): Result = try {
override suspend fun doWork(): Result {
val count = doWorkImpl()
Result.success(workDataOf(DATA_COUNT to count))
} catch (t: Throwable) {
Result.failure()
val outputData = workDataOf(DATA_COUNT to count)
return Result.success(outputData)
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val title = applicationContext.getString(R.string.suggestions_updating)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
WORKER_CHANNEL_ID,
title,
NotificationManager.IMPORTANCE_LOW
)
channel.setShowBadge(false)
channel.enableVibration(false)
channel.setSound(null, null)
channel.enableLights(false)
manager.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID)
.setContentTitle(title)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setDefaults(0)
.setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark))
.setSilent(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
.setOngoing(true)
.build()
return ForegroundInfo(WORKER_NOTIFICATION_ID, notification)
}
private suspend fun doWorkImpl(): Int {
@@ -33,47 +76,75 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
suggestionRepository.clear()
return 0
}
val rawResults = ArrayList<Manga>()
val allTags = historyRepository.getAllTags()
val blacklistTagRegex = appSettings.getSuggestionsTagsBlacklistRegex()
val allTags = historyRepository.getPopularTags(TAGS_LIMIT).filterNot {
blacklistTagRegex?.containsMatchIn(it.title) ?: false
}
if (allTags.isEmpty()) {
return 0
}
if (TAG in tags) { // not expedited
trySetForeground()
}
val tagsBySources = allTags.groupBy { x -> x.source }
for ((source, tags) in tagsBySources) {
val repo = MangaRepository(source)
tags.flatMapTo(rawResults) { tag ->
repo.getList(
offset = 0,
sortOrder = SortOrder.UPDATED,
tags = setOf(tag),
)
}
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val rawResults = coroutineScope {
tagsBySources.flatMap { (source, tags) ->
val repo = MangaRepository(source)
tags.map { tag ->
async(dispatcher) {
repo.getList(
offset = 0,
sortOrder = SortOrder.UPDATED,
tags = setOf(tag),
)
}
}
}.awaitAll().flatten().asArrayList()
}
if (appSettings.isSuggestionsExcludeNsfw) {
rawResults.removeAll { it.isNsfw }
}
if (blacklistTagRegex != null) {
rawResults.removeAll {
it.tags.any { x -> blacklistTagRegex.containsMatchIn(x.title) }
}
}
if (rawResults.isEmpty()) {
return 0
}
val suggestions = rawResults.distinctBy { manga ->
manga.id
}.map { manga ->
val jointTags = manga.tags intersect allTags
MangaSuggestion(
manga = manga,
relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(),
relevance = computeRelevance(manga.tags, allTags)
)
}.sortedBy { it.relevance }.take(LIMIT)
suggestionRepository.replace(suggestions)
return suggestions.size
}
@FloatRange(from = 0.0, to = 1.0)
private fun computeRelevance(mangaTags: Set<MangaTag>, allTags: List<MangaTag>): Float {
val maxWeight = (allTags.size + allTags.size + 1 - mangaTags.size) * mangaTags.size / 2.0
val weight = mangaTags.sumOf { tag ->
val index = allTags.indexOf(tag)
if (index < 0) 0 else allTags.size - index
}
return (weight / maxWeight).pow(2.0).toFloat()
}
companion object {
private const val TAG = "suggestions"
private const val TAG_ONESHOT = "suggestions_oneshot"
private const val LIMIT = 140
private const val TAGS_LIMIT = 20
private const val MAX_PARALLELISM = 4
private const val DATA_COUNT = "count"
private const val WORKER_CHANNEL_ID = "suggestion_worker"
private const val WORKER_NOTIFICATION_ID = 36
fun setup(context: Context) {
val constraints = Constraints.Builder()
@@ -96,6 +167,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
val request = OneTimeWorkRequestBuilder<SuggestionsWorker>()
.setConstraints(constraints)
.addTag(TAG_ONESHOT)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
.enqueue(request)

View File

@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground
import org.koitharu.kotatsu.utils.progress.Progress
import java.util.concurrent.TimeUnit
@@ -53,7 +54,9 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
if (tracks.isEmpty()) {
return Result.success()
}
setForeground(createForegroundInfo())
if (TAG in tags) { // not expedited
trySetForeground()
}
var success = 0
val workData = Data.Builder()
.putInt(DATA_TOTAL, tracks.size)
@@ -201,7 +204,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
}
}
private fun createForegroundInfo(): ForegroundInfo {
override suspend fun getForegroundInfo(): ForegroundInfo {
val title = applicationContext.getString(R.string.check_for_new_chapters)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
@@ -281,6 +284,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
val request = OneTimeWorkRequestBuilder<TrackWorker>()
.setConstraints(constraints)
.addTag(TAG_ONESHOT)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
.enqueue(request)

View File

@@ -5,6 +5,7 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import android.net.Uri
import androidx.work.CoroutineWorker
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -28,4 +29,9 @@ suspend fun ConnectivityManager.waitForNetwork(): Network {
}
}
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching {
val info = getForegroundInfo()
setForeground(info)
}.isSuccess

View File

@@ -3,46 +3,12 @@ package org.koitharu.kotatsu.utils.ext
import androidx.collection.ArraySet
import java.util.*
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
clear()
addAll(subject)
}
fun <T> List<T>.medianOrNull(): T? = when {
isEmpty() -> null
else -> get((size / 2).coerceIn(indices))
}
inline fun <T, R> Collection<T>.mapToSet(transform: (T) -> R): Set<R> {
return mapTo(ArraySet(size), transform)
}
fun LongArray.toArraySet(): Set<Long> = createSet(size) { i -> this[i] }
fun <T : Enum<T>> Array<T>.names() = Array(size) { i ->
this[i].name
}
fun <T> Collection<T>.isDistinct(): Boolean {
val set = HashSet<T>(size)
for (item in this) {
if (!set.add(item)) {
return false
}
}
return set.size == size
}
fun <T, K> Collection<T>.isDistinctBy(selector: (T) -> K): Boolean {
val set = HashSet<K>(size)
for (item in this) {
if (!set.add(selector(item))) {
return false
}
}
return set.size == size
}
fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
if (sourceIndex <= targetIndex) {
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
@@ -51,20 +17,6 @@ fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
}
}
inline fun <T> List<T>.areItemsEquals(other: List<T>, equals: (T, T) -> Boolean): Boolean {
if (size != other.size) {
return false
}
for (i in indices) {
val a = this[i]
val b = other[i]
if (!equals(a, b)) {
return false
}
}
return true
}
@Suppress("FunctionName")
inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> {
val set = ArraySet<T>(size)
@@ -82,4 +34,10 @@ inline fun <T> createList(size: Int, init: (index: Int) -> T): List<T> = when (s
0 -> emptyList()
1 -> Collections.singletonList(init(0))
else -> MutableList(size, init)
}
fun <T> List<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) {
this as ArrayList<T>
} else {
ArrayList(this)
}

View File

@@ -2,14 +2,15 @@ package org.koitharu.kotatsu.utils.ext
import android.content.res.Resources
import android.util.Log
import java.io.FileNotFoundException
import java.net.SocketTimeoutException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import java.io.FileNotFoundException
import java.net.SocketTimeoutException
import org.koitharu.kotatsu.parsers.util.format
fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
is AuthRequiredException -> resources.getString(R.string.auth_required)

View File

@@ -1,27 +1,3 @@
package org.koitharu.kotatsu.utils.ext
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String {
val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat
val symbols = formatter.decimalFormatSymbols
if (thousandsSep != null) {
symbols.groupingSeparator = thousandsSep
formatter.isGroupingUsed = true
} else {
formatter.isGroupingUsed = false
}
symbols.decimalSeparator = decPoint
formatter.decimalFormatSymbols = symbols
formatter.minimumFractionDigits = decimals
formatter.maximumFractionDigits = decimals
return when (this) {
is Float,
is Double -> formatter.format(this.toDouble())
else -> formatter.format(this.toLong())
}
}
inline fun Int.ifZero(defaultValue: () -> Int): Int = if (this == 0) defaultValue() else 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,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" />
</vector>

View File

@@ -1,16 +0,0 @@
<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:pathData="M3.333 11.965a8.632 8.632 0 1 1 17.264 0 8.632 8.632 0 1 1-17.264 0"
android:strokeWidth="2"
android:strokeColor="@android:color/white" />
<path
android:fillColor="@android:color/white"
android:pathData="M6.889 10.777h2.453a2.81 2.87 0 1 1 0 2.393H6.89a5.15 5.263 0 1 0 0-2.392z"
android:strokeWidth="1" />
</vector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -8,12 +8,14 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:elevation="@dimen/design_appbar_elevation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
@@ -22,23 +24,9 @@
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.card.MaterialCardView
style="?materialCardViewOutlinedStyle"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintWidth_percent="0.6">
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height_expanded"
app:toolbarId="@id/toolbar"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textIsSelectable="true" />
</ScrollView>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/message"
style="?android:attr/textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="48dp"
android:labelFor="@android:id/edit"
android:textColor="?android:attr/textColorSecondary"
tools:ignore="LabelFor" />
<MultiAutoCompleteTextView
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:importantForAutofill="no"
android:minHeight="48dp" />
</LinearLayout>
</ScrollView>

View File

@@ -1,24 +0,0 @@
<p><a href="https://github.com/nv95/Kotatsu">Kotatsu</a> is a free and open source manga reader for Android.</p>
<p>Copyright (C) 2020 by Koitharu</p>
<p>This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.</p>
<p>This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.</p>
<p>You should have received a copy of the GNU General Public License
along with this program. If not, see <a href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.</p>
<h3>Open Source Licenses</h3>
<ul>
<li>AOSP: <a href="https://source.android.com/setup/start/licenses">APL 2.0</a></li>
<li>OkHttp: <a href="https://github.com/square/okhttp/blob/master/LICENSE.txt">APL 2.0</a></li>
<li>Okio: <a href="https://github.com/square/okio/blob/master/LICENSE.txt">APL 2.0</a></li>
<li>jsoup: <a href="https://github.com/jhy/jsoup/blob/master/LICENSE">MIT</a></li>
<li>AdapterDelegates: <a href="https://github.com/sockeqwe/AdapterDelegates/blob/master/LICENSE">APL 2.0</a></li>
<li>Koin: <a href="https://github.com/InsertKoinIO/koin/blob/master/LICENSE">APL 2.0</a></li>
<li>Coil: <a href="https://github.com/coil-kt/coil/blob/main/LICENSE.txt">APL 2.0</a></li>
<li>Subsampling Scale Image View: <a href="https://github.com/davemorrissey/subsampling-scale-image-view/blob/master/LICENSE">APL 2.0</a></li>
<li>Disk LRU Cache: <a href="https://github.com/solkin/disk-lru-cache/blob/master/LICENSE">MIT</a></li>
</ul>

View File

@@ -217,13 +217,7 @@
<string name="chapter_is_missing_text">Гэтая глава адсутнічае на вашай прыладзе. Спампуйце ціпрачытайце яе онлайн.</string>
<string name="text_downloads_holder">На дадзены момант няма актыўных спамповак</string>
<string name="queued">У чарзе</string>
<string name="about_license">Ліцэнзія</string>
<string name="about_copyright_and_licenses">Аўтарскія правы і ліцэнзіі</string>
<string name="about_gratitudes_summary">Гэтыя людзі робяць Kotatsu лепш</string>
<string name="about_gratitudes">Падзякі</string>
<string name="about_support_developer_summary">Калі вам падабаецца гэтая праграма, вы можаце дапамагчы фінансава з дапамогай ЮMoney (был. Яндекс.Деньги)</string>
<string name="about_support_developer">Падтрымаць распрацоўшчыка</string>
<string name="about_feedback_4pda">Тэма на 4PDA</string>
<string name="about_feedback_4pda">Тэма на 4PDA</string>
<string name="about_feedback">Зваротная сувязь</string>
<string name="about_app_translation_summary">Дапамагчы з перакладам праграмы</string>
<string name="about_app_translation">Пераклад</string>

View File

@@ -217,13 +217,7 @@
<string name="chapter_is_missing_text">Dieses Kapitel fehlt auf deinem Gerät. Lade ihn herunter oder lese ihn online.</string>
<string name="text_downloads_holder">Zurzeit sind keine aktiven Datenübertragungen vorhanden</string>
<string name="queued">In Warteschlange</string>
<string name="about_support_developer_summary">Wenn diese Anwendung dir gefällt, kannst du über Yoomoney (Yandex.Money) finanziell helfen</string>
<string name="about_license">Lizenz</string>
<string name="about_copyright_and_licenses">Urheberrecht und Lizenzen</string>
<string name="about_gratitudes_summary">Diese Leute machen Kotatsu besser</string>
<string name="about_gratitudes">Danksagung</string>
<string name="about_support_developer">Den Entwickler unterstützen</string>
<string name="about_feedback_4pda">Thema auf 4PDA</string>
<string name="about_feedback_4pda">Thema auf 4PDA</string>
<string name="about_feedback">Rückmeldung</string>
<string name="about_app_translation">Übersetzung</string>
<string name="about_app_translation_summary">Diese Anwendung übersetzen</string>

View File

@@ -205,14 +205,8 @@
<string name="text_history_holder_secondary">Encuentra qué leer en el menú lateral.</string>
<string name="text_history_holder_primary">Lo que leas se mostrará aquí</string>
<string name="text_empty_holder_primary">Está un poco vacío aquí…</string>
<string name="about_gratitudes">Agradecimientos</string>
<string name="about_support_developer_summary">Si te gusta esta aplicación, puedes ayudar económicamente a través de Yoomoney (ex. Yandex.Money)</string>
<string name="about_support_developer">Apoyar al desarrollador</string>
<string name="search_only_on_s">Buscar sólo en %s</string>
<string name="about_gratitudes_summary">Todas estas personas hicieron que Kotatsu fuera mejor</string>
<string name="about_license">Licencia</string>
<string name="about_copyright_and_licenses">Derechos de autor y licencias</string>
<string name="chapter_is_missing">Falta un capítulo</string>
<string name="search_only_on_s">Buscar sólo en %s</string>
<string name="chapter_is_missing">Falta un capítulo</string>
<string name="about_app_translation_summary">Traducir esta aplicación</string>
<string name="about_feedback">Comentarios</string>
<string name="about_app_translation">Traducción</string>

View File

@@ -4,13 +4,7 @@
<string name="genres">Tyylilajit</string>
<string name="auth_not_supported_by">%s -valtuutusta ei tueta</string>
<string name="auth_complete">Valtuutus valmis</string>
<string name="about_license">Lisenssi</string>
<string name="about_copyright_and_licenses">Tekijänoikeudet ja lisenssit</string>
<string name="about_gratitudes_summary">Nämä ihmiset tekevät Kotatsusta paremman</string>
<string name="about_gratitudes">Kiitokset</string>
<string name="about_support_developer_summary">Jos pidät tästä sovelluksesta, voit auttaa taloudellisesti Yoomoneyn (ent. Yandex.Money) kautta</string>
<string name="about_support_developer">Tue kehittäjää</string>
<string name="about_feedback_4pda">Aihe 4PDA: ssa</string>
<string name="about_feedback_4pda">Aihe 4PDA: ssa</string>
<string name="about_feedback">Palaute</string>
<string name="about_app_translation">Käännös</string>
<string name="about_app_translation_summary">Käännä tämä sovellus</string>

View File

@@ -217,13 +217,7 @@
<string name="chapter_is_missing_text">Téléchargez ou lisez ce chapitre manquant en ligne.</string>
<string name="text_downloads_holder">Aucun téléchargement actif</string>
<string name="queued">En file d\'attente</string>
<string name="about_support_developer_summary">Si vous aimez cette application, vous pouvez envoyer de l\'argent via Yoomoney (ex. Yandex.Money)</string>
<string name="about_support_developer">Soutenir le concepteur</string>
<string name="about_license">Licence</string>
<string name="about_copyright_and_licenses">Droits d\'auteur et licences</string>
<string name="about_gratitudes_summary">Ces personnes ont toutes rendu Kotatsu meilleur</string>
<string name="about_gratitudes">Remerciements</string>
<string name="about_feedback_4pda">Sujet sur 4PDA</string>
<string name="about_feedback_4pda">Sujet sur 4PDA</string>
<string name="about_feedback">Remarques</string>
<string name="about_app_translation">Traduction</string>
<string name="about_app_translation_summary">Traduire cette application</string>

View File

@@ -217,13 +217,7 @@
<string name="chapter_is_missing_text">Questo capitolo manca sul tuo dispositivo. Scaricalo o leggilo in linea.</string>
<string name="text_downloads_holder">Attualmente non ci sono scaricamenti attivi</string>
<string name="queued">In coda</string>
<string name="about_license">Licenza</string>
<string name="about_copyright_and_licenses">Diritti d\'autore e licenze</string>
<string name="about_gratitudes_summary">Queste persone rendono Kotatsu migliore</string>
<string name="about_gratitudes">Ringraziamenti</string>
<string name="about_support_developer_summary">Se ti piace questa applicazione, puoi aiutare finanziariamente via Yoomoney (Yandex.Money)</string>
<string name="about_support_developer">Sostieni lo sviluppatore</string>
<string name="about_feedback_4pda">Argomento su 4PDA</string>
<string name="about_feedback_4pda">Argomento su 4PDA</string>
<string name="about_feedback">Commenti</string>
<string name="about_app_translation">Traduzione</string>
<string name="about_app_translation_summary">Traduci questa applicazione</string>

View File

@@ -163,10 +163,7 @@
<string name="prefer_rtl_reader">右から左(←)の読書を好む</string>
<string name="about_feedback">フィードバック</string>
<string name="about_feedback_4pda">4PDAに関する話題</string>
<string name="about_support_developer">開発者をサポートします(Yoomoneyが開きます)</string>
<string name="about_support_developer_summary">このアプリが気に入ったら、Yoomoney(Yandex.Moneyなど)から開発者をサポートできます</string>
<string name="about_license">ライセンス</string>
<string name="auth_complete">承認済み</string>
<string name="auth_complete">承認済み</string>
<string name="auth_not_supported_by">%sへのログインはサポートされていません</string>
<string name="state_finished">完成</string>
<string name="state_ongoing">進行中</string>
@@ -212,12 +209,10 @@
<string name="about_app_translation">このアプリを翻訳</string>
<string name="data_restored_success">全てのデータが復元されました</string>
<string name="data_restored">復元</string>
<string name="about_gratitudes">感謝の気持ち</string>
<string name="preparing_">準備中…</string>
<string name="preparing_">準備中…</string>
<string name="zoom_mode_keep_start">開始時に維持</string>
<string name="backup_restore">バックアップと復元</string>
<string name="about_copyright_and_licenses">著作権とライセン</string>
<string name="reverse">リバース</string>
<string name="reverse">リバー</string>
<string name="reader_mode_hint">選択した構成はこの漫画のために記憶されます</string>
<string name="clear_cookies">クッキーを削除</string>
<string name="report_github">GitHubでissueを作成する(GitHubのアカウントが必要です)</string>
@@ -235,8 +230,7 @@
<string name="scale_mode">スケールモード</string>
<string name="error_empty_name">名前を入力する必要があります</string>
<string name="today">今日</string>
<string name="about_gratitudes_summary">この人達のお陰でKotatsuがより良くなりました</string>
<string name="about_app_translation_summary">Kotatsuを翻訳する(Weblateのサイトに移動します)</string>
<string name="about_app_translation_summary">Kotatsuを翻訳する(Weblateのサイトに移動します)</string>
<string name="next">次のページ</string>
<string name="silent">サイレント</string>
<string name="sign_in">サインイン</string>

View File

@@ -223,13 +223,7 @@
<string name="about_app_translation">Oversettelse</string>
<string name="about_feedback">Tilbakemelding</string>
<string name="about_feedback_4pda">Emne på 4PDA</string>
<string name="about_support_developer">Støtt utvikleren</string>
<string name="about_support_developer_summary">Hvis du liker programmet kan du kronerulle det på Yoomoney (tidligere Yandex.Money)</string>
<string name="about_gratitudes">Takk rettes til</string>
<string name="about_gratitudes_summary">Folk som gjorde Kotatsu enda bedre</string>
<string name="about_copyright_and_licenses">Opphavsrett og lisenser</string>
<string name="about_license">Lisens</string>
<string name="auth_complete">Identitetsbekreftet</string>
<string name="auth_complete">Identitetsbekreftet</string>
<string name="auth_not_supported_by">Innlogging på %s støttes ikke</string>
<string name="text_clear_cookies_prompt">Du vil bli utlogget fra alle kilder</string>
<string name="genres">Sjangere</string>

View File

@@ -183,12 +183,7 @@
<string name="about_app_translation">Tradução</string>
<string name="about_feedback">Comentar</string>
<string name="about_feedback_4pda">Tópico no 4PDA</string>
<string name="about_support_developer">Apoie o desenvolvedor</string>
<string name="about_support_developer_summary">Se você gosta deste aplicativo, você pode enviar dinheiro através do Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">Gratidão</string>
<string name="about_gratitudes_summary">Todas essas pessoas tornaram o Kotatsu melhor</string>
<string name="about_license">Licença</string>
<string name="auth_complete">Autorizado</string>
<string name="auth_complete">Autorizado</string>
<string name="auth_not_supported_by">O login em %s não é suportado</string>
<string name="text_clear_cookies_prompt">Você será desconectado de todas as fontes</string>
<string name="genres">Gêneros</string>
@@ -266,8 +261,7 @@
<string name="data_restored_with_errors">Os dados foram restaurados, mas há erros</string>
<string name="report_github">Criar problema no GitHub</string>
<string name="file_not_found">Arquivo não encontrado</string>
<string name="about_copyright_and_licenses">Direitos autorais e licenças</string>
<string name="search_chapters">Localizar capítulo</string>
<string name="search_chapters">Localizar capítulo</string>
<string name="chapters_empty">Não há capítulos neste mangá</string>
<string name="percent_string_pattern">%1$s%%</string>
</resources>

View File

@@ -185,13 +185,7 @@
<string name="about_app_translation_summary">Traduzir esta aplicação</string>
<string name="about_feedback">Comentar</string>
<string name="about_feedback_4pda">Tópico no 4PDA</string>
<string name="about_support_developer">Apoiar o desenvolvedor</string>
<string name="about_support_developer_summary">Se você gosta deste aplicativo, você pode enviar dinheiro através do Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">Agradecimentos</string>
<string name="about_gratitudes_summary">Todas essas pessoas tornaram o Kotatsu melhor</string>
<string name="about_copyright_and_licenses">Direitos de autor e licenças</string>
<string name="about_license">Licença</string>
<string name="chapter_is_missing_text">Baixe ou leia este capítulo perdido online.</string>
<string name="chapter_is_missing_text">Baixe ou leia este capítulo perdido online.</string>
<string name="chapter_is_missing">O capítulo está em falta</string>
<string name="auth_complete">Autorizado</string>
<string name="auth_not_supported_by">O login em %s não é suportado</string>

View File

@@ -220,13 +220,7 @@
<string name="about_app_translation">Перевод</string>
<string name="about_feedback_4pda">Тема на 4PDA</string>
<string name="about_feedback">Обратная связь</string>
<string name="about_support_developer">Поддержать разработчика</string>
<string name="about_support_developer_summary">Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги)</string>
<string name="about_gratitudes">Благодарности</string>
<string name="about_gratitudes_summary">Все эти люди сделали Kotatsu лучше</string>
<string name="about_copyright_and_licenses">Авторские права и лицензии</string>
<string name="about_license">Лицензия</string>
<string name="auth_complete">Авторизация выполнена</string>
<string name="auth_complete">Авторизация выполнена</string>
<string name="auth_not_supported_by">Вход в %s не поддерживается</string>
<string name="text_clear_cookies_prompt">Вы выйдете из всех источников</string>
<string name="genres">Жанры</string>
@@ -269,4 +263,9 @@
<string name="various_languages">Разные языки</string>
<string name="search_chapters">Найти главу</string>
<string name="chapters_empty">В этой манге нет глав</string>
<string name="appearance">Оформление</string>
<string name="content">Контент</string>
<string name="suggestions_updating">Обновление рекомендаций</string>
<string name="suggestions_excluded_genres">Исключить жанры</string>
<string name="suggestions_excluded_genres_summary">Укажите жанры, которые Вы не хотите видеть в рекомендациях</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<bool name="config_materialPreferenceIconSpaceReserved" tools:ignore="MissingDefaultResource,UnusedAttribute" tools:node="replace">
false
</bool>
</resources>

View File

@@ -131,8 +131,7 @@
<string name="restore_backup">Yedekten geri yükle</string>
<string name="update">Güncelle</string>
<string name="sign_in">Oturum aç</string>
<string name="about_license">Lisans</string>
<string name="state_finished">Bitti</string>
<string name="state_finished">Bitti</string>
<string name="about">Hakkında</string>
<string name="auth_required">Bu içeriği görüntülemek için oturum açın</string>
<string name="confirm">Onayla</string>
@@ -185,9 +184,7 @@
<string name="about_app_translation">Çeviri</string>
<string name="search_only_on_s">Yalnızca %s içinde ara</string>
<string name="about_feedback_4pda">4PDA\'daki konu</string>
<string name="about_gratitudes">Teşekkürler</string>
<string name="about_gratitudes_summary">Tüm bu insanlar Kotatsu\'yu daha iyi hale getirdi</string>
<string name="state_ongoing">Devam ediyor</string>
<string name="state_ongoing">Devam ediyor</string>
<string name="text_clear_cookies_prompt">Tüm kaynaklardaki oturumunuz kapatılacak</string>
<string name="enabled_sources">Kullanılan kaynaklar</string>
<string name="available_sources">Kullanılabilir kaynaklar</string>
@@ -226,16 +223,13 @@
<string name="captcha_required">CAPTCHA gerekli</string>
<string name="cookies_cleared">Tüm çerezler kaldırıldı</string>
<string name="reader_mode_hint">Seçilen yapılandırma bu manga için hatırlanacak</string>
<string name="about_support_developer_summary">Bu uygulamayı beğendiyseniz Yoomoney (eski Yandex.Money) üzerinden para gönderebilirsiniz</string>
<string name="text_clear_updates_feed_prompt">Tüm güncelleme geçmişi kalıcı olarak silinsin mi\?</string>
<string name="text_clear_updates_feed_prompt">Tüm güncelleme geçmişi kalıcı olarak silinsin mi\?</string>
<string name="_and_x_more">…ve %1$d daha fazlası</string>
<string name="protect_application_subtitle">Uygulamayı başlatmak için bir parola girin</string>
<string name="text_clear_search_history_prompt">Tüm son arama sorguları kalıcı olarak kaldırılsın mı\?</string>
<string name="about_feedback">Geri bildirim</string>
<string name="backup_saved">Yedek kaydedildi</string>
<string name="about_support_developer">Geliştiriciyi destekleyin</string>
<string name="about_copyright_and_licenses">Telif Hakkı ve Lisanslar</string>
<string name="genres">Türler</string>
<string name="genres">Türler</string>
<string name="date_format">Tarih biçimi</string>
<string name="system_default">Öntanımlı</string>
<string name="error_empty_name">Bir ad girmelisiniz</string>

View File

@@ -2,6 +2,7 @@
<resources>
<attr name="sliderPreferenceStyle" />
<attr name="multiAutoCompleteTextViewPreferenceStyle" />
<attr name="listItemTextViewStyle" />
<declare-styleable name="Theme">

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<bool name="is_tablet">false</bool>
<bool name="light_status_bar">true</bool>
<bool name="light_navigation_bar">false</bool>

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="url_github_issues">https://github.com/nv95/Kotatsu/issues</string>
<string name="url_discord">https://discord.gg/NNJ5RgVBC5</string>
<string name="url_forpda">https://4pda.to/forum/index.php?showtopic=697669</string>
<string name="url_weblate">https://hosted.weblate.org/engage/kotatsu</string>
<string-array name="values_theme" translatable="false">
<item>-1</item>
<item>1</item>

View File

@@ -222,13 +222,7 @@
<string name="about_app_translation">Translation</string>
<string name="about_feedback">Feedback</string>
<string name="about_feedback_4pda">Topic on 4PDA</string>
<string name="about_support_developer">Support the developer</string>
<string name="about_support_developer_summary">If you like this app, you can send money through Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">Gratitudes</string>
<string name="about_gratitudes_summary">These people all made Kotatsu better</string>
<string name="about_copyright_and_licenses">Copyright and Licenses</string>
<string name="about_license">License</string>
<string name="auth_complete">Authorized</string>
<string name="auth_complete">Authorized</string>
<string name="auth_not_supported_by">Logging in on %s is not supported</string>
<string name="text_clear_cookies_prompt">You will be logged out from all sources</string>
<string name="genres">Genres</string>
@@ -270,6 +264,13 @@
<string name="search_chapters">Find chapter</string>
<string name="chapters_empty">No chapters in this manga</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="appearance">Appearance</string>
<string name="content">Content</string>
<string name="github" translatable="false">GitHub</string>
<string name="discord" translatable="false">Discord</string>
<string name="suggestions_updating">Suggestions updating</string>
<string name="suggestions_excluded_genres">Exclude genres</string>
<string name="suggestions_excluded_genres_summary">Specify genres that you do not want to see in the suggestions</string>
<string name="text_delete_local_manga_batch">Delete selected items from device permanently?</string>
<string name="removal_completed">Removal completed</string>
<string name="batch_manga_save_confirm">Are you sure you want to download all selected manga with all its chapters? This action can consume a lot of traffic and storage</string>

View File

@@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Toolbars-->
@@ -149,7 +149,11 @@
</style>
<style name="Preference.Slider" parent="Preference.SeekBarPreference.Material">
<item name="android:layout">@layout/pref_slider</item>
<item name="android:layout">@layout/preference_slider</item>
</style>
<style name="Preference.MultiAutoCompleteTextView" parent="Preference.DialogPreference.EditTextPreference.Material">
<item name="android:dialogLayout">@layout/preference_dialog_multiautocompletetextview</item>
</style>
</resources>

View File

@@ -1,84 +1,44 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
app:initialExpandedChildrenCount="5">
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/app_name">
<PreferenceCategory android:title="@string/app_name">
<Preference
app:iconSpaceReserved="false"
app:key="app_version"
app:persistent="false"
app:summary="@string/check_for_updates" />
android:key="app_version"
android:persistent="false"
android:summary="@string/check_for_updates" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:iconSpaceReserved="false"
app:isPreferenceVisible="false"
app:key="app_update_auto"
app:summary="@string/show_notification_app_update"
app:title="@string/application_update"
tools:isPreferenceVisible="true" />
<Preference
app:iconSpaceReserved="false"
app:key="about_app_translation"
app:summary="@string/about_app_translation_summary"
app:title="@string/about_app_translation" />
<Preference
app:iconSpaceReserved="false"
app:key="about_support_developer"
app:summary="@string/about_support_developer_summary"
app:title="@string/about_support_developer" />
<Preference
app:iconSpaceReserved="false"
app:key="about_gratitudes"
app:summary="@string/about_gratitudes_summary"
app:title="@string/about_gratitudes" />
android:defaultValue="true"
android:key="app_update_auto"
android:summary="@string/show_notification_app_update"
android:title="@string/application_update" />
</PreferenceCategory>
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/about_feedback">
<PreferenceCategory android:title="@string/about_feedback">
<Preference
app:iconSpaceReserved="false"
app:key="about_feedback_4pda"
app:summary="https://4pda.to/forum/index.php?showtopic=697669"
app:title="@string/about_feedback_4pda" />
android:key="about_feedback_github"
android:persistent="false"
android:summary="@string/url_github_issues"
android:title="@string/github" />
<Preference
app:iconSpaceReserved="false"
app:key="about_feedback_discord"
app:summary="https://discord.gg/NNJ5RgVBC5"
app:title="Discord" />
android:key="about_feedback_4pda"
android:summary="@string/url_forpda"
android:title="@string/about_feedback_4pda" />
<Preference
app:iconSpaceReserved="false"
app:key="about_feedback_github"
app:summary="https://github.com/nv95/Kotatsu/issues"
app:title="GitHub" />
</PreferenceCategory>
<PreferenceCategory
app:iconSpaceReserved="false"
app:key="copyright"
app:title="@string/about_copyright_and_licenses">
android:key="about_feedback_discord"
android:summary="@string/url_discord"
android:title="@string/discord" />
<Preference
android:fragment="org.koitharu.kotatsu.settings.about.LicenseFragment"
app:icon="@drawable/ic_copyleft"
app:iconSpaceReserved="false"
app:key="about_license"
app:title="@string/about_license" />
android:key="about_app_translation"
android:summary="@string/about_app_translation_summary"
android:title="@string/about_app_translation" />
</PreferenceCategory>

View File

@@ -0,0 +1,53 @@
<?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="theme"
android:title="@string/theme"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="dynamic_theme"
android:summary="@string/dynamic_theme_summary"
android:title="@string/dynamic_theme"
app:isPreferenceVisible="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="amoled_theme"
android:summary="@string/black_dark_theme_summary"
android:title="@string/black_dark_theme" />
<ListPreference
android:key="date_format"
android:title="@string/date_format" />
<ListPreference
android:entries="@array/list_modes"
android:key="list_mode_2"
android:title="@string/list_mode"
app:allowDividerAbove="true"
app:useSimpleSummaryProvider="true" />
<org.koitharu.kotatsu.settings.utils.SliderPreference
android:key="grid_size"
android:stepSize="5"
android:title="@string/grid_size"
android:valueFrom="50"
android:valueTo="150"
app:defaultValue="100" />
<SwitchPreferenceCompat
android:key="protect_app"
android:persistent="false"
android:summary="@string/protect_application_summary"
android:title="@string/protect_application"
app:allowDividerAbove="true" />
</PreferenceScreen>

View File

@@ -6,14 +6,12 @@
<Preference
android:key="backup"
android:persistent="false"
android:title="@string/create_backup"
app:iconSpaceReserved="false" />
android:title="@string/create_backup" />
<Preference
android:key="restore"
android:persistent="false"
android:title="@string/restore_backup"
app:iconSpaceReserved="false" />
android:title="@string/restore_backup" />
<Preference
android:icon="@drawable/ic_info_outline"

View File

@@ -0,0 +1,27 @@
<?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">
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment"
android:key="remote_sources"
android:title="@string/remote_sources" />
<Preference
android:key="local_storage"
android:persistent="false"
android:title="@string/manga_save_location" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.SuggestionsSettingsFragment"
android:key="suggestions"
android:persistent="false"
android:title="@string/suggestions" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
android:title="@string/backup_restore"
app:allowDividerAbove="true" />
</PreferenceScreen>

View File

@@ -1,51 +1,42 @@
<?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">
xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="search_history_clear"
android:persistent="false"
android:summary="@string/loading_"
android:title="@string/clear_search_history"
app:iconSpaceReserved="false" />
android:title="@string/clear_search_history" />
<Preference
android:key="updates_feed_clear"
android:persistent="false"
android:summary="@string/loading_"
android:title="@string/clear_updates_feed"
app:iconSpaceReserved="false" />
android:title="@string/clear_updates_feed" />
<SwitchPreferenceCompat
android:key="history_exclude_nsfw"
android:title="@string/exclude_nsfw_from_history"
app:iconSpaceReserved="false" />
android:title="@string/exclude_nsfw_from_history" />
<PreferenceCategory
android:title="@string/cache"
app:iconSpaceReserved="false">
<PreferenceCategory android:title="@string/cache">
<Preference
android:key="thumbs_cache_clear"
android:persistent="false"
android:summary="@string/computing_"
android:title="@string/clear_thumbs_cache"
app:iconSpaceReserved="false" />
android:title="@string/clear_thumbs_cache" />
<Preference
android:key="pages_cache_clear"
android:persistent="false"
android:summary="@string/computing_"
android:title="@string/clear_pages_cache"
app:iconSpaceReserved="false" />
android:title="@string/clear_pages_cache" />
</PreferenceCategory>
<Preference
android:key="cookies_clear"
android:persistent="false"
android:title="@string/clear_cookies"
app:iconSpaceReserved="false" />
android:title="@string/clear_cookies" />
</PreferenceScreen>

View File

@@ -1,102 +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="theme"
android:title="@string/theme"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="dynamic_theme"
android:summary="@string/dynamic_theme_summary"
android:title="@string/dynamic_theme"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="amoled_theme"
android:summary="@string/black_dark_theme_summary"
android:title="@string/black_dark_theme"
app:iconSpaceReserved="false" />
<ListPreference
android:key="date_format"
android:title="@string/date_format"
app:iconSpaceReserved="false" />
<ListPreference
android:entries="@array/list_modes"
android:key="list_mode_2"
android:title="@string/list_mode"
app:allowDividerAbove="true"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<org.koitharu.kotatsu.settings.utils.SliderPreference
android:key="grid_size"
android:stepSize="5"
android:title="@string/grid_size"
android:valueFrom="50"
android:valueTo="150"
app:defaultValue="100"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment"
android:key="remote_sources"
android:title="@string/remote_sources"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.SuggestionsSettingsFragment"
android:key="suggestions"
android:persistent="false"
android:title="@string/suggestions"
app:iconSpaceReserved="false" />
<Preference
android:key="local_storage"
android:title="@string/manga_save_location"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.HistorySettingsFragment"
android:title="@string/history_and_cache"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:key="protect_app"
android:persistent="false"
android:summary="@string/protect_application_summary"
android:title="@string/protect_application"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.ReaderSettingsFragment"
android:title="@string/reader_settings"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.TrackerSettingsFragment"
android:title="@string/check_for_new_chapters"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
android:title="@string/backup_restore"
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment"
android:title="@string/about"
app:iconSpaceReserved="false" />
</PreferenceScreen>

View File

@@ -1,23 +1,19 @@
<?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">
xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="notifications_sound"
android:title="@string/notification_sound"
app:iconSpaceReserved="false" />
android:title="@string/notification_sound" />
<CheckBoxPreference
android:defaultValue="false"
android:key="notifications_vibrate"
android:title="@string/vibration"
app:iconSpaceReserved="false" />
android:title="@string/vibration" />
<CheckBoxPreference
android:defaultValue="true"
android:key="notifications_light"
android:title="@string/light_indicator"
app:iconSpaceReserved="false" />
android:title="@string/light_indicator" />
</PreferenceScreen>

View File

@@ -7,14 +7,13 @@
android:defaultValue="false"
android:key="reader_prefer_rtl"
android:summary="@string/prefer_rtl_reader_summary"
android:title="@string/prefer_rtl_reader"
app:iconSpaceReserved="false" />
android:title="@string/prefer_rtl_reader" />
<ListPreference
android:entries="@array/zoom_modes"
android:key="zoom_mode"
android:title="@string/scale_mode"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<MultiSelectListPreference
@@ -23,20 +22,17 @@
android:entryValues="@array/values_reader_switchers"
android:key="reader_switchers"
android:title="@string/switch_pages"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
app:allowDividerAbove="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="reader_animation"
android:title="@string/pages_animation"
app:iconSpaceReserved="false" />
android:title="@string/pages_animation" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pages_numbers"
android:title="@string/show_pages_numbers"
app:iconSpaceReserved="false" />
android:title="@string/show_pages_numbers" />
<ListPreference
android:entries="@array/screenshots_policy"
@@ -44,7 +40,7 @@
android:key="screenshots_policy"
android:title="@string/screenshots_policy"
app:defaultValue="allow"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
<ListPreference
@@ -53,7 +49,7 @@
android:key="pages_preload"
android:title="@string/preload_pages"
app:defaultValue="2"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
</PreferenceScreen>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.AppearanceSettingsFragment"
android:icon="@drawable/ic_appearance"
android:key="appearance"
android:title="@string/appearance" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.ContentSettingsFragment"
android:icon="@drawable/ic_manga_source"
android:key="content"
android:title="@string/content" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.HistorySettingsFragment"
android:icon="@drawable/ic_history"
android:title="@string/history_and_cache" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.ReaderSettingsFragment"
android:icon="@drawable/ic_book_page"
android:title="@string/reader_settings" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.TrackerSettingsFragment"
android:icon="@drawable/ic_feed"
android:title="@string/check_for_new_chapters" />
<PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment"
android:icon="@drawable/ic_info_outline"
android:title="@string/about" />
</PreferenceScreen>

View File

@@ -5,10 +5,9 @@
<Preference
android:key="auth"
android:order="100"
android:persistent="false"
android:title="@string/sign_in"
android:order="100"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
app:allowDividerAbove="true" />
</PreferenceScreen>

View File

@@ -7,14 +7,18 @@
android:defaultValue="false"
android:key="suggestions"
android:summary="@string/suggestions_summary"
android:title="@string/suggestions_enable"
app:iconSpaceReserved="false" />
android:title="@string/suggestions_enable" />
<org.koitharu.kotatsu.settings.utils.MultiAutoCompleteTextViewPreference
android:dependency="suggestions"
android:key="suggestions_exclude_tags"
android:summary="@string/suggestions_excluded_genres_summary"
android:title="@string/suggestions_excluded_genres" />
<SwitchPreferenceCompat
android:dependency="suggestions"
android:key="suggestions_exclude_nsfw"
android:title="@string/exclude_nsfw_from_suggestions"
app:iconSpaceReserved="false" />
android:title="@string/exclude_nsfw_from_suggestions" />
<Preference
android:icon="@drawable/ic_info_outline"

View File

@@ -8,25 +8,23 @@
android:entries="@array/track_sources"
android:entryValues="@array/values_track_sources"
android:key="track_sources"
android:title="@string/track_sources"
app:iconSpaceReserved="false" />
android:title="@string/track_sources" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="tracker_notifications"
android:summary="@string/show_notification_new_chapters"
android:title="@string/notifications"
app:iconSpaceReserved="false" />
android:title="@string/notifications" />
<Preference
android:dependency="tracker_notifications"
android:fragment="org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment"
android:key="notifications_settings"
android:title="@string/notifications_settings"
app:iconSpaceReserved="false" />
android:title="@string/notifications_settings" />
<org.koitharu.kotatsu.settings.utils.LinksPreference
android:key="track_warning"
android:icon="@drawable/ic_info_outline"
android:key="track_warning"
android:persistent="false"
android:selectable="false"
android:summary="@string/tracker_warning"

View File

@@ -1,12 +1,13 @@
Kotatsu is a free and open source manga reader for Android.<br>
Main Features:
<ul>
<li>Online manga catalogues</li>
<li>Search manga by name and genre</li>
<li>Reading history</li>
<li>Favourites organized by user-defined categories</li>
<li>Downloading manga and reading it offline. Third-party CBZ archives also supported</li>
<li>Tablet-optimized material design UI</li>
<li>Standard and Webtoon-optimized reader</li>
<li>Notifications about new chapters with updates feed</li>
</ul>
**Main Features:**
- Online manga catalogues
- Search manga by name and genre
- Reading history
- Favourites organized by user-defined categories
- Downloading manga and reading it offline. Third-party CBZ archives also supported
- Tablet-optimized material design UI
- Standard and Webtoon-optimized reader
- Notifications about new chapters with updates feed
- Available in multiple languages
- Password protect access to the app

View File

@@ -1,12 +1,11 @@
Kotatsu - приложения для чтения манги с открытым исходным кодом.<br>
Основные возможности:
<ul>
<li>Онлайн каталоги с мангой</li>
<li>Поиск манги по имени и жанрам</li>
<li>История чтения</li>
<li>Избранное с пользовательскими категориями</li>
<li>Возможность сохранять мангу и читать её оффлайн. Поддержка сторонних комиксов в формате CBZ</li>
<li>Интерфейс также оптимизирован для планшетов</li>
<li>Поддержка манхвы (Webtoon)</li>
<li>Уведомления о новых главах и лента обновлений</li>
</ul>
**Основные возможности:**
- Онлайн каталоги с мангой
- Поиск манги по имени и жанрам
- История чтения
- Избранное с пользовательскими категориями
- Возможность сохранять мангу и читать её оффлайн. Поддержка сторонних комиксов в формате CBZ
- Интерфейс также оптимизирован для планшетов
- Поддержка манхвы (Webtoon)
- Уведомления о новых главах и лента обновлений