diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..e32589680 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,29 @@ +**PLEASE READ THIS** + +I acknowledge that: + +- I have updated to the latest version of the app (https://github.com/KotatsuApp/Kotatsu/releases/latest) +- If this is an issue with a parser, that I should be opening an issue in https://github.com/KotatsuApp/kotatsu-parsers +- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue +- I will fill out the title and the information in this template + +Note that the issue will be automatically closed if you do not fill out the title or requested information. + +**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** + +--- + +## Device information +* Kotatsu version: ? +* Android version: ? +* Device: ? + +## Steps to reproduce +1. First step +2. Second step + +## Issue/Request +? + +## Other details +Additional details and attachments. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b6d4254a1..c7cc0c855 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: ⚠️ Source issue - url: https://github.com/nv95/kotatsu-parsers/issues/new + url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new about: Issues and requests for sources should be opened in the kotatsu-parsers repository instead \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index d0766690f..4bc2d2de9 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -44,7 +44,7 @@ body: label: Kotatsu version description: You can find your Kotatsu version in **Settings → About**. placeholder: | - Example: "3.3.1" + Example: "3.3" validations: required: true @@ -87,7 +87,5 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.3.1](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. - required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index 7bd0e002e..b49ba479b 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -21,6 +21,16 @@ body: placeholder: | Additional details and attachments. + - type: input + id: kotatsu-version + attributes: + label: Kotatsu version + description: You can find your Kotatsu version in **Settings → About**. + placeholder: | + Example: "3.3" + validations: + required: true + - type: checkboxes id: acknowledgements attributes: @@ -33,7 +43,5 @@ body: required: true - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.3.1](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. - required: true - label: I will fill out all of the requested information in this form. required: true \ No newline at end of file diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml new file mode 100644 index 000000000..ef256ed06 --- /dev/null +++ b/.github/workflows/issue_moderator.yml @@ -0,0 +1,29 @@ +name: Issue moderator + +on: + issues: + types: [opened, edited, reopened] + issue_comment: + types: [created] + +jobs: + moderate: + runs-on: ubuntu-latest + steps: + - name: Moderate issues + uses: tachiyomiorg/issue-moderator-action@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + auto-close-rules: | + [ + { + "type": "body", + "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*", + "message": "The acknowledgment section was not removed." + }, + { + "type": "body", + "regex": ".*\\* (Kotatsu version|Android version|Device): \\?.*", + "message": "Requested information in the template was not filled out." + } + ] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9b63e14e7..5611db9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /.idea/kotlinScripting.xml /.idea/deploymentTargetDropDown.xml /.idea/androidTestResultsUserPreferences.xml +/.idea/render.experimental.xml .DS_Store /build /captures diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml deleted file mode 100644 index 5cad4e0be..000000000 --- a/.idea/render.experimental.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 6714250e3..1d693109e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Kotatsu is a free and open source manga reader for Android. alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/org.koitharu.kotatsu) -Download APK from Github Releases: +Download APK from GitHub Releases: - [Latest release](https://github.com/KotatsuApp/Kotatsu/releases/latest) diff --git a/app/build.gradle b/app/build.gradle index 8ab8a1cad..6496c2699 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 410 - versionName '3.3.1' + versionCode 411 + versionName '3.3.2' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 0ab9b2d1e..d7f25396e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -94,6 +94,7 @@ class KotatsuApp : Application() { ReportField.PHONE_MODEL, ReportField.CRASH_CONFIGURATION, ReportField.STACK_TRACE, + ReportField.CUSTOM_DATA, ReportField.SHARED_PREFERENCES, ) dialog { diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt index 2125d044c..7db0f6e22 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt @@ -6,14 +6,12 @@ import androidx.annotation.CallSuper import androidx.annotation.StringRes import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import androidx.fragment.app.Fragment import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView import org.koin.android.ext.android.inject import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsHeadersFragment abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/TextInputDialog.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/TextInputDialog.kt deleted file mode 100644 index 4b5c02ca6..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/TextInputDialog.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.koitharu.kotatsu.base.ui.dialog - -import android.content.Context -import android.content.DialogInterface -import android.text.InputFilter -import android.view.LayoutInflater -import androidx.annotation.StringRes -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.koitharu.kotatsu.databinding.DialogInputBinding - -class TextInputDialog private constructor( - private val delegate: AlertDialog, -) : DialogInterface by delegate { - - fun show() = delegate.show() - - class Builder(context: Context) { - - private val binding = DialogInputBinding.inflate(LayoutInflater.from(context)) - - private val delegate = MaterialAlertDialogBuilder(context) - .setView(binding.root) - - fun setTitle(@StringRes titleResId: Int): Builder { - delegate.setTitle(titleResId) - return this - } - - fun setTitle(title: CharSequence): Builder { - delegate.setTitle(title) - return this - } - - fun setHint(@StringRes hintResId: Int): Builder { - binding.inputEdit.hint = binding.root.context.getString(hintResId) - return this - } - - fun setMaxLength(maxLength: Int, strict: Boolean): Builder { - with(binding.inputLayout) { - counterMaxLength = maxLength - isCounterEnabled = maxLength > 0 - } - if (strict && maxLength > 0) { - binding.inputEdit.filters += InputFilter.LengthFilter(maxLength) - } - return this - } - - fun setInputType(inputType: Int): Builder { - binding.inputEdit.inputType = inputType - return this - } - - fun setText(text: String): Builder { - binding.inputEdit.setText(text) - binding.inputEdit.setSelection(text.length) - return this - } - - fun setPositiveButton( - @StringRes textId: Int, - listener: (DialogInterface, String) -> Unit - ): Builder { - delegate.setPositiveButton(textId) { dialog, _ -> - listener(dialog, binding.inputEdit.text?.toString().orEmpty()) - } - return this - } - - fun setNegativeButton( - @StringRes textId: Int, - listener: DialogInterface.OnClickListener? = null - ): Builder { - delegate.setNegativeButton(textId, listener) - return this - } - - fun setOnCancelListener(listener: DialogInterface.OnCancelListener): Builder { - delegate.setOnCancelListener(listener) - return this - } - - fun create() = - TextInputDialog(delegate.create()) - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt index 9c85787a7..909252b48 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/FadingSnackbar.kt @@ -17,19 +17,28 @@ package org.koitharu.kotatsu.base.ui.widgets import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater -import android.widget.Button import android.widget.FrameLayout -import android.widget.TextView +import androidx.annotation.ColorInt import androidx.annotation.StringRes +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.postDelayed -import org.koitharu.kotatsu.R +import com.google.android.material.color.MaterialColors +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel +import com.google.android.material.snackbar.Snackbar +import org.koitharu.kotatsu.databinding.FadingSnackbarLayoutBinding +import org.koitharu.kotatsu.utils.ext.getThemeColorStateList +import com.google.android.material.R as materialR private const val ENTER_DURATION = 300L private const val EXIT_DURATION = 200L -private const val SHORT_DURATION = 1_500L -private const val LONG_DURATION = 2_750L +private const val SHORT_DURATION_MS = 1_500L +private const val LONG_DURATION_MS = 2_750L + /** * A custom snackbar implementation allowing more control over placement and entry/exit animations. * @@ -40,16 +49,13 @@ private const val LONG_DURATION = 2_750L class FadingSnackbar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + defStyleAttr: Int = 0, ) : FrameLayout(context, attrs, defStyleAttr) { - private val message: TextView - private val action: Button + private val binding = FadingSnackbarLayoutBinding.inflate(LayoutInflater.from(context), this) init { - val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true) - message = view.findViewById(R.id.snackbar_text) - action = view.findViewById(R.id.snackbar_action) + binding.snackbarLayout.background = createThemedBackground() } fun dismiss() { @@ -62,33 +68,66 @@ class FadingSnackbar @JvmOverloads constructor( } fun show( - messageText: CharSequence? = null, - @StringRes actionId: Int? = null, - longDuration: Boolean = true, - actionClick: () -> Unit = { dismiss() }, - dismissListener: () -> Unit = { } + messageText: CharSequence?, + @StringRes actionId: Int = 0, + duration: Int = Snackbar.LENGTH_SHORT, + onActionClick: (FadingSnackbar.() -> Unit)? = null, + onDismiss: (() -> Unit)? = null, ) { - message.text = messageText - if (actionId != null) { - action.run { + binding.snackbarText.text = messageText + if (actionId != 0) { + with(binding.snackbarAction) { visibility = VISIBLE text = context.getString(actionId) setOnClickListener { - actionClick() + onActionClick?.invoke(this@FadingSnackbar) ?: dismiss() } } } else { - action.visibility = GONE + binding.snackbarAction.visibility = GONE } alpha = 0f visibility = VISIBLE animate() .alpha(1f) .duration = ENTER_DURATION - val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION - postDelayed(showDuration) { + if (duration == Snackbar.LENGTH_INDEFINITE) { + return + } + val durationMs = ENTER_DURATION + if (duration == Snackbar.LENGTH_LONG) LONG_DURATION_MS else SHORT_DURATION_MS + postDelayed(durationMs) { dismiss() - dismissListener() + onDismiss?.invoke() } } + + private fun createThemedBackground(): Drawable { + val backgroundColor = MaterialColors.layer(this, materialR.attr.colorSurface, materialR.attr.colorOnSurface, 1f) + val shapeAppearanceModel = ShapeAppearanceModel.builder( + context, + materialR.style.ShapeAppearance_Material3_Corner_ExtraSmall, + 0 + ).build() + val background = createMaterialShapeDrawableBackground( + backgroundColor, + shapeAppearanceModel, + ) + val backgroundTint = context.getThemeColorStateList(materialR.attr.colorSurfaceInverse) + return if (backgroundTint != null) { + val wrappedDrawable = DrawableCompat.wrap(background) + DrawableCompat.setTintList(wrappedDrawable, backgroundTint) + wrappedDrawable + } else { + DrawableCompat.wrap(background) + } + } + + private fun createMaterialShapeDrawableBackground( + @ColorInt backgroundColor: Int, + shapeAppearanceModel: ShapeAppearanceModel, + ): MaterialShapeDrawable { + val background = MaterialShapeDrawable(shapeAppearanceModel) + background.fillColor = ColorStateList.valueOf(backgroundColor) + return background + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt index f01dc73d9..8a6217d04 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupZipOutput.kt @@ -15,11 +15,11 @@ class BackupZipOutput(val file: File) : Closeable { private val output = ZipOutput(file, Deflater.BEST_COMPRESSION) - suspend fun put(entry: BackupEntry) { + suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) { output.put(entry.name, entry.data.toString(2)) } - suspend fun finish() { + suspend fun finish() = runInterruptible(Dispatchers.IO) { output.finish() } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt index 5a8cd055c..ef20b4fb0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CloudFlareProtectedException.kt @@ -1,8 +1,6 @@ package org.koitharu.kotatsu.core.exceptions -import androidx.annotation.StringRes import okio.IOException -import org.koitharu.kotatsu.R class CloudFlareProtectedException( val url: String diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index e89a782df..166d9a330 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -126,6 +126,10 @@ class AppSettings(context: Context) { get() = prefs.getString(KEY_APP_PASSWORD, null) set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) } + var isBiometricProtectionEnabled: Boolean + get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true) + set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) } + var sourcesOrder: List get() = prefs.getString(KEY_SOURCES_ORDER, null) ?.split('|') @@ -286,6 +290,7 @@ class AppSettings(context: Context) { const val KEY_READER_MODE_DETECT = "reader_mode_detect" const val KEY_APP_PASSWORD = "app_password" const val KEY_PROTECT_APP = "protect_app" + const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio" const val KEY_APP_VERSION = "app_version" const val KEY_ZOOM_MODE = "zoom_mode" const val KEY_BACKUP = "backup" @@ -310,9 +315,6 @@ class AppSettings(context: Context) { const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_APP_TRANSLATION = "about_app_translation" - const val KEY_FEEDBACK_4PDA = "about_feedback_4pda" - const val KEY_FEEDBACK_DISCORD = "about_feedback_discord" - const val KEY_FEEDBACK_GITHUB = "about_feedback_github" private const val NETWORK_NEVER = 0 private const val NETWORK_ALWAYS = 1 diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 44ee1e7ff..84155b516 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -21,9 +21,11 @@ import androidx.core.view.updatePadding import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.launch +import org.acra.ktx.sendWithAcra import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -37,6 +39,7 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapNotNullToSet @@ -82,7 +85,7 @@ class DetailsActivity : viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) viewModel.onError.observe(this, ::onError) viewModel.onShowToast.observe(this) { - binding.snackbar.show(messageText = getString(it), longDuration = false) + binding.snackbar.show(messageText = getString(it)) } registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE)) @@ -115,6 +118,21 @@ class DetailsActivity : Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() finishAfterTransition() } + e is ParseException || e is IllegalArgumentException || e is IllegalStateException -> { + binding.snackbar.show( + messageText = e.getDisplayMessage(resources), + actionId = R.string.report, + duration = if (viewModel.manga.value?.chapters == null) { + Snackbar.LENGTH_INDEFINITE + } else { + Snackbar.LENGTH_LONG + }, + onActionClick = { + e.sendWithAcra() + dismiss() + } + ) + } else -> { binding.snackbar.show(e.getDisplayMessage(resources)) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 1ef416990..71a6c49fb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* @@ -32,6 +31,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import java.io.IOException class DetailsViewModel( intent: MangaIntent, diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt index 07f03dbda..c6c45ecc1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui import androidx.core.os.LocaleListCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import org.acra.ACRA import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException @@ -13,6 +14,7 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource @@ -20,6 +22,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.setCurrentManga class MangaDetailsDelegate( private val intent: MangaIntent, @@ -32,6 +35,7 @@ class MangaDetailsDelegate( private val mangaData = MutableStateFlow(intent.manga) val selectedBranch = MutableStateFlow(null) + // Remote manga for saved and saved for remote val relatedManga = MutableStateFlow(null) val manga: StateFlow @@ -41,6 +45,7 @@ class MangaDetailsDelegate( suspend fun doLoad() { var manga = mangaDataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") + ACRA.setCurrentManga(manga) mangaData.value = manga manga = MangaRepository(manga.source).getDetails(manga) // find default branch diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt index 37fa78f5a..8347b0a7e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/scrobbling/ScrobblingInfoBottomSheet.kt @@ -29,7 +29,6 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import org.koitharu.kotatsu.utils.ext.requireValue class ScrobblingInfoBottomSheet : BaseBottomSheet(), diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt index f7a98c078..cece6607f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesEditDelegate.kt @@ -4,6 +4,7 @@ import android.content.Context import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.FavouriteCategory +import com.google.android.material.R as materialR class CategoriesEditDelegate( private val context: Context, @@ -11,9 +12,10 @@ class CategoriesEditDelegate( ) { fun deleteCategory(category: FavouriteCategory) { - MaterialAlertDialogBuilder(context) + MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) .setMessage(context.getString(R.string.category_delete_confirm, category.title)) .setTitle(R.string.remove_category) + .setIcon(R.drawable.ic_delete) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.remove) { _, _ -> callback.onDeleteCategory(category) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt index b27629ce6..fe0daa58c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListMenuProvider.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import androidx.core.view.MenuProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R +import com.google.android.material.R as materialR class HistoryListMenuProvider( private val context: Context, @@ -19,9 +20,10 @@ class HistoryListMenuProvider( override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { R.id.action_clear_history -> { - MaterialAlertDialogBuilder(context) + MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) .setTitle(R.string.clear_history) .setMessage(R.string.text_clear_history_prompt) + .setIcon(R.drawable.ic_delete) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.clear) { _, _ -> viewModel.clearHistory() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 28556ac48..568bb6427 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -9,7 +9,6 @@ import androidx.collection.ArraySet import androidx.core.graphics.Insets import androidx.core.view.isNotEmpty import androidx.core.view.updatePadding -import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 55cd36559..b327ae030 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -299,8 +299,9 @@ class MainActivity : } override fun onClearSearchHistory() { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(this, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) .setTitle(R.string.clear_search_history) + .setIcon(R.drawable.ic_clear_all) .setMessage(R.string.text_clear_search_history_prompt) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.clear) { _, _ -> diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt index 953991baa..0da2ee55f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt @@ -96,6 +96,9 @@ class ProtectActivity : } private fun useFingerprint(): Boolean { + if (!viewModel.isBiometricEnabled) { + return false + } if (BiometricManager.from(this).canAuthenticate(BIOMETRIC_WEAK) != BIOMETRIC_SUCCESS) { return false } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt index 69e671c01..85ffe23cb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt @@ -19,6 +19,9 @@ class ProtectViewModel( val onUnlockSuccess = SingleLiveEvent() + val isBiometricEnabled + get() = settings.isBiometricProtectionEnabled + fun tryUnlock(password: String) { if (job?.isActive == true) { return diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index e4a29b948..dfd3af976 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.acra.ktx.sendWithAcra import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -214,6 +215,8 @@ class ReaderActivity : val resolveTextId = ExceptionResolver.getResolveStringId(e) if (resolveTextId != 0) { dialog.setPositiveButton(resolveTextId, listener) + } else { + dialog.setPositiveButton(R.string.report, listener) } dialog.show() } @@ -368,7 +371,11 @@ class ReaderActivity : override fun onClick(dialog: DialogInterface?, which: Int) { if (which == DialogInterface.BUTTON_POSITIVE) { dialog?.dismiss() - tryResolve(exception) + if (ExceptionResolver.canResolve(exception)) { + tryResolve(exception) + } else { + exception.sendWithAcra() + } } else { onCancel(dialog) } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 931461de0..bb8555941 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -6,9 +6,9 @@ import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.util.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import org.acra.ACRA import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent @@ -32,6 +32,8 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.utils.ext.setCurrentManga +import java.util.* private const val BOUNDS_PAGE_OFFSET = 2 private const val PAGES_TRIM_THRESHOLD = 120 @@ -257,6 +259,7 @@ class ReaderViewModel( private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") + ACRA.setCurrentManga(manga) mangaData.value = manga val repo = MangaRepository(manga.source) manga = repo.getDetails(manga) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt index 30b696297..d3980c687 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt @@ -16,6 +16,7 @@ abstract class BasePageHolder( exceptionResolver: ExceptionResolver ) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback { + @Suppress("LeakingThis") protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver) protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt index b6adc87b1..d097c1bc2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt @@ -11,10 +11,11 @@ import org.koitharu.kotatsu.utils.ext.resetTransformations import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +@Suppress("LeakingThis") abstract class BaseReaderAdapter>( private val loader: PageLoader, private val settings: AppSettings, - private val exceptionResolver: ExceptionResolver + private val exceptionResolver: ExceptionResolver, ) : RecyclerView.Adapter() { private val differ = AsyncListDiffer(this, DiffCallback()) diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt index 3d075c1b3..ea5551701 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchActivity.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.search.ui import android.content.Context import android.content.Intent import android.os.Bundle -import android.os.Parcelable import android.view.ViewGroup import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt index ea566d4bb..91f9c5987 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -91,7 +91,7 @@ class SettingsActivity : val fm = supportFragmentManager val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false) fragment.arguments = pref.extras - // fragment.setTargetFragment(caller, 0) + fragment.setTargetFragment(caller, 0) openFragment(fragment) return true } @@ -122,6 +122,7 @@ class SettingsActivity : ACTION_READER -> ReaderSettingsFragment() ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() ACTION_SHIKIMORI -> ShikimoriSettingsFragment() + ACTION_TRACKER -> TrackerSettingsFragment() ACTION_SOURCE -> SourceSettingsFragment.newInstance( intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL ) @@ -146,6 +147,7 @@ class SettingsActivity : private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" private const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS" + private const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER" private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" private const val ACTION_SHIKIMORI = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SHIKIMORI_SETTINGS" private const val EXTRA_SOURCE = "source" @@ -166,6 +168,10 @@ class SettingsActivity : Intent(context, SettingsActivity::class.java) .setAction(ACTION_SUGGESTIONS) + fun newTrackerSettingsIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_TRACKER) + fun newSourceSettingsIntent(context: Context, source: MangaSource) = Intent(context, SettingsActivity::class.java) .setAction(ACTION_SOURCE) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt index a0145362a..f88a8dad9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupActivity.kt @@ -1,5 +1,7 @@ package org.koitharu.kotatsu.settings.protect +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -7,9 +9,11 @@ import android.view.KeyEvent import android.view.View import android.view.WindowManager import android.view.inputmethod.EditorInfo +import android.widget.CompoundButton import android.widget.TextView import androidx.core.graphics.Insets import androidx.core.view.isGone +import androidx.core.view.isVisible import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity @@ -18,7 +22,7 @@ import org.koitharu.kotatsu.databinding.ActivitySetupProtectBinding private const val MIN_PASSWORD_LENGTH = 4 class ProtectSetupActivity : BaseActivity(), TextWatcher, - View.OnClickListener, TextView.OnEditorActionListener { + View.OnClickListener, TextView.OnEditorActionListener, CompoundButton.OnCheckedChangeListener { private val viewModel by viewModel() @@ -31,6 +35,9 @@ class ProtectSetupActivity : BaseActivity(), TextWa binding.buttonNext.setOnClickListener(this) binding.buttonCancel.setOnClickListener(this) + binding.switchBiometric.isChecked = viewModel.isBiometricEnabled + binding.switchBiometric.setOnCheckedChangeListener(this) + viewModel.isSecondStep.observe(this, this::onStepChanged) viewModel.onPasswordSet.observe(this) { finishAfterTransition() @@ -62,6 +69,10 @@ class ProtectSetupActivity : BaseActivity(), TextWa } } + override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { + viewModel.setBiometricEnabled(isChecked) + } + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { return if (actionId == EditorInfo.IME_ACTION_DONE && binding.buttonNext.isEnabled) { binding.buttonNext.performClick() @@ -85,6 +96,7 @@ class ProtectSetupActivity : BaseActivity(), TextWa private fun onStepChanged(isSecondStep: Boolean) { binding.buttonCancel.isGone = isSecondStep + binding.switchBiometric.isVisible = isSecondStep && isBiometricAvailable() if (isSecondStep) { binding.layoutPassword.helperText = getString(R.string.repeat_password) binding.buttonNext.setText(R.string.confirm) @@ -93,4 +105,9 @@ class ProtectSetupActivity : BaseActivity(), TextWa binding.buttonNext.setText(R.string.next) } } + + private fun isBiometricAvailable(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt index 1244c836e..c9013d23d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/protect/ProtectSetupViewModel.kt @@ -22,6 +22,9 @@ class ProtectSetupViewModel( val onPasswordMismatch = SingleLiveEvent() val onClearText = SingleLiveEvent() + val isBiometricEnabled + get() = settings.isBiometricProtectionEnabled + fun onNextClick(password: String) { if (firstPassword.value == null) { firstPassword.value = password @@ -35,4 +38,8 @@ class ProtectSetupViewModel( } } } + + fun setBiometricEnabled(isEnabled: Boolean) { + settings.isBiometricProtectionEnabled = isEnabled + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt index 1bf8b7856..3920cb32c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/RingtonePickContract.kt @@ -29,6 +29,6 @@ class RingtonePickContract(private val title: String?) : ActivityResultContract< } override fun parseResult(resultCode: Int, intent: Intent?): Uri? { - return intent?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + return intent?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt index d334f31ef..398a0a0f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -32,14 +32,14 @@ class SuggestionRepository( suspend fun replace(suggestions: Iterable) { db.withTransaction { db.suggestionDao.deleteAll() - suggestions.forEach { x -> - val tags = x.manga.tags.toEntities() + suggestions.forEach { (manga, relevance) -> + val tags = manga.tags.toEntities() db.tagsDao.upsert(tags) - db.mangaDao.upsert(x.manga.toEntity(), tags) + db.mangaDao.upsert(manga.toEntity(), tags) db.suggestionDao.upsert( SuggestionEntity( - mangaId = x.manga.id, - relevance = x.relevance, + mangaId = manga.id, + relevance = relevance, createdAt = System.currentTimeMillis(), ) ) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt index 6787ff7da..655f02f8b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedMenuProvider.kt @@ -9,6 +9,7 @@ import androidx.core.view.MenuProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.tracker.work.TrackWorker class FeedMenuProvider( @@ -43,6 +44,11 @@ class FeedMenuProvider( }.show() true } + R.id.action_settings -> { + val intent = SettingsActivity.newTrackerSettingsIntent(context) + context.startActivity(intent) + true + } else -> false } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt index 6c856f4d4..4cecbd2a8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ScreenOrientationHelper.kt @@ -35,7 +35,7 @@ class ScreenOrientationHelper(private val activity: Activity) { isLandscape = !isLandscape } - fun observeAutoOrientation() = callbackFlow { + fun observeAutoOrientation() = callbackFlow { val observer = object : ContentObserver(Handler(activity.mainLooper)) { override fun onChange(selfChange: Boolean) { trySendBlocking(isAutoRotationEnabled) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt deleted file mode 100644 index 7276dab57..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/InsetsExt.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.koitharu.kotatsu.utils.ext - -import android.view.View -import androidx.core.graphics.Insets - -fun Insets.getStart(view: View): Int { - return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - right - } else { - left - } -} - -fun Insets.getEnd(view: View): Int { - return if (view.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - left - } else { - right - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt index 3ac39ee9c..c4172000f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt @@ -4,11 +4,11 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.liveData -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.koitharu.kotatsu.utils.BufferedObserver +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext fun LiveData.observeNotNull(owner: LifecycleOwner, observer: Observer) { this.observe(owner) { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt rename to app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt index 22f95e4a4..6dac10c3b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt @@ -2,14 +2,16 @@ package org.koitharu.kotatsu.utils.ext import android.content.ActivityNotFoundException import android.content.res.Resources -import java.io.FileNotFoundException -import java.net.SocketTimeoutException +import okio.FileNotFoundException +import org.acra.ACRA import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.model.Manga +import java.net.SocketTimeoutException fun Throwable.getDisplayMessage(resources: Resources) = when (this) { is AuthRequiredException -> resources.getString(R.string.auth_required) @@ -22,4 +24,6 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) { is SocketTimeoutException -> resources.getString(R.string.network_error) is WrongPasswordException -> resources.getString(R.string.wrong_password) else -> localizedMessage ?: resources.getString(R.string.error_occurred) -} \ No newline at end of file +} + +fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString()) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 67c30830e..586e40eef 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -5,8 +5,6 @@ import android.graphics.Rect import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager -import androidx.annotation.StringRes -import androidx.appcompat.widget.TooltipCompat import androidx.core.view.children import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt index 3e4125a21..04c25f382 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentListFactory.kt @@ -7,13 +7,15 @@ import android.widget.RemoteViewsService import coil.ImageLoader import coil.executeBlocking import coil.request.ImageRequest +import coil.size.Scale +import coil.size.Size +import coil.transform.RoundedCornersTransformation import kotlinx.coroutines.runBlocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.requireBitmap -import java.io.IOException class RecentListFactory( private val context: Context, @@ -22,9 +24,15 @@ class RecentListFactory( ) : RemoteViewsService.RemoteViewsFactory { private val dataSet = ArrayList() + private val transformation = RoundedCornersTransformation( + context.resources.getDimension(R.dimen.appwidget_corner_radius_inner) + ) + private val coverSize = Size( + context.resources.getDimensionPixelSize(R.dimen.widget_cover_width), + context.resources.getDimensionPixelSize(R.dimen.widget_cover_height), + ) - override fun onCreate() { - } + override fun onCreate() = Unit override fun getLoadingView() = null @@ -41,14 +49,18 @@ class RecentListFactory( override fun getViewAt(position: Int): RemoteViews { val views = RemoteViews(context.packageName, R.layout.item_recent) val item = dataSet[position] - try { - val cover = coil.executeBlocking( + runCatching { + coil.executeBlocking( ImageRequest.Builder(context) .data(item.coverUrl) + .size(coverSize) + .scale(Scale.FILL) + .transformations(transformation) .build() ).requireBitmap() + }.onSuccess { cover -> views.setImageViewBitmap(R.id.imageView_cover, cover) - } catch (e: IOException) { + }.onFailure { views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder) } val intent = Intent() @@ -61,6 +73,5 @@ class RecentListFactory( override fun getViewTypeCount() = 1 - override fun onDestroy() { - } + override fun onDestroy() = Unit } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt index b404b3286..44a2cc632 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfConfigActivity.kt @@ -10,8 +10,6 @@ import androidx.core.graphics.Insets import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R @@ -40,9 +38,6 @@ class ShelfConfigActivity : BaseActivity(), setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } adapter = CategorySelectAdapter(this) - binding.recyclerView.addItemDecoration( - MaterialDividerItemDecoration(this, RecyclerView.VERTICAL) - ) binding.recyclerView.adapter = adapter binding.buttonDone.isVisible = true binding.buttonDone.setOnClickListener(this) diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt index 61fd0d9ab..1676e5a49 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfListFactory.kt @@ -7,6 +7,9 @@ import android.widget.RemoteViewsService import coil.ImageLoader import coil.executeBlocking import coil.request.ImageRequest +import coil.size.Scale +import coil.size.Size +import coil.transform.RoundedCornersTransformation import kotlinx.coroutines.runBlocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaIntent @@ -14,20 +17,25 @@ import org.koitharu.kotatsu.core.prefs.AppWidgetConfig import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.requireBitmap -import java.io.IOException class ShelfListFactory( private val context: Context, private val favouritesRepository: FavouritesRepository, private val coil: ImageLoader, - widgetId: Int + widgetId: Int, ) : RemoteViewsService.RemoteViewsFactory { private val dataSet = ArrayList() private val config = AppWidgetConfig(context, widgetId) + private val transformation = RoundedCornersTransformation( + context.resources.getDimension(R.dimen.appwidget_corner_radius_inner) + ) + private val coverSize = Size( + context.resources.getDimensionPixelSize(R.dimen.widget_cover_width), + context.resources.getDimensionPixelSize(R.dimen.widget_cover_height), + ) - override fun onCreate() { - } + override fun onCreate() = Unit override fun getLoadingView() = null @@ -52,14 +60,18 @@ class ShelfListFactory( val views = RemoteViews(context.packageName, R.layout.item_shelf) val item = dataSet[position] views.setTextViewText(R.id.textView_title, item.title) - try { - val cover = coil.executeBlocking( + runCatching { + coil.executeBlocking( ImageRequest.Builder(context) .data(item.coverUrl) + .size(coverSize) + .scale(Scale.FILL) + .transformations(transformation) .build() ).requireBitmap() + }.onSuccess { cover -> views.setImageViewBitmap(R.id.imageView_cover, cover) - } catch (e: IOException) { + }.onFailure { views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder) } val intent = Intent() diff --git a/app/src/main/res/color-v23/selector_switch_thumb.xml b/app/src/main/res/color-v23/selector_switch_thumb.xml deleted file mode 100644 index ef1a3cc36..000000000 --- a/app/src/main/res/color-v23/selector_switch_thumb.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color-v23/selector_switch_track.xml b/app/src/main/res/color-v23/selector_switch_track.xml deleted file mode 100644 index 3779a794a..000000000 --- a/app/src/main/res/color-v23/selector_switch_track.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_switch_thumb.xml b/app/src/main/res/color/selector_switch_thumb.xml deleted file mode 100644 index de8892285..000000000 --- a/app/src/main/res/color/selector_switch_thumb.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_switch_track.xml b/app/src/main/res/color/selector_switch_track.xml deleted file mode 100644 index 7b6c7c468..000000000 --- a/app/src/main/res/color/selector_switch_track.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/badge.xml b/app/src/main/res/drawable/bg_appwidget_card.xml similarity index 50% rename from app/src/main/res/drawable/badge.xml rename to app/src/main/res/drawable/bg_appwidget_card.xml index a15864699..35a460504 100644 --- a/app/src/main/res/drawable/badge.xml +++ b/app/src/main/res/drawable/bg_appwidget_card.xml @@ -2,6 +2,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_list_add.xml b/app/src/main/res/drawable/ic_list_add.xml deleted file mode 100644 index 26ca07f23..000000000 --- a/app/src/main/res/drawable/ic_list_add.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml deleted file mode 100644 index e63766078..000000000 --- a/app/src/main/res/drawable/ic_pause.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_resume.xml b/app/src/main/res/drawable/ic_resume.xml deleted file mode 100644 index 448628b18..000000000 --- a/app/src/main/res/drawable/ic_resume.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/switch_thumb.xml b/app/src/main/res/drawable/switch_thumb.xml deleted file mode 100644 index 12222e7f5..000000000 --- a/app/src/main/res/drawable/switch_thumb.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/switch_track.xml b/app/src/main/res/drawable/switch_track.xml deleted file mode 100644 index 005647294..000000000 --- a/app/src/main/res/drawable/switch_track.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_category_edit.xml b/app/src/main/res/layout/activity_category_edit.xml index 4cd3a7afe..93eef7b49 100644 --- a/app/src/main/res/layout/activity_category_edit.xml +++ b/app/src/main/res/layout/activity_category_edit.xml @@ -68,7 +68,7 @@ - + + - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_list_mode.xml b/app/src/main/res/layout/dialog_list_mode.xml index cfbd8e018..e8341c954 100644 --- a/app/src/main/res/layout/dialog_list_mode.xml +++ b/app/src/main/res/layout/dialog_list_mode.xml @@ -1,70 +1,75 @@ - + android:layout_height="wrap_content"> - - + android:layout_margin="16dp" + android:orientation="vertical"> - + + + + + + + + + android:paddingLeft="?attr/dialogPreferredPadding" + android:paddingRight="?attr/dialogPreferredPadding" + android:singleLine="true" + android:text="@string/grid_size" + android:visibility="gone" + tools:visibility="visible" /> - + android:layout_marginHorizontal="16dp" + android:stepSize="5" + android:valueFrom="50" + android:valueTo="150" + android:visibility="gone" + app:labelBehavior="floating" + app:tickVisible="false" + tools:value="100" + tools:visibility="visible" /> - - - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fading_snackbar_layout.xml b/app/src/main/res/layout/fading_snackbar_layout.xml index 4c75ce8b9..0ebbb7bdd 100644 --- a/app/src/main/res/layout/fading_snackbar_layout.xml +++ b/app/src/main/res/layout/fading_snackbar_layout.xml @@ -1,5 +1,4 @@ - - @style/TextAppearance.Kotatsu.Menu @@ -102,4 +103,8 @@ @style/Theme.Kotatsu.ActionMode.CloseButton + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index a1fe42507..d560bd956 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -1,5 +1,6 @@ - + - + diff --git a/app/src/main/res/xml/widget_recent.xml b/app/src/main/res/xml/widget_recent.xml index 4f4beea5a..476c83cda 100644 --- a/app/src/main/res/xml/widget_recent.xml +++ b/app/src/main/res/xml/widget_recent.xml @@ -1,12 +1,17 @@ + android:widgetCategory="home_screen" + tools:ignore="UnusedAttribute" /> diff --git a/app/src/main/res/xml/widget_shelf.xml b/app/src/main/res/xml/widget_shelf.xml index 45c37262e..c1e4879de 100644 --- a/app/src/main/res/xml/widget_shelf.xml +++ b/app/src/main/res/xml/widget_shelf.xml @@ -1,13 +1,19 @@ + android:widgetCategory="home_screen" + android:widgetFeatures="reconfigurable" + tools:ignore="UnusedAttribute" /> diff --git a/metadata/ru/full_description.txt b/metadata/ru/full_description.txt index 87944e3da..df079ccdd 100644 --- a/metadata/ru/full_description.txt +++ b/metadata/ru/full_description.txt @@ -5,7 +5,7 @@ Kotatsu - приложения для чтения манги с открыты - Поиск манги по имени и жанрам - История чтения - Избранное с пользовательскими категориями -- Возможность сохранять мангу и читать её оффлайн. Поддержка сторонних комиксов в формате CBZ +- Возможность сохранять мангу и читать её офлайн. Поддержка сторонних комиксов в формате CBZ - Интерфейс также оптимизирован для планшетов - Поддержка манхвы (Webtoon) - Уведомления о новых главах и лента обновлений