Compare commits

..

34 Commits

Author SHA1 Message Date
Koitharu
5d1a2fcf77 Statistics filters 2024-03-04 16:31:39 +02:00
Koitharu
876675445d Stats chart for single manga 2024-03-04 14:42:31 +02:00
Koitharu
f7a70680bd Timeline stats per manga 2024-03-01 15:00:38 +02:00
Koitharu
8e82db441c Empty stats state 2024-03-01 10:34:31 +02:00
Koitharu
f2626c668d Switch and click preference 2024-02-29 16:15:44 +02:00
Koitharu
4694215ccc Statistics periods 2024-02-29 15:28:57 +02:00
Koitharu
096f5b15dc Clearing stats 2024-02-29 14:27:52 +02:00
Koitharu
101d357eff Stats activity 2024-02-29 14:01:31 +02:00
Koitharu
11cd5609bb Use stats for reading time estimation 2024-02-29 12:12:09 +02:00
Koitharu
fda59996aa Improve stats ui 2024-02-29 12:01:09 +02:00
Koitharu
20461112d2 Merge branch 'devel' into feature/stats 2024-02-29 11:20:31 +02:00
Koitharu
f98bb87d6e Use numeric keyboard if app password is numeric 2024-02-29 11:20:10 +02:00
Koitharu
c451952a1e Merge branch 'devel' into feature/stats 2024-02-29 10:00:49 +02:00
Koitharu
f8cbc9692f Fix local manga directories chapters 2024-02-28 16:12:49 +02:00
Koitharu
9f3113363b Merge remote-tracking branch 'weblate/devel' into devel 2024-02-28 14:40:02 +02:00
Koitharu
dba36838d4 Download format preference 2024-02-28 14:28:59 +02:00
Koitharu
f6de1b02d7 Fix download item ui 2024-02-28 14:06:08 +02:00
abc0922001
d6b8e2fd9e Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
a
5227240478 Translated using Weblate (Portuguese)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: a <cooki3yt2004@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Infy's Tagalog Translations
8f65ea6535 Translated using Weblate (Filipino)
Currently translated at 99.8% (596 of 597 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Kyoya
7d7a6eadd2 Translated using Weblate (Turkish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Kyoya <thelol9181@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Scrambled777
40f1ad3181 Translated using Weblate (Hindi)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Anon
a28c9447d7 Translated using Weblate (Serbian)
Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-28 12:59:46 +01:00
gallegonovato
a84cf97982 Translated using Weblate (Spanish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Lokmane Abdelhakim Djilani
3a8eb58fd1 Translated using Weblate (Arabic)
Currently translated at 58.1% (347 of 597 strings)

Co-authored-by: Lokmane Abdelhakim Djilani <lokdabdo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
gekka
5d75e9af4a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Oğuz Ersen
d4684e7462 Translated using Weblate (Turkish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Çınar
c0a2f0b533 Translated using Weblate (Turkish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Çınar <cinardogan110@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Макар Разин
40867dd2b6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Koitharu
c3294e6459 Fix double pages mode enabling 2024-02-28 13:58:23 +02:00
Koitharu
5139feb51a Fix pages saving 2024-02-28 13:55:02 +02:00
Koitharu
35a2ac4b04 Simple reading stats display 2024-02-21 09:49:47 +02:00
Koitharu
f39ccb6223 Stats settings 2024-02-18 13:38:46 +02:00
Koitharu
6cb6c891dd Collecting reading stats 2024-02-18 13:11:41 +02:00
112 changed files with 2540 additions and 819 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 625
versionName = '6.7.3'
versionCode = 626
versionName = '6.7.4'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -126,8 +126,8 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.50'
kapt 'com.google.dagger:hilt-compiler:2.50'
implementation 'com.google.dagger:hilt-android:2.51'
kapt 'com.google.dagger:hilt-compiler:2.51'
implementation 'androidx.hilt:hilt-work:1.2.0'
kapt 'androidx.hilt:hilt-compiler:1.2.0'
@@ -160,6 +160,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.6.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.50'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.50'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51'
}

View File

@@ -239,6 +239,9 @@
<data android:scheme="kotatsu+kitsu" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.stats.ui.StatsActivity"
android:label="@string/reading_stats" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -54,6 +54,7 @@ class JsonDeserializer(private val json: JSONObject) {
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat(),
percent = json.getFloatOrDefault("percent", -1f),
chaptersCount = json.getIntOrDefault("chapters", -1),
deletedAt = 0L,
)

View File

@@ -41,6 +41,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("page", e.page)
put("scroll", e.scroll)
put("percent", e.percent)
put("chapters", e.chaptersCount)
},
)

View File

@@ -30,6 +30,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration14To15
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
import org.koitharu.kotatsu.core.db.migrations.Migration16To17
import org.koitharu.kotatsu.core.db.migrations.Migration17To18
import org.koitharu.kotatsu.core.db.migrations.Migration18To19
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -48,20 +49,22 @@ import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingDao
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.stats.data.StatsDao
import org.koitharu.kotatsu.stats.data.StatsEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 18
const val DATABASE_VERSION = 19
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class,
],
version = DATABASE_VERSION,
)
@@ -90,6 +93,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun getScrobblingDao(): ScrobblingDao
abstract fun getSourcesDao(): MangaSourcesDao
abstract fun getStatsDao(): StatsDao
}
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
@@ -110,6 +115,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration15To16(),
Migration16To17(context),
Migration17To18(),
Migration18To19(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration18To19 : Migration(18, 19) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE history ADD COLUMN `chapters` INTEGER NOT NULL DEFAULT -1")
db.execSQL("CREATE TABLE IF NOT EXISTS `stats` (`manga_id` INTEGER NOT NULL, `started_at` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `pages` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `started_at`), FOREIGN KEY(`manga_id`) REFERENCES `history`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
}
}

View File

@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.isNumeric
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
@@ -191,11 +192,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit {
if (value != null) putString(KEY_APP_PASSWORD, value) else remove(
KEY_APP_PASSWORD,
)
if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD)
}
var isAppPasswordNumeric: Boolean
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
val isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
@@ -277,6 +280,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isDownloadsWiFiOnly: Boolean
get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
val preferredDownloadFormat: DownloadFormat
get() = prefs.getEnumValue(KEY_DOWNLOADS_FORMAT, DownloadFormat.AUTOMATIC)
var isSuggestionsEnabled: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) }
@@ -416,6 +422,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isPagesSavingAskEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_SAVE_ASK, true)
val isStatsEnabled: Boolean
get() = prefs.getBoolean(KEY_STATS_ENABLED, false)
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -525,6 +534,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password"
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
const val KEY_PROTECT_APP = "protect_app"
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
const val KEY_APP_VERSION = "app_version"
@@ -552,6 +562,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_MAL = "mal"
const val KEY_KITSU = "kitsu"
const val KEY_DOWNLOADS_WIFI = "downloads_wifi"
const val KEY_DOWNLOADS_FORMAT = "downloads_format"
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
const val KEY_DOH = "doh"
const val KEY_EXIT_CONFIRM = "exit_confirm"
@@ -606,8 +617,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READING_TIME = "reading_time"
const val KEY_PAGES_SAVE_DIR = "pages_dir"
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
// About
const val KEY_STATS_ENABLED = "stats_on"
const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_TRANSLATION = "about_app_translation"
}

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.core.prefs
enum class DownloadFormat {
AUTOMATIC,
SINGLE_CBZ,
MULTIPLE_CBZ,
}

View File

@@ -29,8 +29,9 @@ open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
return this
}
fun addListListener(listListener: ListListener<T>) {
fun addListListener(listListener: ListListener<T>): BaseListAdapter<T> {
differ.addListListener(listListener)
return this
}
fun removeListListener(listListener: ListListener<T>) {

View File

@@ -68,6 +68,13 @@ abstract class BaseViewModel : ViewModel() {
errorEvent.call(error)
}
protected inline suspend fun <T> withLoading(block: () -> T): T = try {
loadingCounter.increment()
block()
} finally {
loadingCounter.decrement()
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }

View File

@@ -12,12 +12,10 @@ import android.graphics.RectF
import android.graphics.drawable.Drawable
import androidx.annotation.StyleRes
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.withClip
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.Colors
import kotlin.math.absoluteValue
import org.koitharu.kotatsu.core.util.KotatsuColors
class FaviconDrawable(
context: Context,
@@ -45,7 +43,7 @@ class FaviconDrawable(
}
paint.textAlign = Paint.Align.CENTER
paint.isFakeBoldText = true
colorForeground = MaterialColors.harmonize(Colors.random(name), colorBackground)
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
}
override fun draw(canvas: Canvas) {

View File

@@ -110,7 +110,7 @@ class ChipsView @JvmOverloads constructor(
chip.isChipIconVisible = false
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setEnsureMinTouchTargetSize(false) // TODO remove
chip.setOnClickListener(chipOnClickListener)
addView(chip)
return chip

View File

@@ -1,397 +0,0 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.CornerPathEffect
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build
import android.os.Parcelable
import android.text.Layout
import android.text.StaticLayout
import android.text.TextDirectionHeuristic
import android.text.TextDirectionHeuristics
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import androidx.annotation.RequiresApi
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.draw
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.resolveSp
class PieChart @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), PieChartInterface {
private var marginTextFirst: Float = context.resources.resolveDp(DEFAULT_MARGIN_TEXT_1)
private var marginTextSecond: Float = context.resources.resolveDp(DEFAULT_MARGIN_TEXT_2)
private var marginTextThird: Float = context.resources.resolveDp(DEFAULT_MARGIN_TEXT_3)
private var marginSmallCircle: Float = context.resources.resolveDp(DEFAULT_MARGIN_SMALL_CIRCLE)
private val marginText: Float = marginTextFirst + marginTextSecond
private val circleRect = RectF()
private var circleStrokeWidth: Float = context.resources.resolveDp(6f)
private var circleRadius: Float = 0f
private var circlePadding: Float = context.resources.resolveDp(8f)
private var circlePaintRoundSize: Boolean = true
private var circleSectionSpace: Float = 3f
private var circleCenterX: Float = 0f
private var circleCenterY: Float = 0f
private var numberTextPaint: TextPaint = TextPaint()
private var descriptionTextPain: TextPaint = TextPaint()
private var amountTextPaint: TextPaint = TextPaint()
private var textStartX: Float = 0f
private var textStartY: Float = 0f
private var textHeight: Int = 0
private var textCircleRadius: Float = context.resources.resolveDp(4f)
private var textAmountStr: String = ""
private var textAmountY: Float = 0f
private var textAmountXNumber: Float = 0f
private var textAmountXDescription: Float = 0f
private var textAmountYDescription: Float = 0f
private var totalAmount: Int = 0
private var pieChartColors: List<String> = listOf()
private var percentageCircleList: List<PieChartModel> = listOf()
private var textRowList: MutableList<StaticLayout> = mutableListOf()
private var dataList: List<Pair<Int, String>> = listOf()
private var animationSweepAngle: Int = 0
init {
var textAmountSize: Float = context.resources.resolveSp(22f)
var textNumberSize: Float = context.resources.resolveSp(20f)
var textDescriptionSize: Float = context.resources.resolveSp(14f)
var textAmountColor: Int = Color.WHITE
var textNumberColor: Int = Color.WHITE
var textDescriptionColor: Int = Color.GRAY
if (attrs != null) {
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.PieChart)
val colorResId = typeArray.getResourceId(R.styleable.PieChart_pieChartColors, 0)
pieChartColors = typeArray.resources.getStringArray(colorResId).toList()
marginTextFirst = typeArray.getDimension(R.styleable.PieChart_pieChartMarginTextFirst, marginTextFirst)
marginTextSecond = typeArray.getDimension(R.styleable.PieChart_pieChartMarginTextSecond, marginTextSecond)
marginTextThird = typeArray.getDimension(R.styleable.PieChart_pieChartMarginTextThird, marginTextThird)
marginSmallCircle =
typeArray.getDimension(R.styleable.PieChart_pieChartMarginSmallCircle, marginSmallCircle)
circleStrokeWidth =
typeArray.getDimension(R.styleable.PieChart_pieChartCircleStrokeWidth, circleStrokeWidth)
circlePadding = typeArray.getDimension(R.styleable.PieChart_pieChartCirclePadding, circlePadding)
circlePaintRoundSize =
typeArray.getBoolean(R.styleable.PieChart_pieChartCirclePaintRoundSize, circlePaintRoundSize)
circleSectionSpace = typeArray.getFloat(R.styleable.PieChart_pieChartCircleSectionSpace, circleSectionSpace)
textCircleRadius = typeArray.getDimension(R.styleable.PieChart_pieChartTextCircleRadius, textCircleRadius)
textAmountSize = typeArray.getDimension(R.styleable.PieChart_pieChartTextAmountSize, textAmountSize)
textNumberSize = typeArray.getDimension(R.styleable.PieChart_pieChartTextNumberSize, textNumberSize)
textDescriptionSize =
typeArray.getDimension(R.styleable.PieChart_pieChartTextDescriptionSize, textDescriptionSize)
textAmountColor = typeArray.getColor(R.styleable.PieChart_pieChartTextAmountColor, textAmountColor)
textNumberColor = typeArray.getColor(R.styleable.PieChart_pieChartTextNumberColor, textNumberColor)
textDescriptionColor =
typeArray.getColor(R.styleable.PieChart_pieChartTextDescriptionColor, textDescriptionColor)
textAmountStr = typeArray.getString(R.styleable.PieChart_pieChartTextAmount) ?: ""
typeArray.recycle()
}
circlePadding += circleStrokeWidth
// Инициализация кистей View
initPaints(amountTextPaint, textAmountSize, textAmountColor)
initPaints(numberTextPaint, textNumberSize, textNumberColor)
initPaints(descriptionTextPain, textDescriptionSize, textDescriptionColor, true)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
textRowList.clear()
val initSizeWidth = resolveDefaultSize(widthMeasureSpec, DEFAULT_VIEW_SIZE_WIDTH)
val textTextWidth = (initSizeWidth * TEXT_WIDTH_PERCENT)
val initSizeHeight = calculateViewHeight(heightMeasureSpec, textTextWidth.toInt())
textStartX = initSizeWidth - textTextWidth.toFloat()
textStartY = initSizeHeight.toFloat() / 2 - textHeight / 2
calculateCircleRadius(initSizeWidth, initSizeHeight)
setMeasuredDimension(initSizeWidth, initSizeHeight)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawCircle(canvas)
drawText(canvas)
}
override fun onRestoreInstanceState(state: Parcelable?) {
val pieChartState = state as? PieChartState
super.onRestoreInstanceState(pieChartState?.superState ?: state)
dataList = pieChartState?.dataList ?: listOf()
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
return PieChartState(superState, dataList)
}
override fun setDataChart(list: List<Pair<Int, String>>) {
dataList = list
calculatePercentageOfData()
}
override fun startAnimation() {
val animator = ValueAnimator.ofInt(0, 360).apply {
duration = context.getAnimationDuration(android.R.integer.config_longAnimTime)
interpolator = FastOutSlowInInterpolator()
addUpdateListener { valueAnimator ->
animationSweepAngle = valueAnimator.animatedValue as Int
invalidate()
}
}
animator.start()
}
private fun drawCircle(canvas: Canvas) {
for (percent in percentageCircleList) {
if (animationSweepAngle > percent.percentToStartAt + percent.percentOfCircle) {
canvas.drawArc(circleRect, percent.percentToStartAt, percent.percentOfCircle, false, percent.paint)
} else if (animationSweepAngle > percent.percentToStartAt) {
canvas.drawArc(
circleRect,
percent.percentToStartAt,
animationSweepAngle - percent.percentToStartAt,
false,
percent.paint,
)
}
}
}
private fun drawText(canvas: Canvas) {
var textBuffY = textStartY
textRowList.forEachIndexed { index, staticLayout ->
if (index % 2 == 0) {
staticLayout.draw(canvas, textStartX + marginSmallCircle + textCircleRadius, textBuffY)
canvas.drawCircle(
textStartX + marginSmallCircle / 2,
textBuffY + staticLayout.height / 2 + textCircleRadius / 2,
textCircleRadius,
Paint().apply { color = Color.parseColor(pieChartColors[(index / 2) % pieChartColors.size]) },
)
textBuffY += staticLayout.height + marginTextFirst
} else {
staticLayout.draw(canvas, textStartX, textBuffY)
textBuffY += staticLayout.height + marginTextSecond
}
}
canvas.drawText(totalAmount.toString(), textAmountXNumber, textAmountY, amountTextPaint)
canvas.drawText(textAmountStr, textAmountXDescription, textAmountYDescription, descriptionTextPain)
}
private fun initPaints(textPaint: TextPaint, textSize: Float, textColor: Int, isDescription: Boolean = false) {
textPaint.color = textColor
textPaint.textSize = textSize
textPaint.isAntiAlias = true
if (!isDescription) textPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
}
private fun resolveDefaultSize(spec: Int, defValue: Int): Int {
return when (MeasureSpec.getMode(spec)) {
MeasureSpec.UNSPECIFIED -> resources.resolveDp(defValue)
else -> MeasureSpec.getSize(spec)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun calculateViewHeight(heightMeasureSpec: Int, textWidth: Int): Int {
val initSizeHeight = resolveDefaultSize(heightMeasureSpec, DEFAULT_VIEW_SIZE_HEIGHT)
textHeight = (dataList.size * marginText + getTextViewHeight(textWidth)).toInt()
val textHeightWithPadding = textHeight + paddingTop + paddingBottom
return if (textHeightWithPadding > initSizeHeight) textHeightWithPadding else initSizeHeight
}
private fun calculateCircleRadius(width: Int, height: Int) {
val circleViewWidth = (width * CIRCLE_WIDTH_PERCENT)
circleRadius = if (circleViewWidth > height) {
(height.toFloat() - circlePadding) / 2
} else {
circleViewWidth.toFloat() / 2
}
with(circleRect) {
left = circlePadding
top = height / 2 - circleRadius
right = circleRadius * 2 + circlePadding
bottom = height / 2 + circleRadius
}
circleCenterX = (circleRadius * 2 + circlePadding + circlePadding) / 2
circleCenterY = (height / 2 + circleRadius + (height / 2 - circleRadius)) / 2
textAmountY = circleCenterY
val sizeTextAmountNumber = getWidthOfAmountText(
totalAmount.toString(),
amountTextPaint,
)
textAmountXNumber = circleCenterX - sizeTextAmountNumber.width() / 2
textAmountXDescription = circleCenterX - getWidthOfAmountText(textAmountStr, descriptionTextPain).width() / 2
textAmountYDescription = circleCenterY + sizeTextAmountNumber.height() + marginTextThird
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getTextViewHeight(maxWidth: Int): Int {
var textHeight = 0
dataList.forEach {
val textLayoutNumber = getMultilineText(
text = it.first.toString(),
textPaint = numberTextPaint,
width = maxWidth,
)
val textLayoutDescription = getMultilineText(
text = it.second,
textPaint = descriptionTextPain,
width = maxWidth,
)
textRowList.apply {
add(textLayoutNumber)
add(textLayoutDescription)
}
textHeight += textLayoutNumber.height + textLayoutDescription.height
}
return textHeight
}
private fun calculatePercentageOfData() {
totalAmount = dataList.fold(0) { res, value -> res + value.first }
var startAt = circleSectionSpace
percentageCircleList = dataList.mapIndexed { index, pair ->
var percent = pair.first * 100 / totalAmount.toFloat() - circleSectionSpace
percent = if (percent < 0f) 0f else percent
val resultModel = PieChartModel(
percentOfCircle = percent,
percentToStartAt = startAt,
colorOfLine = Color.parseColor(pieChartColors[index % pieChartColors.size]),
stroke = circleStrokeWidth,
paintRound = circlePaintRoundSize,
)
if (percent != 0f) startAt += percent + circleSectionSpace
resultModel
}
}
private fun getWidthOfAmountText(text: String, textPaint: TextPaint): Rect {
val bounds = Rect()
textPaint.getTextBounds(text, 0, text.length, bounds)
return bounds
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getMultilineText(
text: CharSequence,
textPaint: TextPaint,
width: Int,
start: Int = 0,
end: Int = text.length,
alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
textDir: TextDirectionHeuristic = TextDirectionHeuristics.LTR,
spacingMult: Float = 1f,
spacingAdd: Float = 0f
): StaticLayout {
return StaticLayout.Builder
.obtain(text, start, end, textPaint, width)
.setAlignment(alignment)
.setTextDirection(textDir)
.setLineSpacing(spacingAdd, spacingMult)
.build()
}
companion object {
private const val DEFAULT_MARGIN_TEXT_1 = 2f
private const val DEFAULT_MARGIN_TEXT_2 = 10f
private const val DEFAULT_MARGIN_TEXT_3 = 2f
private const val DEFAULT_MARGIN_SMALL_CIRCLE = 12f
private const val TEXT_WIDTH_PERCENT = 0.40
private const val CIRCLE_WIDTH_PERCENT = 0.50
const val DEFAULT_VIEW_SIZE_HEIGHT = 150
const val DEFAULT_VIEW_SIZE_WIDTH = 250
}
}
interface PieChartInterface {
fun setDataChart(list: List<Pair<Int, String>>)
fun startAnimation()
}
data class PieChartModel(
var percentOfCircle: Float = 0f,
var percentToStartAt: Float = 0f,
var colorOfLine: Int = 0,
var stroke: Float = 0f,
var paint: Paint = Paint(),
var paintRound: Boolean = true
) {
init {
if (percentOfCircle < 0 || percentOfCircle > 100) {
percentOfCircle = 100f
}
percentOfCircle = 360 * percentOfCircle / 100
if (percentToStartAt < 0 || percentToStartAt > 100) {
percentToStartAt = 0f
}
percentToStartAt = 360 * percentToStartAt / 100
if (colorOfLine == 0) {
colorOfLine = Color.parseColor("#000000")
}
paint = Paint()
paint.color = colorOfLine
paint.isAntiAlias = true
paint.style = Paint.Style.STROKE
paint.strokeWidth = stroke
paint.isDither = true
if (paintRound) {
paint.strokeJoin = Paint.Join.ROUND
paint.strokeCap = Paint.Cap.ROUND
paint.pathEffect = CornerPathEffect(8f)
}
}
}
class PieChartState(
superSavedState: Parcelable?,
val dataList: List<Pair<Int, String>>
) : View.BaseSavedState(superSavedState), Parcelable

View File

@@ -7,9 +7,10 @@ import androidx.core.graphics.ColorUtils
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.math.absoluteValue
object Colors {
object KotatsuColors {
@ColorInt
fun segmentColor(context: Context, @AttrRes resId: Int): Int {
@@ -20,11 +21,24 @@ object Colors {
return MaterialColors.harmonize(color, backgroundColor)
}
@ColorInt
fun random(seed: Any): Int {
val hue = (seed.hashCode() % 360).absoluteValue.toFloat()
return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
}
@ColorInt
fun ofManga(context: Context, manga: Manga?): Int {
val color = if (manga != null) {
val hue = (manga.id.absoluteValue % 360).toFloat()
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
} else {
context.getThemeColor(R.attr.colorSurface)
}
val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh)
return MaterialColors.harmonize(color, backgroundColor)
}
private fun getHue(hex: String): Float {
val r = (hex.substring(0, 2).toInt(16)).toFloat()
val g = (hex.substring(2, 4).toInt(16)).toFloat()

View File

@@ -19,6 +19,7 @@ import java.nio.file.attribute.BasicFileAttributes
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.PathWalkOption
import kotlin.io.path.readAttributes
import kotlin.io.path.walk
@@ -72,7 +73,7 @@ fun ContentResolver.resolveName(uri: Uri): String? {
}
suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
walkCompat().sumOf { it.length() }
walkCompat(includeDirectories = false).sumOf { it.length() }
}
fun File.children() = FileSequence(this)
@@ -87,10 +88,16 @@ val File.creationTime
}
@OptIn(ExperimentalPathApi::class)
fun File.walkCompat() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
fun File.walkCompat(includeDirectories: Boolean): Sequence<File> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Use lazy loading on Android 8.0 and later
toPath().walk().map { it.toFile() }
val walk = if (includeDirectories) {
toPath().walk(PathWalkOption.INCLUDE_DIRECTORIES)
} else {
toPath().walk()
}
walk.map { it.toFile() }
} else {
// Directories are excluded by default in Path.walk(), so do it here as well
walk().filter { it.isFile }
val walk = walk()
if (includeDirectories) walk else walk.filter { it.isFile }
}

View File

@@ -10,6 +10,7 @@ data class ReadingTime(
) {
fun format(resources: Resources): String = when {
hours == 0 && minutes == 0 -> resources.getString(R.string.less_than_minute)
hours == 0 -> resources.getQuantityString(R.plurals.minutes, minutes, minutes)
minutes == 0 -> resources.getQuantityString(R.plurals.hours, hours, hours)
else -> resources.getString(

View File

@@ -5,25 +5,27 @@ import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.stats.data.StatsRepository
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.roundToInt
class ReadingTimeUseCase @Inject constructor(
private val settings: AppSettings,
private val statsRepository: StatsRepository,
) {
fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? {
suspend fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? {
if (!settings.isReadingTimeEstimationEnabled) {
return null
}
// FIXME MAXIMUM HARDCODE!!! To do calculation with user's page read speed and his favourites/history mangas average pages in chapter
val chapters = manga?.chapters?.get(branch)
if (chapters.isNullOrEmpty()) {
return null
}
val isOnHistoryBranch = history != null && chapters.findById(history.chapterId) != null
// Impossible task, I guess. Good luck on this.
var averageTimeSec: Int = 20 * 10 * chapters.size // 20 pages, 10 seconds per page
var averageTimeSec: Int = 20 /* pages */ * getSecondsPerPage(manga.id) * chapters.size
if (isOnHistoryBranch) {
averageTimeSec = (averageTimeSec * (1f - checkNotNull(history).percent)).roundToInt()
}
@@ -36,4 +38,16 @@ class ReadingTimeUseCase @Inject constructor(
isContinue = isOnHistoryBranch,
)
}
private suspend fun getSecondsPerPage(mangaId: Long): Int {
var time = if (settings.isStatsEnabled) {
TimeUnit.MILLISECONDS.toSeconds(statsRepository.getTimePerPage(mangaId)).toInt()
} else {
0
}
if (time == 0) {
time = 10 // default
}
return time
}
}

View File

@@ -138,7 +138,10 @@ class DetailsActivity :
},
),
)
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom))
viewModel.onActionDone.observeEvent(
this,
ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom),
)
viewModel.onShowTip.observeEvent(this) { showTip() }
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranch.observe(this) {
@@ -150,6 +153,7 @@ class DetailsActivity :
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
val menuInvalidator = MenuInvalidator(this)
viewModel.favouriteCategories.observe(this, menuInvalidator)
viewModel.isStatsEnabled.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
class DetailsMenuProvider(
private val activity: FragmentActivity,
@@ -43,6 +44,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsEnabled.value
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
@@ -101,6 +103,12 @@ class DetailsMenuProvider(
}
}
R.id.action_stats -> {
viewModel.manga.value?.let {
MangaStatsSheet.show(activity.supportFragmentManager, it)
}
}
R.id.action_scrobbling -> {
viewModel.manga.value?.let {
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)

View File

@@ -100,6 +100,10 @@ class DetailsViewModel @Inject constructor(
val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val isStatsEnabled = settings.observeAsStateFlow(viewModelScope + Dispatchers.Default, AppSettings.KEY_STATS_ENABLED) {
isStatsEnabled
}
val remoteManga = MutableStateFlow<Manga?>(null)
val newChaptersCount = details.flatMapLatest { d ->

View File

@@ -91,6 +91,7 @@ class DownloadWorker @AssistedInject constructor(
private val localMangaRepository: LocalMangaRepository,
private val mangaDataRepository: MangaDataRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val settings: AppSettings,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
notificationFactoryFactory: DownloadNotificationFactory.Factory,
) : CoroutineWorker(appContext, params) {
@@ -182,7 +183,7 @@ class DownloadWorker @AssistedInject constructor(
}
val repo = mangaRepositoryFactory.create(manga.source)
val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, mangaDetails)
output = LocalMangaOutput.getOrCreate(destination, mangaDetails, settings.preferredDownloadFormat)
val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
if (coverUrl.isNotEmpty()) {
downloadFile(coverUrl, destination, repo.source).let { file ->

View File

@@ -15,8 +15,8 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE,
)
]
),
],
)
data class HistoryEntity(
@PrimaryKey(autoGenerate = false)
@@ -28,4 +28,5 @@ data class HistoryEntity(
@ColumnInfo(name = "scroll") val scroll: Float,
@ColumnInfo(name = "percent") val percent: Float,
@ColumnInfo(name = "deleted_at") val deletedAt: Long,
@ColumnInfo(name = "chapters") val chaptersCount: Int,
)

View File

@@ -94,6 +94,7 @@ class HistoryRepository @Inject constructor(
if (!force && shouldSkip(manga)) {
return
}
assert(manga.chapters != null)
db.withTransaction {
mangaRepository.storeManga(manga)
db.getHistoryDao().upsert(
@@ -105,6 +106,7 @@ class HistoryRepository @Inject constructor(
page = page,
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
percent = percent,
chaptersCount = manga.chapters?.size ?: -1,
deletedAt = 0L,
),
)

View File

@@ -11,7 +11,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
@@ -27,6 +29,7 @@ class HistoryListFragment : MangaListFragment() {
super.onViewBindingCreated(binding, savedInstanceState)
RecyclerScrollKeeper(binding.recyclerView).attach()
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
}
override fun onScrolledToEnd() = Unit

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.history.ui
import android.content.Context
import android.content.Intent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
@@ -9,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.stats.ui.StatsActivity
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
@@ -24,6 +26,11 @@ class HistoryListMenuProvider(
menuInflater.inflate(R.menu.opt_history, menu)
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_stats)?.isVisible = viewModel.isStatsEnabled.value
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_clear_history -> {
@@ -31,6 +38,11 @@ class HistoryListMenuProvider(
true
}
R.id.action_stats -> {
context.startActivity(Intent(context, StatsActivity::class.java))
true
}
else -> false
}
}

View File

@@ -71,6 +71,12 @@ class HistoryListViewModel @Inject constructor(
g && s.isGroupingSupported()
}
val isStatsEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_STATS_ENABLED,
valueProducer = { isStatsEnabled },
)
override val content = combine(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
isGroupingEnabled,

View File

@@ -18,3 +18,5 @@ fun File.hasCbzExtension() = isCbzExtension(extension)
fun Uri.isZipUri() = scheme.let {
it == URI_SCHEME_ZIP || it == "cbz" || it == "zip"
}
fun Uri.isFileUri() = scheme == "file"

View File

@@ -5,6 +5,7 @@ import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.creationTime
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.toListSorted
@@ -100,8 +101,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
val file = chapter.url.toUri().toFile()
if (file.isDirectory) {
file.walkCompat()
.filter { hasImageExtension(it) }
file.children()
.filter { it.isFile && hasImageExtension(it) }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map {
val pageUri = it.toUri().toString()
@@ -129,14 +130,16 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
private fun getChaptersFiles() = root.walkCompat()
.filter { it.hasCbzExtension() }
private fun getChaptersFiles() = root.walkCompat(includeDirectories = true)
.filter { it != root && it.isChapterDirectory() || it.hasCbzExtension() }
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
private fun findFirstImageEntry(): String? {
return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
return root.walkCompat(includeDirectories = false)
.firstOrNull { hasImageExtension(it) }?.toUri()?.toString()
?: run {
val cbz = root.walkCompat().firstOrNull { it.hasCbzExtension() } ?: return null
val cbz = root.walkCompat(includeDirectories = false)
.firstOrNull { it.hasCbzExtension() } ?: return null
ZipFile(cbz).use { zip ->
zip.entries().asSequence()
.firstOrNull { !it.isDirectory && hasImageExtension(it.name) }
@@ -148,4 +151,8 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
private fun fileUri(base: File, name: String): String {
return File(base, name).toUri().toString()
}
private fun File.isChapterDirectory(): Boolean {
return isDirectory && children().any { hasImageExtension(it) }
}
}

View File

@@ -4,7 +4,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.internal.format
import okio.Closeable
import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.parsers.model.Manga
@@ -35,22 +37,32 @@ sealed class LocalMangaOutput(
const val SUFFIX_TMP = ".tmp"
private val mutex = Mutex()
suspend fun getOrCreate(root: File, manga: Manga): LocalMangaOutput = withContext(Dispatchers.IO) {
val preferSingleCbz = manga.chapters.let {
it != null && it.size <= 3
suspend fun getOrCreate(
root: File,
manga: Manga,
format: DownloadFormat,
): LocalMangaOutput = withContext(Dispatchers.IO) {
val targetFormat = if (format == DownloadFormat.AUTOMATIC) {
if (manga.chapters.let { it != null && it.size <= 3 }) {
DownloadFormat.SINGLE_CBZ
} else {
DownloadFormat.MULTIPLE_CBZ
}
} else {
format
}
checkNotNull(getImpl(root, manga, onlyIfExists = false, preferSingleCbz))
checkNotNull(getImpl(root, manga, onlyIfExists = false, format = targetFormat))
}
suspend fun get(root: File, manga: Manga): LocalMangaOutput? = withContext(Dispatchers.IO) {
getImpl(root, manga, onlyIfExists = true, preferSingleCbz = false)
getImpl(root, manga, onlyIfExists = true, format = DownloadFormat.AUTOMATIC)
}
private suspend fun getImpl(
root: File,
manga: Manga,
onlyIfExists: Boolean,
preferSingleCbz: Boolean,
format: DownloadFormat,
): LocalMangaOutput? {
mutex.withLock {
var i = 0
@@ -75,10 +87,10 @@ sealed class LocalMangaOutput(
continue
}
!onlyIfExists -> if (preferSingleCbz) {
LocalMangaZipOutput(zip, manga)
} else {
LocalMangaDirOutput(dir, manga)
!onlyIfExists -> when (format) {
DownloadFormat.AUTOMATIC -> null
DownloadFormat.SINGLE_CBZ -> LocalMangaZipOutput(zip, manga)
DownloadFormat.MULTIPLE_CBZ -> LocalMangaDirOutput(dir, manga)
}
else -> null

View File

@@ -2,18 +2,11 @@ package org.koitharu.kotatsu.local.ui.info
import android.content.res.ColorStateList
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.core.graphics.ColorUtils
import androidx.core.widget.TextViewCompat
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
@@ -21,16 +14,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.settings.userdata.StorageUsage
import com.google.android.material.R as materialR
@AndroidEntryPoint
@@ -67,7 +57,7 @@ class LocalInfoDialog : AlertDialogFragment<DialogLocalInfoBinding>() {
val total = size + available
val segment = SegmentedBarView.Segment(
percent = (size.toDouble() / total.toDouble()).toFloat(),
color = Colors.segmentColor(view.context, materialR.attr.colorPrimary),
color = KotatsuColors.segmentColor(view.context, materialR.attr.colorPrimary),
)
requireViewBinding().labelUsed.text = view.context.getString(
R.string.memory_usage_pattern,

View File

@@ -44,6 +44,12 @@ class ProtectActivity :
viewBinding.buttonNext.setOnClickListener(this)
viewBinding.buttonCancel.setOnClickListener(this)
viewBinding.editPassword.inputType = if (viewModel.isNumericPassword) {
EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD
} else {
EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
viewModel.onError.observeEvent(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.onUnlockSuccess.observeEvent(this) {

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.parsers.util.isNumeric
import org.koitharu.kotatsu.parsers.util.md5
import javax.inject.Inject
@@ -26,6 +27,9 @@ class ProtectViewModel @Inject constructor(
val isBiometricEnabled
get() = settings.isBiometricProtectionEnabled
val isNumericPassword
get() = settings.isAppPasswordNumeric
fun tryUnlock(password: String) {
if (job?.isActive == true) {
return

View File

@@ -47,6 +47,7 @@ import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -203,20 +204,23 @@ class PageLoader @Inject constructor(
val pageUrl = getPageUrl(page)
check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" }
val uri = Uri.parse(pageUrl)
return if (uri.isZipUri()) {
if (uri.scheme == URI_SCHEME_ZIP) {
return when {
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {
uri
} else { // legacy uri
uri.buildUpon().scheme(URI_SCHEME_ZIP).build()
}
} else {
val request = createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
cache.put(pageUrl, it.source())
}
}.toUri()
uri.isFileUri() -> uri
else -> {
val request = createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
cache.put(pageUrl, it.source())
}
}.toUri()
}
}
}

View File

@@ -14,6 +14,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
@@ -74,6 +75,7 @@ class ReaderActivity :
ReaderControlDelegate.OnInteractionListener,
OnApplyWindowInsetsListener,
IdlingDetector.Callback,
ActivityResultCallback<Uri?>,
ZoomControl.ZoomControlListener {
@Inject
@@ -83,6 +85,7 @@ class ReaderActivity :
lateinit var tapGridSettings: TapGridSettings
private val idlingDetector = IdlingDetector(TimeUnit.SECONDS.toMillis(10), this)
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private val viewModel: ReaderViewModel by viewModels()
@@ -158,6 +161,10 @@ class ReaderActivity :
viewBinding.toolbarBottom.addMenuProvider(ReaderBottomMenuProvider(this, readerManager, viewModel))
}
override fun onActivityResult(result: Uri?) {
viewModel.onActivityResult(result)
}
override fun getParentActivityIntent(): Intent? {
val manga = viewModel.manga?.toManga() ?: return null
return DetailsActivity.newIntent(this, manga)
@@ -169,6 +176,11 @@ class ReaderActivity :
idlingDetector.onUserInteraction()
}
override fun onPause() {
super.onPause()
viewModel.onPause()
}
override fun onIdle() {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
}
@@ -371,6 +383,11 @@ class ReaderActivity :
return reader.isResumed && supportFragmentManager.fragments.lastOrNull() === reader
}
override fun onSavePageClick() {
val page = viewModel.getCurrentPage() ?: return
viewModel.saveCurrentPage(page, savePageRequest)
}
private fun onReaderBarChanged(isBarEnabled: Boolean) {
viewBinding.infoBar.isVisible = isBarEnabled && viewBinding.appbarTop.isGone
}

View File

@@ -25,8 +25,7 @@ class ReaderManager(
private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)
init {
val useDoublePages = container.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& settings.isReaderDoubleOnLandscape
val useDoublePages = isLandscape() && settings.isReaderDoubleOnLandscape
invalidateTypesMap(useDoublePages)
}
@@ -49,7 +48,7 @@ class ReaderManager(
fun setDoubleReaderMode(isEnabled: Boolean) {
val prevMode = currentMode
invalidateTypesMap(isEnabled)
invalidateTypesMap(isEnabled && isLandscape())
val newMode = currentMode ?: return
if (newMode != prevMode) {
replace(newMode)
@@ -70,4 +69,6 @@ class ReaderManager(
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
modeMap[ReaderMode.VERTICAL] = VerticalReaderFragment::class.java
}
private fun isLandscape() = container.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}

View File

@@ -58,6 +58,7 @@ import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.stats.domain.StatsCollector
import java.time.Instant
import javax.inject.Inject
@@ -78,11 +79,11 @@ class ReaderViewModel @Inject constructor(
private val detailsLoadUseCase: DetailsLoadUseCase,
private val historyUpdateUseCase: HistoryUpdateUseCase,
private val detectReaderModeUseCase: DetectReaderModeUseCase,
private val statsCollector: StatsCollector,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
private val preselectedBranch = savedStateHandle.get<String>(ReaderActivity.EXTRA_BRANCH)
private val isIncognito = savedStateHandle.get<Boolean>(ReaderActivity.EXTRA_INCOGNITO) ?: false
private var loadingJob: Job? = null
private var pageSaveJob: Job? = null
@@ -98,7 +99,7 @@ class ReaderViewModel @Inject constructor(
val onShowToast = MutableEventFlow<Int>()
val uiState = MutableStateFlow<ReaderUiState?>(null)
val incognitoMode = if (isIncognito) {
val incognitoMode = if (savedStateHandle.get<Boolean>(ReaderActivity.EXTRA_INCOGNITO) == true) {
MutableStateFlow(true)
} else mangaFlow.map {
it != null && historyRepository.shouldSkip(it)
@@ -190,6 +191,12 @@ class ReaderViewModel @Inject constructor(
loadImpl()
}
fun onPause() {
manga?.let {
statsCollector.onPause(it.id)
}
}
fun switchMode(newMode: ReaderMode) {
launchJob {
val manga = checkNotNull(mangaData.value?.toManga())
@@ -208,7 +215,7 @@ class ReaderViewModel @Inject constructor(
if (state != null) {
currentState.value = state
}
if (isIncognito) {
if (incognitoMode.value) {
return
}
val readerState = state ?: currentState.value ?: return
@@ -377,7 +384,7 @@ class ReaderViewModel @Inject constructor(
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
// save state
if (!isIncognito) {
if (!incognitoMode.value) {
currentState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase.invoke(manga, it, percent)
@@ -426,6 +433,9 @@ class ReaderViewModel @Inject constructor(
percent = computePercent(state.chapterId, state.page),
)
uiState.value = newState
if (!incognitoMode.value) {
statsCollector.onStateChanged(m.id, state)
}
}
private fun computePercent(chapterId: Long, pageIndex: Int): Float {

View File

@@ -39,14 +39,12 @@ import javax.inject.Inject
@AndroidEntryPoint
class ReaderConfigSheet :
BaseAdaptiveSheet<SheetReaderConfigBinding>(),
ActivityResultCallback<Uri?>,
View.OnClickListener,
MaterialButtonToggleGroup.OnButtonCheckedListener,
Slider.OnChangeListener,
CompoundButton.OnCheckedChangeListener {
private val viewModel by activityViewModels<ReaderViewModel>()
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
@Inject
lateinit var orientationHelper: ScreenOrientationHelper
@@ -115,8 +113,7 @@ class ReaderConfigSheet :
}
R.id.button_save_page -> {
val page = viewModel.getCurrentPage() ?: return
viewModel.saveCurrentPage(page, savePageRequest)
findCallback()?.onSavePageClick() ?: return
dismissAllowingStateLoss()
}
@@ -181,11 +178,6 @@ class ReaderConfigSheet :
(viewBinding ?: return).labelTimerValue.text = getString(R.string.speed_value, value * 10f)
}
override fun onActivityResult(result: Uri?) {
viewModel.onActivityResult(result)
dismissAllowingStateLoss()
}
private fun observeScreenOrientation() {
orientationHelper.observeAutoOrientation()
.onEach {
@@ -215,6 +207,8 @@ class ReaderConfigSheet :
fun onReaderModeChanged(mode: ReaderMode)
fun onDoubleModeChanged(isEnabled: Boolean)
fun onSavePageClick()
}
companion object {

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import android.content.Context
import android.webkit.MimeTypeMap
import androidx.core.net.toFile
import androidx.core.net.toUri
import coil.ImageLoader
import coil.decode.DataSource
@@ -20,6 +22,7 @@ import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaPage
@@ -56,8 +59,8 @@ class MangaPageFetcher(
private suspend fun loadPage(pageUrl: String): SourceResult {
val uri = pageUrl.toUri()
return if (uri.isZipUri()) {
runInterruptible(Dispatchers.IO) {
return when {
uri.isZipUri() -> runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
SourceResult(
@@ -66,32 +69,48 @@ class MangaPageFetcher(
context = context,
metadata = MangaPageMetadata(page),
),
mimeType = null,
mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(entry.name.substringAfterLast('.', "")),
dataSource = DataSource.DISK,
)
}
} else {
val request = PageLoader.createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
check(response.isSuccessful) {
"Invalid response: ${response.code} ${response.message} at $pageUrl"
}
val body = checkNotNull(response.body) {
"Null response"
}
val mimeType = response.mimeType
val file = body.use {
pagesCache.put(pageUrl, it.source())
}
uri.isFileUri() -> runInterruptible(Dispatchers.IO) {
val file = uri.toFile()
SourceResult(
source = ImageSource(
file = file.toOkioPath(),
source = file.source().buffer(),
context = context,
metadata = MangaPageMetadata(page),
),
mimeType = mimeType,
dataSource = DataSource.NETWORK,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension),
dataSource = DataSource.DISK,
)
}
else -> {
val request = PageLoader.createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
check(response.isSuccessful) {
"Invalid response: ${response.code} ${response.message} at $pageUrl"
}
val body = checkNotNull(response.body) {
"Null response"
}
val mimeType = response.mimeType
val file = body.use {
pagesCache.put(pageUrl, it.source())
}
SourceResult(
source = ImageSource(
file = file.toOkioPath(),
metadata = MangaPageMetadata(page),
),
mimeType = mimeType,
dataSource = DataSource.NETWORK,
)
}
}
}
}

View File

@@ -54,7 +54,7 @@ class AppearanceSettingsFragment :
}
summaryProvider = Preference.SummaryProvider<ActivityListPreference> {
val locale = AppCompatDelegate.getApplicationLocales().get(0)
locale?.getDisplayName(locale)?.toTitleCase(locale) ?: getString(R.string.automatic)
locale?.getDisplayName(locale)?.toTitleCase(locale) ?: getString(R.string.follow_system)
}
setDefaultValueCompat("")
}
@@ -105,7 +105,7 @@ class AppearanceSettingsFragment :
.sortedWithSafe(LocaleComparator())
preference.entries = Array(locales.size + 1) { i ->
if (i == 0) {
getString(R.string.automatic)
getString(R.string.follow_system)
} else {
val lc = locales[i - 1]
lc.getDisplayName(lc).toTitleCase(lc)

View File

@@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.preference.ListPreference
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -15,13 +16,17 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
import org.koitharu.kotatsu.settings.utils.DozeHelper
@@ -46,6 +51,10 @@ class DownloadsSettingsFragment :
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_downloads)
findPreference<ListPreference>(AppSettings.KEY_DOWNLOADS_FORMAT)?.run {
entryValues = DownloadFormat.entries.names()
setDefaultValueCompat(DownloadFormat.AUTOMATIC.name)
}
dozeHelper.updatePreference()
}

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.settings
import android.accounts.AccountManager
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
@@ -17,6 +16,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerRepository
@@ -29,6 +29,8 @@ import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.scrobbling.kitsu.ui.KitsuAuthActivity
import org.koitharu.kotatsu.settings.utils.SplitSwitchPreference
import org.koitharu.kotatsu.stats.ui.StatsActivity
import javax.inject.Inject
@AndroidEntryPoint
@@ -52,11 +54,18 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_services)
bindSuggestionsSummary()
findPreference<SplitSwitchPreference>(AppSettings.KEY_STATS_ENABLED)?.let {
it.onContainerClickListener = Preference.OnPreferenceClickListener {
it.context.startActivity(Intent(it.context, StatsActivity::class.java))
true
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindSuggestionsSummary()
bindStatsSummary()
settings.subscribe(this)
}
@@ -77,6 +86,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_SUGGESTIONS -> bindSuggestionsSummary()
AppSettings.KEY_STATS_ENABLED -> bindStatsSummary()
}
}
@@ -195,4 +205,10 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled,
)
}
private fun bindStatsSummary() {
findPreference<Preference>(AppSettings.KEY_STATS_ENABLED)?.setSummary(
if (settings.isStatsEnabled) R.string.enabled else R.string.disabled,
)
}
}

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.parsers.util.isNumeric
import org.koitharu.kotatsu.parsers.util.md5
import javax.inject.Inject
@@ -39,6 +40,7 @@ class ProtectSetupViewModel @Inject constructor(
} else {
if (firstPassword.value == password) {
settings.appPassword = password.md5()
settings.isAppPasswordNumeric = password.isNumeric()
onPasswordSet.call(Unit)
} else {
onPasswordMismatch.call(Unit)

View File

@@ -3,20 +3,15 @@ package org.koitharu.kotatsu.settings.userdata
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.core.graphics.ColorUtils
import androidx.core.widget.TextViewCompat
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.util.Colors
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.databinding.PreferenceMemoryUsageBinding
import com.google.android.material.R as materialR
@@ -39,15 +34,15 @@ class StorageUsagePreference @JvmOverloads constructor(
val binding = PreferenceMemoryUsageBinding.bind(holder.itemView)
val storageSegment = SegmentedBarView.Segment(
usage?.savedManga?.percent ?: 0f,
Colors.segmentColor(context, materialR.attr.colorPrimary),
KotatsuColors.segmentColor(context, materialR.attr.colorPrimary),
)
val pagesSegment = SegmentedBarView.Segment(
usage?.pagesCache?.percent ?: 0f,
Colors.segmentColor(context, materialR.attr.colorSecondary),
KotatsuColors.segmentColor(context, materialR.attr.colorSecondary),
)
val otherSegment = SegmentedBarView.Segment(
usage?.otherCache?.percent ?: 0f,
Colors.segmentColor(context, materialR.attr.colorTertiary),
KotatsuColors.segmentColor(context, materialR.attr.colorTertiary),
)
with(binding) {

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.settings.utils
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreferenceCompat
import org.koitharu.kotatsu.R
class SplitSwitchPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.preference.R.attr.switchPreferenceCompatStyle,
defStyleRes: Int = 0
) : SwitchPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) {
init {
layoutResource = R.layout.preference_split_switch
}
var onContainerClickListener: OnPreferenceClickListener? = null
private val containerClickListener = View.OnClickListener { v ->
onContainerClickListener?.onPreferenceClick(this)
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.findViewById(R.id.press_container)?.setOnClickListener(containerClickListener)
}
}

View File

@@ -0,0 +1,69 @@
package org.koitharu.kotatsu.stats.data
import android.database.sqlite.SQLiteQueryBuilder
import androidx.room.Dao
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.history.data.HistoryWithManga
@Dao
abstract class StatsDao {
@Query("SELECT * FROM stats ORDER BY started_at")
abstract suspend fun findAll(): List<StatsEntity>
@Query("SELECT * FROM stats WHERE manga_id = :mangaId ORDER BY started_at")
abstract suspend fun findAll(mangaId: Long): List<StatsEntity>
@Query("SELECT IFNULL(SUM(pages),0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getReadPagesCount(mangaId: Long): Int
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getAverageTimePerPage(mangaId: Long): Long
@Query("SELECT IFNULL(SUM(duration)/SUM(pages), 0) FROM stats")
abstract suspend fun getAverageTimePerPage(): Long
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats WHERE manga_id = :mangaId")
abstract suspend fun getReadingTime(mangaId: Long): Long
@Query("SELECT IFNULL(SUM(duration), 0) FROM stats")
abstract suspend fun getTotalReadingTime(): Long
@Query("DELETE FROM stats")
abstract suspend fun clear()
@Upsert
abstract suspend fun upsert(entity: StatsEntity)
suspend fun getDurationStats(fromDate: Long, isNsfw: Boolean?, favouriteCategories: Set<Long>): Map<MangaEntity, Long> {
val conditions = ArrayList<String>()
conditions.add("stats.started_at >= $fromDate")
if (favouriteCategories.isNotEmpty()) {
val ids = favouriteCategories.joinToString(",")
conditions.add("stats.manga_id IN (SELECT manga_id FROM favourites WHERE category_id IN ($ids))")
}
if (isNsfw != null) {
val flag = if (isNsfw) 1 else 0
conditions.add("manga.nsfw = $flag")
}
val where = conditions.joinToString(separator = " AND ")
val query = SimpleSQLiteQuery(
"SELECT manga.*, SUM(duration) AS d FROM stats LEFT JOIN manga ON manga.manga_id = stats.manga_id WHERE $where GROUP BY manga.manga_id ORDER BY d DESC",
)
return getDurationStatsImpl(query)
}
@RawQuery
protected abstract fun getDurationStatsImpl(
query: SupportSQLiteQuery
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>
}

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.stats.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
@Entity(
tableName = "stats",
primaryKeys = ["manga_id", "started_at"],
foreignKeys = [
ForeignKey(
entity = HistoryEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE,
),
],
)
data class StatsEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "started_at") val startedAt: Long,
@ColumnInfo(name = "duration") val duration: Long,
@ColumnInfo(name = "pages") val pages: Int,
)

View File

@@ -0,0 +1,74 @@
package org.koitharu.kotatsu.stats.data
import androidx.collection.LongIntMap
import androidx.collection.MutableLongIntMap
import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.stats.domain.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.util.NavigableMap
import java.util.TreeMap
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class StatsRepository @Inject constructor(
private val db: MangaDatabase,
) {
suspend fun getReadingStats(period: StatsPeriod, categories: Set<Long>): List<StatsRecord> {
val fromDate = if (period == StatsPeriod.ALL) {
0L
} else {
System.currentTimeMillis() - TimeUnit.DAYS.toMillis(period.days.toLong())
}
val stats = db.getStatsDao().getDurationStats(fromDate, null, categories)
val result = ArrayList<StatsRecord>(stats.size)
var other = StatsRecord(null, 0)
val total = stats.values.sum()
for ((mangaEntity, duration) in stats) {
val manga = mangaEntity.toManga(emptySet())
val percent = duration.toDouble() / total
if (percent < 0.05) {
other = other.copy(duration = other.duration + duration)
} else {
result += StatsRecord(
manga = manga,
duration = duration,
)
}
}
if (other.duration != 0L) {
result += other
}
return result
}
suspend fun getTimePerPage(mangaId: Long): Long = db.withTransaction {
val dao = db.getStatsDao()
val pages = dao.getReadPagesCount(mangaId)
val time = if (pages >= 10) {
dao.getAverageTimePerPage(mangaId)
} else {
dao.getAverageTimePerPage()
}
time
}
suspend fun getTotalPagesRead(mangaId: Long): Int {
return db.getStatsDao().getReadPagesCount(mangaId)
}
suspend fun getMangaTimeline(mangaId: Long): NavigableMap<Long, Int> {
val entities = db.getStatsDao().findAll(mangaId)
val map = TreeMap<Long, Int>()
for (e in entities) {
map[e.startedAt] = e.pages
}
return map
}
suspend fun clearStats() {
db.getStatsDao().clear()
}
}

View File

@@ -0,0 +1,73 @@
package org.koitharu.kotatsu.stats.domain
import androidx.collection.LongSparseArray
import androidx.collection.set
import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.stats.data.StatsEntity
import javax.inject.Inject
@ViewModelScoped
class StatsCollector @Inject constructor(
private val db: MangaDatabase,
private val settings: AppSettings,
lifecycle: ViewModelLifecycle,
) {
private val viewModelScope = RetainedLifecycleCoroutineScope(lifecycle)
private val stats = LongSparseArray<Entry>(1)
@Synchronized
fun onStateChanged(mangaId: Long, state: ReaderState) {
if (!settings.isStatsEnabled) {
return
}
val now = System.currentTimeMillis()
val entry = stats[mangaId]
if (entry == null) {
stats[mangaId] = Entry(
state = state,
stats = StatsEntity(
mangaId = mangaId,
startedAt = now,
duration = 0,
pages = 0,
),
)
return
}
val pagesDelta = if (entry.state.page != state.page || entry.state.chapterId != state.chapterId) 1 else 0
val newEntry = entry.copy(
stats = StatsEntity(
mangaId = mangaId,
startedAt = entry.stats.startedAt,
duration = now - entry.stats.startedAt,
pages = entry.stats.pages + pagesDelta,
),
)
stats[mangaId] = newEntry
commit(newEntry.stats)
}
@Synchronized
fun onPause(mangaId: Long) {
stats.remove(mangaId)
}
private fun commit(entity: StatsEntity) {
viewModelScope.launch(Dispatchers.Default) {
db.getStatsDao().upsert(entity)
}
}
private data class Entry(
val state: ReaderState,
val stats: StatsEntity,
)
}

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.stats.domain
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
enum class StatsPeriod(
@StringRes val titleResId: Int,
val days: Int,
) {
DAY(R.string.day, 1),
WEEK(R.string.week, 7),
MONTH(R.string.month, 30),
MONTHS_3(R.string.three_months, 90),
ALL(R.string.all_time, Int.MAX_VALUE),
}

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.stats.domain
import android.content.Context
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
data class StatsRecord(
val manga: Manga?,
val duration: Long,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is StatsRecord && other.manga == manga
}
val time: ReadingTime
init {
val minutes = TimeUnit.MILLISECONDS.toMinutes(duration).toInt()
time = ReadingTime(
minutes = minutes % 60,
hours = minutes / 60,
isContinue = false,
)
}
}

View File

@@ -0,0 +1,28 @@
package org.koitharu.kotatsu.stats.ui
import android.content.res.ColorStateList
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.databinding.ItemStatsBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsRecord
fun statsAD(
listener: OnListItemClickListener<Manga>,
) = adapterDelegateViewBinding<StatsRecord, StatsRecord, ItemStatsBinding>(
{ layoutInflater, parent -> ItemStatsBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v ->
listener.onItemClick(item.manga ?: return@setOnClickListener, v)
}
bind {
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.other_manga)
binding.textViewSummary.text = item.time.format(context.resources)
binding.imageViewBadge.imageTintList = ColorStateList.valueOf(KotatsuColors.ofManga(context, item.manga))
binding.root.isClickable = item.manga != null
}
}

View File

@@ -0,0 +1,197 @@
package org.koitharu.kotatsu.stats.ui
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewStub
import android.widget.CompoundButton
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.AsyncListDiffer
import coil.ImageLoader
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.databinding.ActivityStatsBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.stats.domain.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
import org.koitharu.kotatsu.stats.ui.views.PieChartView
import javax.inject.Inject
@AndroidEntryPoint
class StatsActivity : BaseActivity<ActivityStatsBinding>(),
OnListItemClickListener<Manga>,
PieChartView.OnSegmentClickListener,
AsyncListDiffer.ListListener<StatsRecord>,
ViewStub.OnInflateListener, View.OnClickListener, CompoundButton.OnCheckedChangeListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel: StatsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityStatsBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = BaseListAdapter<StatsRecord>()
.addDelegate(ListItemType.FEED, statsAD(this))
.addListListener(this)
viewBinding.recyclerView.adapter = adapter
viewBinding.chart.onSegmentClickListener = this
viewBinding.stubEmpty.setOnInflateListener(this)
viewBinding.chipPeriod.setOnClickListener(this)
viewModel.isLoading.observe(this) {
viewBinding.progressBar.showOrHide(it)
}
viewModel.period.observe(this) {
viewBinding.chipPeriod.setText(it.titleResId)
}
viewModel.favoriteCategories.observe(this, ::createCategoriesChips)
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.recyclerView))
viewModel.readingStats.observe(this) {
val sum = it.sumOf { it.duration }
viewBinding.chart.setData(
it.map { v ->
PieChartView.Segment(
value = (v.duration / 1000).toInt(),
label = v.manga?.title ?: getString(R.string.other_manga),
percent = (v.duration.toDouble() / sum).toFloat(),
color = KotatsuColors.ofManga(this, v.manga),
tag = v.manga,
)
},
)
adapter.emit(it)
}
}
override fun onWindowInsetsChanged(insets: Insets) = Unit
override fun onClick(v: View) {
when (v.id) {
R.id.chip_period -> showPeriodSelector()
}
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
val category = buttonView?.tag as? FavouriteCategory ?: return
viewModel.setCategoryChecked(category, isChecked)
}
override fun onItemClick(item: Manga, view: View) {
MangaStatsSheet.show(supportFragmentManager, item)
}
override fun onSegmentClick(view: PieChartView, segment: PieChartView.Segment) {
val manga = segment.tag as? Manga ?: return
onItemClick(manga, view)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_stats, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_clear -> {
showClearConfirmDialog()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCurrentListChanged(previousList: MutableList<StatsRecord>, currentList: MutableList<StatsRecord>) {
val isEmpty = currentList.isEmpty()
with(viewBinding) {
chart.isGone = isEmpty
recyclerView.isGone = isEmpty
stubEmpty.isVisible = isEmpty
}
}
override fun onInflate(stub: ViewStub?, inflated: View) {
val stubBinding = ItemEmptyStateBinding.bind(inflated)
stubBinding.icon.newImageRequest(this, R.drawable.ic_empty_history)?.enqueueWith(coil)
stubBinding.textPrimary.setText(R.string.text_empty_holder_primary)
stubBinding.textSecondary.setTextAndVisible(R.string.empty_stats_text)
stubBinding.buttonRetry.isVisible = false
}
private fun createCategoriesChips(categories: List<FavouriteCategory>) {
val container = viewBinding.layoutChips
if (container.childCount > 1) {
// avoid duplication
return
}
val checkedIds = viewModel.selectedCategories.value
for (category in categories) {
val chip = Chip(this)
val drawable = ChipDrawable.createFromAttributes(this, null, 0, R.style.Widget_Kotatsu_Chip_Filter)
chip.setChipDrawable(drawable)
chip.text = category.title
chip.tag = category
chip.isChecked = category.id in checkedIds
chip.setOnCheckedChangeListener(this)
container.addView(chip)
}
}
private fun showClearConfirmDialog() {
MaterialAlertDialogBuilder(this, DIALOG_THEME_CENTERED)
.setMessage(R.string.clear_stats_confirm)
.setTitle(R.string.clear_stats)
.setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.clear) { _, _ ->
viewModel.clear()
}.show()
}
private fun showPeriodSelector() {
val menu = PopupMenu(this, viewBinding.chipPeriod)
val selected = viewModel.period.value
for ((i, branch) in StatsPeriod.entries.withIndex()) {
val item = menu.menu.add(R.id.group_period, Menu.NONE, i, branch.titleResId)
item.isCheckable = true
item.isChecked = selected.ordinal == i
}
menu.menu.setGroupCheckable(R.id.group_period, true, true)
menu.setOnMenuItemClickListener {
StatsPeriod.entries.getOrNull(it.order)?.also {
viewModel.period.value = it
} != null
}
menu.show()
}
}

View File

@@ -0,0 +1,73 @@
package org.koitharu.kotatsu.stats.ui
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.stats.data.StatsRepository
import org.koitharu.kotatsu.stats.domain.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import javax.inject.Inject
@HiltViewModel
class StatsViewModel @Inject constructor(
private val repository: StatsRepository,
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
val period = MutableStateFlow(StatsPeriod.WEEK)
val onActionDone = MutableEventFlow<ReversibleAction>()
val selectedCategories = MutableStateFlow<Set<Long>>(emptySet())
val favoriteCategories = favouritesRepository.observeCategories()
.take(1)
val readingStats = MutableStateFlow<List<StatsRecord>>(emptyList())
init {
launchJob(Dispatchers.Default) {
combine<StatsPeriod, Set<Long>, Pair<StatsPeriod, Set<Long>>>(
period,
selectedCategories,
::Pair,
).collectLatest { p ->
readingStats.value = withLoading {
repository.getReadingStats(p.first, p.second)
}
}
}
}
fun setCategoryChecked(category: FavouriteCategory, checked: Boolean) {
val snapshot = selectedCategories.value.toMutableSet()
if (checked) {
snapshot.add(category.id)
} else {
snapshot.remove(category.id)
}
selectedCategories.value = snapshot
}
fun clear() {
launchLoadingJob(Dispatchers.Default) {
repository.clearStats()
readingStats.value = emptyList()
onActionDone.call(ReversibleAction(R.string.stats_cleared, null))
}
}
}

View File

@@ -0,0 +1,82 @@
package org.koitharu.kotatsu.stats.ui.sheet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.collection.IntList
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetStatsMangaBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.stats.ui.views.BarChartView
@AndroidEntryPoint
class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>(), View.OnClickListener {
private val viewModel: MangaStatsViewModel by viewModels()
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetStatsMangaBinding {
return SheetStatsMangaBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: SheetStatsMangaBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.textViewTitle.text = viewModel.manga.title
binding.chartView.barColor = KotatsuColors.ofManga(binding.root.context, viewModel.manga)
viewModel.stats.observe(viewLifecycleOwner, ::onStatsChanged)
viewModel.startDate.observe(viewLifecycleOwner) {
binding.textViewStart.textAndVisible = it?.format(resources)
}
viewModel.totalPagesRead.observe(viewLifecycleOwner) {
binding.textViewPages.text = getString(R.string.pages_read_s, it.format())
}
binding.buttonOpen.setOnClickListener(this)
}
override fun onClick(v: View) {
startActivity(DetailsActivity.newIntent(v.context, viewModel.manga))
}
private fun onStatsChanged(stats: IntList) {
val chartView = viewBinding?.chartView ?: return
if (stats.isEmpty()) {
chartView.setData(emptyList())
return
}
val bars = ArrayList<BarChartView.Bar>(stats.size)
stats.forEach { pages ->
bars.add(
BarChartView.Bar(
value = pages,
label = pages.toString(),
),
)
}
chartView.setData(bars)
}
companion object {
const val ARG_MANGA = "manga"
private const val TAG = "MangaStatsSheet"
fun show(fm: FragmentManager, manga: Manga) {
MangaStatsSheet().withArgs(1) {
putParcelable(ARG_MANGA, ParcelableManga(manga))
}.showDistinct(fm, TAG)
}
}
}

View File

@@ -0,0 +1,58 @@
package org.koitharu.kotatsu.stats.ui.sheet
import androidx.collection.IntList
import androidx.collection.LongIntMap
import androidx.collection.MutableIntList
import androidx.collection.emptyIntList
import androidx.collection.emptyLongIntMap
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.stats.data.StatsRepository
import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.time.Instant
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltViewModel
class MangaStatsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: StatsRepository,
) : BaseViewModel() {
val manga = savedStateHandle.require<ParcelableManga>(MangaStatsSheet.ARG_MANGA).manga
val stats = MutableStateFlow<IntList>(emptyIntList())
val startDate = MutableStateFlow<DateTimeAgo?>(null)
val totalPagesRead = MutableStateFlow(0)
init {
launchLoadingJob(Dispatchers.Default) {
val timeline = repository.getMangaTimeline(manga.id)
if (timeline.isEmpty()) {
startDate.value = null
stats.value = emptyIntList()
} else {
val startDay = TimeUnit.MILLISECONDS.toDays(timeline.firstKey())
val endDay = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis())
val res = MutableIntList((endDay - startDay).toInt() + 1)
for (day in startDay..endDay) {
val from = TimeUnit.DAYS.toMillis(day)
val to = TimeUnit.DAYS.toMillis(day + 1)
res.add(timeline.subMap(from, true, to, false).values.sum())
}
stats.value = res
startDate.value = calculateTimeAgo(Instant.ofEpochMilli(timeline.firstKey()))
}
}
launchLoadingJob(Dispatchers.Default) {
totalPagesRead.value = repository.getTotalPagesRead(manga.id)
}
}
}

View File

@@ -0,0 +1,172 @@
package org.koitharu.kotatsu.stats.ui.views
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.PathEffect
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.graphics.Xfermode
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorInt
import androidx.collection.MutableIntList
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.minus
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.setPadding
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.replaceWith
import org.koitharu.kotatsu.parsers.util.toIntUp
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.sqrt
import kotlin.random.Random
import com.google.android.material.R as materialR
class BarChartView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rawData = ArrayList<Bar>()
private val bars = ArrayList<Bar>()
private var maxValue: Int = 0
private val minBarSpacing = context.resources.resolveDp(12f)
private val minSpace = context.resources.resolveDp(20f)
private val barWidth = context.resources.resolveDp(12f)
private val outlineColor = context.getThemeColor(materialR.attr.colorOutline)
private val dottedEffect = DashPathEffect(
floatArrayOf(
context.resources.resolveDp(6f),
context.resources.resolveDp(6f),
),
0f,
)
private val chartBounds = RectF()
@ColorInt
var barColor: Int = context.getThemeColor(materialR.attr.colorAccent)
set(value) {
field = value
invalidate()
}
init {
paint.strokeWidth = context.resources.resolveDp(1f)
if (isInEditMode) {
setData(
List(Random.nextInt(20, 60)) {
Bar(
value = Random.nextInt(-20, 400).coerceAtLeast(0),
label = it.toString(),
)
},
)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (bars.isEmpty() || chartBounds.isEmpty) {
return
}
val spacing = (chartBounds.width() - (barWidth * bars.size.toFloat())) / (bars.size + 1).toFloat()
// dashed horizontal lines
paint.color = outlineColor
paint.style = Paint.Style.STROKE
canvas.drawLine(chartBounds.left, chartBounds.bottom, chartBounds.right, chartBounds.bottom, paint)
paint.pathEffect = dottedEffect
for (i in (0..maxValue).step(computeValueStep())) {
val y = chartBounds.top + (chartBounds.height() * i / maxValue.toFloat())
canvas.drawLine(paddingLeft.toFloat(), y, (width - paddingLeft - paddingRight).toFloat(), y, paint)
}
// bottom line
paint.color = outlineColor
paint.style = Paint.Style.STROKE
canvas.drawLine(chartBounds.left, chartBounds.bottom, chartBounds.right, chartBounds.bottom, paint)
// bars
paint.style = Paint.Style.FILL
paint.color = barColor
paint.pathEffect = null
val corner = barWidth / 2f
for ((i, bar) in bars.withIndex()) {
if (bar.value == 0) {
continue
}
val h = (chartBounds.height() * bar.value / maxValue.toFloat()).coerceAtLeast(barWidth)
val x = spacing + i * (barWidth + spacing) + paddingLeft
canvas.drawRoundRect(x, chartBounds.bottom - h, x + barWidth, chartBounds.bottom, corner, corner, paint)
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
invalidateBounds()
}
fun setData(value: List<Bar>) {
rawData.replaceWith(value)
compressBars()
invalidate()
}
private fun compressBars() {
if (rawData.isEmpty() || width <= 0) {
maxValue = 0
bars.clear()
return
}
var fullWidth = rawData.size * (barWidth + minBarSpacing) + minBarSpacing
val windowSize = (fullWidth / width.toFloat()).toIntUp()
bars.replaceWith(
rawData.chunked(windowSize) { it.average() },
)
maxValue = bars.maxOf { it.value }
}
private fun computeValueStep(): Int {
val h = chartBounds.height()
var step = 1
while (h / (maxValue / step).toFloat() <= minSpace) {
step++
}
return step
}
private fun invalidateBounds() {
val inset = paint.strokeWidth
chartBounds.set(
paddingLeft.toFloat() + inset,
paddingTop.toFloat() + inset,
(width - paddingLeft - paddingRight).toFloat() - inset,
(height - paddingTop - paddingBottom).toFloat() - inset,
)
compressBars()
}
private fun Collection<Bar>.average(): Bar {
return when (size) {
0 -> Bar(0, "")
1 -> first()
else -> Bar(
value = (sumOf { it.value } / size.toFloat()).roundToInt(),
label = "%s - %s".format(first().label, last().label),
)
}
}
class Bar(
val value: Int,
val label: String,
)
}

View File

@@ -0,0 +1,171 @@
package org.koitharu.kotatsu.stats.ui.views
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.graphics.Xfermode
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorInt
import androidx.collection.MutableIntList
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.minus
import androidx.core.view.GestureDetectorCompat
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.replaceWith
import kotlin.math.absoluteValue
import kotlin.math.sqrt
import com.google.android.material.R as materialR
class PieChartView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), GestureDetector.OnGestureListener {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val segments = ArrayList<Segment>()
private val chartBounds = RectF()
private val clearColor = context.getThemeColor(android.R.attr.colorBackground)
private val touchDetector = GestureDetectorCompat(context, this)
private var hightlightedSegment = -1
var onSegmentClickListener: OnSegmentClickListener? = null
init {
touchDetector.setIsLongpressEnabled(false)
paint.strokeWidth = context.resources.resolveDp(2f)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
var angle = 0f
for ((i, segment) in segments.withIndex()) {
paint.color = segment.color
if (i == hightlightedSegment) {
paint.color = ColorUtils.setAlphaComponent(paint.color, 180)
}
paint.style = Paint.Style.FILL
val sweepAngle = segment.percent * 360f
canvas.drawArc(
chartBounds,
angle,
sweepAngle,
true,
paint,
)
paint.color = clearColor
paint.style = Paint.Style.STROKE
canvas.drawArc(
chartBounds,
angle,
sweepAngle,
true,
paint,
)
angle += sweepAngle
}
paint.style = Paint.Style.FILL
paint.color = clearColor
canvas.drawCircle(chartBounds.centerX(), chartBounds.centerY(), chartBounds.height() / 4f, paint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val size = minOf(w, h).toFloat()
val inset = paint.strokeWidth
chartBounds.set(inset, inset, size - inset, size - inset)
chartBounds.offset(
(w - size) / 2f,
(h - size) / 2f,
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_CANCEL || event.actionMasked == MotionEvent.ACTION_UP) {
hightlightedSegment = -1
invalidate()
}
return super.onTouchEvent(event) || touchDetector.onTouchEvent(event)
}
override fun onDown(e: MotionEvent): Boolean {
if (onSegmentClickListener == null) {
return false
}
val segment = findSegmentIndex(e.x, e.y)
if (segment != hightlightedSegment) {
hightlightedSegment = segment
invalidate()
return true
} else {
return false
}
}
override fun onShowPress(e: MotionEvent) = Unit
override fun onSingleTapUp(e: MotionEvent): Boolean {
onSegmentClickListener?.run {
val segment = segments.getOrNull(findSegmentIndex(e.x, e.y))
if (segment != null) {
onSegmentClick(this@PieChartView, segment)
}
}
return true
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean = false
override fun onLongPress(e: MotionEvent) = Unit
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean = false
fun setData(value: List<Segment>) {
segments.replaceWith(value)
invalidate()
}
private fun findSegmentIndex(x: Float, y: Float): Int {
val dy = (y - chartBounds.centerY()).toDouble()
val dx = (x - chartBounds.centerX()).toDouble()
val distance = sqrt(dx * dx + dy * dy).toFloat()
if (distance < chartBounds.height() / 4f || distance > chartBounds.centerX()) {
return -1
}
var touchAngle = Math.toDegrees(Math.atan2(dy, dx)).toFloat()
if (touchAngle < 0) {
touchAngle += 360
}
var angle = 0f
for ((i, segment) in segments.withIndex()) {
val sweepAngle = segment.percent * 360f
if (touchAngle in angle..(angle + sweepAngle)) {
return i
}
angle += sweepAngle
}
return -1
}
class Segment(
val value: Int,
val label: String,
val percent: Float,
val color: Int,
val tag: Any?,
)
interface OnSegmentClickListener {
fun onSegmentClick(view: PieChartView, segment: Segment)
}
}

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="noScroll">
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward"
app:layout_constraintBottom_toBottomOf="@id/appbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:showAnimationBehavior="inward"
app:trackCornerRadius="0dp"
tools:visibility="visible" />
<HorizontalScrollView
android:id="@+id/scrollView_chips"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingHorizontal="12dp"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar">
<com.google.android.material.chip.ChipGroup
android:id="@+id/layout_chips"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/chip_period"
style="@style/Widget.Kotatsu.Chip.Dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/week"
app:chipIcon="@drawable/ic_history" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<org.koitharu.kotatsu.stats.ui.views.PieChartView
android:id="@+id/chart"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="24dp"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/scrollView_chips" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:overScrollMode="ifContentScrolls"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chart"
tools:itemCount="4"
tools:listitem="@layout/item_stats" />
<ViewStub
android:id="@+id/stub_empty"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout="@layout/item_empty_state"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/scrollView_chips"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -61,7 +61,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="imageView_cover,textView_status,imageView_expand" />
app:constraint_referenced_ids="imageView_cover,textView_status,imageView_expand,textView_details" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
@@ -141,7 +141,7 @@
app:layout_constraintEnd_toStartOf="@id/textView_percent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_status"
tools:text="@tools:sample/lorem[3]" />
tools:text="@tools:sample/lorem[10]" />
<Button
android:id="@+id/button_pause"

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_selector"
android:clipChildren="false"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd">
<ImageView
android:id="@+id/imageView_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:srcCompat="@drawable/bg_rounded_square" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textView_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:clipToPadding="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:ignore="RtlSymmetry">
<LinearLayout
android:id="@+id/press_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackground"
android:baselineAligned="false"
android:clipToPadding="false"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="?android:attr/listPreferredItemPaddingStart">
<include layout="@layout/image_frame" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem" />
<TextView
android:id="@android:id/summary"
style="@style/PreferenceSummaryTextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignStart="@android:id/title"
android:layout_gravity="start"
android:maxLines="10"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary" />
</RelativeLayout>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginVertical="16dp"
android:background="?dividerVertical" />
<!-- Preference should place its actual preference widget here. -->
<LinearLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="end|center_vertical"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="0dp" />
</LinearLayout>

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="@dimen/margin_small"
android:paddingBottom="@dimen/margin_normal">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/dragHandle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:layout_below="@id/dragHandle"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/textView_label"
android:paddingHorizontal="@dimen/margin_small"
android:paddingBottom="@dimen/margin_small"
android:singleLine="true"
android:text="@string/grid_size"
android:textAppearance="?textAppearanceTitleMedium" />
<TextView
android:id="@+id/textView_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/textView_title"
android:layout_alignParentEnd="true"
android:paddingHorizontal="@dimen/margin_small"
android:singleLine="true"
android:textAppearance="?textAppearanceLabelLarge"
tools:text="100%" />
<ImageView
android:id="@+id/button_small"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_alignTop="@id/slider_grid"
android:layout_alignBottom="@id/slider_grid"
android:layout_alignParentStart="true"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_size_small"
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar" />
<ImageView
android:id="@+id/button_large"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_alignTop="@id/slider_grid"
android:layout_alignBottom="@id/slider_grid"
android:layout_alignParentEnd="true"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_size_large"
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar" />
<com.google.android.material.slider.Slider
android:id="@+id/slider_grid"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/textView_title"
android:layout_toStartOf="@id/button_large"
android:layout_toEndOf="@id/button_small"
android:stepSize="5"
android:valueFrom="50"
android:valueTo="150"
app:labelBehavior="gone"
app:tickVisible="false"
tools:value="100" />
</RelativeLayout>

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/screen_padding">
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
android:id="@+id/headerBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/reading_stats" />
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollIndicators="top">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/screen_padding">
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?textAppearanceTitleMedium"
tools:text="@tools:sample/lorem[4]" />
<ImageButton
android:id="@+id/button_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:minWidth="?minTouchTargetSize"
android:minHeight="?minTouchTargetSize"
app:srcCompat="@drawable/ic_open_external" />
</LinearLayout>
<org.koitharu.kotatsu.stats.ui.views.BarChartView
android:id="@+id/chartView"
android:layout_width="match_parent"
android:layout_height="240dp"
android:layout_marginTop="12dp"
android:paddingHorizontal="@dimen/screen_padding" />
<TextView
android:id="@+id/textView_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:paddingHorizontal="@dimen/screen_padding"
android:textAppearance="?textAppearanceLabelSmall"
tools:text="Week ago" />
<TextView
android:id="@+id/textView_pages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingHorizontal="@dimen/screen_padding"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="Total pages read: 250" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -37,6 +37,12 @@
android:title="@string/tracking"
app:showAsAction="never" />
<item
android:id="@+id/action_stats"
android:orderInCategory="50"
android:title="@string/statistics"
app:showAsAction="never" />
<item
android:id="@+id/action_related"
android:orderInCategory="50"

View File

@@ -9,4 +9,10 @@
android:title="@string/clear_history"
app:showAsAction="never" />
<item
android:id="@+id/action_stats"
android:orderInCategory="40"
android:title="@string/statistics"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_clear"
android:title="@string/clear_stats"
android:titleCondensed="@string/clear"
app:showAsAction="never" />
</menu>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="detailed_list">تفاصيل القائمة</string>
<string name="detailed_list">قائمة مفصلة</string>
<string name="error_occurred">حدث خطأ</string>
<string name="details">التفاصيل</string>
<string name="grid">شبكة</string>
@@ -11,8 +11,8 @@
<string name="favourites">المفضلة</string>
<string name="network_error">‌خطاء في الشبكة</string>
<string name="loading_">جار التحميل…</string>
<string name="chapter_d_of_d">فصل %1$d في %2$d</string>
<string name="close">غلق</string>
<string name="chapter_d_of_d">فصل %1$d من %2$d</string>
<string name="close">إغلاق</string>
<string name="try_again">حاول مجدداً</string>
<string name="computing_">جاري الحوسبة …</string>
<string name="local_storage">التخزين المحلي</string>
@@ -28,16 +28,16 @@
<string name="newest">الأحدث</string>
<string name="by_rating">تقييم</string>
<string name="pages">صفحات</string>
<string name="read">اقرأ</string>
<string name="read">إقرأ</string>
<string name="share">شارك</string>
<string name="nothing_found">لم يتم عثور على اي شيء</string>
<string name="nothing_found">لا شيء موجود</string>
<string name="you_have_not_favourites_yet">لا مفضلة بعد</string>
<string name="search">بحث</string>
<string name="search_manga">البحث في المانجا</string>
<string name="manga_downloading_">جاري التنزيل…</string>
<string name="create_shortcut">انشاء اختصار…</string>
<string name="theme">مظهر</string>
<string name="automatic">حسب النظام</string>
<string name="follow_system">حسب النظام</string>
<string name="share_s">شارك %s</string>
<string name="processing_">في طور معالجة…</string>
<string name="updated">محدث</string>
@@ -48,7 +48,7 @@
<string name="clear">مسح</string>
<string name="remove">ازالة</string>
<string name="popular">شائع</string>
<string name="add_new_category">أضف فئة جديدة</string>
<string name="add_new_category">فئة جديدة</string>
<string name="download_complete">تم التنزيل</string>
<string name="text_clear_history_prompt">هل تريد محو سجل القراءة بالكامل بشكل دائم؟</string>
<string name="save_page">احفظ الصفحة</string>
@@ -211,7 +211,7 @@
<string name="detect_reader_mode_summary">اكتشف تلقائيًا ما إذا كانت المانجا عبارة عن webtoon</string>
<string name="appwidget_recent_description">المانجا التي قرأتها مؤخرًا</string>
<string name="appearance">مظهر</string>
<string name="bookmark_remove">حذف من المحفظة</string>
<string name="bookmark_remove">حذف الإشارة المرجعية</string>
<string name="disable_battery_optimization_summary">يساعد في فحص التحديثات في الخلفية</string>
<string name="auth_not_supported_by">تسجيل الدخول على %s غير مدعوم</string>
<string name="status_on_hold">معلقَّة</string>
@@ -228,7 +228,7 @@
<string name="sync_title">مزامنة بياناتك</string>
<string name="appwidget_shelf_description">مانغا من المفضلة لديك</string>
<string name="send">إرسال</string>
<string name="bookmark_add">اضافة للمحفظة</string>
<string name="bookmark_add">اضافة إشارة مرجعية</string>
<string name="screenshots_block_all">احظر دائما</string>
<string name="new_sources_text">تتوفر مصادر مانغا جديدة</string>
<string name="zoom_mode_fit_height">مناسب للارتفاع</string>
@@ -338,4 +338,12 @@
<string name="folder_with_images_import_description">يمكنك اختيار مكان في الذاكرة يحتوي على أرشيفات أو صور. سيتم التعرف على كل أرشيف (أو مجلد فرعي) على أنه فصل.</string>
<string name="speed">السرعة</string>
<string name="restore_backup_description">استيراد نسخة احتياطية تم إنشاؤها لبيانات المستخدم.</string>
</resources>
<string name="feed">الموجز</string>
<string name="light_indicator">مؤشر إل إي دي</string>
<string name="comics_archive">أرشيف القصص المصورة</string>
<string name="importing_manga">استيراد المانجا</string>
<string name="import_completed">تم الإستيراد</string>
<string name="import_completed_hint">يمكنك حذف الملف الأصلي من التخزين لتوفير مساحة</string>
<string name="import_will_start_soon">الإستيراد سيبدأ عن قريب</string>
<string name="history_shortcuts">إظهار اختصارات المانجا الحديثة</string>
</resources>

View File

@@ -45,7 +45,7 @@
<string name="theme">Тэма</string>
<string name="light">Светлая</string>
<string name="dark">Цёмная</string>
<string name="automatic">Як у сістэме</string>
<string name="follow_system">Як у сістэме</string>
<string name="pages">Старонкi</string>
<string name="clear">Ачысціць</string>
<string name="text_clear_history_prompt">Вы ўпэўненыя, што жадаеце ачысціць гісторыю\?</string>
@@ -201,7 +201,7 @@
<string name="screenshots_block_all">Заўсёды блакуйце</string>
<string name="screenshots_block_nsfw">Забараніць для NSFW</string>
<string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string>
<string name="disabled">Адключаны</string>
<string name="disabled">Адкл.</string>
<string name="enabled">Ўключаны</string>
<string name="exclude_nsfw_from_suggestions">Ня прапаноўваць NSFW мангу</string>
<string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string>
@@ -469,7 +469,7 @@
<string name="disable_nsfw">Адключыць NSFW</string>
<string name="too_many_requests_message">Занадта шмат запытаў. Паўтарыце спробу пазней</string>
<string name="related_manga_summary">Паказаць спіс звязанай мангі. У некаторых выпадках ён можа быць недакладным або адсутнічаць</string>
<string name="advanced">Пашыраныя</string>
<string name="advanced">Прасунутая</string>
<string name="default_section">Раздзел па змаўчанні</string>
<string name="manga_list">Спіс мангі</string>
<string name="error_corrupted_file">Вяртаюцца няправільныя дадзеныя ці файл пашкоджаны</string>
@@ -593,4 +593,5 @@
<string name="default_page_save_dir">Каталог захавання старонкі па змаўчанні</string>
<string name="remove_from_history">Выдаліць з гісторыі</string>
<string name="pages_saving">Захаванне старонак</string>
</resources>
<string name="location">Размяшчэнне</string>
</resources>

View File

@@ -64,7 +64,7 @@
<string name="theme">থিম</string>
<string name="light">আলো</string>
<string name="dark">আঁধার</string>
<string name="automatic">সিস্টেম অনুযায়ী</string>
<string name="follow_system">সিস্টেম অনুযায়ী</string>
<string name="pages">পৃষ্ঠাগুলি</string>
<string name="webtoon">ওয়েবটুন</string>
<string name="read_mode">পড়ার মোড</string>
@@ -159,4 +159,4 @@
<string name="suggestion_manga">পরামর্শ: %s</string>
<string name="text_empty_holder_primary">এখানে খালি…</string>
<string name="done">সম্পন্ন</string>
</resources>
</resources>

View File

@@ -33,7 +33,7 @@
<string name="theme">Téma</string>
<string name="light">Světlé</string>
<string name="dark">Tmavé</string>
<string name="automatic">Následovat systém</string>
<string name="follow_system">Následovat systém</string>
<string name="remove">Odstranit</string>
<string name="_s_deleted_from_local_storage">\"%s\" smazáno z místního uložiště</string>
<string name="share_image">Sdílet obrázek</string>
@@ -444,4 +444,4 @@
<string name="clear_source_cookies_summary">Vyčistit cookies pouze pro specifikované domény. Ve většině případech bude neplatná autorizace</string>
<string name="download_option_manual_selection">Vyberte kapitoly manuálně</string>
<string name="description">Popis</string>
</resources>
</resources>

View File

@@ -4,7 +4,7 @@
<string name="text_clear_history_prompt">Gesamten Leseverlauf unwiderruflich löschen\?</string>
<string name="theme">Design</string>
<string name="pages">Seiten</string>
<string name="automatic">Wie System</string>
<string name="follow_system">Wie System</string>
<string name="dark">Dunkel</string>
<string name="light">Hell</string>
<string name="filter">Filter</string>
@@ -580,4 +580,4 @@
<string name="two_pages">Zwei Seiten</string>
<string name="next_chapter">Nächstes Kapitel</string>
<string name="prev_page">Vorherige Seite</string>
</resources>
</resources>

View File

@@ -31,7 +31,7 @@
<string name="by_rating">Βαθμολογία</string>
<string name="filter">Φίλτρο</string>
<string name="dark">Σκοτεινό</string>
<string name="automatic">Όπως στο σύστημα</string>
<string name="follow_system">Όπως στο σύστημα</string>
<string name="clear">Εκκαθάριση</string>
<string name="text_clear_history_prompt">Να διαγράψετε μόνιμα όλο το ιστορικό ανάγνωσης;</string>
<string name="remove">Διαγραφή</string>
@@ -556,4 +556,4 @@
<string name="appwidget_recent_description">Τα πρόσφατα διαβασμένα manga σου</string>
<string name="clear_cookies_summary">Μπορεί να βοηθήσει σε περίπτωση κάποιων προβλημάτων. Όλες οι εξουσιοδοτήσεις θα ανακληθούν</string>
<string name="category_hidden_done">Αυτή η κατηγορία αποκρύφτηκε από την αρχική οθόνη και είναι προσβάσιμη μέσω του Μενού → Διαχείριση κατηγοριών</string>
</resources>
</resources>

View File

@@ -45,7 +45,7 @@
<string name="theme">Tema</string>
<string name="light">Claro</string>
<string name="dark">Oscuro</string>
<string name="automatic">De acuerdo al sistema</string>
<string name="follow_system">De acuerdo al sistema</string>
<string name="pages">Páginas</string>
<string name="clear">Limpiar</string>
<string name="text_clear_history_prompt">Borrar todo el historial de lectura de forma permanente\?</string>
@@ -328,7 +328,7 @@
<string name="color_correction_hint">Los ajustes de color elegidos serán recordados para este manga</string>
<string name="feed">Fuente</string>
<string name="history_shortcuts">Mostrar los accesos directos a los mangas recientes</string>
<string name="reader_control_ltr_summary">Navegar a continuación siempre te lleva a la página siguiente cuando utilizas el ratón y el teclado.</string>
<string name="reader_control_ltr_summary">Tocando en el borde derecho, o pulsando la tecla derecha, se pasa siempre a la página siguiente.</string>
<string name="reader_control_ltr">Control ergonómico del lector</string>
<string name="color_correction">Corrección del color</string>
<string name="brightness">Brillo</string>
@@ -593,4 +593,5 @@
<string name="remove_from_history">Eliminar del historial</string>
<string name="pages_saving">Guardar páginas</string>
<string name="default_page_save_dir">Directorio predeterminado para guardar páginas</string>
</resources>
<string name="location">Ubicación</string>
</resources>

View File

@@ -168,7 +168,7 @@
<string name="scale_mode">skaala mood</string>
<string name="advanced">Täiustatud</string>
<string name="only_using_wifi">Ainult Wi-Fi-l</string>
<string name="automatic">Jälgne süsteemile</string>
<string name="follow_system">Jälgne süsteemile</string>
<string name="sync_settings">Sünkroniseeri seadeid</string>
<string name="black_dark_theme">Must</string>
<string name="text_history_holder_primary">Mis sa loed näidatakse siin</string>
@@ -439,4 +439,4 @@
<string name="downloads_resumed">Allalaadimised on jätkanud</string>
<string name="invert_colors">Värvide ümberpööramine</string>
<string name="proxy">Puhverserver</string>
</resources>
</resources>

View File

@@ -14,7 +14,7 @@
<string name="internal_storage">حافظه ی درونی</string>
<string name="right_to_left">راست به چپ</string>
<string name="reader_mode_hint">پیکربندی انتخاب شده برای این مانگا بخاطر خواهد ماند</string>
<string name="automatic">تم سیستم</string>
<string name="follow_system">تم سیستم</string>
<string name="pages">صفحات</string>
<string name="clear">پاکسازی</string>
<string name="domain">دامنه</string>
@@ -255,4 +255,4 @@
<string name="notifications_enable">فعال کردن اعلان ها</string>
<string name="bookmark_remove">حذف نشانه</string>
<string name="bookmarks">نشانه ها</string>
</resources>
</resources>

View File

@@ -137,7 +137,7 @@
<string name="text_clear_history_prompt">Haluatko todella tyhjentää koko lukuhistoriasi\?</string>
<string name="clear">Tyhjennä</string>
<string name="pages">Sivut</string>
<string name="automatic">Automaattinen</string>
<string name="follow_system">Automaattinen</string>
<string name="dark">Tumma</string>
<string name="light">Vaalea</string>
<string name="theme">Teema</string>

View File

@@ -7,7 +7,7 @@
<string name="filter">Pansala</string>
<string name="theme">Tema</string>
<string name="dark">Madilim</string>
<string name="automatic">Sundan ang sistema</string>
<string name="follow_system">Sundan ang sistema</string>
<string name="error_occurred">May nangyaring error</string>
<string name="network_error">Error sa network</string>
<string name="details">Mga detalye</string>
@@ -593,4 +593,5 @@
<string name="check_for_new_chapters_disabled">Naka-disable ang pagsuri para sa mga bagong kabanata</string>
<string name="reading_time_estimation">Ipakita ang tinantyang oras ng pagbabasa</string>
<string name="reading_time_estimation_summary">Maaaring hindi tumpak ang halaga ng pagtatantya ng oras</string>
</resources>
<string name="location">Lokasyon</string>
</resources>

View File

@@ -134,7 +134,7 @@
<string name="text_clear_history_prompt">Effacer définitivement l\'historique de lecture \?</string>
<string name="clear">Effacer</string>
<string name="pages">Pages</string>
<string name="automatic">Suivre le système</string>
<string name="follow_system">Suivre le système</string>
<string name="dark">Sombre</string>
<string name="light">Clair</string>
<string name="theme">Thème</string>
@@ -593,4 +593,4 @@
<string name="show_labels_in_navbar">Afficher les étiquettes dans la barre de navigation</string>
<string name="pages_saving">Sauvegarder les pages</string>
<string name="default_webtoon_zoom_out">Zoom webtoon par défaut</string>
</resources>
</resources>

View File

@@ -6,15 +6,15 @@
<string name="history_is_empty">अभी तक कोई इतिहास नहीं है</string>
<string name="read">पढ़ें</string>
<string name="add_to_favourites">इसे पसंद करें</string>
<string name="add">जोड़</string>
<string name="save">संचय करो</string>
<string name="add">जोड़ें</string>
<string name="save">हेजे</string>
<string name="newest">नवीनतम</string>
<string name="light">उजाला</string>
<string name="dark">अँधेरा</string>
<string name="close">बंद कर</string>
<string name="light">हल्की</string>
<string name="dark">गहरी</string>
<string name="close">बंद कर</string>
<string name="try_again">पुनः प्रयास करें</string>
<string name="you_have_not_favourites_yet">अभी तक कोई पसंदीदा नहीं है</string>
<string name="remove">निकालो</string>
<string name="remove">हटाएँ</string>
<string name="by_name">नाम</string>
<string name="popular">लोकप्रिय</string>
<string name="local_storage">स्थानीय स्टॉरेज</string>
@@ -22,69 +22,69 @@
<string name="network_error">नेटवर्क समस्या</string>
<string name="favourites">पसंदीदा</string>
<string name="detailed_list">विस्तृत सूची</string>
<string name="settings">सेटिंग्स</string>
<string name="list_mode">सूची रुपी</string>
<string name="settings">सेटिंग्स</string>
<string name="list_mode">सूची मोड</string>
<string name="chapter_d_of_d">अध्याय %1$d, %2$d में से</string>
<string name="computing_">गणना हो रही है…</string>
<string name="add_new_category">नई श्रेणी</string>
<string name="clear_history">इतिहास मिटाए</string>
<string name="share">भेजो</string>
<string name="share">शेयर</string>
<string name="create_shortcut">शॉर्टकट बनाएं…</string>
<string name="share_s">%s भेजो</string>
<string name="search">खोज</string>
<string name="search_manga">ंगा खोज</string>
<string name="share_s">%s साझा करें</string>
<string name="search">खोजें</string>
<string name="search_manga">मंगा खोजें</string>
<string name="manga_downloading_">डाउनलोड हो रहा है…</string>
<string name="downloads">डाउनलोड किए गए मांगा</string>
<string name="downloads">डाउनलोड्स</string>
<string name="by_rating">रेटिंग</string>
<string name="clear">साफ करें</string>
<string name="page_saved">पन्ना संचय हो गया</string>
<string name="share_image">चित्र को भेजें</string>
<string name="page_saved">सहेजा गया</string>
<string name="share_image">छवि साझा करें</string>
<string name="delete">मिटाएं</string>
<string name="clear_pages_cache">न्ने के कैछ को मिटाए</string>
<string name="clear_pages_cache">ेज कैश साफ़ करे</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">सामान्य</string>
<string name="webtoon">वैबटून</string>
<string name="webtoon">Webtoon</string>
<string name="remote_sources">मांगा स्रोत</string>
<string name="download_complete">डाउनलोड हो गया</string>
<string name="processing_">प्रक्रिया चल रही है…</string>
<string name="history">इतिहास</string>
<string name="grid">ग्रिड</string>
<string name="loading_">लोड हो रहा है…</string>
<string name="text_file_not_supported">या तो झीप नहीं तो सीबीझेड फाईल को चुनें।</string>
<string name="text_file_not_supported">ZIP या CBZ मैं से एक फ़ाइल चुनें।</string>
<string name="updated">अपडेट हो गया</string>
<string name="_s_deleted_from_local_storage">\"%s\", स्थानीय स्टरेज में से मिट गईं</string>
<string name="_s_deleted_from_local_storage">\"%s\" को स्थानीय स्टरेज से हटा दिया गया</string>
<string name="text_clear_history_prompt">पढ़ने का इतिहास सदा के लिए मिटाए\?</string>
<string name="save_page">न्ना संचय करो</string>
<string name="_import">आयात करें</string>
<string name="save_page">ेज सहेजें</string>
<string name="_import">आयात</string>
<string name="operation_not_supported">यह कार्य समर्थित नहीं है</string>
<string name="sort_order">छंटाई क्रम</string>
<string name="sort_order">क्रमबद्धता क्रम</string>
<string name="list">सूची</string>
<string name="filter">फिल्टर</string>
<string name="theme">थीम</string>
<string name="automatic">फोन जैसा</string>
<string name="follow_system">सिस्टम की पालन करें</string>
<string name="pages">पन्ने</string>
<string name="no_description">कोई विवरण नहीं है</string>
<string name="updates">अपडेट</string>
<string name="manga_shelf">शेल्फ</string>
<string name="text_history_holder_secondary">«अन्वेषण» विभाग में जो भी आपको पढ़ना है उसे खोजे</string>
<string name="all_favourites">र्वे प्रिय</string>
<string name="light_indicator">LED इंडिकेटर</string>
<string name="favourites_categories">पसंदिता केटेगरी</string>
<string name="text_history_holder_secondary">\"एक्सप्लोर करें\" अनुभाग में जानें कि क्या पढ़ना है</string>
<string name="all_favourites">भी पसंदीदा</string>
<string name="light_indicator">LED सूचक</string>
<string name="favourites_categories">पसंदीदा श्रेणियां</string>
<string name="gestures_only">केवल जेस्चर</string>
<string name="clear_thumbs_cache">थंबनेल कैच को साफ करे</string>
<string name="taps_on_edges">किनारे पर टैप</string>
<string name="switch_pages">ेज को बदले</string>
<string name="rotate_screen">स्क्रीन ुमा</string>
<string name="text_shelf_holder_secondary">«अन्वेषण» विभाग में जो भी पढ़ना है उसे खोजे</string>
<string name="vibration">वैब्रेश</string>
<string name="remove_category">निकालो</string>
<string name="switch_pages">न्नो को बदले</string>
<string name="rotate_screen">स्क्रीन ुमायें</string>
<string name="text_shelf_holder_secondary">\"एक्सप्लोर करें\" अनुभाग में जानें कि क्या पढ़ना है</string>
<string name="vibration">कंप</string>
<string name="remove_category">हटाएँ</string>
<string name="read_mode">पढ़ने की विधि</string>
<string name="internal_storage">आंतरिक स्टोरेज</string>
<string name="read_later">बाद में पढ़े</string>
<string name="cannot_find_available_storage">स्टोरेज उपलब्ध नहीं हैं</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%2$d में से %1$d</string>
<string name="text_feed_holder">आप जो पढ़ रहे हैं उसके नए अध्याय यहां दिखाए गए हैं</string>
<string name="favourites_category_empty">केटेगोरी खाली हैं</string>
<string name="favourites_category_empty">रिक्त श्रेणी</string>
<string name="manga_save_location">डाउनलोड फ़ोल्डर</string>
<string name="updates_feed_cleared">साफ हो गया</string>
<string name="update">अपडेट</string>
@@ -92,40 +92,506 @@
<string name="app_update_available">इस ऐप का नया संस्करण उपलब्ध हैं</string>
<string name="new_version_s">नया संस्करण: %s</string>
<string name="text_delete_local_manga">डिवाइस से \"%s\" को स्थायी रूप से हटाएं?</string>
<string name="text_history_holder_primary">जो भी आप पढ़गे वे सब यहां दिखेगा</string>
<string name="text_history_holder_primary">आप जो पढ़ेंगे व यहां प्रदर्शित किया जाएगा</string>
<string name="delete_manga">मंगा हटाएं</string>
<string name="notification_sound">सूचना की ध्वनि</string>
<string name="search_history_cleared">साफ हो गया</string>
<string name="open_in_browser">ब्राउर में खोले</string>
<string name="open_in_browser">वेब ब्राउज़र में खोले</string>
<string name="notifications">सूचनाएं</string>
<string name="not_available">उपल्ब्ध नहीं हैं</string>
<string name="track_sources">अपडेट के लिए देखे</string>
<string name="track_sources">अपडेट देखे</string>
<string name="clear_search_history">खोजा हुवा इतिहास को साफ करे</string>
<string name="download">डाउनलोड</string>
<string name="size_s">साइज: %s</string>
<string name="text_shelf_holder_primary">आपकंगा यहाँ दिखाई देंगे</string>
<string name="size_s">आकार: %s</string>
<string name="text_shelf_holder_primary">आपक मंगा यहां प्रदर्शित किया जाएगा</string>
<string name="new_chapters">नये अध्याय</string>
<string name="volume_buttons">वॉल्यूम बटन</string>
<string name="clear_updates_feed">अपडेट्स फीड को साफ करे</string>
<string name="clear_updates_feed">अपडेट फीड साफ करे</string>
<string name="notifications_settings">सुचना के सेटिंग</string>
<string name="domain">क्षेत्र</string>
<string name="save_manga">जमा करो</string>
<string name="large_manga_save_confirm">इस मंगा में %s हैं। सबको जमा करे\?</string>
<string name="domain">डोमेन</string>
<string name="save_manga">सहेजें</string>
<string name="large_manga_save_confirm">इस मंगा में %s है। यह सब सहेजें?</string>
<string name="reader_settings">रीडर के सेटिंग</string>
<string name="text_search_holder_secondary">क्वेरी को पुन बनाने का प्रयास करें।</string>
<string name="text_search_holder_secondary">क्वेरी को पुन: तैयार करने का प्रयास करें।</string>
<string name="error">त्रुटि</string>
<string name="grid_size">ग्रिड का आकार</string>
<string name="_continue">जारी रखें</string>
<string name="recent_manga">अभी के</string>
<string name="search_on_s">%s पर खोज</string>
<string name="recent_manga">हालिया</string>
<string name="search_on_s">%s पर खोजें</string>
<string name="search_results">खोज के परिणाम</string>
<string name="pages_animation">पेज का एनीमेशन</string>
<string name="pages_animation">पेज एनीमेशन</string>
<string name="other_storage">अन्य स्टोरेज</string>
<string name="external_storage">बाहरी स्टोरेज</string>
<string name="text_local_holder_secondary">किसी ऑनलाइन कैटलॉग से कुछ सहेजें या किसी फ़ाइल से आयात करें।</string>
<string name="text_local_holder_primary">पहले कुछ जमा करे</string>
<string name="text_empty_holder_primary">लगता है यहां तोह कुछ नहीं है</string>
<string name="done">होगया</string>
<string name="text_local_holder_primary">पहले कुछ सहेजें</string>
<string name="text_empty_holder_primary">यहाँ कुछ खाली सा है…</string>
<string name="done">हो गया</string>
<string name="dont_check">जाँच मत करो</string>
<string name="enter_password">पासवर्ड दर्ज करें</string>
</resources>
<string name="advanced">विकसित</string>
<string name="catalog">कैटलॉग</string>
<string name="manage_sources">स्रोत प्रबंधित करें</string>
<string name="screenshots_policy">स्क्रीनशॉट नीति</string>
<string name="screenshots_allow">अनुमति दें</string>
<string name="suggestions">सुझाव</string>
<string name="suggestions_enable">सुझाव सक्षम करें</string>
<string name="suggestions_summary">अपनी प्राथमिकताओं के आधार पर मंगा का सुझाव दें</string>
<string name="suggestions_info">सभी डेटा का विश्लेषण केवल इस डिवाइस पर स्थानीय रूप से किया जाता है और कभी भी कहीं नहीं भेजा जाता है।</string>
<string name="filter_load_error">शैलियों की सूची लोड करने में असमर्थ</string>
<string name="reset_filter">फ़िल्टर रीसेट करें</string>
<string name="onboard_text">उन भाषाओं का चयन करें जिन्हें आप मंगा पढ़ना चाहते हैं। आप इसे बाद में सेटिंग में बदल सकते हैं।</string>
<string name="chapters_empty">इस मंगा में कोई अध्याय नहीं</string>
<string name="suggestions_excluded_genres">शैलियों को छोड़ें</string>
<string name="removal_completed">निष्कासन पूरा हुआ</string>
<string name="download_slowdown_summary">आपके IP पते को ब्लॉक होने से बचाने में मदद करता है</string>
<string name="local_manga_processing">सहेजे गए मंगा का प्रसंस्करण</string>
<string name="chapters_will_removed_background">अध्याय पृष्ठभूमि में हटा दिए जाएंगे</string>
<string name="comics_archive">कॉमिक्स संग्रह</string>
<string name="webtoon_zoom">वेबटून ज़ूम</string>
<string name="repeat_password">पासवर्ड दोहराएँ</string>
<string name="passwords_mismatch">बेमेल पासवर्ड</string>
<string name="app_version">संस्करण %s</string>
<string name="check_for_updates">अपडेट के लिए जांचें</string>
<string name="no_update_available">कोई अपडेट उपलब्ध नहीं</string>
<string name="scale_mode">स्केल मोड</string>
<string name="black_dark_theme">काली</string>
<string name="black_dark_theme_summary">AMOLED स्क्रीन पर कम पावर का उपयोग होता है</string>
<string name="create_backup">डेटा बैकअप बनाएं</string>
<string name="restore_backup">बैकअप से पुनर्स्थापित करें</string>
<string name="data_restored">पुनर्स्थापित किया गया</string>
<string name="data_restored_with_errors">डेटा पुनर्स्थापित कर दिया गया था, लेकिन त्रुटियाँ हैं</string>
<string name="just_now">अभी</string>
<string name="yesterday">कल</string>
<string name="long_ago">बहुत पहले</string>
<string name="group">समूह</string>
<string name="today">आज</string>
<string name="tap_to_try_again">दोबारा प्रयास करने के लिए टैप करें</string>
<string name="reader_mode_hint">इस मंगा के लिए चुना गया कॉन्फ़िगरेशन याद रखा जाएगा</string>
<string name="silent">खामोश</string>
<string name="captcha_required">CAPTCHA आवश्यक है</string>
<string name="captcha_solve">हल करें</string>
<string name="cookies_cleared">सभी कुकीज़ हटा दी गईं</string>
<string name="backup_saved">बैकअप सहेजा गया</string>
<string name="welcome">स्वागत है</string>
<string name="tracker_warning">कुछ डिवाइसों में अलग-अलग सिस्टम व्यवहार होता है, जो पृष्ठभूमि कार्यों को बाधित कर सकता है।</string>
<string name="read_more">और पढ़ें</string>
<string name="chapter_is_missing">अध्याय गायब है</string>
<string name="about_app_translation_summary">इस ऐप का अनुवाद करें</string>
<string name="auth_complete">अधिकृत</string>
<string name="auth_not_supported_by">%s पर लॉग इन करना समर्थित नहीं है</string>
<string name="text_clear_cookies_prompt">आप सभी स्रोतों से लॉग आउट हो जायेंगे</string>
<string name="state_finished">समाप्त</string>
<string name="state_ongoing">चल रही है</string>
<string name="system_default">डिफ़ॉल्ट</string>
<string name="exclude_nsfw_from_history">इतिहास से NSFW मंगा को बाहर करें</string>
<string name="show_pages_numbers">क्रमांकित पन्ने</string>
<string name="enabled_sources">प्रयुक्त स्रोत</string>
<string name="available_sources">उपलब्ध स्रोत</string>
<string name="screenshots_block_nsfw">NSFW पर रोक लगाएं</string>
<string name="screenshots_block_all">हमेशा ब्लॉक करें</string>
<string name="text_suggestion_holder">मंगा पढ़ना शुरू करें और आपको व्यक्तिगत सुझाव मिलेंगे</string>
<string name="exclude_nsfw_from_suggestions">NSFW मंगा का सुझाव न दें</string>
<string name="disabled">अक्षम</string>
<string name="never">कभी नहीं</string>
<string name="only_using_wifi">केवल Wi-Fi पर</string>
<string name="always">हमेशा</string>
<string name="nsfw">18+</string>
<string name="various_languages">विभिन्न भाषाएँ</string>
<string name="search_chapters">अध्याय खोजें</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="appearance">दिखावट</string>
<string name="preload_pages">पन्ने प्रीलोड करें</string>
<string name="edit">संपादित करें</string>
<string name="download_slowdown">धीमी गति से डाउनलोड करें</string>
<string name="edit_category">श्रेणी संपादित करें</string>
<string name="tracking">ट्रैकिंग</string>
<string name="empty_favourite_categories">कोई पसंदीदा श्रेणियां नहीं</string>
<string name="logout">लॉग आउट</string>
<string name="bookmark_add">बुकमार्क जोड़ें</string>
<string name="bookmark_remove">बुकमार्क हटाएँ</string>
<string name="bookmarks">बुकमार्क्स</string>
<string name="bookmark_removed">बुकमार्क हटा दिया गया</string>
<string name="bookmark_added">बुकमार्क जोड़ा गया</string>
<string name="undo">पूर्ववत</string>
<string name="removed_from_history">इतिहास से हटा दिया गया</string>
<string name="detect_reader_mode">ऑटोडिटेक्ट रीडर मोड</string>
<string name="detect_reader_mode_summary">स्वचालित रूप से पता लगाएं कि मंगा वेबटून है या नहीं</string>
<string name="disable_battery_optimization">बैटरी अनुकूलन अक्षम करें</string>
<string name="send">भेजें</string>
<string name="disable_all">सब अक्षम करें</string>
<string name="use_fingerprint">यदि उपलब्ध हो तो फ़िंगरप्रिंट का उपयोग करें</string>
<string name="appwidget_shelf_description">आपके पसंदीदा में से मंगा</string>
<string name="appwidget_recent_description">आपने हाल ही में पढ़ा मंगा</string>
<string name="report">रिपोर्ट</string>
<string name="status_planned">योजना बनाई</string>
<string name="status_reading">पढ़ रहा हूँ</string>
<string name="status_re_reading">दोबारा पढ़ना</string>
<string name="status_completed">पूरा किया हुआ</string>
<string name="status_on_hold">होल्ड पर</string>
<string name="status_dropped">गिरा दिया गया</string>
<string name="show_reading_indicators">पढ़ने की प्रगति संकेतक दिखाएँ</string>
<string name="data_deletion">डेटा विलोपन</string>
<string name="exclude_nsfw_from_history_summary">NSFW के रूप में चिह्नित मंगा को इतिहास में कभी नहीं जोड़ा जाएगा और आपकी प्रगति सहेजी नहीं जाएगी</string>
<string name="clear_cookies_summary">कुछ समस्या होने पर मदद मिल सकती है. सभी प्राधिकरण अमान्य कर दिए जाएंगे</string>
<string name="invalid_domain_message">अमान्य डोमेन</string>
<string name="select_range">रेंज चुनें</string>
<string name="clear_all_history">सारा इतिहास साफ़ करें</string>
<string name="last_2_hours">पिछले 2 घंटे</string>
<string name="history_cleared">इतिहास साफ़ हो गया</string>
<string name="manage">प्रबंधित करें</string>
<string name="explore">अन्वेषण करें</string>
<string name="confirm_exit">बाहर निकलने के लिए फिर से वापस दबाएँ</string>
<string name="exit_confirmation">बाहर निकलने की पुष्टि</string>
<string name="saved_manga">सहेजा गया मंगा</string>
<string name="pages_cache">पन्नो का कैश</string>
<string name="other_cache">अन्य कैश</string>
<string name="available">उपलब्ध</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="removed_from_favourites">पसंदीदा से हटाया गया</string>
<string name="options">विकल्प</string>
<string name="not_found_404">सामग्री नहीं मिली या हटाई गई</string>
<string name="incognito_mode">गुप्त मोड</string>
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
<string name="reader_info_bar">रीडर में सूचना पट्टी दिखाएं</string>
<string name="folder_with_images">छवियों वाला फ़ोल्डर</string>
<string name="import_completed">आयात पूरा हुआ</string>
<string name="history_shortcuts_summary">एप्लिकेशन आइकन पर लंबे समय तक दबाकर हालिया मंगा को उपलब्ध कराएं</string>
<string name="reader_control_ltr">एर्गोनोमिक रीडर नियंत्रण</string>
<string name="color_correction">रंग सुधार</string>
<string name="brightness">चमक</string>
<string name="storage_usage">स्टोरेज उपयोग</string>
<string name="contrast">कंट्रास्ट</string>
<string name="reset">रीसेट</string>
<string name="color_correction_hint">इस मंगा के लिए चुनी गई रंग सेटिंग्स याद रखी जाएंगी</string>
<string name="text_unsaved_changes_prompt">सहेजे न गए परिवर्तन सहेजें या हटाएँ?</string>
<string name="error_no_space_left">डिवाइस पर जगह समाप्त</string>
<string name="reader_slider">पेज स्विचिंग स्लाइडर दिखाएँ</string>
<string name="different_languages">विभिन्न भाषाएं</string>
<string name="network_unavailable">नेटवर्क उपलब्ध नहीं है</string>
<string name="server_error">सर्वर साइड त्रुटि (%1$d). कृपया बाद में पुन: प्रयास करें</string>
<string name="clear_new_chapters_counters">नए अध्यायों के बारे में भी स्पष्ट जानकारी</string>
<string name="compact">सघन</string>
<string name="mark_as_current">वर्तमान के रूप में चिह्नित करें</string>
<string name="language">भाषा</string>
<string name="enable_logging">लॉगिंग सक्षम करें</string>
<string name="show_suspicious_content">संदिग्ध सामग्री दिखाएं</string>
<string name="theme_name_dynamic">गतिशील</string>
<string name="color_theme">रंग योजना</string>
<string name="show_in_grid_view">ग्रिड दृश्य में दिखाएँ</string>
<string name="theme_name_miku">Miku</string>
<string name="theme_name_rikka">Rikka</string>
<string name="theme_name_kanade">Kanade</string>
<string name="scrobbling_empty_hint">पढ़ने की प्रगति को ट्रैक करने के लिए, मंगा विवरण स्क्रीन पर मेनू → ट्रैक का चयन करें।</string>
<string name="services">सेवाएं</string>
<string name="settings_apply_restart_required">कृपया इन परिवर्तनों को लागू करने के लिए एप्लिकेशन को पुनः आरंभ करें</string>
<string name="comics_archive_import_description">आप एक या अधिक .cbz या .zip फ़ाइलों का चयन कर सकते हैं, प्रत्येक फ़ाइल को एक अलग मंगा के रूप में पहचाना जाएगा।</string>
<string name="user_agent">UserAgent हेडर</string>
<string name="speed">गति</string>
<string name="restore_backup_description">उपयोगकर्ता डेटा का पहले से बनाया गया बैकअप आयात करें</string>
<string name="show_on_shelf">शेल्फ पर दिखाएँ</string>
<string name="sync_auth_hint">आप किसी मौजूदा खाते में साइन इन कर सकते हैं या एक नया खाता बना सकते हैं</string>
<string name="mirror_switching_summary">यदि मिरर उपलब्ध हैं तो त्रुटियों पर मंगा स्रोतों के लिए स्वचालित रूप से डोमेन स्विच करें</string>
<string name="find_similar">समान खोजें</string>
<string name="pause">विराम</string>
<string name="resume">फिर से शुरू करें</string>
<string name="paused">रुका हुआ</string>
<string name="cancel_all">सभी रद्द करें</string>
<string name="mirror_switching">स्वचालित रूप से मिरर चुनें</string>
<string name="downloads_wifi_only_summary">मोबाइल नेटवर्क पर स्विच करते समय डाउनलोड करना बंद कर दें</string>
<string name="suggestion_manga">सुझाव: %s</string>
<string name="suggestions_notifications_summary">कभी-कभी सुझाए गए मंगा के साथ सूचनाएं दिखाएं</string>
<string name="more">अधिक</string>
<string name="enable">सक्षम</string>
<string name="no_thanks">जी नहीं, धन्यवाद</string>
<string name="cancel_all_downloads_confirm">सभी सक्रिय डाउनलोड रद्द कर दिए जाएंगे, आंशिक रूप से डाउनलोड किया गया डेटा खो जाएगा</string>
<string name="remove_completed_downloads_confirm">आपका डाउनलोड इतिहास स्थायी रूप से हटा दिया जाएगा</string>
<string name="text_downloads_list_holder">आपके पास कोई डाउनलोड नहीं है</string>
<string name="downloads_resumed">डाउनलोड फिर से शुरू कर दिए गए हैं</string>
<string name="downloads_paused">डाउनलोड रोक दिए गए हैं</string>
<string name="downloads_removed">डाउनलोड हटा दिए गए हैं</string>
<string name="suggestions_enable_prompt">क्या आप वैयक्तिकृत मंगा सुझाव प्राप्त करना चाहते हैं?</string>
<string name="web_view_unavailable">WebView उपलब्ध नहीं है: जांचें कि WebView प्रदाता स्थापित है या नहीं</string>
<string name="clear_network_cache">नेटवर्क कैश साफ़ करें</string>
<string name="type">प्रकार</string>
<string name="address">पता</string>
<string name="port">पोर्ट</string>
<string name="proxy">प्रॉक्सी</string>
<string name="invalid_value_message">अमान्य मान</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="password">पासवर्ड</string>
<string name="invert_colors">रंगों को उलटा करें</string>
<string name="invalid_port_number">अमान्य पोर्ट नंबर</string>
<string name="network">नेटवर्क</string>
<string name="pages_animation_summary">एनिमेट पेज स्विचिंग</string>
<string name="show_pages_numbers_summary">निचले कोने में पेज संख्याएँ दिखाएँ</string>
<string name="restore_summary">पहले बनाए गए बैकअप को पुनर्स्थापित करें</string>
<string name="webtoon_zoom_summary">वेबटून मोड में ज़ूम इन जेस्चर की अनुमति दें</string>
<string name="reader_info_bar_summary">स्क्रीन के शीर्ष पर वर्तमान समय और पढ़ने की प्रगति दिखाएं</string>
<string name="volume_">वॉल्यूम %d</string>
<string name="volume_unknown">अज्ञात वॉल्यूम</string>
<string name="downloads_settings_info">यदि आपको सर्वर-साइड ब्लॉकिंग की समस्या हो रही है तो आप स्रोत सेटिंग्स में प्रत्येक मंगा स्रोत के लिए व्यक्तिगत रूप से डाउनलोड मंदी को सक्षम कर सकते हैं</string>
<string name="approximate_reading_time">अनुमानित पढ़ने का समय</string>
<string name="remove_from_history">इतिहास से हटा दें</string>
<string name="translations">अनुवाद</string>
<string name="skip">छोड़ें</string>
<string name="incognito_mode_hint">आपकी पढ़ने की प्रगति सहेजी नहीं जाएगी</string>
<string name="content_rating">सामग्री मूल्यांकन</string>
<string name="genres_exclude">शैलियों को छोड़ें</string>
<string name="rating_safe">सुरक्षित</string>
<string name="rating_adult">वयस्क</string>
<string name="rating_suggestive">सुझावात्मक</string>
<string name="last_read">अंतिम पढ़ा</string>
<string name="lock_screen_rotation">लॉक स्क्रीन रोटेशन</string>
<string name="vertical">लंबवत</string>
<string name="download_started">डाउनलोड प्रारंभ हुआ</string>
<string name="manga_list">मंगा सूची</string>
<string name="disable_nsfw">NSFW अक्षम करें</string>
<string name="images_proxy_title">छवियाँ अनुकूलन प्रॉक्सी</string>
<string name="data_and_privacy">डेटा और गोपनीयता</string>
<string name="email_password_enter_hint">जारी रखने के लिए अपना ईमेल और पासवर्ड डालें</string>
<string name="clear_source_cookies_summary">केवल निर्दिष्ट डोमेन के लिए कुकीज़ साफ़ करें। अधिकांश मामलों में प्राधिकरण अमान्य हो जाएगा</string>
<string name="details_button_tip">अधिक विकल्प देखने के लिए पढ़ें बटन को दबाकर रखें</string>
<string name="download_option_next_unread_n_chapters">अगला अपठित %s</string>
<string name="no_access_to_file">आपके पास इस फ़ाइल या डॉयरेक्टरी तक कोई पहुंच नहीं है</string>
<string name="voice_search">ध्वनि खोज</string>
<string name="related_manga">संबंधित मंगा</string>
<string name="description">विवरण</string>
<string name="this_month">इस महीने</string>
<string name="background">पृष्ठभूमि</string>
<string name="local_manga_directories">स्थानीय मंगा डॉयरेक्टरी</string>
<string name="data_not_restored_text">सुनिश्चित करें कि आपने सही बैकअप फ़ाइल का चयन किया है</string>
<string name="data_not_restored">डेटा पुनर्स्थापित नहीं किया गया</string>
<string name="suggestions_wifi_only_summary">मीटर्ड नेटवर्क कनेक्शन का उपयोग करके सुझावों को अपडेट न करें</string>
<string name="tracker_wifi_only_summary">मीटर्ड नेटवर्क कनेक्शन का उपयोग करके नए अध्यायों की जाँच न करें</string>
<string name="search_hint">मंगा शीर्षक, शैली या स्रोत का नाम दर्ज करें</string>
<string name="progress">प्रगति</string>
<string name="order_added">जोड़ा गया</string>
<string name="show">दिखाएँ</string>
<string name="languages">भाषाएं</string>
<string name="unknown">अज्ञात</string>
<string name="in_progress">प्रगति पर है</string>
<string name="error_corrupted_file">अमान्य डेटा लौटाया गया है या फ़ाइल दूषित है</string>
<string name="items_limit_exceeded">कोई और आइटम नहीं जोड़ा जा सकता</string>
<string name="on_device">डिवाइस पर</string>
<string name="main_screen_sections">मुख्य स्क्रीन अनुभाग</string>
<string name="directories">डॉयरेक्टरी</string>
<string name="to_top">शीर्ष पर</string>
<string name="moved_to_top">शीर्ष पर ले जाया गया</string>
<string name="zoom_out">ज़ूम आउट</string>
<string name="zoom_in">ज़ूम इन</string>
<string name="reader_zoom_buttons">ज़ूम बटन दिखाएँ</string>
<string name="reader_zoom_buttons_summary">निचले दाएं कोने में ज़ूम नियंत्रण बटन दिखाना है या नहीं</string>
<string name="keep_screen_on">स्क्रीन चालू रखें</string>
<string name="keep_screen_on_summary">जब आप मंगा पढ़ रहे हों तो स्क्रीन बंद न करें</string>
<string name="enhanced_colors_summary">बैंडिंग को कम करता है, लेकिन प्रदर्शन को प्रभावित कर सकता है</string>
<string name="enhanced_colors">32-बिट रंग मोड</string>
<string name="suggest_new_sources">ऐप अपडेट के बाद नए स्रोत सुझाएं</string>
<string name="suggest_new_sources_summary">एप्लिकेशन को अपडेट करने के बाद नए जोड़े गए स्रोतों को सक्षम करने का संकेत दें</string>
<string name="online_variant">ऑनलाइन संस्करण</string>
<string name="periodic_backups">आवधिक बैकअप</string>
<string name="backup_frequency">बैकअप निर्माण आवृत्ति</string>
<string name="frequency_every_day">प्रतिदिन</string>
<string name="frequency_once_per_week">हर हफ्ते एक बार</string>
<string name="frequency_twice_per_month">प्रति माह दो बार</string>
<string name="frequency_once_per_month">प्रति महीना एक बार</string>
<string name="frequency_every_2_days">हर 2 दिन में</string>
<string name="periodic_backups_enable">आवधिक बैकअप सक्षम करें</string>
<string name="backups_output_directory">बैकअप आउटपुट डायरेक्टरी</string>
<string name="last_successful_backup">अंतिम सफल बैकअप: %s</string>
<string name="speed_value">x%.1f</string>
<string name="content_type_manga">मंगा</string>
<string name="content_type_hentai">हेंताई</string>
<string name="content_type_comics">कॉमिक्स</string>
<string name="source_summary_pattern">%1$s, %2$s</string>
<string name="content_type_other">अन्य</string>
<string name="source_enabled">स्रोत सक्षम</string>
<string name="sources_catalog">स्रोत कैटलॉग</string>
<string name="no_manga_sources_catalog_text">इस अनुभाग में कोई स्रोत उपलब्ध नहीं है, या यह सब पहले ही जोड़ा जा चुका होगा।
\nबने रहें</string>
<string name="no_manga_sources_found">आपकी क्वेरी से कोई उपलब्ध मंगा स्रोत नहीं मिला</string>
<string name="manual">मैन्युअल</string>
<string name="available_d">उपलब्ध: %1$d</string>
<string name="disable_nsfw_summary">यदि संभव हो तो NSFW स्रोतों को अक्षम करें और वयस्क मंगा को सूची से छिपाएँ</string>
<string name="state_paused">रोके गए</string>
<string name="reader_optimize">मेमोरी खपत कम करें (beta)</string>
<string name="state">अवस्था</string>
<string name="error_multiple_genres_not_supported">अनेक शैलियों द्वारा फ़िल्टर करना इस मंगा स्रोत द्वारा समर्थित नहीं है</string>
<string name="error_search_not_supported">खोज इस मंगा स्रोत द्वारा समर्थित नहीं है</string>
<string name="genres_search_hint">शैली का नाम लिखना प्रारंभ करें</string>
<string name="disable_battery_optimization_summary_downloads">यदि आपको इसमें कोई समस्या है तो डाउनलोड शुरू करने में मदद मिल सकती है</string>
<string name="restore">पुनर्स्थापित करें</string>
<string name="backup_date_">बैकअप दिनांक: %s</string>
<string name="state_upcoming">आगामी</string>
<string name="by_name_reverse">नाम उलटा</string>
<string name="mark_as_completed">पूर्ण के रूप में चिह्नित करें</string>
<string name="mark_as_completed_prompt">चयनित मंगा को पूरी तरह से पढ़ा गया के रूप में चिह्नित करें?
\n
\nचेतावनी: वर्तमान पठन प्रगति नष्ट हो जाएगी।</string>
<string name="approximate_remaining_time">अनुमानित समय शेष है</string>
<string name="remaining_time_pattern">%1$s %2$s</string>
<string name="two_pages">दो पन्ने</string>
<string name="show_menu">मेन्यू दिखाएँ</string>
<string name="long_tap_action">लंबे टैप पर कार्रवाई</string>
<string name="tap_action">टैप पर कार्रवाई</string>
<string name="none">कोई नहीं</string>
<string name="config_reset_confirm">सेटिंग्स को डिफ़ॉल्ट मानों पर रीसेट करें? इस एक्शन को वापस नहीं किया जा सकता।</string>
<string name="use_two_pages_landscape">लैंडस्केप ओरिएंटेशन पर दो पेज लेआउट का उपयोग करें (beta)</string>
<string name="got_it">समझ गया</string>
<string name="default_tab">डिफ़ॉल्ट टैब</string>
<string name="download_option_all_chapters">अनुवाद सहित सभी अध्याय %s</string>
<string name="download_option_whole_manga">संपूर्ण मंगा</string>
<string name="download_option_first_n_chapters">प्रथम %s</string>
<string name="download_option_all_unread">सभी अपठित अध्याय</string>
<string name="download_option_all_unread_b">सभी अपठित अध्याय (%s)</string>
<string name="download_option_manual_selection">अध्यायों का चयन मैन्युअल रूप से करें</string>
<string name="color_light">हल्का</string>
<string name="color_dark">गहरा</string>
<string name="color_white">सफ़ेद</string>
<string name="color_black">काला</string>
<string name="view_list">सूची देखें</string>
<string name="manage_categories">श्रेणी व्यवस्थित करें</string>
<string name="downloaded">डाउनलोड किया गया</string>
<string name="too_many_requests_message">बहुत सारे अनुरोध. बाद में पुन: प्रयास</string>
<string name="related_manga_summary">संबंधित मंगा की एक सूची दिखाएं. कुछ मामलों में यह ग़लत या गायब हो सकता है</string>
<string name="custom_directory">कस्टम डायरेक्टरी</string>
<string name="pick_custom_directory">कस्टम डायरेक्टरी चुनें</string>
<string name="default_webtoon_zoom_out">डिफ़ॉल्ट वेबटून ज़ूम आउट</string>
<string name="captcha_required_summary">%s को ठीक से काम करने के लिए कैप्चा को हल करने की आवश्यकता है</string>
<string name="fullscreen_mode">पूर्ण स्क्रीन मोड</string>
<string name="reader_fullscreen_summary">सिस्टम स्थिति और नेविगेशन बार छिपाएँ</string>
<string name="username">उपयोक्तानाम</string>
<string name="authorization_optional">प्राधिकरण (वैकल्पिक)</string>
<string name="category_hidden_done">यह श्रेणी मुख्य स्क्रीन से छिपी हुई थी और मेनू → श्रेणियों को प्रबंधित करें के माध्यम से पहुंच योग्य है</string>
<string name="globally">वैश्विक स्तर पर</string>
<string name="grayscale">ग्रेस्केल</string>
<string name="apply">लागू करें</string>
<string name="ignore_ssl_errors">SSL त्रुटियों को नजरअंदाज करें</string>
<string name="downloads_wifi_only">केवल Wi-Fi के ज़रिए डाउनलोड करें</string>
<string name="show_notification_new_chapters_off">आपको सूचनाएं प्राप्त नहीं होंगी लेकिन नए अध्याय सूचियों में हाइलाइट किए जाएंगे</string>
<string name="notifications_enable">सूचनाएं सक्षम करें</string>
<string name="name">नाम</string>
<string name="bookmarks_removed">बुकमार्क हटा दिए गए</string>
<string name="no_manga_sources">कोई मंगा स्रोत नहीं</string>
<string name="no_manga_sources_text">मंगा को ऑनलाइन पढ़ने के लिए मंगा स्रोतों को सक्षम करें</string>
<string name="random">यादृच्छिक</string>
<string name="reorder">पुन: व्यवस्थित करें</string>
<string name="empty">खाली</string>
<string name="import_will_start_soon">आयात जल्द शुरू होगा</string>
<string name="feed">फ़ीड</string>
<string name="history_shortcuts">हाल के मंगा शॉर्टकट दिखाएँ</string>
<string name="discard">खारिज</string>
<string name="sources_reorder_tip">किसी आइटम को पुन: व्यवस्थित करने के लिए उस पर टैप करके रखें</string>
<string name="wrong_password">गलत पासवर्ड</string>
<string name="protect_application">ऐप को सुरक्षित रखें</string>
<string name="protect_application_summary">Kotatsu शुरू करते समय पासवर्ड मांगें</string>
<string name="right_to_left">दाएं-से-बाएं</string>
<string name="create_category">नई श्रेणी</string>
<string name="zoom_mode_fit_center">केंद्र फिट</string>
<string name="zoom_mode_fit_height">ऊंचाई के अनुरूप</string>
<string name="zoom_mode_fit_width">चौड़ाई के अनुरूप</string>
<string name="zoom_mode_keep_start">प्रारंभ में रखें</string>
<string name="clear_cookies">कूकीज साफ़ करें</string>
<string name="clear_feed">फ़ीड साफ़ करें</string>
<string name="text_clear_updates_feed_prompt">सभी अपडेट इतिहास स्थायी रूप से साफ़ करें?</string>
<string name="check_for_new_chapters">नए अध्यायों की जाँच करें</string>
<string name="reverse">उलटा</string>
<string name="sign_in">साइन इन</string>
<string name="canceled">रद्द किया गया</string>
<string name="account_already_exists">खाता पहले से मौजूद है</string>
<string name="back">पीछे</string>
<string name="sync">सिंक्रनाइज़ेशन</string>
<string name="sync_title">अपना डेटा सिंक करें</string>
<string name="email_enter_hint">जारी रखने के लिए अपना ईमेल दर्ज करें</string>
<string name="hide">छुपाएं</string>
<string name="new_sources_text">नए मंगा स्रोत उपलब्ध हैं</string>
<string name="check_new_chapters_title">नए अध्यायों की जाँच करें और इसके बारे में सूचित करें</string>
<string name="remove_completed">पूर्ण हटा दें</string>
<string name="toggle_ui">UI दिखाएँ/छिपाएँ</string>
<string name="next_chapter">अगला अध्याय</string>
<string name="reader_actions">पाठक क्रियाएँ</string>
<string name="switch_pages_volume_buttons">वॉल्यूम बटन सक्षम करें</string>
<string name="next_page">अगला पेज</string>
<string name="reading_time_estimation">पढ़ने का अनुमानित समय दिखाएँ</string>
<string name="reading_time_estimation_summary">समय अनुमान मान ग़लत हो सकता है</string>
<string name="location">जगह</string>
<string name="queued">कतारबद्ध</string>
<string name="about_app_translation">अनुवाद</string>
<string name="enabled">सक्रिय</string>
<string name="auth_required">इस सामग्री को देखने के लिए साइन इन करें</string>
<string name="default_s">डिफ़ॉल्ट: %s</string>
<string name="next">अगला</string>
<string name="genres">शैलियां</string>
<string name="logged_in_as">%s के रूप में लॉग इन किया गया</string>
<string name="protect_application_subtitle">ऐप शुरू करने के लिए एक पासवर्ड दर्ज करें</string>
<string name="suggestions_updating">सुझाव अपडेट हो रहे हैं</string>
<string name="confirm">पुष्टि करें</string>
<string name="suggestions_excluded_genres_summary">वे शैलियाँ निर्दिष्ट करें जिन्हें आप सुझावों में नहीं देखना चाहते</string>
<string name="password_length_hint">पासवर्ड 4 अक्षर या अधिक का होना चाहिए</string>
<string name="text_delete_local_manga_batch">डिवाइस से चयनित आइटम स्थायी रूप से हटाएं?</string>
<string name="text_clear_search_history_prompt">हाल की सभी खोज क्वेरी को स्थायी रूप से हटा दें?</string>
<string name="about">बारे में</string>
<string name="backup_restore">बैकअप और पुनर्स्थापना</string>
<string name="preparing_">तैयार कर रहे हैं…</string>
<string name="file_not_found">फाइल नहीं मिली</string>
<string name="data_restored_success">सारा डेटा पुनर्स्थापित कर दिया गया</string>
<string name="backup_information">आप अपने इतिहास और पसंदीदा का बैकअप बना सकते हैं और उसे पुनर्स्थापित कर सकते हैं</string>
<string name="show_notification_new_chapters_on">आप जो मंगा पढ़ रहे हैं उसके अपडेट के बारे में आपको सूचनाएं प्राप्त होंगी</string>
<string name="dns_over_https">HTTPS पर DNS</string>
<string name="default_mode">डिफ़ॉल्ट मोड</string>
<string name="disable_battery_optimization_summary">बैकग्राउंड अपडेट जांच में मदद करता है</string>
<string name="crash_text">कुछ गलत हो गया। कृपया इसे ठीक करने में हमारी सहायता के लिए डेवलपर्स को एक बग रिपोर्ट सबमिट करें।</string>
<string name="show_reading_indicators_summary">इतिहास और पसंदीदा में पढ़ा गया प्रतिशत दिखाएँ</string>
<string name="show_all">सब दिखाएं</string>
<string name="downloads_cancelled">डाउनलोड रद्द कर दिए गए हैं</string>
<string name="default_section">डिफ़ॉल्ट अनुभाग</string>
<string name="no_bookmarks_yet">अभी तक कोई बुकमार्क नहीं</string>
<string name="no_bookmarks_summary">आप मंगा पढ़ते समय बुकमार्क बना सकते हैं</string>
<string name="sync_settings">सिंक्रोनाइज़ेशन सेटिंग्स</string>
<string name="exit_confirmation_summary">ऐप से बाहर निकलने के लिए बैक को दो बार दबाएँ</string>
<string name="server_address">सर्वर पता</string>
<string name="sync_host_description">आप स्व-होस्टेड सिंक्रनाइज़ेशन सर्वर या डिफ़ॉल्ट सर्वर का उपयोग कर सकते हैं। यदि आप निश्चित नहीं हैं कि आप क्या कर रहे हैं तो इसे न बदलें।</string>
<string name="no_chapters">कोई अध्याय नहीं</string>
<string name="automatic_scroll">स्वचालित स्क्रॉल</string>
<string name="importing_manga">मंगा आयात किया जा रहा है</string>
<string name="import_completed_hint">स्थान बचाने के लिए आप मूल फ़ाइल को स्टोरेज से हटा सकते हैं</string>
<string name="network_unavailable_hint">मंगा को ऑनलाइन पढ़ने के लिए Wi-Fi या मोबाइल नेटवर्क चालू करें</string>
<string name="source_disabled">स्रोत अक्षम किया गया</string>
<string name="prefetch_content">सामग्री प्रीलोड हो रही है</string>
<string name="share_logs">लॉग साझा करें</string>
<string name="enable_logging_summary">डिबग उद्देश्यों के लिए कुछ क्रियाएँ रिकॉर्ड करें। यदि आप निश्चित नहीं हैं कि आप क्या कर रहे हैं तो इसे चालू न करें</string>
<string name="theme_name_asuka">Asuka</string>
<string name="theme_name_mion">Mion</string>
<string name="theme_name_sakura">Sakura</string>
<string name="theme_name_mamimi">Mamimi</string>
<string name="allow_unstable_updates">अस्थिर अपडेट की अनुमति दें</string>
<string name="nothing_here">यहां कुछ नहीं है</string>
<string name="allow_unstable_updates_summary">अस्थिर बिल्ड के बारे में सूचनाएं प्राप्त करें</string>
<string name="categories_delete_confirm">क्या आप वाकई चयनित पसंदीदा श्रेणियां हटाना चाहते हैं?
\nइसमें मौजूद सारा मंगा नष्ट हो जाएगा और इसे पूर्ववत नहीं किया जा सकता।</string>
<string name="manga_error_description_pattern">त्रुटि विवरण:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. यह सुनिश्चित करने के लिए कि यह अपने स्रोत पर उपलब्ध है &lt;a href=%2$s&gt;मंगा को वेब ब्राउज़र में खोलने का प्रयास करें&lt;/a&gt;&lt;br&gt;2। सुनिश्चित करें कि आप &lt;a href=kotatsu://about&gt;Kotatsu के नवीनतम संस्करण&lt;/a&gt;&lt;br&gt;3 का उपयोग कर रहे हैं। यदि यह उपलब्ध है, तो डेवलपर्स को एक त्रुटि रिपोर्ट भेजें।</string>
<string name="folder_with_images_import_description">आप अभिलेखों या छवियों वाली एक डॉयरेक्टरी का चयन कर सकते हैं। प्रत्येक संग्रह (या उपडॉयरेक्टरी) को एक अध्याय के रूप में पहचाना जाएगा।</string>
<string name="images_procy_description">यदि संभव हो तो ट्रैफ़िक उपयोग को कम करने और छवि लोडिंग को तेज़ करने के लिए wsrv.nl सेवा का उपयोग करें</string>
<string name="state_abandoned">गिरा दिया गया</string>
<string name="list_options">विकल्पों की सूची बनाएं</string>
<string name="by_relevance">प्रासंगिकता</string>
<string name="categories">श्रेणियाँ</string>
<string name="reader_optimize_summary">कम मेमोरी का उपयोग करने के लिए ऑफस्क्रीन पन्नो की गुणवत्ता कम करें</string>
<string name="error_multiple_states_not_supported">एकाधिक राज्यों द्वारा फ़िल्टर करना इस मंगा स्रोत द्वारा समर्थित नहीं है</string>
<string name="color_correction_apply_text">ये सेटिंग्स विश्व स्तर पर या केवल वर्तमान मंगा पर लागू की जा सकती हैं। यदि विश्व स्तर पर लागू किया जाता है, तो व्यक्तिगत सेटिंग्स को ओवरराइड नहीं किया जाएगा।</string>
<string name="this_manga">यह मंगा</string>
<string name="error_filter_locale_genre_not_supported">शैलियों और स्थान दोनों के आधार पर फ़िल्टर करना इस स्रोत द्वारा समर्थित नहीं है</string>
<string name="welcome_text">कृपया चुनें कि आप कौन से सामग्री स्रोत सक्षम करना चाहते हैं। इसे बाद में सेटिंग्स में भी कॉन्फ़िगर किया जा सकता है</string>
<string name="sync_auth">खाता सिंक करने के लिए लॉगिन करें</string>
<string name="error_filter_states_genre_not_supported">शैलियों और राज्यों दोनों द्वारा फ़िल्टर करना इस स्रोत द्वारा समर्थित नहीं है</string>
<string name="prev_chapter">पिछला अध्याय</string>
<string name="default_page_save_dir">डिफॉल्ट पेज सेव डायरेक्टरी</string>
<string name="reader_actions_summary">टैप करने योग्य स्क्रीन क्षेत्रों के लिए क्रियाएँ कॉन्फ़िगर करें</string>
<string name="reader_control_ltr_summary">दाएँ किनारे पर टैप करने या दाएँ कुंजी दबाने से हमेशा अगले पेज पर स्विच हो जाता है।</string>
<string name="prev_page">पिछला पेज</string>
<string name="switch_pages_volume_buttons_summary">पन्ने बदलने के लिए वॉल्यूम बटन का उपयोग करें</string>
<string name="suggestions_unavailable_text">सुझाव सुविधा अक्षम है</string>
<string name="check_for_new_chapters_disabled">नए अध्यायों की जाँच अक्षम है</string>
<string name="show_labels_in_navbar">नेविगेशन बार में लेबल दिखाएँ</string>
<string name="pages_saving">पन्ने सहेजा जा रहा है</string>
<string name="ask_for_dest_dir_every_time">हर बार गंतव्य स्थान के लिए पूछें</string>
</resources>

View File

@@ -32,7 +32,7 @@
<string name="by_rating">Értékelés</string>
<string name="filter">Szűrő</string>
<string name="light">Fényes</string>
<string name="automatic">Rendszer alapján</string>
<string name="follow_system">Rendszer alapján</string>
<string name="clear">Törlés</string>
<string name="text_clear_history_prompt">Véglegesen törli az összes olvasási előzményt?</string>
<string name="remove">Eltávolítás</string>
@@ -60,4 +60,4 @@
<string name="_s_deleted_from_local_storage">%s törölve lett a helyi tárhelyből</string>
<string name="page_saved">Mentve</string>
<string name="delete">Törlés</string>
</resources>
</resources>

View File

@@ -42,7 +42,7 @@
<string name="theme">Tema</string>
<string name="light">Terang</string>
<string name="dark">Gelap</string>
<string name="automatic">Ikuti sistem</string>
<string name="follow_system">Ikuti sistem</string>
<string name="pages">Halaman</string>
<string name="clear">Bersihkan</string>
<string name="remove">Hapus</string>
@@ -581,4 +581,4 @@
<string name="config_reset_confirm">Kembalikan pengaturan ke bawaan? Tindakan ini tidak bisa dibatalkan.</string>
<string name="use_two_pages_landscape">Gunakan tata letak dua halaman pada orientasi landscape (beta)</string>
<string name="email_password_enter_hint">Masukkan email dan sandi untuk melanjutkan</string>
</resources>
</resources>

View File

@@ -9,7 +9,7 @@
<string name="page_saved">Pagina salvata correttamente</string>
<string name="save_page">Salva la pagina</string>
<string name="pages">Pagine</string>
<string name="automatic">Automatico</string>
<string name="follow_system">Automatico</string>
<string name="dark">Scuro</string>
<string name="light">Chiaro</string>
<string name="theme">Tema</string>
@@ -561,4 +561,4 @@
<string name="category_hidden_done">Questa categoria è stata nascosta dalla schermata principale ed è accessibile tramite Menù → Gestisci categorie</string>
<string name="approximate_remaining_time">Tempo rimanente approssimativo</string>
<string name="remaining_time_pattern">%1$s %2$s</string>
</resources>
</resources>

View File

@@ -38,7 +38,7 @@
<string name="newest">最新</string>
<string name="by_rating">評価</string>
<string name="sort_order">並べ替え</string>
<string name="automatic">システムに従う</string>
<string name="follow_system">システムに従う</string>
<string name="clear">消去</string>
<string name="text_clear_history_prompt">すべての履歴を永久に消去しますか?</string>
<string name="remove">削除</string>
@@ -483,4 +483,4 @@
<string name="reader_zoom_buttons_summary">右下にズームコントロールボタンを表示するかどうか</string>
<string name="reader_zoom_buttons">ズームボタンを表示</string>
<string name="zoom_out">ズームアウト</string>
</resources>
</resources>

View File

@@ -48,7 +48,7 @@
<string name="theme">Кейіп</string>
<string name="dark">Қараңғы</string>
<string name="light">Ақшыл</string>
<string name="automatic">Жүйедегідей</string>
<string name="follow_system">Жүйедегідей</string>
<string name="pages">Беттер</string>
<string name="clear">Тазалау</string>
<string name="remove">Жою</string>
@@ -520,4 +520,4 @@
<string name="no_manga_sources_catalog_text">Әзірге мына жерде қолжетімді дереккөз жоқ. Жаңарту күтіңіз</string>
<string name="available_d">Қолжетімді: %1$d</string>
<string name="content_type_other">Басқа</string>
</resources>
</resources>

View File

@@ -62,7 +62,7 @@
<string name="processing_">처리중…</string>
<string name="updated">최근 업데이트 순</string>
<string name="newest">최근 발간 순</string>
<string name="automatic">시스템 설정</string>
<string name="follow_system">시스템 설정</string>
<string name="delete">지우기</string>
<string name="text_file_sizes">바이트|kB|MB|GB|TB</string>
<string name="clear_pages_cache">페이지 캐시 지우기</string>
@@ -352,4 +352,4 @@
<string name="reset">초기화</string>
<string name="text_unsaved_changes_prompt">저장되지 않은 변경 사항을 저장하거나 삭제하시겠습니까\?</string>
<string name="manga_error_description_pattern">오류 세부정보:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. &lt;a href=%2$s&gt;웹 브라우저에서 만화를 열어&lt;/a&gt; 소스에서 사용할 수 있는지 확인하세요&lt;br&gt;2. &lt;a href=kotatsu://about&gt;최신 버전의 Kotatsu&lt;/a&gt;&lt;br&gt;를 사용하고 있는지 확인하세요.3. 사용 가능한 경우 개발자에게 오류 보고서를 보냅니다.</string>
</resources>
</resources>

View File

@@ -45,7 +45,7 @@
<string name="filter">Tapis</string>
<string name="light">Terang</string>
<string name="dark">Gelap</string>
<string name="automatic">Ikut sistem</string>
<string name="follow_system">Ikut sistem</string>
<string name="pages">Muka surat</string>
<string name="clear">Kosongkan</string>
<string name="text_clear_history_prompt">Kosongkan semua sejarah pembacaan selama-lamanya?</string>
@@ -314,4 +314,4 @@
<string name="error_corrupted_file">Data tidak sah dikembalikan atau fail rosak</string>
<string name="on_device">Pada peranti</string>
<string name="directories">Panduan</string>
</resources>
</resources>

View File

@@ -137,7 +137,7 @@
<string name="remove">Fjern</string>
<string name="clear">Tøm</string>
<string name="pages">Sider</string>
<string name="automatic">Følg systemet</string>
<string name="follow_system">Følg systemet</string>
<string name="dark">Mørk</string>
<string name="light">Lys</string>
<string name="by_name">Navn</string>

View File

@@ -34,7 +34,7 @@
<string name="sort_order">क्रमबद्ध क्रम</string>
<string name="theme">थीम</string>
<string name="light">उज्यालो</string>
<string name="automatic">सिस्टम पालना गर्नुहोस्</string>
<string name="follow_system">सिस्टम पालना गर्नुहोस्</string>
<string name="computing_">कम्प्युटिङ…</string>
<string name="favourites">मनपर्ने</string>
<string name="details">विवरण</string>
@@ -231,4 +231,4 @@
<string name="manga_error_description_pattern">त्रुटि विवरण:&lt;br&gt; &lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt; 1. &lt;a href=%2$s&gt;वेब ब्राउजरमा मंगा खोल्ने&lt;/a&gt; प्रयास गर्नुहोस् कि यो यसको स्रोतमा उपलब्ध छ&lt;br&gt; 2. निश्चित गर्नुहोस् कि तपाइँ &lt;a href=kotatsu://about&gt;Kotatsu को नवीनतम संस्करण&lt;/a&gt; प्रयोग गर्दै हुनुहुन्छ&lt;br&gt; 3. यदि यो उपलब्ध छ भने, विकासकर्ताहरूलाई त्रुटि रिपोर्ट पठाउनुहोस्।</string>
<string name="suggestions_excluded_genres">जानरा अलग गर्नु</string>
<string name="suggestions_updating">सुझावहरू अपडेट गर्दै</string>
</resources>
</resources>

View File

@@ -100,7 +100,7 @@
<string name="by_name">Namn</string>
<string name="updated">Oppdatert</string>
<string name="theme">Vising</string>
<string name="automatic">Lyd systemet</string>
<string name="follow_system">Lyd systemet</string>
<string name="dark">Mørk</string>
<string name="text_local_holder_primary">Hent noko først</string>
<string name="not_available">Ikkje tilgjengeleg</string>
@@ -404,4 +404,4 @@
<string name="downloads_paused">Stansa hentingane</string>
<string name="sync_auth_hint">Du kan logge inn på ein konto du alt har, eller lage ein ny ein</string>
<string name="invalid_value_message">Ugild verdi</string>
</resources>
</resources>

View File

@@ -174,7 +174,7 @@
<string name="notifications_settings">Ustawienia powiadomień</string>
<string name="remote_sources">Zewnętrzne źródła</string>
<string name="theme">Motyw</string>
<string name="automatic">Systemowy</string>
<string name="follow_system">Systemowy</string>
<string name="clear_pages_cache">Wyczyść pamięć podręczną stron</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="grid_size">Wielkość siatki</string>
@@ -486,4 +486,4 @@
<string name="reader_zoom_buttons_summary">Określa, czy wyświetlać elementy sterujące powiększeniem w prawym dolnym rogu</string>
<string name="keep_screen_on">Pozostaw ekran włączony</string>
<string name="keep_screen_on_summary">Nie wyłączaj ekranu podczas czytania mangi</string>
</resources>
</resources>

View File

@@ -188,7 +188,7 @@
<string name="various_languages">Vários idiomas</string>
<string name="text_clear_history_prompt">Limpar todo o histórico de leitura permanentemente\?</string>
<string name="operation_not_supported">Esta operação não é suportada</string>
<string name="automatic">Automático (segue o sistema)</string>
<string name="follow_system">Automático (segue o sistema)</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d de %2$d em</string>
<string name="webtoon">Webtoon</string>
<string name="switch_pages">Alternar páginas</string>
@@ -584,4 +584,4 @@
<string name="default_webtoon_zoom_out">Diminuir zoom padrão do webtoon</string>
<string name="fullscreen_mode">Modo de tela cheia</string>
<string name="reader_fullscreen_summary">Ocultar a barra de status e navegação</string>
</resources>
</resources>

View File

@@ -34,7 +34,7 @@
<string name="sort_order">Ordem de classificação</string>
<string name="filter">Filtro</string>
<string name="dark">Escuro</string>
<string name="automatic">Siga o sistema</string>
<string name="follow_system">Siga o sistema</string>
<string name="pages">Páginas</string>
<string name="clear">Limpar</string>
<string name="text_clear_history_prompt">Limpar todo o histórico de leitura permanentemente\?</string>
@@ -343,7 +343,7 @@
<string name="import_will_start_soon">A importação começará em breve</string>
<string name="feed">Fluxo</string>
<string name="manga_error_description_pattern">Detalhes do erro:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Tente &lt;a href=%2$s&gt;abra a página do mangá em um navegador da web&lt;/a&gt; para garantir que o mesmo esteja disponível em sua fonte&lt;br&gt;2. Se estiver disponível, envie um relatório de erro para os desenvolvedores.</string>
<string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita sempre muda para a próxima página</string>
<string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita, sempre muda para a próxima página.</string>
<string name="reader_control_ltr">Controle ergonômico do leitor</string>
<string name="color_correction_hint">As configurações de cores escolhidas serão lembradas para este mangá</string>
<string name="discard">Descartar</string>
@@ -588,4 +588,10 @@
<string name="check_for_new_chapters_disabled">A verificação de novos capítulos está desativada</string>
<string name="reading_time_estimation">Mostrar tempo estimado de leitura</string>
<string name="reading_time_estimation_summary">O valor do tempo estimado pode ser impreciso</string>
</resources>
<string name="remove_from_history">Remover do histórico</string>
<string name="show_labels_in_navbar">Mostrar rótulos na barra de navegação</string>
<string name="default_page_save_dir">Diretório de salvamento de página padrão</string>
<string name="location">Localização</string>
<string name="pages_saving">Salvando páginas</string>
<string name="ask_for_dest_dir_every_time">Sempre pedir diretório de destinação</string>
</resources>

View File

@@ -45,7 +45,7 @@
<string name="theme">Тема</string>
<string name="light">Светлая</string>
<string name="dark">Тёмная</string>
<string name="automatic">Как в системе</string>
<string name="follow_system">Как в системе</string>
<string name="pages">Страницы</string>
<string name="clear">Очистить</string>
<string name="text_clear_history_prompt">Очистить всю историю чтения полностью\?</string>
@@ -206,7 +206,7 @@
<string name="text_suggestion_holder">Начните читать мангу, чтобы получать персональные предложения</string>
<string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string>
<string name="enabled">Включено</string>
<string name="disabled">Выключено</string>
<string name="disabled">Выкл.</string>
<string name="filter_load_error">Не удалось загрузить список жанров</string>
<string name="computing_">Вычисление…</string>
<string name="reset_filter">Сбросить фильтр</string>
@@ -469,7 +469,7 @@
<string name="disable_nsfw">Отключить NSFW</string>
<string name="too_many_requests_message">Слишком много запросов. Попробуйте повторить позже</string>
<string name="related_manga_summary">Показывать список связанной манги. В некоторых случаях список может быть нерелевантным или отсутствовать вовсе</string>
<string name="advanced">Расширенные</string>
<string name="advanced">Продвинутая</string>
<string name="default_section">Раздел по умолчанию</string>
<string name="manga_list">Список манги</string>
<string name="error_corrupted_file">Возвращаются неверные данные или файл поврежден</string>
@@ -593,4 +593,5 @@
<string name="remove_from_history">Убрать из истории</string>
<string name="show_labels_in_navbar">Показывать подписи на панели навигации</string>
<string name="default_page_save_dir">Папка для сохранений по умолчанию</string>
</resources>
<string name="location">Расположение</string>
</resources>

View File

@@ -6,7 +6,7 @@
<item quantity="other">%1$d ставки</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">пре %1$d минута</item>
<item quantity="one">пре %1$d минут</item>
<item quantity="few">пре %1$d минута</item>
<item quantity="other">пре %1$d минута</item>
</plurals>
@@ -26,7 +26,7 @@
<item quantity="other">%1$d нових поглавља</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d поглављe</item>
<item quantity="one">%1$d поглавље</item>
<item quantity="few">%1$d поглавља</item>
<item quantity="other">%1$d поглавља</item>
</plurals>

View File

@@ -52,7 +52,7 @@
<string name="notification_sound">Звук обавештења</string>
<string name="vibration">Вибрација</string>
<string name="text_empty_holder_primary">Овде је некако празно…</string>
<string name="pages_animation">Анимација превлачења</string>
<string name="pages_animation">Анимација превлачења странице</string>
<string name="about">О апликацији</string>
<string name="app_version">Верзија %s</string>
<string name="black_dark_theme">Црна</string>
@@ -85,7 +85,7 @@
<string name="newest">Најновије</string>
<string name="light">Светла</string>
<string name="dark">Мрачна</string>
<string name="automatic">Прати систем</string>
<string name="follow_system">Прати систем</string>
<string name="filter">Филтер</string>
<string name="theme">Тема</string>
<string name="pages">Странице</string>
@@ -118,7 +118,7 @@
<string name="volume_buttons">Притисни дугмад за јачину звука</string>
<string name="notifications">Обавештења</string>
<string name="pages_cache">Кеш страница</string>
<string name="text_shelf_holder_secondary">Пронађите шта да читате у одељку „Преглед“</string>
<string name="text_shelf_holder_secondary">Пронађи шта ћеш да читаш у одељку „Преглед“</string>
<string name="manga_shelf">Полица</string>
<string name="check_for_updates">Провери ажурирања</string>
<string name="feed">Новости</string>
@@ -127,13 +127,13 @@
<string name="explore">Преглед</string>
<string name="options">Опције</string>
<string name="add_to_favourites">Додај у омиљене</string>
<string name="text_history_holder_secondary">Пронађите ствари за читање у одељку „Преглед“</string>
<string name="light_indicator">ЛЕД показатељ</string>
<string name="text_history_holder_secondary">Пронађи ствари за читање у одељку „Преглед“</string>
<string name="light_indicator">Показатељ ЛЕД светла</string>
<string name="favourites_categories">Омиљене категорије</string>
<string name="remove_category">Избриши</string>
<string name="remove_category">Уклони</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">Омогућено је %1$d од %2$d</string>
<string name="clear">Избриши</string>
<string name="text_history_holder_primary">Оно што прочитате биће приказано овде</string>
<string name="text_history_holder_primary">Оно што прочиташ биће приказано овде</string>
<string name="delete">Избриши</string>
<string name="search_history_cleared">Очишћено</string>
<string name="_s_deleted_from_local_storage">\"%s\" избрисано из локалне меморије</string>
@@ -418,7 +418,7 @@
<string name="no_access_to_file">Немаш приступ овој датотеци или директоријуму</string>
<string name="exclude_nsfw_from_history_summary">Манга означена за одрасле никада неће бити додата у историју и ваш напредак неће бити сачуван</string>
<string name="mark_as_current">Означи као тренутно</string>
<string name="protect_application_summary">Затражи лозинку када покрећете Котатсу</string>
<string name="protect_application_summary">Затражи лозинку када покрећеш Kotatsu</string>
<string name="right_to_left">Са десна на лево</string>
<string name="show_reading_indicators_summary">Прикажи проценат читања у историји и омиљеним</string>
<string name="random">Насумично</string>
@@ -442,7 +442,7 @@
<string name="theme_name_rikka">Рикка</string>
<string name="disabled">Онемогући</string>
<string name="long_ago">Давно</string>
<string name="reader_control_ltr_summary">Додиром на десну ивицу или притиском на десну стрелицу пребацује се на следећу страницу</string>
<string name="reader_control_ltr_summary">Додиривање десне ивице, или притискање десне стрелице, увек прелази на следећу страницу.</string>
<string name="incognito_mode">Режим без чувања</string>
<string name="no_bookmarks_summary">Можеш направити обележивач док читаш мангу</string>
<string name="theme_name_mamimi">Мамими</string>
@@ -588,4 +588,10 @@
<string name="reading_time_estimation_summary">Вредност процене времена може бити нетачна</string>
<string name="check_for_new_chapters_disabled">Провера нових поглавља је искључена</string>
<string name="fullscreen_mode">Режим целог екрана</string>
</resources>
<string name="remove_from_history">Уклони из историје</string>
<string name="location">Локација</string>
<string name="ask_for_dest_dir_every_time">Затражи одредишни дииректоријум сваки пут</string>
<string name="default_page_save_dir">Подразумевани директоријум за чување странице</string>
<string name="show_labels_in_navbar">Прикажи ознаке на навигационој траци</string>
<string name="pages_saving">Чување страница</string>
</resources>

View File

@@ -39,7 +39,7 @@
<string name="manga_downloading_">Laddar ned…</string>
<string name="processing_">Behandlar…</string>
<string name="theme">Tema</string>
<string name="automatic">Systemtema</string>
<string name="follow_system">Systemtema</string>
<string name="remove">Ta bort</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="add_new_category">Ny kategori</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="light">สว่าง</string>
<string name="automatic">ตั้งค่าตามเครื่อง</string>
<string name="follow_system">ตั้งค่าตามเครื่อง</string>
<string name="text_clear_history_prompt">จะเคลียร์ประวัติการอ่านทั้งหมดแบบถาวรใช่ไหม\?</string>
<string name="remove">ลบ</string>
<string name="_s_deleted_from_local_storage">\"%s\" ได้ถูกลบจากที่จัดเก็บในเครื่องแล้ว</string>
@@ -402,4 +402,4 @@
<string name="resume">ดำเนินการต่อ</string>
<string name="paused">หยุดชั่วคราว</string>
<string name="pause">หยุด</string>
</resources>
</resources>

View File

@@ -37,7 +37,7 @@
<string name="theme">Tema</string>
<string name="light">ık</string>
<string name="dark">Koyu</string>
<string name="automatic">Sistemle uyumlu</string>
<string name="follow_system">Sistemle uyumlu</string>
<string name="pages">Sayfalar</string>
<string name="clear">Temizle</string>
<string name="text_clear_history_prompt">Tüm okuma geçmişi kalıcı olarak silinsin mi\?</string>
@@ -331,7 +331,7 @@
<string name="contrast">Kontrast</string>
<string name="reset">Sıfırla</string>
<string name="color_correction_hint">Seçilen renk ayarları bu manga için hatırlanacaktır</string>
<string name="text_unsaved_changes_prompt">Kaydedilmeyen değişiklikler kaydedilsin mi?</string>
<string name="text_unsaved_changes_prompt">Kaydedilmeyen değişiklikler kaydedilsin mi yoksa yok mu sayılsın?</string>
<string name="discard">Yoksay</string>
<string name="error_no_space_left">Cihazda yer yok</string>
<string name="webtoon_zoom">Webtoon yakınlaştırma</string>
@@ -578,10 +578,10 @@
<string name="next_chapter">Sonraki bölüm</string>
<string name="prev_page">Önceki sayfa</string>
<string name="next_page">Sonraki sayfa</string>
<string name="config_reset_confirm">Ayarlar öntanımlı değerlere sıfırlansın mı? Bu eylem geri alınamaz.</string>
<string name="config_reset_confirm">Ayarlar varsayılan değerlere sıfırlansın mı? Bu eylem geri alınamaz.</string>
<string name="use_two_pages_landscape">Yatay yönde iki sayfa düzeni kullan (beta)</string>
<string name="email_password_enter_hint">Devam etmek için e-posta adresinizi ve parolanızı girin</string>
<string name="default_webtoon_zoom_out">Öntanımlı webtoon uzaklaştırması</string>
<string name="default_webtoon_zoom_out">Varsayılan webtoon uzaklaştırması</string>
<string name="fullscreen_mode">Tam ekran modu</string>
<string name="reader_fullscreen_summary">Sistem durumunu ve gezinme çubuklarını gizle</string>
<string name="suggestions_unavailable_text">Öneriler özelliği devre dışı</string>
@@ -591,6 +591,7 @@
<string name="show_labels_in_navbar">Gezinme çubuğunda etiketleri göster</string>
<string name="pages_saving">Sayfalar kaydediliyor</string>
<string name="ask_for_dest_dir_every_time">Her seferinde hedef dizini sor</string>
<string name="default_page_save_dir">Öntanımlı sayfa kaydetme dizini</string>
<string name="default_page_save_dir">Varsayılan sayfa kaydetme konumu</string>
<string name="remove_from_history">Geçmişten kaldır</string>
</resources>
<string name="location">Konum</string>
</resources>

View File

@@ -191,7 +191,7 @@
<string name="manga_downloading_">Завантаження…</string>
<string name="clear">Очистити</string>
<string name="downloads">Завантаження</string>
<string name="automatic">Як в системі</string>
<string name="follow_system">Як в системі</string>
<string name="chapter_is_missing">Розділ відсутній</string>
<string name="genres">Жанри</string>
<string name="system_default">За умовчанням</string>
@@ -469,7 +469,7 @@
<string name="disable_nsfw">Вимкнути NSFW</string>
<string name="related_manga_summary">Показати список пов\'язаної манґи. У деяких випадках він може бути неточним або відсутнім</string>
<string name="too_many_requests_message">Занадто багато запитів. Спробуйте пізніше</string>
<string name="advanced">Розширені</string>
<string name="advanced">Просунута</string>
<string name="default_section">Розділ за умовчанням</string>
<string name="manga_list">Список манґи</string>
<string name="error_corrupted_file">Повертаються неправильні дані або файл пошкоджено</string>
@@ -593,4 +593,5 @@
<string name="remove_from_history">Видалити з історії</string>
<string name="show_labels_in_navbar">Показувати мітки на панелі навігації</string>
<string name="default_page_save_dir">Директорія збереження сторінки за замовчуванням</string>
</resources>
<string name="location">Розташування</string>
</resources>

View File

@@ -201,7 +201,7 @@
<string name="sort_order">Sắp xếp theo</string>
<string name="dark">Tối</string>
<string name="light">Sáng</string>
<string name="automatic">Theo hệ thống</string>
<string name="follow_system">Theo hệ thống</string>
<string name="_s_deleted_from_local_storage">\"%s\" đã bị xoá khỏi bộ nhớ cục bộ</string>
<string name="share_image">Chia sẻ hình ảnh</string>
<string name="page_saved">Đã lưu</string>
@@ -448,4 +448,4 @@
<string name="color_white">Trắng</string>
<string name="status_planned">Đã lên kế hoạch</string>
<string name="color_black">Đen</string>
</resources>
</resources>

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