Compare commits
34 Commits
v6.7.3
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d1a2fcf77 | ||
|
|
876675445d | ||
|
|
f7a70680bd | ||
|
|
8e82db441c | ||
|
|
f2626c668d | ||
|
|
4694215ccc | ||
|
|
096f5b15dc | ||
|
|
101d357eff | ||
|
|
11cd5609bb | ||
|
|
fda59996aa | ||
|
|
20461112d2 | ||
|
|
f98bb87d6e | ||
|
|
c451952a1e | ||
|
|
f8cbc9692f | ||
|
|
9f3113363b | ||
|
|
dba36838d4 | ||
|
|
f6de1b02d7 | ||
|
|
d6b8e2fd9e | ||
|
|
5227240478 | ||
|
|
8f65ea6535 | ||
|
|
7d7a6eadd2 | ||
|
|
40f1ad3181 | ||
|
|
a28c9447d7 | ||
|
|
a84cf97982 | ||
|
|
3a8eb58fd1 | ||
|
|
5d75e9af4a | ||
|
|
d4684e7462 | ||
|
|
c0a2f0b533 | ||
|
|
40867dd2b6 | ||
|
|
c3294e6459 | ||
|
|
5139feb51a | ||
|
|
35a2ac4b04 | ||
|
|
f39ccb6223 | ||
|
|
6cb6c891dd |
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 )")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
enum class DownloadFormat {
|
||||
|
||||
AUTOMATIC,
|
||||
SINGLE_CBZ,
|
||||
MULTIPLE_CBZ,
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
28
app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsAD.kt
Normal file
28
app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsAD.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
108
app/src/main/res/layout/activity_stats.xml
Normal file
108
app/src/main/res/layout/activity_stats.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
52
app/src/main/res/layout/item_stats.xml
Normal file
52
app/src/main/res/layout/item_stats.xml
Normal 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>
|
||||
74
app/src/main/res/layout/preference_split_switch.xml
Normal file
74
app/src/main/res/layout/preference_split_switch.xml
Normal 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>
|
||||
@@ -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>
|
||||
82
app/src/main/res/layout/sheet_stats_manga.xml
Normal file
82
app/src/main/res/layout/sheet_stats_manga.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
13
app/src/main/res/menu/opt_stats.xml
Normal file
13
app/src/main/res/menu/opt_stats.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">त्रुटि विवरण:<br><tt>%1$s</tt><br><br>1. यह सुनिश्चित करने के लिए कि यह अपने स्रोत पर उपलब्ध है <a href=%2$s>मंगा को वेब ब्राउज़र में खोलने का प्रयास करें</a><br>2। सुनिश्चित करें कि आप <a href=kotatsu://about>Kotatsu के नवीनतम संस्करण</a><br>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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">오류 세부정보:<br><tt>%1$s</tt><br><br>1. <a href=%2$s>웹 브라우저에서 만화를 열어</a> 소스에서 사용할 수 있는지 확인하세요<br>2. <a href=kotatsu://about>최신 버전의 Kotatsu</a><br>를 사용하고 있는지 확인하세요.3. 사용 가능한 경우 개발자에게 오류 보고서를 보냅니다.</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">त्रुटि विवरण:<br> <tt>%1$s</tt><br><br> 1. <a href=%2$s>वेब ब्राउजरमा मंगा खोल्ने</a> प्रयास गर्नुहोस् कि यो यसको स्रोतमा उपलब्ध छ<br> 2. निश्चित गर्नुहोस् कि तपाइँ <a href=kotatsu://about>Kotatsu को नवीनतम संस्करण</a> प्रयोग गर्दै हुनुहुन्छ<br> 3. यदि यो उपलब्ध छ भने, विकासकर्ताहरूलाई त्रुटि रिपोर्ट पठाउनुहोस्।</string>
|
||||
<string name="suggestions_excluded_genres">जानरा अलग गर्नु</string>
|
||||
<string name="suggestions_updating">सुझावहरू अपडेट गर्दै</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:<br><tt>%1$s</tt><br><br>1. Tente <a href=%2$s>abra a página do mangá em um navegador da web</a> para garantir que o mesmo esteja disponível em sua fonte<br>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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<string name="theme">Tema</string>
|
||||
<string name="light">Açı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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user