From ffad6a4ae656bef008e1e8c8aa2fdc79c9c29a37 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 4 May 2022 13:17:02 +0300 Subject: [PATCH 01/45] Upgrade coil to v2 --- app/build.gradle | 7 +-- .../kotatsu/core/os/ShortcutsRepository.kt | 10 ++-- .../kotatsu/core/parser/FaviconMapper.kt | 10 ++-- .../org/koitharu/kotatsu/core/ui/uiModule.kt | 20 ++++++-- .../kotatsu/details/ui/DetailsFragment.kt | 2 +- .../kotatsu/image/ui/ImageActivity.kt | 9 +--- .../list/ui/adapter/MangaGridItemAD.kt | 2 +- .../ui/adapter/MangaListDetailedItemAD.kt | 2 +- .../list/ui/adapter/MangaListItemAD.kt | 2 +- .../koitharu/kotatsu/local/data/CbzFetcher.kt | 51 +++++++++++-------- .../ui/thumbnails/adapter/PageThumbnailAD.kt | 4 +- .../progress/ImageRequestIndicatorListener.kt | 7 +-- 12 files changed, 73 insertions(+), 53 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ff599eb34..b2d55cf7c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,7 @@ android { '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-opt-in=kotlinx.coroutines.FlowPreview', '-opt-in=kotlin.contracts.ExperimentalContracts', + '-opt-in=coil.annotation.ExperimentalCoilApi', ] } lint { @@ -86,7 +87,7 @@ dependencies { implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' - implementation 'com.google.android.material:material:1.6.0-rc01' + implementation 'com.google.android.material:material:1.7.0-alpha01' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' @@ -101,11 +102,11 @@ dependencies { implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'io.insert-koin:koin-android:3.1.6' - implementation 'io.coil-kt:coil-base:1.4.0' + implementation 'io.coil-kt:coil-base:2.0.0-rc03' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' testImplementation 'junit:junit:4.13.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt index 98ebe3a6a..9e8b18a52 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt @@ -5,12 +5,12 @@ import android.content.Context import android.content.pm.ShortcutManager import android.media.ThumbnailUtils import android.os.Build +import android.util.Size import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import coil.ImageLoader import coil.request.ImageRequest -import coil.size.PixelSize import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R @@ -54,7 +54,7 @@ class ShortcutsRepository( val bmp = coil.execute( ImageRequest.Builder(context) .data(manga.coverUrl) - .size(iconSize) + .size(iconSize.width, iconSize.height) .build() ).requireBitmap() ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) @@ -74,14 +74,14 @@ class ShortcutsRepository( ) } - private fun getIconSize(context: Context): PixelSize { + private fun getIconSize(context: Context): Size { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { (context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let { - PixelSize(it.iconMaxWidth, it.iconMaxHeight) + Size(it.iconMaxWidth, it.iconMaxHeight) } } else { (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let { - PixelSize(it, it) + Size(it, it) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt index 4a386d3f8..ba5412a50 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt @@ -2,17 +2,19 @@ package org.koitharu.kotatsu.core.parser import android.net.Uri import coil.map.Mapper +import coil.request.Options import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import org.koitharu.kotatsu.parsers.model.MangaSource -class FaviconMapper() : Mapper { +class FaviconMapper : Mapper { - override fun map(data: Uri): HttpUrl { + override fun map(data: Uri, options: Options): HttpUrl? { + if (data.scheme != "favicon") { + return null + } val mangaSource = MangaSource.valueOf(data.schemeSpecificPart) val repo = MangaRepository(mangaSource) as RemoteMangaRepository return repo.getFaviconUrl().toHttpUrl() } - - override fun handles(data: Uri) = data.scheme == "favicon" } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt index 26ccbf44a..54529b37c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt @@ -2,11 +2,13 @@ package org.koitharu.kotatsu.core.ui import coil.ComponentRegistry import coil.ImageLoader -import coil.util.CoilUtils +import coil.disk.DiskCache +import kotlinx.coroutines.Dispatchers import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import org.koitharu.kotatsu.core.parser.FaviconMapper +import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CbzFetcher val uiModule @@ -14,15 +16,23 @@ val uiModule single { val httpClientFactory = { get().newBuilder() - .cache(CoilUtils.createDefaultCache(androidContext())) + .cache(null) + .build() + } + val diskCacheFactory = { + val context = androidContext() + val rootDir = context.externalCacheDir ?: context.cacheDir + DiskCache.Builder() + .directory(rootDir.resolve(CacheDir.THUMBS.dir)) .build() } ImageLoader.Builder(androidContext()) .okHttpClient(httpClientFactory) - .launchInterceptorChainOnMainThread(false) - .componentRegistry( + .interceptorDispatcher(Dispatchers.Default) + .diskCache(diskCacheFactory) + .components( ComponentRegistry.Builder() - .add(CbzFetcher()) + .add(CbzFetcher.Factory()) .add(FaviconMapper()) .build() ).build() diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index df4fc2de9..99c7319e3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -283,7 +283,7 @@ class DetailsFragment : .target(binding.imageViewCover) if (currentCover != null) { request.data(manga.largeCoverUrl ?: return) - .placeholderMemoryCacheKey(CoilUtils.metadata(binding.imageViewCover)?.memoryCacheKey) + .placeholderMemoryCacheKey(CoilUtils.result(binding.imageViewCover)?.request?.memoryCacheKey) .fallback(currentCover) } else { request.crossfade(true) diff --git a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt b/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt index 8e674701a..28f58887b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/image/ui/ImageActivity.kt @@ -6,7 +6,6 @@ import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.view.ViewGroup -import androidx.appcompat.app.AppCompatDelegate import androidx.core.graphics.Insets import androidx.core.graphics.drawable.toBitmap import androidx.core.view.updateLayoutParams @@ -14,7 +13,7 @@ import androidx.core.view.updatePadding import coil.ImageLoader import coil.request.CachePolicy import coil.request.ImageRequest -import coil.target.PoolableViewTarget +import coil.target.ViewTarget import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import org.koin.android.ext.android.inject @@ -61,16 +60,12 @@ class ImageActivity : BaseActivity() { private class SsivTarget( override val view: SubsamplingScaleImageView, - ) : PoolableViewTarget { - - override fun onStart(placeholder: Drawable?) = setDrawable(placeholder) + ) : ViewTarget { override fun onError(error: Drawable?) = setDrawable(error) override fun onSuccess(result: Drawable) = setDrawable(result) - override fun onClear() = setDrawable(null) - override fun equals(other: Any?): Boolean { return (this === other) || (other is SsivTarget && view == other.view) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index ced1697b0..bc716c935 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -53,7 +53,7 @@ fun mangaGridItemAD( badge = null imageRequest?.dispose() imageRequest = null - CoilUtils.clear(binding.imageViewCover) + CoilUtils.dispose(binding.imageViewCover) binding.imageViewCover.setImageDrawable(null) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 10e9a473d..60420df4d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -57,7 +57,7 @@ fun mangaListDetailedItemAD( badge = null imageRequest?.dispose() imageRequest = null - CoilUtils.clear(binding.imageViewCover) + CoilUtils.dispose(binding.imageViewCover) binding.imageViewCover.setImageDrawable(null) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 18696de6b..d709d954d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -55,7 +55,7 @@ fun mangaListItemAD( badge = null imageRequest?.dispose() imageRequest = null - CoilUtils.clear(binding.imageViewCover) + CoilUtils.dispose(binding.imageViewCover) binding.imageViewCover.setImageDrawable(null) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt index ff04a2eff..c773a05d3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt @@ -2,41 +2,52 @@ package org.koitharu.kotatsu.local.data import android.net.Uri import android.webkit.MimeTypeMap -import coil.bitmap.BitmapPool +import coil.ImageLoader import coil.decode.DataSource -import coil.decode.Options -import coil.fetch.FetchResult +import coil.decode.ImageSource import coil.fetch.Fetcher import coil.fetch.SourceResult -import coil.size.Size -import java.util.zip.ZipFile +import coil.request.Options import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okio.buffer import okio.source +import java.util.zip.ZipFile -class CbzFetcher : Fetcher { +class CbzFetcher( + private val uri: Uri, + private val options: Options +) : Fetcher { - override suspend fun fetch( - pool: BitmapPool, - data: Uri, - size: Size, - options: Options, - ): FetchResult = runInterruptible(Dispatchers.IO) { - val zip = ZipFile(data.schemeSpecificPart) - val entry = zip.getEntry(data.fragment) + override suspend fun fetch() = runInterruptible(Dispatchers.IO) { + val zip = ZipFile(uri.schemeSpecificPart) + val entry = zip.getEntry(uri.fragment) val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name) + val bufferedSource = ExtraCloseableBufferedSource( + zip.getInputStream(entry).source().buffer(), + zip, + ) SourceResult( - source = ExtraCloseableBufferedSource( - zip.getInputStream(entry).source().buffer(), - zip, + source = ImageSource( + source = bufferedSource, + context = options.context, + metadata = CbzMetadata(uri), ), mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext), - dataSource = DataSource.DISK + dataSource = DataSource.DISK, ) } - override fun key(data: Uri) = data.toString() + class Factory : Fetcher.Factory { - override fun handles(data: Uri) = data.scheme == "cbz" + override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? { + return if (data.scheme == "cbz") { + CbzFetcher(data, options) + } else { + null + } + } + } + + class CbzMetadata(val uri: Uri) : ImageSource.Metadata() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt index 201a4f4ae..8cddae963 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter import android.graphics.drawable.Drawable import coil.ImageLoader import coil.request.ImageRequest -import coil.size.PixelSize +import coil.size.Size import com.google.android.material.R as materialR import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import kotlinx.coroutines.* @@ -27,7 +27,7 @@ fun pageThumbnailAD( var job: Job? = null val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width) - val thumbSize = PixelSize( + val thumbSize = Size( width = gridWidth, height = (gridWidth * 13f / 18f).toInt() ) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt index eb38e1d32..317c77c5f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/progress/ImageRequestIndicatorListener.kt @@ -1,7 +1,8 @@ package org.koitharu.kotatsu.utils.progress +import coil.request.ErrorResult import coil.request.ImageRequest -import coil.request.ImageResult +import coil.request.SuccessResult import com.google.android.material.progressindicator.BaseProgressIndicator class ImageRequestIndicatorListener( @@ -10,9 +11,9 @@ class ImageRequestIndicatorListener( override fun onCancel(request: ImageRequest) = indicator.hide() - override fun onError(request: ImageRequest, throwable: Throwable) = indicator.hide() + override fun onError(request: ImageRequest, result: ErrorResult) = indicator.hide() override fun onStart(request: ImageRequest) = indicator.show() - override fun onSuccess(request: ImageRequest, metadata: ImageResult.Metadata) = indicator.hide() + override fun onSuccess(request: ImageRequest, result: SuccessResult) = indicator.hide() } \ No newline at end of file From 1d1931f721baa8c2b1eb072ee7cd21622948e714 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Wed, 4 May 2022 19:01:32 +0300 Subject: [PATCH 02/45] [Issue template] Update version --- .github/ISSUE_TEMPLATE/report_issue.yml | 4 ++-- .github/ISSUE_TEMPLATE/request_feature.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index 69983d414..c6c05a681 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.2" + Example: "3.2.1" validations: required: true @@ -87,7 +87,7 @@ 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/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.2.1](https://github.com/nv95/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 a46a0648f..262c63ebb 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ 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/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.2.1](https://github.com/nv95/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 From 4e4024c1827834ad1fc5a3486cdbb42f370f5c40 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Wed, 4 May 2022 23:00:44 +0300 Subject: [PATCH 03/45] Fix `FavouriteCategoriesDialog` toolbar in album orientation --- app/src/main/res/layout/dialog_favorite_categories.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/dialog_favorite_categories.xml b/app/src/main/res/layout/dialog_favorite_categories.xml index 2085c72f7..0acdd440e 100644 --- a/app/src/main/res/layout/dialog_favorite_categories.xml +++ b/app/src/main/res/layout/dialog_favorite_categories.xml @@ -10,7 +10,7 @@ From 11fc8b66422588f702ecadfb28039c921e66ff1c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 13:44:26 +0300 Subject: [PATCH 04/45] Configure manga tracker for each favourite category --- .../kotatsu/core/backup/BackupRepository.kt | 1 + .../kotatsu/core/backup/RestoreRepository.kt | 1 + .../kotatsu/core/db/DatabaseModule.kt | 2 +- .../koitharu/kotatsu/core/db/MangaDatabase.kt | 40 +++-- .../koitharu/kotatsu/core/db/dao/TracksDao.kt | 3 + .../core/db/migrations/Migration9To10.kt | 11 ++ .../kotatsu/core/github/GithubModule.kt | 2 +- .../kotatsu/core/model/FavouriteCategory.kt | 3 +- .../kotatsu/core/prefs/AppSettings.kt | 9 +- .../kotatsu/favourites/FavouritesModule.kt | 2 +- .../kotatsu/favourites/data/EntityMapping.kt | 1 + .../favourites/data/FavouriteCategoriesDao.kt | 3 + .../data/FavouriteCategoryEntity.kt | 1 + .../kotatsu/favourites/data/FavouritesDao.kt | 3 + .../favourites/domain/FavouritesRepository.kt | 17 ++- .../ui/categories/CategoriesActivity.kt | 12 +- .../FavouritesCategoriesViewModel.kt | 11 +- .../koitharu/kotatsu/history/HistoryModule.kt | 2 +- .../org/koitharu/kotatsu/local/LocalModule.kt | 2 +- .../org/koitharu/kotatsu/main/MainModule.kt | 2 +- .../koitharu/kotatsu/reader/ReaderModule.kt | 2 +- .../koitharu/kotatsu/search/SearchModule.kt | 3 +- .../NotificationSettingsLegacyFragment.kt | 33 +++- .../kotatsu/settings/SettingsModule.kt | 4 +- .../settings/TrackerSettingsFragment.kt | 86 ++++++++++- .../kotatsu/settings/backup/AppBackupAgent.kt | 4 +- .../koitharu/kotatsu/tracker/TrackerModule.kt | 5 +- .../tracker/domain/TrackingRepository.kt | 41 +++-- .../kotatsu/tracker/work/TrackWorker.kt | 105 ++++++++----- .../work/TrackerNotificationChannels.kt | 143 ++++++++++++++++++ .../kotatsu/tracker/work/TrackingItem.kt | 31 ++++ .../res/layout/preference_toggle_header.xml | 64 ++++++++ app/src/main/res/menu/popup_category.xml | 5 + app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 5 +- app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values/strings.xml | 5 +- app/src/main/res/xml/pref_notifications.xml | 21 ++- app/src/main/res/xml/pref_suggestions.xml | 2 +- app/src/main/res/xml/pref_tracker.xml | 17 ++- 50 files changed, 584 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt create mode 100644 app/src/main/res/layout/preference_toggle_header.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 3d2f668a6..4b42b4c60 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -121,6 +121,7 @@ class BackupRepository(private val db: MangaDatabase) { jo.put("sort_key", sortKey) jo.put("title", title) jo.put("order", order) + jo.put("track", track) return jo } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt index ba4a8b6a3..57fd4d6ee 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt @@ -104,6 +104,7 @@ class RestoreRepository(private val db: MangaDatabase) { sortKey = json.getInt("sort_key"), title = json.getString("title"), order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name, + track = json.getBooleanOrDefault("track", true), ) private fun parseFavourite(json: JSONObject) = FavouriteEntity( diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt index dadbb05eb..215d02259 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt @@ -5,5 +5,5 @@ import org.koin.dsl.module val databaseModule get() = module { - single { MangaDatabase.create(androidContext()) } + single { MangaDatabase(androidContext()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index 010a2653f..436455014 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -22,7 +22,7 @@ import org.koitharu.kotatsu.suggestions.data.SuggestionEntity FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class ], - version = 9 + version = 10 ) abstract class MangaDatabase : RoomDatabase() { @@ -43,24 +43,22 @@ abstract class MangaDatabase : RoomDatabase() { abstract val trackLogsDao: TrackLogsDao abstract val suggestionDao: SuggestionDao +} - companion object { - - fun create(context: Context): MangaDatabase = Room.databaseBuilder( - context, - MangaDatabase::class.java, - "kotatsu-db" - ).addMigrations( - Migration1To2(), - Migration2To3(), - Migration3To4(), - Migration4To5(), - Migration5To6(), - Migration6To7(), - Migration7To8(), - Migration8To9(), - ).addCallback( - DatabasePrePopulateCallback(context.resources) - ).build() - } -} \ No newline at end of file +fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder( + context, + MangaDatabase::class.java, + "kotatsu-db" +).addMigrations( + Migration1To2(), + Migration2To3(), + Migration3To4(), + Migration4To5(), + Migration5To6(), + Migration6To7(), + Migration7To8(), + Migration8To9(), + Migration9To10(), +).addCallback( + DatabasePrePopulateCallback(context.resources) +).build() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt index 4bd188966..f8352524b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt @@ -10,6 +10,9 @@ abstract class TracksDao { @Query("SELECT * FROM tracks") abstract suspend fun findAll(): List + @Query("SELECT * FROM tracks WHERE manga_id IN (:ids)") + abstract suspend fun findAll(ids: Collection): List + @Query("SELECT * FROM tracks WHERE manga_id = :mangaId") abstract suspend fun find(mangaId: Long): TrackEntity? diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt new file mode 100644 index 000000000..59cba96ef --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration9To10.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration9To10 : Migration(9, 10) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt index de5256337..7da9e309f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt @@ -4,7 +4,7 @@ import org.koin.dsl.module val githubModule get() = module { - single { + factory { GithubRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt index 655a0eb08..798ec2fbd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt @@ -1,9 +1,9 @@ package org.koitharu.kotatsu.core.model import android.os.Parcelable +import java.util.* import kotlinx.parcelize.Parcelize import org.koitharu.kotatsu.parsers.model.SortOrder -import java.util.* @Parcelize data class FavouriteCategory( @@ -12,4 +12,5 @@ data class FavouriteCategory( val sortKey: Int, val order: SortOrder, val createdAt: Date, + val isTrackingEnabled: Boolean, ) : Parcelable \ No newline at end of file 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 d9178713c..185336542 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 @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.prefs +import android.annotation.TargetApi import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager @@ -78,7 +79,10 @@ class AppSettings(context: Context) { get() = prefs.getLong(KEY_APP_UPDATE, 0L) set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) } - val trackerNotifications: Boolean + val isTrackerEnabled: Boolean + get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) + + val isTrackerNotificationsEnabled: Boolean get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true) var notificationSound: Uri @@ -269,13 +273,16 @@ class AppSettings(context: Context) { const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_READER_SWITCHERS = "reader_switchers" + const val KEY_TRACKER_ENABLED = "tracker_enabled" const val KEY_TRACK_SOURCES = "track_sources" + const val KEY_TRACK_CATEGORIES = "track_categories" const val KEY_TRACK_WARNING = "track_warning" const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications" const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings" const val KEY_NOTIFICATIONS_SOUND = "notifications_sound" const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate" const val KEY_NOTIFICATIONS_LIGHT = "notifications_light" + const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info" const val KEY_READER_ANIMATION = "reader_animation" const val KEY_READER_PREFER_RTL = "reader_prefer_rtl" const val KEY_APP_PASSWORD = "app_password" diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt index 8222c0f2b..0f10b81ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt @@ -10,7 +10,7 @@ import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel val favouritesModule get() = module { - single { FavouritesRepository(get()) } + factory { FavouritesRepository(get(), get()) } viewModel { categoryId -> FavouritesListViewModel(categoryId.get(), get(), get(), get()) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt index 801f2566a..c6a65c78e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/EntityMapping.kt @@ -11,4 +11,5 @@ fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) sortKey = sortKey, order = SortOrder(order, SortOrder.NEWEST), createdAt = Date(createdAt), + isTrackingEnabled = track, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index 436dc12ea..65d429706 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -30,6 +30,9 @@ abstract class FavouriteCategoriesDao { @Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id") abstract suspend fun updateOrder(id: Long, order: String) + @Query("UPDATE favourite_categories SET `track` = :isEnabled WHERE category_id = :id") + abstract suspend fun updateTracking(id: Long, isEnabled: Boolean) + @Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id") abstract suspend fun updateSortKey(id: Long, sortKey: Int) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt index d2b0bc7ed..5fe02f019 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt @@ -12,4 +12,5 @@ class FavouriteCategoryEntity( @ColumnInfo(name = "sort_key") val sortKey: Int, @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "order") val order: String, + @ColumnInfo(name = "track") val track: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index 9e5da45f7..89fcc92fb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -43,6 +43,9 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset") abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE category_id = :categoryId)") + abstract suspend fun findAllManga(categoryId: Int): List + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites)") abstract suspend fun findAllManga(): List diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 731d7ff5e..35a362e95 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -13,9 +13,13 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.utils.ext.mapItems -class FavouritesRepository(private val db: MangaDatabase) { +class FavouritesRepository( + private val db: MangaDatabase, + private val channels: TrackerNotificationChannels, +) { suspend fun getAllManga(): List { val entities = db.favouritesDao.findAll() @@ -65,23 +69,32 @@ class FavouritesRepository(private val db: MangaDatabase) { sortKey = db.favouriteCategoriesDao.getNextSortKey(), categoryId = 0, order = SortOrder.NEWEST.name, + track = true, ) val id = db.favouriteCategoriesDao.insert(entity) - return entity.toFavouriteCategory(id) + val category = entity.toFavouriteCategory(id) + channels.createChannel(category) + return category } suspend fun renameCategory(id: Long, title: String) { db.favouriteCategoriesDao.updateTitle(id, title) + channels.renameChannel(id, title) } suspend fun removeCategory(id: Long) { db.favouriteCategoriesDao.delete(id) + channels.deleteChannel(id) } suspend fun setCategoryOrder(id: Long, order: SortOrder) { db.favouriteCategoriesDao.updateOrder(id, order.name) } + suspend fun setCategoryTracking(id: Long, isEnabled: Boolean) { + db.favouriteCategoriesDao.updateTracking(id, isEnabled) + } + suspend fun reorderCategories(orderedIds: List) { val dao = db.favouriteCategoriesDao db.withTransaction { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt index 5a2eaf8df..d2615c826 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt @@ -30,7 +30,8 @@ class CategoriesActivity : BaseActivity(), OnListItemClickListener, View.OnClickListener, - CategoriesEditDelegate.CategoriesEditCallback, AllCategoriesToggleListener { + CategoriesEditDelegate.CategoriesEditCallback, + AllCategoriesToggleListener { private val viewModel by viewModel() @@ -63,11 +64,12 @@ class CategoriesActivity : override fun onItemClick(item: FavouriteCategory, view: View) { val menu = PopupMenu(view.context, view) menu.inflate(R.menu.popup_category) - createOrderSubmenu(menu.menu, item) + prepareCategoryMenu(menu.menu, item) menu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.action_remove -> editDelegate.deleteCategory(item) R.id.action_rename -> editDelegate.renameCategory(item) + R.id.action_tracking -> viewModel.setCategoryTracking(item.id, !item.isTrackingEnabled) R.id.action_order -> return@setOnMenuItemClickListener false else -> { val order = SORT_ORDERS.getOrNull(menuItem.order) ?: return@setOnMenuItemClickListener false @@ -124,7 +126,7 @@ class CategoriesActivity : viewModel.createCategory(name) } - private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { + private fun prepareCategoryMenu(menu: Menu, category: FavouriteCategory) { val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return for ((i, item) in SORT_ORDERS.withIndex()) { val menuItem = submenu.add( @@ -137,6 +139,10 @@ class CategoriesActivity : menuItem.isChecked = item == category.order } submenu.setGroupCheckable(R.id.group_order, true, true) + menu.findItem(R.id.action_tracking)?.run { + isVisible = viewModel.isFavouritesTrackerEnabled + isChecked = category.isTrackingEnabled + } } private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 7aac74e62..9e84a9d89 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.lifecycle.viewModelScope +import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* @@ -11,7 +12,6 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* class FavouritesCategoriesViewModel( private val repository: FavouritesRepository, @@ -34,6 +34,9 @@ class FavouritesCategoriesViewModel( mapCategories(list, showAll, showAll) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val isFavouritesTrackerEnabled: Boolean + get() = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources + fun createCategory(name: String) { launchJob { repository.addCategory(name) @@ -58,6 +61,12 @@ class FavouritesCategoriesViewModel( } } + fun setCategoryTracking(id: Long, isEnabled: Boolean) { + launchJob { + repository.setCategoryTracking(id, isEnabled) + } + } + fun setAllCategoriesVisible(isVisible: Boolean) { settings.isAllFavouritesVisible = isVisible } diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt index 74fb0ef40..246cb3a5f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -8,6 +8,6 @@ import org.koitharu.kotatsu.history.ui.HistoryListViewModel val historyModule get() = module { - single { HistoryRepository(get(), get(), get()) } + factory { HistoryRepository(get(), get(), get()) } viewModel { HistoryListViewModel(get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt index 3366248a4..f8cb28657 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.local.ui.LocalListViewModel val localModule get() = module { - single { LocalStorageManager(androidContext(), get()) } + factory { LocalStorageManager(androidContext(), get()) } single { LocalMangaRepository(get()) } factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt index e8c69d824..c6e11107b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel val mainModule get() = module { single { AppProtectHelper(get()) } - single { ShortcutsRepository(androidContext(), get(), get(), get()) } + factory { ShortcutsRepository(androidContext(), get(), get(), get()) } viewModel { MainViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt index f27f061c6..c83ad608b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel val readerModule get() = module { - single { MangaDataRepository(get()) } + factory { MangaDataRepository(get()) } single { PagesCache(get()) } factory { PageSaveHelper(get(), androidContext()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt index 1f14c6ee3..1d1fb43fc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/SearchModule.kt @@ -13,8 +13,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel val searchModule get() = module { - single { MangaSearchRepository(get(), get(), androidContext(), get()) } - + factory { MangaSearchRepository(get(), get(), androidContext(), get()) } factory { MangaSuggestionsProvider.createSuggestions(androidContext()) } viewModel { params -> diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt index b8852f47e..ee15c796a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/NotificationSettingsLegacyFragment.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.settings import android.content.Context +import android.content.SharedPreferences import android.media.RingtoneManager import android.os.Bundle import android.view.View @@ -11,7 +12,9 @@ import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.utils.RingtonePickContract -class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notifications) { +class NotificationSettingsLegacyFragment : + BasePreferenceFragment(R.string.notifications), + SharedPreferences.OnSharedPreferenceChangeListener { private val ringtonePickContract = registerForActivityResult( RingtonePickContract(get().getString(R.string.notification_sound)) @@ -25,15 +28,28 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_notifications) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) findPreference(AppSettings.KEY_NOTIFICATIONS_SOUND)?.run { val uri = settings.notificationSound summary = RingtoneManager.getRingtone(context, uri)?.getTitle(context) ?: getString(R.string.silent) } + updateInfo() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateInfo() + } } override fun onPreferenceTreeClick(preference: Preference): Boolean { @@ -45,4 +61,9 @@ class NotificationSettingsLegacyFragment : BasePreferenceFragment(R.string.notif else -> super.onPreferenceTreeClick(preference) } } -} + + private fun updateInfo() { + findPreference(AppSettings.KEY_NOTIFICATIONS_INFO) + ?.isVisible = !settings.isTrackerNotificationsEnabled + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index b1fd14c50..230ab0d32 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -17,8 +17,8 @@ import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel val settingsModule get() = module { - single { BackupRepository(get()) } - single { RestoreRepository(get()) } + factory { BackupRepository(get()) } + factory { RestoreRepository(get()) } single(createdAtStart = true) { AppSettings(androidContext()) } viewModel { BackupViewModel(get(), androidContext()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt index 1e993f845..f1637def6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt @@ -1,21 +1,34 @@ package org.koitharu.kotatsu.settings import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import android.text.style.URLSpan +import android.view.View import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.preference.MultiSelectListPreference import androidx.preference.Preference +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider -import org.koitharu.kotatsu.tracker.work.TrackWorker +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels +import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_chapters) { +class TrackerSettingsFragment : + BasePreferenceFragment(R.string.check_for_new_chapters), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val repository by inject() + private val channels by inject() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_tracker) @@ -32,22 +45,81 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch } } } + updateCategoriesEnabled() + } + + override fun onResume() { + super.onResume() + updateCategoriesSummary() + updateNotificationsSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) { + when (key) { + AppSettings.KEY_TRACKER_NOTIFICATIONS -> updateNotificationsSummary() + AppSettings.KEY_TRACK_SOURCES, + AppSettings.KEY_TRACKER_ENABLED -> updateCategoriesEnabled() + } } override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { - AppSettings.KEY_NOTIFICATIONS_SETTINGS -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + AppSettings.KEY_NOTIFICATIONS_SETTINGS -> when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - .putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID) startActivity(intent) true - } else { + } + channels.areNotificationsDisabled -> { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", requireContext().packageName, null)) + startActivity(intent) + true + } + else -> { super.onPreferenceTreeClick(preference) } } + AppSettings.KEY_TRACK_CATEGORIES -> { + startActivity(CategoriesActivity.newIntent(preference.context)) + true + } else -> super.onPreferenceTreeClick(preference) } } + + private fun updateNotificationsSummary() { + val pref = findPreference(AppSettings.KEY_NOTIFICATIONS_SETTINGS) ?: return + pref.setSummary( + when { + channels.areNotificationsDisabled -> R.string.disabled + channels.isNotificationGroupEnabled() -> R.string.show_notification_new_chapters_on + else -> R.string.show_notification_new_chapters_off + } + ) + } + + private fun updateCategoriesEnabled() { + val pref = findPreference(AppSettings.KEY_TRACK_CATEGORIES) ?: return + pref.isEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources + } + + private fun updateCategoriesSummary() { + val pref = findPreference(AppSettings.KEY_TRACK_CATEGORIES) ?: return + viewLifecycleScope.launch { + val count = repository.getCategoriesCount() + pref.summary = getString(R.string.enabled_d_of_d, count[0], count[1]) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt index 278995dab..baa2a5217 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt @@ -51,7 +51,7 @@ class AppBackupAgent : BackupAgent() { } private fun createBackupFile() = runBlocking { - val repository = BackupRepository(MangaDatabase.create(applicationContext)) + val repository = BackupRepository(MangaDatabase(applicationContext)) BackupZipOutput(this@AppBackupAgent).use { backup -> backup.put(repository.createIndex()) backup.put(repository.dumpHistory()) @@ -63,7 +63,7 @@ class AppBackupAgent : BackupAgent() { } private fun restoreBackupFile(fd: FileDescriptor, size: Long) { - val repository = RestoreRepository(MangaDatabase.create(applicationContext)) + val repository = RestoreRepository(MangaDatabase(applicationContext)) val tempFile = File.createTempFile("backup_", ".tmp") FileInputStream(fd).use { input -> tempFile.outputStream().use { output -> diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt index a08495f54..975e96d66 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/TrackerModule.kt @@ -1,14 +1,17 @@ package org.koitharu.kotatsu.tracker +import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.ui.FeedViewModel +import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels val trackerModule get() = module { - single { TrackingRepository(get()) } + factory { TrackingRepository(get()) } + factory { TrackerNotificationChannels(androidContext(), get()) } viewModel { FeedViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 661f68bba..aefa9a69a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -2,15 +2,15 @@ package org.koitharu.kotatsu.tracker.domain import androidx.room.withTransaction import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.entity.TrackEntity -import org.koitharu.kotatsu.core.db.entity.TrackLogEntity -import org.koitharu.kotatsu.core.db.entity.toManga -import org.koitharu.kotatsu.core.db.entity.toTrackingLogItem +import org.koitharu.kotatsu.core.db.entity.* +import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.MangaTracking import org.koitharu.kotatsu.core.model.TrackingLogItem +import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.mapToSet import java.util.* class TrackingRepository( @@ -21,16 +21,29 @@ class TrackingRepository( return db.tracksDao.findNewChapters(mangaId) ?: 0 } - suspend fun getAllTracks(useFavourites: Boolean, useHistory: Boolean): List { - val mangaList = ArrayList() - if (useFavourites) { - db.favouritesDao.findAllManga().mapTo(mangaList) { it.toManga(emptySet()) } + suspend fun getHistoryManga(): List { + return db.historyDao.findAllManga().toMangaList() + } + + suspend fun getFavouritesManga(): Map> { + val categories = db.favouriteCategoriesDao.findAll() + return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity -> + categoryEntity.toFavouriteCategory() to db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList() } - if (useHistory) { - db.historyDao.findAllManga().mapTo(mangaList) { it.toManga(emptySet()) } - } - val tracks = db.tracksDao.findAll().groupBy { it.mangaId } - return mangaList + } + + suspend fun getCategoriesCount(): IntArray { + val categories = db.favouriteCategoriesDao.findAll() + return intArrayOf( + categories.count { it.track }, + categories.size, + ) + } + + suspend fun getTracks(mangaList: Collection): List { + val ids = mangaList.mapToSet { it.id } + val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } + return mangaList // TODO optimize .filterNot { it.source == MangaSource.LOCAL } .distinctBy { it.id } .map { manga -> @@ -103,4 +116,6 @@ class TrackingRepository( ) db.tracksDao.upsert(entity) } + + private fun Collection.toMangaList() = map { it.toManga(emptySet()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index a61b25c47..943572f24 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -5,7 +5,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.os.Build -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData @@ -41,26 +40,22 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : private val coil by inject() private val repository by inject() private val settings by inject() + private val channels by inject() override suspend fun doWork(): Result { - val trackSources = settings.trackSources - if (trackSources.isEmpty()) { - return Result.success() - } - val tracks = repository.getAllTracks( - useFavourites = AppSettings.TRACK_FAVOURITES in trackSources, - useHistory = AppSettings.TRACK_HISTORY in trackSources - ) - if (tracks.isEmpty()) { + if (!settings.isTrackerEnabled) { return Result.success() } if (TAG in tags) { // not expedited trySetForeground() } + val tracks = getAllTracks() + var success = 0 val workData = Data.Builder() .putInt(DATA_TOTAL, tracks.size) - for ((index, track) in tracks.withIndex()) { + for ((index, item) in tracks.withIndex()) { + val (track, channelId) = item val details = runCatching { MangaRepository(track.manga.source).getDetails(track.manga) }.getOrNull() @@ -80,12 +75,12 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { // manga was empty on last check repository.storeTrackResult( mangaId = track.manga.id, - knownChaptersCount = track.knownChaptersCount, + knownChaptersCount = 0, lastChapterId = 0L, previousTrackChapterId = track.lastNotifiedChapterId, newChapters = chapters ) - showNotification(details, chapters) + showNotification(details, channelId, chapters) } chapters.size == track.knownChaptersCount -> { if (chapters.lastOrNull()?.id == track.lastChapterId) { @@ -114,7 +109,8 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : ) showNotification( details, - newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId } + channelId, + newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId }, ) } } @@ -126,11 +122,12 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : knownChaptersCount = track.knownChaptersCount, lastChapterId = track.lastChapterId, previousTrackChapterId = track.lastNotifiedChapterId, - newChapters = newChapters + newChapters = newChapters, ) showNotification( - track.manga, - newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId } + manga = track.manga, + channelId = channelId, + newChapters = newChapters.takeLastWhile { x -> x.id != track.lastNotifiedChapterId }, ) } } @@ -144,13 +141,60 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : } } - private suspend fun showNotification(manga: Manga, newChapters: List) { - if (newChapters.isEmpty() || !settings.trackerNotifications) { + private suspend fun getAllTracks(): List { + val sources = settings.trackSources + if (sources.isEmpty()) { + return emptyList() + } + val knownIds = HashSet() + val result = ArrayList() + // Favourites + if (AppSettings.TRACK_FAVOURITES in sources) { + val favourites = repository.getFavouritesManga() + channels.updateChannels(favourites.keys) + for ((category, mangaList) in favourites) { + if (!category.isTrackingEnabled || mangaList.isEmpty()) { + continue + } + val categoryTracks = repository.getTracks(mangaList) + val channelId = if (channels.isFavouriteNotificationsEnabled(category)) { + channels.getFavouritesChannelId(category.id) + } else { + null + } + for (track in categoryTracks) { + if (knownIds.add(track.manga)) { + result.add(TrackingItem(track, channelId)) + } + } + } + } + // History + if (AppSettings.TRACK_HISTORY in sources) { + val history = repository.getHistoryManga() + val historyTracks = repository.getTracks(history) + val channelId = if (channels.isHistoryNotificationsEnabled()) { + channels.getHistoryChannelId() + } else { + null + } + for (track in historyTracks) { + if (knownIds.add(track.manga)) { + result.add(TrackingItem(track, channelId)) + } + } + } + result.trimToSize() + return result + } + + private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List) { + if (newChapters.isEmpty() || channelId == null) { return } val id = manga.url.hashCode() val colorPrimary = ContextCompat.getColor(applicationContext, R.color.blue_primary) - val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + val builder = NotificationCompat.Builder(applicationContext, channelId) val summary = applicationContext.resources.getQuantityString( R.plurals.new_chapters, newChapters.size, newChapters.size @@ -236,7 +280,6 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : companion object { - const val CHANNEL_ID = "tracking" private const val WORKER_CHANNEL_ID = "track_worker" private const val WORKER_NOTIFICATION_ID = 35 private const val DATA_PROGRESS = "progress" @@ -244,27 +287,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : private const val TAG = "tracking" private const val TAG_ONESHOT = "tracking_oneshot" - @RequiresApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(context: Context) { - val manager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (manager.getNotificationChannel(CHANNEL_ID) == null) { - val channel = NotificationChannel( - CHANNEL_ID, - context.getString(R.string.new_chapters), - NotificationManager.IMPORTANCE_DEFAULT - ) - channel.setShowBadge(true) - channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary_dark) - channel.enableLights(true) - manager.createNotificationChannel(channel) - } - } - fun setup(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(context) - } val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt new file mode 100644 index 000000000..81fcb73d5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackerNotificationChannels.kt @@ -0,0 +1,143 @@ +package org.koitharu.kotatsu.tracker.work + +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationManagerCompat +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.prefs.AppSettings + +class TrackerNotificationChannels( + private val context: Context, + private val settings: AppSettings, +) { + + private val manager = NotificationManagerCompat.from(context) + + val areNotificationsDisabled: Boolean + get() = !manager.areNotificationsEnabled() + + fun updateChannels(categories: Collection) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + manager.deleteNotificationChannel(OLD_CHANNEL_ID) + val group = createGroup() + val existingChannels = group.channels.associateByTo(HashMap()) { it.id } + for (category in categories) { + val id = getFavouritesChannelId(category.id) + if (existingChannels.remove(id)?.name == category.title) { + continue + } + val channel = NotificationChannel(id, category.title, NotificationManager.IMPORTANCE_DEFAULT) + channel.group = GROUP_ID + manager.createNotificationChannel(channel) + } + existingChannels.remove(CHANNEL_ID_HISTORY) + createHistoryChannel() + for (id in existingChannels.keys) { + manager.deleteNotificationChannel(id) + } + } + + fun createChannel(category: FavouriteCategory) { + renameChannel(category.id, category.title) + } + + fun renameChannel(categoryId: Long, name: String) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + val id = getFavouritesChannelId(categoryId) + val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT) + channel.group = createGroup().id + manager.createNotificationChannel(channel) + } + + fun deleteChannel(categoryId: Long) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + manager.deleteNotificationChannel(getFavouritesChannelId(categoryId)) + } + + fun isFavouriteNotificationsEnabled(category: FavouriteCategory): Boolean { + if (!manager.areNotificationsEnabled()) { + return false + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = manager.getNotificationChannel(getFavouritesChannelId(category.id)) + channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE + } else { + // fallback + settings.isTrackerNotificationsEnabled + } + } + + fun isHistoryNotificationsEnabled(): Boolean { + if (!manager.areNotificationsEnabled()) { + return false + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = manager.getNotificationChannel(getHistoryChannelId()) + channel != null && channel.importance != NotificationManager.IMPORTANCE_NONE + } else { + // fallback + settings.isTrackerNotificationsEnabled + } + } + + fun isNotificationGroupEnabled(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return settings.isTrackerNotificationsEnabled + } + val group = manager.getNotificationChannelGroup(GROUP_ID) ?: return true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && group.isBlocked) { + return false + } + return group.channels.any { it.importance != NotificationManagerCompat.IMPORTANCE_NONE } + } + + fun getFavouritesChannelId(categoryId: Long): String { + return CHANNEL_ID_PREFIX + categoryId + } + + fun getHistoryChannelId(): String { + return CHANNEL_ID_HISTORY + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createGroup(): NotificationChannelGroup { + manager.getNotificationChannelGroup(GROUP_ID)?.let { + return it + } + val group = NotificationChannelGroup(GROUP_ID, context.getString(R.string.new_chapters)) + manager.createNotificationChannelGroup(group) + return group + } + + private fun createHistoryChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + val channel = NotificationChannel( + CHANNEL_ID_HISTORY, + context.getString(R.string.history), + NotificationManager.IMPORTANCE_DEFAULT, + ) + channel.group = GROUP_ID + manager.createNotificationChannel(channel) + } + + companion object { + + const val GROUP_ID = "trackers" + private const val CHANNEL_ID_PREFIX = "track_fav_" + private const val CHANNEL_ID_HISTORY = "track_history" + private const val OLD_CHANNEL_ID = "tracking" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt new file mode 100644 index 000000000..933918009 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackingItem.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.tracker.work + +import org.koitharu.kotatsu.core.model.MangaTracking + +class TrackingItem( + val tracking: MangaTracking, + val channelId: String?, +) { + + operator fun component1() = tracking + + operator fun component2() = channelId + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TrackingItem + + if (tracking != other.tracking) return false + if (channelId != other.channelId) return false + + return true + } + + override fun hashCode(): Int { + var result = tracking.hashCode() + result = 31 * result + channelId.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml new file mode 100644 index 000000000..f21e8604e --- /dev/null +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/popup_category.xml b/app/src/main/res/menu/popup_category.xml index 1c4fc96d2..ee78b4b80 100644 --- a/app/src/main/res/menu/popup_category.xml +++ b/app/src/main/res/menu/popup_category.xml @@ -23,4 +23,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index cbd9de9a7..bada7be01 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -101,7 +101,6 @@ Паведамленні Уключана %1$d з %2$d Новыя главы - Апавяшчаць пра абнаўленні мангі, якую вы чытаеце Спампаваць Чытаць з пачатку Перазапусціць diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 42fb3a9ee..9bab88eb0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -46,7 +46,6 @@ Aktualisierungsfeed gelöscht Aktualisierungsfeed löschen Aktualisierungen - Über Aktualisierungen von Manga benachrichtigen, die du liest Benachrichtigung anzeigen, wenn eine Aktualisierung verfügbar ist Anwendungsaktualisierung ist verfügbar Automatisch nach Aktualisierungen suchen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9cde0edc3..ef974cffd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -101,7 +101,6 @@ Notificaciones Activado %1$d de %2$d Nuevos capítulos - Notificar sobre las actualizaciones del manga que estás leyendo Descargar Leer desde el principio Reiniciar diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 69f9c193e..40ee9d1b7 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -118,7 +118,6 @@ Käynnistä uudelleen Lue alusta Lataa - Ilmoita lukemastasi mangan päivityksistä Uusia lukuja Käytössä %1$d / %2$d Ilmoitukset diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 958794036..720c59b4a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -112,7 +112,6 @@ Paramètres des notifications Lire depuis le début Télécharger - Avertir des mises à jour des mangas que vous lisez Nouveaux chapitres %1$d de %2$d activé(s) Notifications diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e6196111b..683112a44 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -154,7 +154,6 @@ Riavvia Leggi dall\'inizio Scarica - Notifica gli aggiornamenti dei manga che stai leggendo Nuovi capitoli Abilitato %1$d di %2$d Notifiche diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d6db95de8..8f9ab14f1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -143,7 +143,6 @@ 検索履歴をクリア 外部ストレージ Kotatsuの新しい更新が利用可能です - あなたが読んでいる漫画の更新について通知 ここは空っぽです… カテゴリーを使用してお気に入りを整理できます。 «+»を押してカテゴリーを作成出来ます 空のカテゴリー diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 74c07b980..41444062e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -54,7 +54,6 @@ Fjern «%s»-kategorien fra favorittene\? \nAlle mangaer i den vil bli tapt. Programomstart - Gi merknad om oppdateringer av det du leser %1$d av %2$d påskrudd Denne mangaen har %s. Lagre hele\? Åpne i nettleser diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 32910e0a3..9e654146f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -232,7 +232,6 @@ Limpar cache de miniaturas Verifique se há novas versões do aplicativo Categorias favoritas - Notificar sobre atualizações de mangá que você está lendo Remover a categoria “%s” dos seus favoritos\? \nTodos os mangás nela serão perdidos. Nenhuma atualização disponível diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b23edc825..d3794d77a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -83,7 +83,6 @@ Salve Notificações Novos capítulos - Notifique sobre atualizações do mangá que está lendo Download Ler desde o início Reiniciar diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 41dad2021..87da7d5d0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -101,7 +101,6 @@ Уведомления Включено %1$d из %2$d Новые главы - Уведомлять об обновлении манги, которую Вы читаете Загрузить Читать с начала Перезапустить @@ -278,4 +277,8 @@ Главы будут удалены в фоновом режиме. Это может занять какое-то время Скрыть Доступны новые источники манги + Проверять новые главы и уведомлять о них + Вы будете получать уведомления об обновлении манги, которую Вы читаете + Вы не будете получать уведомления, но новые главы будут отображаться в списке + Включить уведомления \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 76cfa061a..264ad6c1c 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -105,7 +105,6 @@ Ladda ned Aviseringsinställningar LED-indikator - Avisera om uppdateringar på manga du läser Läs från början Starta om Aviseringsljud diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ff48edbc1..74d931aab 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -234,7 +234,6 @@ Öntanımlı Bir ad girmelisiniz %s üzerinde oturum açma desteklenmiyor - Okunan manga güncellemeleri hakkında bildirimde bulun Daha fazla oku Bazı aygıtların arka plan görevlerini bozabilecek farklı sistem davranışları vardır. Ekran görüntüsü politikası diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 692597cee..083717035 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,7 +103,6 @@ Notifications %1$d of %2$d on New chapters - Notify about updates of manga you are reading Download Read from start Restart @@ -281,4 +280,8 @@ Chapters will be removed in the background. It can take some time Hide New manga sources are available + Check for new chapters and notify about it + You will receive notifications about updates of manga you are reading + You will not receive notifications but new chapters will be highlighted in the lists + Enable notifications \ No newline at end of file diff --git a/app/src/main/res/xml/pref_notifications.xml b/app/src/main/res/xml/pref_notifications.xml index b3c349d72..c3be1d40d 100644 --- a/app/src/main/res/xml/pref_notifications.xml +++ b/app/src/main/res/xml/pref_notifications.xml @@ -1,19 +1,38 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_suggestions.xml b/app/src/main/res/xml/pref_suggestions.xml index 224185a67..b9712a556 100644 --- a/app/src/main/res/xml/pref_suggestions.xml +++ b/app/src/main/res/xml/pref_suggestions.xml @@ -6,7 +6,7 @@ + + - + Date: Wed, 4 May 2022 23:00:44 +0300 Subject: [PATCH 05/45] Fix `FavouriteCategoriesDialog` toolbar in album orientation --- app/src/main/res/layout/dialog_favorite_categories.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/dialog_favorite_categories.xml b/app/src/main/res/layout/dialog_favorite_categories.xml index 2085c72f7..0acdd440e 100644 --- a/app/src/main/res/layout/dialog_favorite_categories.xml +++ b/app/src/main/res/layout/dialog_favorite_categories.xml @@ -10,7 +10,7 @@ From 9166716f2a693d60ebd1bf921732fbaf48268de8 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 15:21:10 +0300 Subject: [PATCH 06/45] Update version --- app/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ff599eb34..b04edbd11 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 405 - versionName '3.2.1' + versionCode 406 + versionName '3.2.2' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -86,7 +86,7 @@ dependencies { implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' - implementation 'com.google.android.material:material:1.6.0-rc01' + implementation 'com.google.android.material:material:1.6.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' @@ -105,7 +105,7 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' testImplementation 'junit:junit:4.13.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' From c695468aeca80029b0147902bd926613e313fccd Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 15:43:07 +0300 Subject: [PATCH 07/45] Fix cold launch --- .../koitharu/kotatsu/main/ui/MainActivity.kt | 20 ++++----- .../kotatsu/settings/AppUpdateChecker.kt | 43 +++++++++---------- .../settings/onboard/OnboardDialogFragment.kt | 6 ++- .../settings/onboard/OnboardViewModel.kt | 3 +- 4 files changed, 35 insertions(+), 37 deletions(-) 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 24805aa84..0e8c85fb9 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 @@ -22,7 +22,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -385,20 +384,19 @@ class MainActivity : } private fun onFirstStart() { - lifecycleScope.launch(Dispatchers.Default) { - TrackWorker.setup(applicationContext) - SuggestionsWorker.setup(applicationContext) - if (AppUpdateChecker.isUpdateSupported(this@MainActivity)) { + lifecycleScope.launchWhenResumed { + val isUpdateSupported = withContext(Dispatchers.Default) { + TrackWorker.setup(applicationContext) + SuggestionsWorker.setup(applicationContext) + AppUpdateChecker.isUpdateSupported(this@MainActivity) + } + if (isUpdateSupported) { AppUpdateChecker(this@MainActivity).checkIfNeeded() } val settings = get() when { - !settings.isSourcesSelected -> withContext(Dispatchers.Main) { - OnboardDialogFragment.showWelcome(supportFragmentManager) - } - settings.newSources.isNotEmpty() -> withContext(Dispatchers.Main) { - NewSourcesDialogFragment.show(supportFragmentManager) - } + !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager) + settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt index 12d7f21ce..5b7bc661e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt @@ -8,6 +8,15 @@ import android.net.Uri import androidx.activity.ComponentActivity import androidx.annotation.MainThread import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.android.ext.android.get @@ -19,15 +28,6 @@ import org.koitharu.kotatsu.core.github.VersionId import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.utils.FileSize -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.security.cert.CertificateEncodingException -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit class AppUpdateChecker(private val activity: ComponentActivity) { @@ -61,25 +61,22 @@ class AppUpdateChecker(private val activity: ComponentActivity) { @MainThread private fun showUpdateDialog(version: AppVersion) { + val message = buildString { + append(activity.getString(R.string.new_version_s, version.name)) + appendLine() + append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize))) + appendLine() + appendLine() + append(version.description) + } MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_update_available) - .setMessage(buildString { - append(activity.getString(R.string.new_version_s, version.name)) - appendLine() - append( - activity.getString( - R.string.size_s, - FileSize.BYTES.format(activity, version.apkSize), - ) - ) - appendLine() - appendLine() - append(version.description) - }) + .setMessage(message) .setPositiveButton(R.string.download) { _, _ -> activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl))) } .setNegativeButton(R.string.close, null) + .setCancelable(false) .create() .show() } @@ -128,4 +125,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt index 92fee1db9..4f695b154 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -18,8 +18,10 @@ import org.koitharu.kotatsu.utils.ext.observeNotNull import org.koitharu.kotatsu.utils.ext.showAllowStateLoss import org.koitharu.kotatsu.utils.ext.withArgs -class OnboardDialogFragment : AlertDialogFragment(), - OnListItemClickListener, DialogInterface.OnClickListener { +class OnboardDialogFragment : + AlertDialogFragment(), + OnListItemClickListener, + DialogInterface.OnClickListener { private val viewModel by viewModel() private var isWelcome: Boolean = false diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt index f2a929017..2f1495243 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.onboard import androidx.collection.ArraySet import androidx.core.os.LocaleListCompat import androidx.lifecycle.MutableLiveData -import java.util.* import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaSource @@ -12,6 +11,7 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.settings.onboard.model.SourceLocale import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.mapToSet +import java.util.* class OnboardViewModel( private val settings: AppSettings, @@ -55,6 +55,7 @@ class OnboardViewModel( settings.hiddenSources = allSources.filterNot { x -> x.locale in selectedLocales }.mapToSet { x -> x.name } + settings.markKnownSources(settings.newSources) } private fun rebuildList() { From fdd4f5abcaee4d2a420550d4bc18b71ead5f5105 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 15:51:30 +0300 Subject: [PATCH 08/45] Fix bottom sheet navbar color --- app/src/main/res/values-v27/styles.xml | 7 +++++++ app/src/main/res/values/styles.xml | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/values-v27/styles.xml diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 000000000..0f361cb7f --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2333f5d68..772a54b02 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,7 +23,6 @@ From 23e7aa2aaa80ac848837a4fb0b7d4f3a5d9929e6 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 15:57:05 +0300 Subject: [PATCH 09/45] Fix images scale type --- .../org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt | 2 ++ .../koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt | 2 ++ .../org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt | 2 ++ .../java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt | 2 ++ 4 files changed, 8 insertions(+) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index ced1697b0..c7196e37c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable +import coil.size.Scale import coil.util.CoilUtils import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding @@ -43,6 +44,7 @@ fun mangaGridItemAD( .fallback(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder) .allowRgb565(true) + .scale(Scale.FILL) .lifecycle(lifecycleOwner) .enqueueWith(coil) badge = itemView.bindBadge(badge, item.counter) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 10e9a473d..b265c0442 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable +import coil.size.Scale import coil.util.CoilUtils import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding @@ -44,6 +45,7 @@ fun mangaListDetailedItemAD( .placeholder(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder) + .scale(Scale.FILL) .allowRgb565(true) .lifecycle(lifecycleOwner) .enqueueWith(coil) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 18696de6b..19dc24779 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable +import coil.size.Scale import coil.util.CoilUtils import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding @@ -44,6 +45,7 @@ fun mangaListItemAD( .placeholder(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder) + .scale(Scale.FILL) .allowRgb565(true) .lifecycle(lifecycleOwner) .enqueueWith(coil) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt index 74e5468b9..000ff5399 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/adapter/FeedItemAD.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.tracker.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable +import coil.size.Scale import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener @@ -34,6 +35,7 @@ fun feedItemAD( .fallback(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder) .allowRgb565(true) + .scale(Scale.FILL) .lifecycle(lifecycleOwner) .enqueueWith(coil) binding.textViewTitle.text = item.title From b759f8d0a0ac02e75c65d9acda031c6c4e3616a1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 May 2022 16:46:44 +0300 Subject: [PATCH 10/45] Fix pages filename #151 --- .../kotatsu/base/domain/MangaUtils.kt | 9 ++++ .../kotatsu/reader/ui/PageSaveHelper.kt | 47 +++++++++++++------ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt index 09a970eee..03b0dd53b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.medianOrNull +import java.io.File import java.io.InputStream import java.util.zip.ZipFile @@ -59,6 +60,14 @@ object MangaUtils : KoinComponent { } } + suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) { + val options = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeFile(file.path, options)?.recycle() + options.outMimeType + } + private fun getBitmapSize(input: InputStream?): Size { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt index a2d8f7ac5..3e19c7036 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/PageSaveHelper.kt @@ -2,19 +2,26 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context import android.net.Uri +import android.webkit.MimeTypeMap import androidx.activity.result.ActivityResultLauncher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrl import okio.IOException +import org.koitharu.kotatsu.base.domain.MangaUtils import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.util.toFileNameSafe import org.koitharu.kotatsu.reader.domain.PageLoader +import java.io.File import kotlin.coroutines.Continuation -import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume +private const val MAX_FILENAME_LENGTH = 10 +private const val EXTENSION_FALLBACK = "png" + class PageSaveHelper( private val cache: PagesCache, context: Context, @@ -28,22 +35,17 @@ class PageSaveHelper( page: MangaPage, saveLauncher: ActivityResultLauncher, ): Uri { - var pageFile = cache[page.url] - var fileName = pageFile?.name - if (fileName == null) { - fileName = pageLoader.getPageUrl(page).toHttpUrl().pathSegments.last() - } - val cc = coroutineContext - val destination = suspendCancellableCoroutine { cont -> - continuation = cont - Dispatchers.Main.dispatch(cc) { - saveLauncher.launch(fileName) + val pageUrl = pageLoader.getPageUrl(page) + val pageFile = pageLoader.loadPage(page, force = false) + val proposedName = getProposedFileName(pageUrl, pageFile) + val destination = withContext(Dispatchers.Main) { + suspendCancellableCoroutine { cont -> + continuation = cont + saveLauncher.launch(proposedName) + }.also { + continuation = null } } - continuation = null - if (pageFile == null) { - pageFile = pageLoader.loadPage(page, force = false) - } runInterruptible(Dispatchers.IO) { contentResolver.openOutputStream(destination)?.use { output -> pageFile.inputStream().use { input -> @@ -57,4 +59,19 @@ class PageSaveHelper( fun onActivityResult(uri: Uri): Boolean = continuation?.apply { resume(uri) } != null + + private suspend fun getProposedFileName(url: String, file: File): String { + var name = url.toHttpUrl().pathSegments.last() + var extension = name.substringAfterLast('.', "") + name = name.substringBeforeLast('.') + if (extension.length !in 2..4) { + val mimeType = MangaUtils.getImageMimeType(file) + extension = if (mimeType != null) { + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK + } else { + EXTENSION_FALLBACK + } + } + return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension + } } \ No newline at end of file From 878df24a642a8e9ff2aacdcad482b2c6814b26ff Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 6 May 2022 10:52:51 +0300 Subject: [PATCH 11/45] Add voice search --- .../koitharu/kotatsu/main/ui/MainActivity.kt | 44 +++++++++++++- .../ui/suggestion/SearchSuggestionListener.kt | 2 + .../search/ui/widget/SearchEditText.kt | 57 +++++++++++++++---- .../kotatsu/utils/VoiceInputContract.kt | 26 +++++++++ .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 19 ++++++- app/src/main/res/drawable/ic_voice_input.xml | 9 +++ .../res/layout-w720dp-land/activity_main.xml | 3 +- app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/values/styles.xml | 1 + 9 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt create mode 100644 app/src/main/res/drawable/ic_voice_input.xml 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 24805aa84..1d635173d 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 @@ -7,8 +7,10 @@ import android.os.Bundle import android.view.MenuItem import android.view.View import android.view.ViewGroup.MarginLayoutParams +import androidx.activity.result.ActivityResultCallback import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.view.ActionMode +import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.core.graphics.Insets import androidx.core.view.* @@ -17,7 +19,10 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope +import androidx.transition.TransitionManager +import com.google.android.material.R as materialR import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.appbar.AppBarLayout.LayoutParams.* import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar @@ -55,8 +60,8 @@ import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker +import org.koitharu.kotatsu.utils.VoiceInputContract import org.koitharu.kotatsu.utils.ext.* -import com.google.android.material.R as materialR private const val TAG_PRIMARY = "primary" private const val TAG_SEARCH = "search" @@ -75,6 +80,7 @@ class MainActivity : private lateinit var navHeaderBinding: NavigationHeaderBinding private var drawerToggle: ActionBarDrawerToggle? = null private var drawer: DrawerLayout? = null + private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback()) override val appBar: AppBarLayout get() = binding.appbar @@ -119,6 +125,7 @@ class MainActivity : } binding.fab.setOnClickListener(this@MainActivity) + binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let { if (it is HistoryListFragment) binding.fab.show() else binding.fab.hide() @@ -277,6 +284,19 @@ class MainActivity : searchSuggestionViewModel.onQueryChanged(query) } + override fun onVoiceSearchClick() { + val options = binding.searchView.drawableEnd?.bounds?.let { bounds -> + ActivityOptionsCompat.makeScaleUpAnimation( + binding.searchView, + bounds.centerX(), + bounds.centerY(), + bounds.width(), + bounds.height(), + ) + } + voiceInputLauncher.tryLaunch(binding.searchView.hint?.toString(), options) + } + override fun onClearSearchHistory() { MaterialAlertDialogBuilder(this) .setTitle(R.string.clear_search_history) @@ -373,13 +393,26 @@ class MainActivity : } private fun onSearchOpened() { + TransitionManager.beginDelayedTransition(binding.appbar) drawerToggle?.isDrawerIndicatorEnabled = false + binding.toolbarCard.updateLayoutParams { + scrollFlags = SCROLL_FLAG_NO_SCROLL + } + binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant)) + binding.appbar.updatePadding(left = 0, right = 0) adjustDrawerLock() adjustFabVisibility(isSearchOpened = true) } private fun onSearchClosed() { + TransitionManager.beginDelayedTransition(binding.appbar) drawerToggle?.isDrawerIndicatorEnabled = true + binding.toolbarCard.updateLayoutParams { + scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS + } + binding.appbar.background = null + val padding = resources.getDimensionPixelOffset(R.dimen.margin_normal) + binding.appbar.updatePadding(left = padding, right = padding) adjustDrawerLock() adjustFabVisibility(isSearchOpened = false) } @@ -427,4 +460,13 @@ class MainActivity : if (isLocked) DrawerLayout.LOCK_MODE_LOCKED_CLOSED else DrawerLayout.LOCK_MODE_UNLOCKED ) } + + private inner class VoiceInputCallback : ActivityResultCallback { + + override fun onActivityResult(result: String?) { + if (result != null) { + binding.searchView.query = result + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt index 9a942009b..ea9dfd6f2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionListener.kt @@ -14,4 +14,6 @@ interface SearchSuggestionListener { fun onClearSearchHistory() fun onTagClick(tag: MangaTag) + + fun onVoiceSearchClick() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt index 07e24ca16..261648fce 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt @@ -2,15 +2,20 @@ package org.koitharu.kotatsu.search.ui.widget import android.annotation.SuppressLint import android.content.Context +import android.os.Parcelable import android.util.AttributeSet import android.view.KeyEvent import android.view.MotionEvent +import android.view.SoundEffectConstants +import android.view.accessibility.AccessibilityEvent import android.view.inputmethod.EditorInfo import androidx.annotation.AttrRes import androidx.appcompat.widget.AppCompatEditText import androidx.core.content.ContextCompat -import com.google.android.material.R +import com.google.android.material.R as materialR +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.utils.ext.drawableEnd import org.koitharu.kotatsu.utils.ext.drawableStart private const val DRAWABLE_END = 2 @@ -18,11 +23,19 @@ private const val DRAWABLE_END = 2 class SearchEditText @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = R.attr.editTextStyle, + @AttrRes defStyleAttr: Int = materialR.attr.editTextStyle, ) : AppCompatEditText(context, attrs, defStyleAttr) { var searchSuggestionListener: SearchSuggestionListener? = null - private val clearIcon = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_material) + private val clearIcon = ContextCompat.getDrawable(context, materialR.drawable.abc_ic_clear_material) + private val voiceIcon = ContextCompat.getDrawable(context, R.drawable.ic_voice_input) + private var isEmpty = text.isNullOrEmpty() + + var isVoiceSearchEnabled: Boolean = false + set(value) { + field = value + updateActionIcon() + } var query: String get() = text?.trim()?.toString().orEmpty() @@ -57,15 +70,19 @@ class SearchEditText @JvmOverloads constructor( lengthAfter: Int, ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) - setCompoundDrawablesRelativeWithIntrinsicBounds( - drawableStart, - null, - if (text.isNullOrEmpty()) null else clearIcon, - null, - ) + val empty = text.isNullOrEmpty() + if (isEmpty != empty) { + isEmpty = empty + updateActionIcon() + } searchSuggestionListener?.onQueryChanged(query) } + override fun onRestoreInstanceState(state: Parcelable?) { + super.onRestoreInstanceState(state) + updateActionIcon() + } + @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_UP) { @@ -76,7 +93,9 @@ class SearchEditText @JvmOverloads constructor( event.x.toInt() in (width - drawable.bounds.width() - paddingRight)..(width - paddingRight) } if (isOnDrawable) { - text?.clear() + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED) + playSoundEffect(SoundEffectConstants.CLICK) + onActionIconClick() return true } } @@ -87,4 +106,22 @@ class SearchEditText @JvmOverloads constructor( super.clearFocus() text?.clear() } + + private fun onActionIconClick() { + when { + !text.isNullOrEmpty() -> text?.clear() + isVoiceSearchEnabled -> searchSuggestionListener?.onVoiceSearchClick() + } + } + + private fun updateActionIcon() { + val icon = when { + !text.isNullOrEmpty() -> clearIcon + isVoiceSearchEnabled -> voiceIcon + else -> null + } + if (icon !== drawableEnd) { + setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, icon, null) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt b/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt new file mode 100644 index 000000000..e95e0fb96 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt @@ -0,0 +1,26 @@ +package org.koitharu.kotatsu.utils + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.speech.RecognizerIntent +import androidx.activity.result.contract.ActivityResultContract + +class VoiceInputContract : ActivityResultContract() { + + override fun createIntent(context: Context, input: String?): Intent { + val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + intent.putExtra(RecognizerIntent.EXTRA_PROMPT, input) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): String? { + return if (resultCode == Activity.RESULT_OK && intent != null) { + val matches = intent.getStringArrayExtra(RecognizerIntent.EXTRA_RESULTS) + matches?.firstOrNull() + } else { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 733bf17d4..6f15f7cd3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -1,14 +1,17 @@ package org.koitharu.kotatsu.utils.ext import android.content.Context +import android.content.pm.ResolveInfo import android.net.ConnectivityManager import android.net.Network import android.net.NetworkRequest import android.net.Uri import android.os.Build +import androidx.activity.result.ActivityResultLauncher +import androidx.core.app.ActivityOptionsCompat import androidx.work.CoroutineWorker -import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume val Context.connectivityManager: ConnectivityManager get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager @@ -40,4 +43,16 @@ fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching { val info = getForegroundInfo() setForeground(info) -}.isSuccess \ No newline at end of file +}.isSuccess + +fun ActivityResultLauncher.resolve(context: Context, input: I): ResolveInfo? { + val pm = context.packageManager + val intent = contract.createIntent(context, input) + return pm.resolveActivity(intent, 0) +} + +fun ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCompat? = null): Boolean { + return runCatching { + launch(input, options) + }.isSuccess +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_voice_input.xml b/app/src/main/res/drawable/ic_voice_input.xml new file mode 100644 index 000000000..ab46188aa --- /dev/null +++ b/app/src/main/res/drawable/ic_voice_input.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w720dp-land/activity_main.xml b/app/src/main/res/layout-w720dp-land/activity_main.xml index 80a34cfb5..ee362d397 100644 --- a/app/src/main/res/layout-w720dp-land/activity_main.xml +++ b/app/src/main/res/layout-w720dp-land/activity_main.xml @@ -31,8 +31,9 @@ android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp" android:background="@null" + android:clipToPadding="false" + android:paddingLeft="16dp" android:paddingRight="16dp" app:elevation="0dp" app:liftOnScroll="false"> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1366af2c4..43361b15b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -23,6 +23,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@null" + android:clipToPadding="false" android:paddingLeft="16dp" android:paddingRight="16dp" android:stateListAnimator="@null"> @@ -56,7 +57,6 @@ android:hint="@string/search_manga" android:imeOptions="actionSearch" android:importantForAutofill="no" - android:paddingBottom="1dp" android:singleLine="true" tools:drawableEnd="@drawable/abc_ic_clear_material" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2333f5d68..a1efd039d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -39,6 +39,7 @@ - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 772a54b02..2333f5d68 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,6 +23,7 @@ From 2709d40fc0a7ce4ff4624529a354e5f54a0e79a1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 6 May 2022 13:51:55 +0300 Subject: [PATCH 13/45] Fix BottomSheet edge-to-edge mode --- .idea/deploymentTargetDropDown.xml | 21 ++++++++++++++ .../kotatsu/base/ui/BaseBottomSheet.kt | 7 +++-- .../base/ui/dialog/AppBottomSheetDialog.kt | 29 +++++++++++++++++++ .../utils/BottomSheetToolbarController.kt | 5 ++-- app/src/main/res/values-v27/styles.xml | 7 +++++ app/src/main/res/values/styles.xml | 4 +-- 6 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt create mode 100644 app/src/main/res/values-v27/styles.xml diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 27370aa28..4b6850667 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -13,5 +13,26 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt index 0672b880f..75503afc5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseBottomSheet.kt @@ -9,11 +9,12 @@ import android.view.ViewGroup.LayoutParams import androidx.appcompat.app.AppCompatDialog import androidx.core.view.updateLayoutParams import androidx.viewbinding.ViewBinding -import com.google.android.material.R as materialR import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog +import com.google.android.material.R as materialR abstract class BaseBottomSheet : BottomSheetDialogFragment() { @@ -43,7 +44,9 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return if (resources.getBoolean(R.bool.is_tablet)) { AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog) - } else super.onCreateDialog(savedInstanceState) + } else { + AppBottomSheetDialog(requireContext(), theme) + } } protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt new file mode 100644 index 000000000..d3b911ace --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/dialog/AppBottomSheetDialog.kt @@ -0,0 +1,29 @@ +package org.koitharu.kotatsu.base.ui.dialog + +import android.content.Context +import android.graphics.Color +import android.view.View +import com.google.android.material.bottomsheet.BottomSheetDialog + +class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) { + + /** + * https://github.com/material-components/material-components-android/issues/2582 + */ + @Suppress("DEPRECATION") + override fun onAttachedToWindow() { + val window = window + val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0 + super.onAttachedToWindow() + if (window != null) { + // If the navigation bar is translucent at all, the BottomSheet should be edge to edge + val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF + if (drawEdgeToEdge) { + // Copied from super.onAttachedToWindow: + val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + // Fix super-class's window flag bug by respecting the intial system UI visibility: + window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt b/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt index 17d62d3a3..ae40b41f6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/BottomSheetToolbarController.kt @@ -2,15 +2,16 @@ package org.koitharu.kotatsu.utils import android.view.View import androidx.appcompat.widget.Toolbar -import com.google.android.material.R as materialR import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.R as materialR open class BottomSheetToolbarController( protected val toolbar: Toolbar, ) : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_EXPANDED) { + val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && bottomSheet.top <= 0 + if (isExpanded) { toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material) } else { toolbar.navigationIcon = null diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 000000000..88dc7707b --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2333f5d68..d424cf260 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -22,8 +22,8 @@ - From de9c1017b3f7a7522c74fa84796dfd2f9e384f4c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 6 May 2022 15:45:20 +0300 Subject: [PATCH 14/45] Update parsers --- .gitignore | 1 + .idea/deploymentTargetDropDown.xml | 38 ------------------------------ app/build.gradle | 2 +- 3 files changed, 2 insertions(+), 39 deletions(-) delete mode 100644 .idea/deploymentTargetDropDown.xml diff --git a/.gitignore b/.gitignore index d4fcf16ce..3ba4daee9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml /.idea/kotlinScripting.xml +/.idea/deploymentTargetDropDown.xml .DS_Store /build /captures diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 4b6850667..000000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b04edbd11..e899f7b87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,7 +65,7 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:090ad4b256') { + implementation('com.github.nv95:kotatsu-parsers:b495e5e457') { exclude group: 'org.json', module: 'json' } From a40322b2e7f86068a7a34d6bbea042f6449ed2c2 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 6 May 2022 16:53:55 +0300 Subject: [PATCH 15/45] Fix crash on first database initialization --- .../koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt index 35c52192c..6257a8456 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabasePrePopulateCallback.kt @@ -10,8 +10,8 @@ class DatabasePrePopulateCallback(private val resources: Resources) : RoomDataba override fun onCreate(db: SupportSQLiteDatabase) { db.execSQL( - "INSERT INTO favourite_categories (created_at, sort_key, title, `order`) VALUES (?,?,?,?)", - arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name) + "INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track) VALUES (?,?,?,?,?)", + arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later), SortOrder.NEWEST.name, 1) ) } } \ No newline at end of file From 400a2b14f7b375186d357238f195b53c81a91c47 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 6 May 2022 16:54:41 +0300 Subject: [PATCH 16/45] Change a bit `preference_toggle_header` view --- .../res/layout/preference_toggle_header.xml | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index f21e8604e..b1f816337 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -1,18 +1,22 @@ - + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="8dp" + app:cardCornerRadius="24dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:padding="16dp"> + android:textAppearance="@style/TextAppearance.Material3.TitleLarge" + android:textSize="20sp" + tools:text="Title"/> + android:textAppearance="@style/TextAppearance.Material3.TitleSmall" + tools:text="Subtitle"/> @@ -61,4 +67,4 @@ - + From fa150e45ff65b2f935cb04685b7e5391fc92bd59 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 6 May 2022 20:15:29 +0300 Subject: [PATCH 17/45] [Issue template] Update version --- .github/ISSUE_TEMPLATE/report_issue.yml | 4 ++-- .github/ISSUE_TEMPLATE/request_feature.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index 69983d414..97aa936c3 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.2" + Example: "3.2.2" validations: required: true @@ -87,7 +87,7 @@ 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/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.2.2](https://github.com/nv95/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 a46a0648f..2077efe27 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ 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/nv95/kotatsu-parsers/issues/new). required: true - - label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**. + - label: I have updated the app to version **[3.2.2](https://github.com/nv95/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 From 930819ffa2084eed7288f4ca29f4bfcc481787a4 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Fri, 6 May 2022 20:59:43 +0300 Subject: [PATCH 18/45] Fix setting tracker on favourites screen --- .../kotatsu/favourites/ui/FavouritesContainerFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 4433f978d..45cbba17a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -150,6 +150,10 @@ class FavouritesContainerFragment : menuItem.isChecked = item == category.order } submenu.setGroupCheckable(R.id.group_order, true, true) + menu.findItem(R.id.action_tracking)?.run { + isVisible = viewModel.isFavouritesTrackerEnabled + isChecked = category.isTrackingEnabled + } } private fun TabLayout.setTabsEnabled(enabled: Boolean) { @@ -168,6 +172,7 @@ class FavouritesContainerFragment : R.id.action_remove -> editDelegate.deleteCategory(category) R.id.action_rename -> editDelegate.renameCategory(category) R.id.action_create -> editDelegate.createCategory() + R.id.action_tracking -> viewModel.setCategoryTracking(category.id, !category.isTrackingEnabled) R.id.action_order -> return@setOnMenuItemClickListener false else -> { val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order) From 6405523232e25ecb2ea1cfb5c92b13b943d0ac67 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 7 May 2022 08:50:33 +0300 Subject: [PATCH 19/45] Fix CategoryListModel equals/hashcode --- .../kotatsu/favourites/ui/categories/CategoriesAdapter.kt | 5 ++++- .../favourites/ui/categories/adapter/CategoryListModel.kt | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt index e13b31e00..7a5620158 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt @@ -40,7 +40,10 @@ class CategoriesAdapter( newItem: CategoryListModel, ): Any? = when { oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> Unit - else -> super.getChangePayload(oldItem, newItem) + oldItem is CategoryListModel.CategoryItem && + newItem is CategoryListModel.CategoryItem && + oldItem.category.title != newItem.category.title -> null + else -> Unit } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt index 8326f8617..899b73e1c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryListModel.kt @@ -45,6 +45,7 @@ sealed interface CategoryListModel : ListModel { if (category.id != other.category.id) return false if (category.title != other.category.title) return false if (category.order != other.category.order) return false + if (category.isTrackingEnabled != other.category.isTrackingEnabled) return false return true } @@ -53,6 +54,7 @@ sealed interface CategoryListModel : ListModel { var result = category.id.hashCode() result = 31 * result + category.title.hashCode() result = 31 * result + category.order.hashCode() + result = 31 * result + category.isTrackingEnabled.hashCode() return result } } From 46fe2bb8ac1085ad2566588560abd1a2547b83f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Thu, 5 May 2022 16:12:41 +0200 Subject: [PATCH 20/45] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (281 of 281 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ff48edbc1..2f2946ebf 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -278,4 +278,5 @@ IP adresinizin engellenmesinden kaçınmanıza yardımcı olur Kaydedilen manga işleme Gizle + Yeni manga kaynakları var \ No newline at end of file From 9e706ea0961d2a8bea3caa93b911055ea0061e48 Mon Sep 17 00:00:00 2001 From: kuragehime Date: Thu, 5 May 2022 16:12:41 +0200 Subject: [PATCH 21/45] Translated using Weblate (Japanese) Currently translated at 100.0% (281 of 281 strings) Co-authored-by: kuragehime Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/ Translation: Kotatsu/Strings --- app/src/main/res/values-ja/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d6db95de8..99fb80a84 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -278,4 +278,5 @@ 並列ダウンロード チャプターはバックグラウンドで削除されます。時間がかかる場合があります 隠す + 新しいマンガソースが利用可能になりました \ No newline at end of file From 4d5d25834ecc77fa2e8154951d26eba657e2df71 Mon Sep 17 00:00:00 2001 From: Dpper Date: Thu, 5 May 2022 16:12:42 +0200 Subject: [PATCH 22/45] Translated using Weblate (Ukrainian) Currently translated at 100.0% (281 of 281 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Added translation using Weblate (Ukrainian) Added translation using Weblate (Ukrainian) Co-authored-by: Dpper Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/ Translation: Kotatsu/Strings Translation: Kotatsu/plurals --- app/src/main/res/values-uk/plurals.xml | 51 +++++ app/src/main/res/values-uk/strings.xml | 282 +++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 app/src/main/res/values-uk/plurals.xml create mode 100644 app/src/main/res/values-uk/strings.xml diff --git a/app/src/main/res/values-uk/plurals.xml b/app/src/main/res/values-uk/plurals.xml new file mode 100644 index 000000000..0ced943b7 --- /dev/null +++ b/app/src/main/res/values-uk/plurals.xml @@ -0,0 +1,51 @@ + + + + %1$d новий розділ + %1$d нових розділи + %1$d нових розділів + %1$d нових розділів + + + %1$d хвилину тому + %1$d хвилини тому + %1$d хвилин тому + %1$d хвилин тому + + + Всього %1$d сторінка + Всього %1$d сторінки + Всього %1$d сторінок + Всього %1$d сторінок + + + %1$d годину тому + %1$d години тому + %1$d годин тому + %1$d годин тому + + + %1$d день тому + %1$d дні тому + %1$d днів тому + %1$d днів тому + + + %1$d елемент + %1$d елементи + %1$d елементів + %1$d елементів + + + %1$d розділ із %2$d + %1$d розділи з %2$d + %1$d розділів із %2$d + %1$d розділів із %2$d + + + %1$d розділ + %1$d розділи + %1$d розділів + %1$d розділів + + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..e476c3a7b --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,282 @@ + + + Дочекайтеся завершення завантаження… + Видалити + Нічого не знайдено + Додати це до улюблених + Очистити історію + Історії ще немає + Додати + Зберегти + Локальне сховище + Не вдалося підключитися до Інтернету + Деталі + Спробуйте ще раз + Відкрити меню + Улюблених ще немає + Нова категорія + Введіть назву категорії + Завантажено + Уподобання + Історія + Сталася помилка + Розділи + Список + Детальний список + Режим списку + Налаштування + Віддалені джерела + Завантаження… + Обчислення… + Розділ %1$d із %2$d + Закрити + Читати + Таблиця + Поділитися + Створити ярлик… + Поділитися %s + Пошук + Пошук манґи + Обробка… + Ім\'я + Популярна + Оновлена + Нова + Рейтинг + Порядок сортування + Фільтр + Тема + Світла + Темна + Сторінки + Очистити всю історію читання перманентно\? + Видалити + \"%s\" видалено з історії + \"%s\" видалено з локального сховища + Зберегти сторінку + Збережено + Поділитись зображенням + Ця операція не підтримується + Виберіть файл ZIP або CBZ. + Немає опису + Історія та кеш + Очистити кеш сторінок + Кеш + Б|кБ|МБ|ГБ|ТБ + Стандартний + Манхва + Режим читання + Розмір сітки + Пошук по %s + Видалити манґу + Видалити \"%s\" з пристрою перманентно\? + Налаштування читача + Перегортання сторінок + Кнопки гучності + Скасування… + Помилка + Очистити кеш мініатюр + Очистити історію пошуку + Очищено + Тільки жести + Внутрішнє сховище + Зовнішнє сховище + Домен + Перевірити наявність нових версій додатка + Доступна нова версія додатка + Ця манґа має %s. Зберегти все це\? + Зберегти + Сповіщення + Увімкнено %1$d з %2$d + Нові розділи + Повідомляти про оновлення манґи, яку Ви читаєте + Завантажити + Читати з початку + Перезавантажити + Вібрація + Улюблені категорії + Вилучити категорію \"%s\" зі своїх уподобань\? +\nВся манґа в ній буде втрачена. + Видалити + Тут якось пусто… + Спробуйте переформулювати запит. + Те, що ви читаєте, буде показано тут + Знайдіть, що читати, у бічному меню. + Спочатку збережіть щось + Збережіть його з онлайн-джерела або імпортуйте файли. + Полиця + Недавні + Анімація перегортання + Папка для завантажень + Інше сховище + Готово + Усі улюблені + Порожня категорія + Прочитати пізніше + Оновлення + Схожі + Нова версія: %s + Розмір: %s + Очікування мережі… + Очистити стрічку оновлень + Очищено + Повернути екран + Оновити + Оновлення скоро почнеться + Стежити за оновленнями + Не перевіряти + Неправильний пароль + Захистити додаток + Запитувати пароль під час запуску Kotatsu + Повторіть пароль + Паролі не співпадають + Про програму + Версія %s + Перевірити наявність оновлень + Перевірка наявності оновлень… + Не вдалося перевірити оновлення + Немає доступних оновлень + Віддавати перевагу читанню справа наліво (←) + Нова категорія + Режим масштабування + Вмістити в екран + Підігнати по висоті + підігнати по ширині + Вихідний розмір + Чорна + Споживає менше енергії на екранах AMOLED + Потрібен перезапуск + Резервне копіювання та відновлення + Відновлено + Підготовка… + Створити проблему на GitHub + Файл не знайдено + Дані відновлено, але є деякі помилки + Ви можете створити резервну копію своєї історії та уподобань і відновити їх + Тільки що + Торкніться, щоб спробувати ще раз + Обраний режим буде запам\'ятован для цієї манги + Потрібна CAPTCHA + Пройти + Очистити кукі + Всі кукі були видалені + Очистити стрічку + Перевірити нові розділи + В зворотньому порядку + Увійти + Увійдіть, щоб переглянути цей вміст + За замовчуванням: %s + ...і ще %1$d + Далі + Введіть пароль для запуску програми + Підтвердити + Пароль має містити 4 символи або більше + Пошук лише на %s + Ласкаво просимо + Резервна копія збережена + Докладніше + У черзі + Немає активних завантажень + Допомогти з перекладом програми + Переклад + Тема на 4PDA + Авторизація виконана + Вхід на %s не підтримується + Ви вийдете з усіх джерел + Завершена + Триває + Формат дати + Виключити NSFW манґу з історії + Ви повинні ввести ім’я + Показувати номери сторінок + Включені джерела + Застосовує тему програми, засновану на палітрі кольорів шпалер на пристрої + Імпорт манґи: %1$d з %2$d + Політика щодо знімків екрана + Дозволити + Пропонувати манґу на основі ваших уподобань + Усі дані аналізуються локально на цьому пристрої. Передача ваших персональних даних у будь-які сервіси не здійснюється + Почніть читати манґу, і ви отримаєте персоналізовані пропозиції + Увімкнено + Вимкнено + Скинути фільтр + Знайти жанр + Виберіть мови, якими ви хочете читати манґу. Це можливо змінити пізніше в налаштуваннях. + Тільки по Wi-Fi + Попереднє завантаження сторінок + Ви увійшли як %s + 18+ + Різні мови + Знайти розділ + Немає розділів у цій манзі + %1$s%% + Зміст + Оновлення пропозицій + Видалити вибрані елементи з пристрою назавжди\? + Видалення завершено + Ви впевнені, що хочете завантажити всю вибрану манґу з усіма її розділами\? Це може споживати багато трафіку та пам’яті + Завантажувати паралельно + Сповільнення завантаження + Обробка збереженої манґи + Приховати + Доступні нові джерела манґи + Закрити меню + Завантаження… + Очистити + Завантаження + Як в системі + Завантажте або прочитайте цей відсутній розділ онлайн. + Розділ відсутній + Зворотній зв\'язок + Жанри + За замовчуванням + Завжди + Продовжити + Імпорт + Натискання по краях + Попередження + Це може призвести до витрати великої кількості трафіку + Більше не питати + Налаштування сповіщень + Перейменувати + Показувати сповіщення, якщо доступна нова версія + Відкрити у веб-браузері + Недоступно + Немає доступного сховища + Нові розділи того, що ви читаєте, показано тут + Результати пошуку + Введіть пароль + Звук сповіщень + Світлодіодний індикатор + Категорії… + Ви можете використовувати категорії для впорядкування своїх уподобань. Натисніть «+», щоб створити категорію + Учора + Справа наліво (←) + Режим читання можна налаштувати окремо для кожної серії + Створити резервну копію + Відновити з резервної копії + Всі дані були відновлені + Групувати + Сьогодні + Без звуку + Давно + Перевірка наявності нових розділів: %1$d з %2$d + Очистити всю історію оновлень назавжди\? + Деякі пристрої мають різну поведінку системи, що може порушити фонові завдання. + Видалити всі останні пошукові запити назавжди\? + Інше + Доступні джерела + Динамічна тема + Блок на NSFW + Завжди блокувати + Пропозиції + Увімкнути пропозиції + Не пропонувати NSFW манґу + Не вдалося завантажити список жанрів + Ніколи + Зовнішній вигляд + Виключити жанри + Укажіть жанри, які ви не хочете бачити в пропозиціях + Допомагає уникнути блокування вашої IP-адреси + Розділи будуть видалені у фоновому режимі. Це може зайняти деякий час + \ No newline at end of file From f9281850ad46981a4a6a87ad6d87919af3516bf5 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Thu, 5 May 2022 16:12:42 +0200 Subject: [PATCH 23/45] Translated using Weblate (Finnish) Currently translated at 99.6% (280 of 281 strings) Translated using Weblate (French) Currently translated at 100.0% (281 of 281 strings) Translated using Weblate (Italian) Currently translated at 100.0% (281 of 281 strings) Translated using Weblate (German) Currently translated at 100.0% (281 of 281 strings) Co-authored-by: J. Lavoie Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ Translation: Kotatsu/Strings --- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-fi/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + 4 files changed, 4 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 42fb3a9ee..49d08ac30 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -278,4 +278,5 @@ Hilft, das Blockieren Ihrer IP-Adresse zu vermeiden Die Kapitel werden im Hintergrund entfernt. Das kann einige Zeit dauern Ausblenden + Neue Manga-Quellen sind verfügbar \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 69f9c193e..486e04471 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -278,4 +278,5 @@ Oletko varma, että haluat ladata kaikki valitut mangat kaikkine lukuineen\? Tämä toiminto voi kuluttaa paljon liikennettä ja tallennustilaa Tallennettujen mangojen käsittely Piilota + Uusia mangalähteitä on saatavilla \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 958794036..aeb8252f2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -278,4 +278,5 @@ Les chapitres seront supprimés en arrière-plan. Cela peut prendre un certain temps Traitement des mangas sauvegardés Masquer + De nouvelles sources de mangas sont disponibles \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e6196111b..04f7b3d93 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -278,4 +278,5 @@ I capitoli saranno rimossi in sfondo. Può richiedere un po\' di tempo Aiuta ad evitare il blocco del tuo indirizzo IP Nascondi + Sono disponibili nuove fonti di manga \ No newline at end of file From b439e0c2c238f82710b7307a360293f95d343b03 Mon Sep 17 00:00:00 2001 From: Luiz-bro Date: Thu, 5 May 2022 16:12:43 +0200 Subject: [PATCH 24/45] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (281 of 281 strings) Co-authored-by: Luiz-bro Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt-rBR/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 32910e0a3..d4a14e50f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -278,4 +278,5 @@ Processamento de mangá salvo Os capítulos serão removidos em segundo plano. Pode levar algum tempo Downloads paralelos + Novas fontes de mangá estão disponíveis \ No newline at end of file From 03cbd8410fe693a7350f4a21f40a63c30f3533b4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 7 May 2022 09:21:32 +0300 Subject: [PATCH 25/45] Remove travis.ci integration --- .travis.yml | 11 ----------- README.md | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9cec1af6c..000000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: android -dist: trusty -android: - components: - - android-30 - - build-tools-30.0.3 - - platform-tools-30.0.5 - - tools -before_install: - - yes | sdkmanager "platforms;android-30" -script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug \ No newline at end of file diff --git a/README.md b/README.md index b079e87af..cea436ae3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Kotatsu is a free and open source manga reader for Android. -![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) +![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) ### Download From 2a97cb34d7121f2e4ab4ea1bb4b6dc92a0ea3700 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 7 May 2022 14:01:07 +0300 Subject: [PATCH 26/45] Change root view of `preference_toggle_header` --- .../res/layout/preference_toggle_header.xml | 100 ++++++++---------- 1 file changed, 47 insertions(+), 53 deletions(-) diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index b1f816337..4b0ed9431 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -1,70 +1,64 @@ - + style="@style/Widget.Material3.CardView.Filled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="8dp" + android:paddingBottom="8dp" + app:cardCornerRadius="24dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + android:baselineAligned="false" + android:clipChildren="false" + android:clipToPadding="false" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:padding="16dp"> + android:layout_weight="1" + android:orientation="vertical"> - - - - - - - - - + android:textAppearance="@style/TextAppearance.Material3.TitleLarge" + android:textSize="20sp" + tools:text="Title" /> + + - + - + + + \ No newline at end of file From 33ab7f4d9565d35247d6beb5858556bf14ea8d85 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 7 May 2022 15:06:17 +0300 Subject: [PATCH 27/45] Cleanup preference_toggle_header --- app/src/main/res/layout/preference_toggle_header.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/res/layout/preference_toggle_header.xml b/app/src/main/res/layout/preference_toggle_header.xml index 4b0ed9431..a95b5e211 100644 --- a/app/src/main/res/layout/preference_toggle_header.xml +++ b/app/src/main/res/layout/preference_toggle_header.xml @@ -9,10 +9,7 @@ android:layout_marginHorizontal="16dp" android:layout_marginVertical="8dp" android:paddingBottom="8dp" - app:cardCornerRadius="24dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:cardCornerRadius="24dp"> Date: Mon, 9 May 2022 09:02:58 +0300 Subject: [PATCH 28/45] Edit favourite category activity --- app/src/main/AndroidManifest.xml | 14 +- .../kotatsu/favourites/FavouritesModule.kt | 2 + .../favourites/data/FavouriteCategoriesDao.kt | 6 + .../favourites/domain/FavouritesRepository.kt | 28 ++++ .../ui/FavouritesContainerFragment.kt | 38 +---- .../ui/categories/CategoriesActivity.kt | 41 +---- .../ui/categories/CategoriesEditDelegate.kt | 46 ------ .../FavouritesCategoriesViewModel.kt | 30 +--- .../edit/FavouritesCategoryEditActivity.kt | 147 ++++++++++++++++++ .../edit/FavouritesCategoryEditViewModel.kt | 53 +++++++ .../select/FavouriteCategoriesDialog.kt | 12 +- .../select/MangaCategoriesViewModel.kt | 6 - .../ui/list/FavouritesListFragment.kt | 51 +++++- .../ui/list/FavouritesListViewModel.kt | 27 +++- .../res/layout-w720dp-land/activity_main.xml | 1 + .../res/layout/activity_category_edit.xml | 81 ++++++++++ app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/menu/opt_favourites_list.xml | 19 +++ app/src/main/res/menu/popup_category.xml | 22 +-- app/src/main/res/values-large/themes.xml | 34 ++++ app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 2 + app/src/main/res/values/themes.xml | 5 +- 24 files changed, 480 insertions(+), 193 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt create mode 100644 app/src/main/res/layout/activity_category_edit.xml create mode 100644 app/src/main/res/menu/opt_favourites_list.xml create mode 100644 app/src/main/res/values-large/themes.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bba1d89b0..13f5cecbf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,7 +53,8 @@ - + android:label="@string/manga_shelf" + android:theme="@style/Theme.Kotatsu.DialogWhenLarge"> @@ -95,9 +97,13 @@ android:windowSoftInputMode="adjustResize" /> - + android:theme="@style/Theme.Kotatsu.DialogWhenLarge" /> + + MangaCategoriesViewModel(manga.get(), get()) } + viewModel { params -> FavouritesCategoryEditViewModel(params[0], get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index 65d429706..d0ada21da 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -6,6 +6,9 @@ import kotlinx.coroutines.flow.Flow @Dao abstract class FavouriteCategoriesDao { + @Query("SELECT * FROM favourite_categories WHERE category_id = :id") + abstract suspend fun find(id: Int): FavouriteCategoryEntity + @Query("SELECT * FROM favourite_categories ORDER BY sort_key") abstract suspend fun findAll(): List @@ -27,6 +30,9 @@ abstract class FavouriteCategoriesDao { @Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id") abstract suspend fun updateTitle(id: Long, title: String) + @Query("UPDATE favourite_categories SET title = :title, `order` = :order, `track` = :tracker WHERE category_id = :id") + abstract suspend fun update(id: Long, title: String, order: String, tracker: Boolean) + @Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id") abstract suspend fun updateOrder(id: Long, order: String) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 35a362e95..6e995696b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -52,6 +52,11 @@ class FavouritesRepository( }.distinctUntilChanged() } + fun observeCategory(id: Long): Flow { + return db.favouriteCategoriesDao.observe(id) + .map { it.toFavouriteCategory() } + } + fun observeCategories(mangaId: Long): Flow> { return db.favouritesDao.observe(mangaId).map { entity -> entity?.categories?.map { it.toFavouriteCategory() }.orEmpty() @@ -62,6 +67,29 @@ class FavouritesRepository( return db.favouritesDao.observeIds(mangaId).map { it.toSet() } } + suspend fun getCategory(id: Long): FavouriteCategory { + return db.favouriteCategoriesDao.find(id.toInt()).toFavouriteCategory() + } + + suspend fun createCategory(title: String, sortOrder: SortOrder, isTrackerEnabled: Boolean): FavouriteCategory { + val entity = FavouriteCategoryEntity( + title = title, + createdAt = System.currentTimeMillis(), + sortKey = db.favouriteCategoriesDao.getNextSortKey(), + categoryId = 0, + order = sortOrder.name, + track = isTrackerEnabled, + ) + val id = db.favouriteCategoriesDao.insert(entity) + val category = entity.toFavouriteCategory(id) + channels.createChannel(category) + return category + } + + suspend fun updateCategory(id: Long, title: String, sortOrder: SortOrder, isTrackerEnabled: Boolean) { + db.favouriteCategoriesDao.update(id, title, sortOrder.name, isTrackerEnabled) + } + suspend fun addCategory(title: String): FavouriteCategory { val entity = FavouriteCategoryEntity( title = title, diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 45cbba17a..61fae0369 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -16,12 +16,12 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.util.ActionModeListener import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel +import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight @@ -134,28 +134,6 @@ class FavouritesContainerFragment : viewModel.deleteCategory(category.id) } - override fun onRenameCategory(category: FavouriteCategory, newName: String) { - viewModel.renameCategory(category.id, newName) - } - - override fun onCreateCategory(name: String) { - viewModel.createCategory(name) - } - - private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { - val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return - for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { - val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes) - menuItem.isCheckable = true - menuItem.isChecked = item == category.order - } - submenu.setGroupCheckable(R.id.group_order, true, true) - menu.findItem(R.id.action_tracking)?.run { - isVisible = viewModel.isFavouritesTrackerEnabled - isChecked = category.isTrackingEnabled - } - } - private fun TabLayout.setTabsEnabled(enabled: Boolean) { val tabStrip = getChildAt(0) as? ViewGroup ?: return for (tab in tabStrip.children) { @@ -166,19 +144,11 @@ class FavouritesContainerFragment : private fun showCategoryMenu(tabView: View, category: FavouriteCategory) { val menu = PopupMenu(tabView.context, tabView) menu.inflate(R.menu.popup_category) - createOrderSubmenu(menu.menu, category) menu.setOnMenuItemClickListener { when (it.itemId) { R.id.action_remove -> editDelegate.deleteCategory(category) - R.id.action_rename -> editDelegate.renameCategory(category) - R.id.action_create -> editDelegate.createCategory() - R.id.action_tracking -> viewModel.setCategoryTracking(category.id, !category.isTrackingEnabled) - R.id.action_order -> return@setOnMenuItemClickListener false - else -> { - val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order) - ?: return@setOnMenuItemClickListener false - viewModel.setCategoryOrder(category.id, order) - } + R.id.action_edit -> FavouritesCategoryEditActivity.newIntent(tabView.context, category.id) + else -> return@setOnMenuItemClickListener false } true } @@ -190,7 +160,7 @@ class FavouritesContainerFragment : menu.inflate(R.menu.popup_category_all) menu.setOnMenuItemClickListener { when (it.itemId) { - R.id.action_create -> editDelegate.createCategory() + R.id.action_create -> FavouritesCategoryEditActivity.newIntent(requireContext()) R.id.action_hide -> viewModel.setAllCategoriesVisible(false) } true diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt index d2615c826..80fd2a137 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.favourites.ui.categories import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.Menu import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu @@ -19,9 +18,9 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel +import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight @@ -57,24 +56,17 @@ class CategoriesActivity : override fun onClick(v: View) { when (v.id) { - R.id.fab_add -> editDelegate.createCategory() + R.id.fab_add -> startActivity(FavouritesCategoryEditActivity.newIntent(this)) } } override fun onItemClick(item: FavouriteCategory, view: View) { val menu = PopupMenu(view.context, view) menu.inflate(R.menu.popup_category) - prepareCategoryMenu(menu.menu, item) menu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.action_remove -> editDelegate.deleteCategory(item) - R.id.action_rename -> editDelegate.renameCategory(item) - R.id.action_tracking -> viewModel.setCategoryTracking(item.id, !item.isTrackingEnabled) - R.id.action_order -> return@setOnMenuItemClickListener false - else -> { - val order = SORT_ORDERS.getOrNull(menuItem.order) ?: return@setOnMenuItemClickListener false - viewModel.setCategoryOrder(item.id, order) - } + R.id.action_edit -> startActivity(FavouritesCategoryEditActivity.newIntent(this, item.id)) } true } @@ -118,33 +110,6 @@ class CategoriesActivity : viewModel.deleteCategory(category.id) } - override fun onRenameCategory(category: FavouriteCategory, newName: String) { - viewModel.renameCategory(category.id, newName) - } - - override fun onCreateCategory(name: String) { - viewModel.createCategory(name) - } - - private fun prepareCategoryMenu(menu: Menu, category: FavouriteCategory) { - val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return - for ((i, item) in SORT_ORDERS.withIndex()) { - val menuItem = submenu.add( - R.id.group_order, - Menu.NONE, - i, - item.titleRes - ) - menuItem.isCheckable = true - menuItem.isChecked = item == category.order - } - submenu.setGroupCheckable(R.id.group_order, true, true) - menu.findItem(R.id.action_tracking)?.run { - isVisible = viewModel.isFavouritesTrackerEnabled - isChecked = category.isTrackingEnabled - } - } - private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0 ) { 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 603a70ee3..f7a98c078 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 @@ -1,15 +1,10 @@ package org.koitharu.kotatsu.favourites.ui.categories import android.content.Context -import android.text.InputType -import android.widget.Toast import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog import org.koitharu.kotatsu.core.model.FavouriteCategory -private const val MAX_TITLE_LENGTH = 24 - class CategoriesEditDelegate( private val context: Context, private val callback: CategoriesEditCallback @@ -26,49 +21,8 @@ class CategoriesEditDelegate( .show() } - fun renameCategory(category: FavouriteCategory) { - TextInputDialog.Builder(context) - .setTitle(R.string.rename) - .setText(category.title) - .setHint(R.string.enter_category_name) - .setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) - .setNegativeButton(android.R.string.cancel) - .setMaxLength(MAX_TITLE_LENGTH, false) - .setPositiveButton(R.string.rename) { _, name -> - val trimmed = name.trim() - if (trimmed.isEmpty()) { - Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show() - } else { - callback.onRenameCategory(category, name) - } - }.create() - .show() - } - - fun createCategory() { - TextInputDialog.Builder(context) - .setTitle(R.string.add_new_category) - .setHint(R.string.enter_category_name) - .setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) - .setNegativeButton(android.R.string.cancel) - .setMaxLength(MAX_TITLE_LENGTH, false) - .setPositiveButton(R.string.add) { _, name -> - val trimmed = name.trim() - if (trimmed.isEmpty()) { - Toast.makeText(context, R.string.error_empty_name, Toast.LENGTH_SHORT).show() - } else { - callback.onCreateCategory(trimmed) - } - }.create() - .show() - } - interface CategoriesEditCallback { fun onDeleteCategory(category: FavouriteCategory) - - fun onRenameCategory(category: FavouriteCategory, newName: String) - - fun onCreateCategory(name: String) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 9e84a9d89..31c1e11de 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.lifecycle.viewModelScope -import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* @@ -10,8 +9,8 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel -import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import java.util.* class FavouritesCategoriesViewModel( private val repository: FavouritesRepository, @@ -34,39 +33,12 @@ class FavouritesCategoriesViewModel( mapCategories(list, showAll, showAll) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) - val isFavouritesTrackerEnabled: Boolean - get() = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources - - fun createCategory(name: String) { - launchJob { - repository.addCategory(name) - } - } - - fun renameCategory(id: Long, name: String) { - launchJob { - repository.renameCategory(id, name) - } - } - fun deleteCategory(id: Long) { launchJob { repository.removeCategory(id) } } - fun setCategoryOrder(id: Long, order: SortOrder) { - launchJob { - repository.setCategoryOrder(id, order) - } - } - - fun setCategoryTracking(id: Long, isEnabled: Boolean) { - launchJob { - repository.setCategoryTracking(id, isEnabled) - } - } - fun setAllCategoriesVisible(isVisible: Boolean) { settings.isAllFavouritesVisible = isVisible } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt new file mode 100644 index 000000000..27239c0dc --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt @@ -0,0 +1,147 @@ +package org.koitharu.kotatsu.favourites.ui.categories.edit + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.core.graphics.Insets +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.utils.ext.getDisplayMessage + +class FavouritesCategoryEditActivity : BaseActivity(), AdapterView.OnItemClickListener { + + private val viewModel by viewModel { + parametersOf(intent.getLongExtra(EXTRA_ID, NO_ID)) + } + private var selectedSortOrder: SortOrder? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityCategoryEditBinding.inflate(layoutInflater)) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material) + } + initSortSpinner() + + viewModel.onSaved.observe(this) { finishAfterTransition() } + viewModel.category.observe(this, ::onCategoryChanged) + viewModel.isLoading.observe(this, ::onLoadingStateChanged) + viewModel.onError.observe(this, ::onError) + viewModel.isTrackerEnabled.observe(this) { + binding.switchTracker.isVisible = it + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSerializable(KEY_SORT_ORDER, selectedSortOrder) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + val order = savedInstanceState.getSerializable(KEY_SORT_ORDER) + if (order != null && order is SortOrder) { + selectedSortOrder = order + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.opt_config, menu) + menu.findItem(R.id.action_done)?.setTitle(R.string.save) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.action_done -> { + viewModel.save( + title = binding.editName.text?.toString().orEmpty(), + sortOrder = getSelectedSortOrder(), + isTrackerEnabled = binding.switchTracker.isChecked, + ) + true + } + else -> super.onOptionsItemSelected(item) + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.scrollView.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom, + ) + binding.toolbar.updatePadding( + top = insets.top, + ) + } + + override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + selectedSortOrder = CategoriesActivity.SORT_ORDERS.getOrNull(position) + } + + private fun onCategoryChanged(category: FavouriteCategory?) { + setTitle(if (category == null) R.string.create_category else R.string.edit_category) + if (selectedSortOrder != null) { + return + } + binding.editName.setText(category?.title) + selectedSortOrder = category?.order + val sortText = getString((category?.order ?: SortOrder.NEWEST).titleRes) + binding.editSort.setText(sortText, false) + binding.switchTracker.isChecked = category?.isTrackingEnabled ?: true + } + + private fun onError(e: Throwable) { + binding.textViewError.text = e.getDisplayMessage(resources) + binding.textViewError.isVisible = true + } + + private fun onLoadingStateChanged(isLoading: Boolean) { + binding.editSort.isEnabled = !isLoading + binding.editName.isEnabled = !isLoading + binding.switchTracker.isEnabled = !isLoading + if (isLoading) { + binding.textViewError.isVisible = false + } + } + + private fun initSortSpinner() { + val entries = CategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) } + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, entries) + binding.editSort.setAdapter(adapter) + binding.editSort.onItemClickListener = this + } + + private fun getSelectedSortOrder(): SortOrder { + selectedSortOrder?.let { return it } + val entries = CategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) } + val index = entries.indexOf(binding.editSort.text.toString()) + return CategoriesActivity.SORT_ORDERS.getOrNull(index) ?: SortOrder.NEWEST + } + + companion object { + + private const val EXTRA_ID = "id" + private const val KEY_SORT_ORDER = "sort" + private const val NO_ID = -1L + + fun newIntent(context: Context, id: Long = NO_ID): Intent { + return Intent(context, FavouritesCategoryEditActivity::class.java) + .putExtra(EXTRA_ID, id) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt new file mode 100644 index 000000000..78446dd14 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditViewModel.kt @@ -0,0 +1,53 @@ +package org.koitharu.kotatsu.favourites.ui.categories.edit + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.utils.SingleLiveEvent + +private const val NO_ID = -1L + +class FavouritesCategoryEditViewModel( + private val categoryId: Long, + private val repository: FavouritesRepository, + private val settings: AppSettings, +) : BaseViewModel() { + + val onSaved = SingleLiveEvent() + val category = MutableLiveData() + + val isTrackerEnabled = liveData(viewModelScope.coroutineContext + Dispatchers.Default) { + emit(settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources) + } + + init { + launchLoadingJob { + category.value = if (categoryId != NO_ID) { + repository.getCategory(categoryId) + } else { + null + } + } + } + + fun save( + title: String, + sortOrder: SortOrder, + isTrackerEnabled: Boolean, + ) { + launchLoadingJob { + if (categoryId == NO_ID) { + repository.createCategory(title, sortOrder, isTrackerEnabled) + } else { + repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled) + } + onSaved.call(Unit) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt index 43eec185e..95f733006 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/FavouriteCategoriesDialog.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.databinding.DialogFavoriteCategoriesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate +import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.parsers.model.Manga @@ -34,9 +35,6 @@ class FavouriteCategoriesDialog : } private var adapter: MangaCategoriesAdapter? = null - private val editDelegate by lazy(LazyThreadSafetyMode.NONE) { - CategoriesEditDelegate(requireContext(), this@FavouriteCategoriesDialog) - } override fun onInflateView( inflater: LayoutInflater, @@ -61,7 +59,7 @@ class FavouriteCategoriesDialog : override fun onMenuItemClick(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_create -> { - editDelegate.createCategory() + FavouritesCategoryEditActivity.newIntent(requireContext()) true } else -> false @@ -74,12 +72,6 @@ class FavouriteCategoriesDialog : override fun onDeleteCategory(category: FavouriteCategory) = Unit - override fun onRenameCategory(category: FavouriteCategory, newName: String) = Unit - - override fun onCreateCategory(name: String) { - viewModel.createCategory(name) - } - private fun onContentChanged(categories: List) { adapter?.items = categories } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt index 84f9cf8f9..b9f906549 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/select/MangaCategoriesViewModel.kt @@ -38,12 +38,6 @@ class MangaCategoriesViewModel( } } - fun createCategory(name: String) { - launchJob(Dispatchers.Default) { - favouritesRepository.addCategory(name) - } - } - private fun observeCategoriesIds() = if (manga.size == 1) { // Fast path favouritesRepository.observeCategoriesIds(manga[0].id) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt index f2748622c..8d4b9e419 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt @@ -1,11 +1,17 @@ package org.koitharu.kotatsu.favourites.ui.list +import android.os.Bundle import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem +import android.view.View import androidx.appcompat.view.ActionMode +import androidx.core.view.iterator import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.titleRes +import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.utils.ext.withArgs @@ -17,12 +23,54 @@ class FavouritesListFragment : MangaListFragment() { } private val categoryId: Long - get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L + get() = arguments?.getLong(ARG_CATEGORY_ID) ?: NO_ID override val isSwipeRefreshEnabled = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } + } + override fun onScrolledToEnd() = Unit + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + if (categoryId != NO_ID) { + inflater.inflate(R.menu.opt_favourites_list, menu) + menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> + for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { + val menuItem = submenu.add(R.id.group_order, Menu.NONE, i, item.titleRes) + menuItem.isCheckable = true + } + submenu.setGroupCheckable(R.id.group_order, true, true) + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + menu.findItem(R.id.action_order)?.subMenu?.let { submenu -> + val selectedOrder = viewModel.sortOrder.value + for (item in submenu) { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) + item.isChecked = order == selectedOrder + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when { + item.itemId == R.id.action_order -> false + item.groupId == R.id.group_order -> { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(item.order) ?: return false + viewModel.setSortOrder(order) + true + } + else -> super.onOptionsItemSelected(item) + } + } + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.mode_favourites, menu) return super.onCreateActionMode(mode, menu) @@ -48,6 +96,7 @@ class FavouritesListFragment : MangaListFragment() { companion object { + const val NO_ID = 0L private const val ARG_CATEGORY_ID = "category_id" fun newInstance(categoryId: Long) = FavouritesListFragment().withArgs(1) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 476fa6fb1..06d5bee3d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -1,12 +1,16 @@ package org.koitharu.kotatsu.favourites.ui.list +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.list.domain.CountersProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -24,8 +28,16 @@ class FavouritesListViewModel( settings: AppSettings, ) : MangaListViewModel(settings), CountersProvider { + var sortOrder: LiveData = if (categoryId == NO_ID) { + MutableLiveData(null) + } else { + repository.observeCategory(categoryId) + .map { it.order } + .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + } + override val content = combine( - if (categoryId == 0L) { + if (categoryId == NO_ID) { repository.observeAll(SortOrder.NEWEST) } else { repository.observeAll(categoryId) @@ -37,7 +49,7 @@ class FavouritesListViewModel( EmptyState( icon = R.drawable.ic_heart_outline, textPrimary = R.string.text_empty_holder_primary, - textSecondary = if (categoryId == 0L) { + textSecondary = if (categoryId == NO_ID) { R.string.you_have_not_favourites_yet } else { R.string.favourites_category_empty @@ -60,7 +72,7 @@ class FavouritesListViewModel( return } launchJob { - if (categoryId == 0L) { + if (categoryId == NO_ID) { repository.removeFromFavourites(ids) } else { repository.removeFromCategory(categoryId, ids) @@ -68,6 +80,15 @@ class FavouritesListViewModel( } } + fun setSortOrder(order: SortOrder) { + if (categoryId == NO_ID) { + return + } + launchJob { + repository.setCategoryOrder(categoryId, order) + } + } + override suspend fun getCounter(mangaId: Long): Int { return trackingRepository.getNewChaptersCount(mangaId) } diff --git a/app/src/main/res/layout-w720dp-land/activity_main.xml b/app/src/main/res/layout-w720dp-land/activity_main.xml index ee362d397..a442269b3 100644 --- a/app/src/main/res/layout-w720dp-land/activity_main.xml +++ b/app/src/main/res/layout-w720dp-land/activity_main.xml @@ -62,6 +62,7 @@ android:layout_height="match_parent" android:background="@null" android:drawablePadding="16dp" + android:layout_marginEnd="4dp" android:gravity="center_vertical" android:hint="@string/search_manga" android:imeOptions="actionSearch" diff --git a/app/src/main/res/layout/activity_category_edit.xml b/app/src/main/res/layout/activity_category_edit.xml new file mode 100644 index 000000000..e453df674 --- /dev/null +++ b/app/src/main/res/layout/activity_category_edit.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 43361b15b..6314cf7ef 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -51,7 +51,7 @@ style="@style/Widget.Kotatsu.SearchView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginEnd="2dp" + android:layout_marginEnd="4dp" android:background="@null" android:gravity="center_vertical" android:hint="@string/search_manga" diff --git a/app/src/main/res/menu/opt_favourites_list.xml b/app/src/main/res/menu/opt_favourites_list.xml new file mode 100644 index 000000000..269569df7 --- /dev/null +++ b/app/src/main/res/menu/opt_favourites_list.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popup_category.xml b/app/src/main/res/menu/popup_category.xml index ee78b4b80..50c1313be 100644 --- a/app/src/main/res/menu/popup_category.xml +++ b/app/src/main/res/menu/popup_category.xml @@ -7,25 +7,7 @@ android:title="@string/remove" /> - - - - - - - - - - - + android:id="@+id/action_edit" + android:title="@string/edit" /> \ No newline at end of file diff --git a/app/src/main/res/values-large/themes.xml b/app/src/main/res/values-large/themes.xml new file mode 100644 index 000000000..56e1ed965 --- /dev/null +++ b/app/src/main/res/values-large/themes.xml @@ -0,0 +1,34 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 87da7d5d0..012bf4aee 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -281,4 +281,7 @@ Вы будете получать уведомления об обновлении манги, которую Вы читаете Вы не будете получать уведомления, но новые главы будут отображаться в списке Включить уведомления + Название + Изменить + Изменить категорию \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 083717035..2bc68e061 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -284,4 +284,7 @@ You will receive notifications about updates of manga you are reading You will not receive notifications but new chapters will be highlighted in the lists Enable notifications + Name + Edit + Edit category \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b63927f74..b16f50044 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -119,6 +119,8 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 12ccac10f..f6addbe8f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -71,7 +71,8 @@ @style/Widget.Kotatsu.RecyclerView @style/Widget.Kotatsu.ListItemTextView - + + @style/TextAppearance.Kotatsu.Menu ?attr/textAppearanceBodyLarge @style/TextAppearance.Kotatsu.Preference.Secondary @@ -80,6 +81,8 @@ \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c14ddff76..348cff5e0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -26,8 +26,7 @@ - - + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b16f50044..7f96a0bef 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -24,6 +24,7 @@ @@ -86,7 +87,7 @@