Compare commits

...

7 Commits

Author SHA1 Message Date
40f27ae634 Fix page saving 2021-02-25 20:10:38 +02:00
0dfba47d85 Misc small fixes 2021-02-19 20:21:44 +02:00
69e44b10e9 Fix tags on details screen 2021-02-18 20:25:01 +02:00
4cd0cb04a3 Fix reader state restoration 2021-02-16 20:22:18 +02:00
d9d5595bde Ellipsize chapters in feed 2021-02-16 20:03:25 +02:00
ed70ca4e18 Information toast in reader 2021-02-11 19:29:17 +02:00
a371bb6514 Fix activity leak on Android Q 2021-02-08 20:29:32 +02:00
28 changed files with 388 additions and 181 deletions

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_API_S.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-02-19T19:02:37.198775Z" />
</component>
</project>

9
.idea/misc.xml generated
View File

@@ -1,5 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="../../../../../../layout/custom_preview.xml" value="0.1" />
<entry key="app/src/main/res/layout-w600dp/fragment_details.xml" value="0.14583333333333334" />
<entry key="app/src/main/res/layout/fragment_details.xml" value="0.26145833333333335" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>

View File

@@ -16,7 +16,7 @@ android {
minSdkVersion 21
targetSdkVersion 30
versionCode gitCommits
versionName '1.0-rc1'
versionName '1.0-rc2'
kapt {
arguments {
@@ -66,23 +66,23 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation 'androidx.core:core-ktx:1.5.0-beta01'
implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc02'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-service:2.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-process:2.3.0-rc01'
implementation 'androidx.core:core-ktx:1.5.0-beta02'
implementation 'androidx.activity:activity-ktx:1.2.0'
implementation 'androidx.fragment:fragment-ktx:1.3.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-service:2.3.0'
implementation 'androidx.lifecycle:lifecycle-process:2.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta02'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.5.0'
implementation 'com.google.android.material:material:1.3.0-rc01'
implementation 'com.google.android.material:material:1.3.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01'
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0'
implementation 'androidx.room:room-runtime:2.2.6'
implementation 'androidx.room:room-ktx:2.2.6'

View File

@@ -106,6 +106,7 @@
<receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:exported="true"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -116,6 +117,7 @@
</receiver>
<receiver
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetProvider"
android:exported="true"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -125,6 +127,11 @@
android:resource="@xml/widget_recent" />
</receiver>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
</application>
</manifest>

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.base.ui
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuItem
@@ -97,4 +98,16 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
super.onSupportActionModeFinished(mode)
window?.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
}
override fun onBackPressed() {
if ( // https://issuetracker.google.com/issues/139738913
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&
isTaskRoot &&
supportFragmentManager.backStackEntryCount == 0
) {
finishAfterTransition()
} else {
super.onBackPressed()
}
}
}

View File

@@ -0,0 +1,96 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.View.OnClickListener
import androidx.annotation.DrawableRes
import androidx.core.view.children
import com.google.android.material.R
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChipsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.chipGroupStyle
) : ChipGroup(context, attrs, defStyleAttr) {
private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false
private var chipOnClickListener = OnClickListener {
onChipClickListener?.onChipClick(it as Chip, it.tag)
}
var onChipClickListener: OnChipClickListener? = null
set(value) {
field = value
val isChipClickable = value != null
children.forEach { it.isClickable = isChipClickable }
}
override fun requestLayout() {
if (isLayoutSuppressedCompat) {
isLayoutCalledOnSuppressed = true
} else {
super.requestLayout()
}
}
fun setChips(items: List<ChipModel>) {
suppressLayoutCompat(true)
try {
for ((i, model) in items.withIndex()) {
val chip = getChildAt(i) as Chip? ?: addChip()
bindChip(chip, model)
}
for (i in items.size until childCount) {
removeViewAt(i)
}
} finally {
suppressLayoutCompat(false)
}
}
private fun bindChip(chip: Chip, model: ChipModel) {
chip.text = model.title
if (model.icon == 0) {
chip.isChipIconVisible = false
} else {
chip.isCheckedIconVisible = true
chip.setChipIconResource(model.icon)
}
chip.tag = model.data
}
private fun addChip(): Chip {
val chip = Chip(context)
chip.setTextColor(context.getThemeColor(android.R.attr.textColorPrimary))
chip.isCloseIconVisible = false
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isClickable = onChipClickListener != null
addView(chip)
return chip
}
private fun suppressLayoutCompat(suppress: Boolean) {
isLayoutSuppressedCompat = suppress
if (!suppress) {
if (isLayoutCalledOnSuppressed) {
requestLayout()
isLayoutCalledOnSuppressed = false
}
}
}
data class ChipModel(
@DrawableRes val icon: Int,
val title: CharSequence,
val data: Any? = null
)
fun interface OnChipClickListener {
fun onChipClick(chip: Chip, data: Any?)
}
}

View File

@@ -1,34 +0,0 @@
package org.koitharu.kotatsu.core.ui
import android.content.Context
import android.view.View
import androidx.annotation.DrawableRes
import com.google.android.material.chip.Chip
import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChipsFactory(val context: Context) {
fun create(
convertView: Chip? = null,
text: CharSequence,
@DrawableRes iconRes: Int = 0,
tag: Any? = null,
onClickListener: View.OnClickListener? = null
): Chip {
val chip = convertView ?: Chip(context).apply {
setTextColor(context.getThemeColor(android.R.attr.textColorPrimary))
isCloseIconVisible = false
}
chip.text = text
if (iconRes == 0) {
chip.isChipIconVisible = false
} else {
chip.isCheckedIconVisible = true
chip.setChipIconResource(iconRes)
}
chip.tag = tag
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(onClickListener)
return chip
}
}

View File

@@ -13,12 +13,14 @@ import androidx.core.view.updatePadding
import coil.ImageLoader
import coil.util.CoilUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
@@ -34,6 +36,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
private val viewModel by sharedViewModel<DetailsViewModel>()
private val coil by inject<ImageLoader>()
private var tagsJob: Job? = null
override fun onInflateView(
inflater: LayoutInflater,
@@ -67,47 +70,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
ratingBar.progress = (ratingBar.max * manga.rating).roundToInt()
ratingBar.isVisible = true
}
chipsTags.removeAllViews()
manga.author?.let { a ->
chipsTags.addChips(listOf(a)) {
create(
text = it,
iconRes = R.drawable.ic_chip_user,
tag = it
)
}
}
chipsTags.addChips(manga.tags) {
create(
text = it.title,
iconRes = R.drawable.ic_chip_tag,
tag = it
)
}
manga.url.toUri().toFileOrNull()?.let { f ->
viewLifecycleScope.launch {
val size = withContext(Dispatchers.IO) {
f.length()
}
chipsTags.addChips(listOf(f)) {
create(
text = FileSizeUtils.formatBytes(context, size),
iconRes = R.drawable.ic_chip_storage,
tag = it
)
}
}
} ?: chipsTags.addChips(listOf(manga.source)) {
create(
text = it.title,
iconRes = R.drawable.ic_chip_web,
tag = it
)
}
imageViewFavourite.setOnClickListener(this@DetailsFragment)
buttonRead.setOnClickListener(this@DetailsFragment)
buttonRead.setOnLongClickListener(this@DetailsFragment)
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
bindTags(manga)
}
}
@@ -191,4 +158,39 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
bottom = insets.bottom
)
}
private fun bindTags(manga: Manga) {
tagsJob?.cancel()
tagsJob = viewLifecycleScope.launch {
val tags = ArrayList<ChipsView.ChipModel>(manga.tags.size + 2)
if (manga.author != null) {
tags += ChipsView.ChipModel(
title = manga.author,
icon = R.drawable.ic_chip_user
)
}
for (tag in manga.tags) {
tags += ChipsView.ChipModel(
title = tag.title,
icon = R.drawable.ic_chip_tag
)
}
val file = manga.url.toUri().toFileOrNull()
if (file != null) {
val size = withContext(Dispatchers.IO) {
file.length()
}
tags += ChipsView.ChipModel(
title = FileSizeUtils.formatBytes(requireContext(), size),
icon = R.drawable.ic_chip_storage
)
} else {
tags += ChipsView.ChipModel(
title = manga.source.title,
icon = R.drawable.ic_chip_web
)
}
binding.chipsTags.setChips(tags)
}
}
}

View File

@@ -7,6 +7,8 @@ import android.webkit.MimeTypeMap
import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter
@@ -127,14 +129,16 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
)
}
fun getRemoteManga(localManga: Manga): Manga? {
suspend fun getRemoteManga(localManga: Manga): Manga? {
val file = runCatching {
Uri.parse(localManga.url).toFile()
}.getOrNull() ?: return null
val zip = ZipFile(file)
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null
return index.getMangaInfo()
return withContext(Dispatchers.IO) {
val zip = ZipFile(file)
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null
index.getMangaInfo()
}
}
private fun zipUri(file: File, entryName: String) =

View File

@@ -99,6 +99,7 @@ class LocalListViewModel(
runCatching {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.value = mangaList.value?.filterNot { it.id == manga.id }
}
shortcutsRepository.updateShortcuts()
onMangaRemoved.call(manga)

View File

@@ -86,7 +86,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
viewModel.onError.observe(this, this::onError)
viewModel.readerMode.observe(this, this::onInitReader)
viewModel.onPageSaved.observe(this, this::onPageSaved)
viewModel.uiState.observe(this, this::onUiStateChanged)
viewModel.uiState.observeWithPrevious(this, this::onUiStateChanged)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value == true)
@@ -124,11 +124,6 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
}
}
override fun onPause() {
viewModel.saveCurrentState(reader?.getCurrentState())
super.onPause()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_reader_top, menu)
return super.onCreateOptionsMenu(menu)
@@ -204,7 +199,11 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
private fun onLoadingStateChanged(isLoading: Boolean) {
val hasPages = !viewModel.content.value?.pages.isNullOrEmpty()
binding.layoutLoading.isVisible = isLoading && !hasPages
binding.progressBarBottom.isVisible = isLoading && hasPages
if (isLoading && hasPages) {
binding.toastView.show(R.string.loading_, true)
} else {
binding.toastView.hide()
}
}
private fun onError(e: Throwable) {
@@ -336,19 +335,25 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
setUiIsVisible(!binding.appbarTop.isVisible)
}
private fun onUiStateChanged(uiState: ReaderUiState) {
private fun onUiStateChanged(uiState: ReaderUiState, previous: ReaderUiState?) {
title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_)
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal)
} else {
null
}
if (previous?.chapterName != null && uiState.chapterName != previous.chapterName) {
if (!uiState.chapterName.isNullOrEmpty()) {
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
}
}
}
companion object {
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
private const val EXTRA_STATE = "state"
private const val TOAST_DURATION = 1500L
fun newIntent(context: Context, manga: Manga, state: ReaderState?): Intent {
return Intent(context, ReaderActivity::class.java)

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import androidx.annotation.StringRes
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.textview.MaterialTextView
import org.koitharu.kotatsu.utils.anim.Duration
import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.hideAnimated
import org.koitharu.kotatsu.utils.ext.showAnimated
class ReaderToastView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialTextView(context, attrs, defStyleAttr) {
private var hideRunnable = Runnable {
hide()
}
fun show(message: CharSequence, isLoading: Boolean) {
removeCallbacks(hideRunnable)
text = message
this.showAnimated(Motion.Toast, Duration.SHORT)
}
fun show(@StringRes messageId: Int, isLoading: Boolean) {
show(context.getString(messageId), isLoading)
}
fun showTemporary(message: CharSequence, duration: Long) {
show(message, false)
postDelayed(hideRunnable, duration)
}
fun hide() {
removeCallbacks(hideRunnable)
this.hideAnimated(Motion.Toast, Duration.SHORT)
}
override fun onDetachedFromWindow() {
removeCallbacks(hideRunnable)
super.onDetachedFromWindow()
}
// FIXME use it as compound drawable
private fun createProgressDrawable(): CircularProgressDrawable {
val drawable = CircularProgressDrawable(context)
drawable.setStyle(CircularProgressDrawable.DEFAULT)
drawable.arrowEnabled = false
drawable.setColorSchemeColors(Color.WHITE)
drawable.centerRadius = lineHeight / 3f
return drawable
}
}

View File

@@ -8,8 +8,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koitharu.kotatsu.base.domain.MangaDataRepository
@@ -20,11 +18,11 @@ import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.MediaStoreCompat
@@ -156,20 +154,11 @@ class ReaderViewModel(
}?.toMangaPage() ?: error("Page not found")
val repo = page.source.repository
val pageUrl = repo.getPageUrl(page)
val request = Request.Builder()
.url(pageUrl)
.header(CommonHeaders.REFERER, page.referer)
.get()
.build()
val uri = get<OkHttpClient>().newCall(request).await().use { response ->
val fileName =
URLUtil.guessFileName(
pageUrl,
response.contentDisposition,
response.mimeType
)
val file = get<PagesCache>()[pageUrl] ?: error("Page not found in cache")
val uri = file.inputStream().use { input ->
val fileName = URLUtil.guessFileName(pageUrl, null, null)
MediaStoreCompat(resolver).insertImage(fileName) {
checkNotNull(response.body).byteStream().copyTo(it)
input.copyTo(it)
}
}
onPageSaved.postCall(uri)

View File

@@ -18,29 +18,34 @@ abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
PageLoader(lifecycleScope, get(), get())
}
private var lastReaderState: ReaderState? = null
private var stateToSave: ReaderState? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lastReaderState = savedInstanceState?.getParcelable(KEY_STATE) ?: lastReaderState
var restoredState = savedInstanceState?.getParcelable<ReaderState?>(KEY_STATE)
viewModel.content.observe(viewLifecycleOwner) {
onPagesChanged(it.pages, lastReaderState ?: it.state)
lastReaderState = null
onPagesChanged(it.pages, restoredState ?: it.state)
restoredState = null
}
}
override fun onPause() {
super.onPause()
viewModel.saveCurrentState(getCurrentState())
}
override fun onDestroyView() {
lastReaderState = getCurrentState()
stateToSave = getCurrentState()
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
getCurrentState()?.let {
lastReaderState = it
stateToSave = it
}
outState.putParcelable(KEY_STATE, lastReaderState)
outState.putParcelable(KEY_STATE, stateToSave)
}
override fun onWindowInsetsChanged(insets: Insets) = Unit

View File

@@ -28,13 +28,8 @@ class TrackingRepository(
}
val tracks = db.tracksDao.findAll().groupBy { it.mangaId }
return mangaList
.mapNotNull {
if (it.source == MangaSource.LOCAL) {
localMangaRepository.getRemoteManga(it)
} else {
it
}
}.distinctBy { it.id }
.filterNot { it.source == MangaSource.LOCAL }
.distinctBy { it.id }
.map { manga ->
val track = tracks[manga.id]?.singleOrNull()
MangaTracking(

View File

@@ -6,21 +6,37 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.formatRelative
fun TrackingLogItem.toFeedItem(resources: Resources) = FeedItem(
id = id,
imageUrl = manga.coverUrl,
title = manga.title,
subtitle = buildString {
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
append(" ")
append(
resources.getQuantityString(
R.plurals.new_chapters,
chapters.size,
chapters.size
fun TrackingLogItem.toFeedItem(resources: Resources): FeedItem {
val chaptersString = if (chapters.size > MAX_CHAPTERS) {
chapters.joinToString(
separator = "\n",
limit = MAX_CHAPTERS - 1,
truncated = resources.getString(
R.string._and_x_more,
chapters.size - MAX_CHAPTERS + 1
)
)
},
chapters = chapters.joinToString("\n"),
manga = manga
)
} else {
chapters.joinToString("\n")
}
return FeedItem(
id = id,
imageUrl = manga.coverUrl,
title = manga.title,
subtitle = buildString {
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
append(" ")
append(
resources.getQuantityString(
R.plurals.new_chapters,
chapters.size,
chapters.size
)
)
},
chapters = chaptersString,
manga = manga
)
}
private const val MAX_CHAPTERS = 6

View File

@@ -2,7 +2,8 @@ package org.koitharu.kotatsu.utils.anim
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import org.koitharu.kotatsu.utils.ext.measureHeight
sealed class Motion {
@@ -69,27 +70,25 @@ sealed class Motion {
}
}
object CheckEffect : Motion() {
object Toast : Motion() {
override fun reset(v: View) {
v.scaleX = 1f
v.scaleY = 1f
v.alpha = 1f
}
override fun hideView(v: View) {
v.scaleX = 0f
v.scaleY = 0f
v.alpha = 0f
}
override fun hide(v: View, anim: ViewPropertyAnimator) {
anim.scaleX(0f)
anim.scaleY(0f)
anim.interpolator = AccelerateDecelerateInterpolator()
anim.alpha(0f)
anim.translationY(v.measureHeight().toFloat())
anim.interpolator = AccelerateInterpolator()
}
override fun show(v: View, anim: ViewPropertyAnimator) {
anim.scaleY(1f)
anim.scaleX(1f)
anim.interpolator = AccelerateDecelerateInterpolator()
anim.alpha(1f)
anim.translationY(0f)
anim.interpolator = DecelerateInterpolator()
}
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.utils.ext
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.TextView
import androidx.core.view.isGone
var TextView.textAndVisible: CharSequence?
inline get() = text?.takeIf { visibility == View.VISIBLE }
set(value) {
text = value
isGone = value.isNullOrEmpty()
}
var TextView.drawableStart: Drawable?
inline get() = compoundDrawablesRelative[0]
set(value) {
val dr = compoundDrawablesRelative
setCompoundDrawablesRelativeWithIntrinsicBounds(value, dr[1], dr[2], dr[3])
}

View File

@@ -22,7 +22,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.core.ui.ChipsFactory
fun View.hideKeyboard() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
@@ -40,21 +39,6 @@ inline fun <reified T : View> ViewGroup.inflate(@LayoutRes resId: Int) =
val RecyclerView.hasItems: Boolean
get() = (adapter?.itemCount ?: 0) > 0
var TextView.textAndVisible: CharSequence?
get() = text?.takeIf { visibility == View.VISIBLE }
set(value) {
text = value
isGone = value.isNullOrEmpty()
}
inline fun <T> ChipGroup.addChips(data: Iterable<T>, action: ChipsFactory.(T) -> Chip) {
val factory = ChipsFactory(context)
data.forEach {
val chip = factory.action(it)
addView(chip)
}
}
fun RecyclerView.clearItemDecorations() {
while (itemDecorationCount > 0) {
removeItemDecorationAt(0)

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="@color/dim" />
<padding
android:bottom="8dp"
android:left="12dp"
android:right="12dp"
android:top="8dp" />
</shape>

View File

@@ -23,13 +23,14 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3" />
<com.google.android.material.chip.ChipGroup
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:padding="6dp"
app:chipSpacingHorizontal="4dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="@id/imageView_cover"
app:layout_constraintStart_toStartOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/imageView_cover" />

View File

@@ -2,6 +2,7 @@
<FrameLayout
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:keepScreenOn="true">
@@ -11,14 +12,19 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/progressBar_bottom"
android:layout_width="match_parent"
<org.koitharu.kotatsu.reader.ui.ReaderToastView
android:id="@+id/toastView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="bottom"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="20dp"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
android:background="@drawable/bg_reader_indicator"
android:singleLine="true"
android:drawablePadding="6dp"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="?android:textColorPrimary"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark"
tools:text="@string/loading_" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_top"
@@ -45,7 +51,8 @@
android:background="@color/dim"
android:elevation="0dp"
android:theme="@style/AppToolbarTheme"
app:elevation="0dp">
app:elevation="0dp"
tools:visibility="gone">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_bottom"

View File

@@ -113,7 +113,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_title" />
<com.google.android.material.chip.ChipGroup
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@@ -200,4 +200,5 @@
<string name="sign_in">Войти</string>
<string name="auth_required">Для просмотра этого контента требуется авторизация</string>
<string name="default_s">По умолчанию: %s</string>
<string name="_and_x_more">…и ещё %1$d</string>
</resources>

View File

@@ -8,5 +8,6 @@
<item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
<item name="android:statusBarColor">@color/status_bar</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="colorOnPrimary">@android:color/white</item>
</style>
</resources>

View File

@@ -202,4 +202,5 @@
<string name="sign_in">Sign in</string>
<string name="auth_required">You should authorize to view this content</string>
<string name="default_s">Default: %s</string>
<string name="_and_x_more">…and %1$d more</string>
</resources>

View File

@@ -8,6 +8,7 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
<item name="android:statusBarColor">@color/status_bar</item>
<item name="colorOnPrimary">@android:color/white</item>
</style>
<style name="AppTheme" parent="BaseAppTheme">

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.21'
ext.kotlin_version = '1.4.30'
repositories {
google()
jcenter()