diff --git a/README.md b/README.md index cd5c23394..b079e87af 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Download APK from Github Releases: * Tablet-optimized material design UI * Standard and Webtoon-optimized reader * Notifications about new chapters with updates feed +* Available in multiple languages +* Password protect access to the app ### Screenshots @@ -35,6 +37,14 @@ Download APK from Github Releases: | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/nv95/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) | |-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +### Localization + + +Translation status + + +Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, please head over to the Weblate project page + ### License [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) diff --git a/app/build.gradle b/app/build.gradle index 6515eeac4..b90b1bfdc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,12 +65,12 @@ android { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation('com.github.nv95:kotatsu-parsers:3ea7e92e64') { + implementation('com.github.nv95:kotatsu-parsers:0ee689cd2f') { exclude group: 'org.json', module: 'json' } - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.activity:activity-ktx:1.4.0' @@ -108,7 +108,7 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' testImplementation 'io.insert-koin:koin-test-junit4:3.1.5' androidTestImplementation 'androidx.test:runner:1.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 837ee29cb..6c6c112b8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,15 +58,6 @@ - - - - - - intent.manga - intent.mangaId != 0L -> db.mangaDao.find(intent.mangaId)?.toManga() + intent.mangaId != 0L -> findMangaById(intent.mangaId) else -> null // TODO resolve uri } 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 9ed4e714b..09a970eee 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 @@ -14,7 +14,7 @@ import org.koitharu.kotatsu.core.network.CommonHeaders 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.utils.ext.medianOrNull +import org.koitharu.kotatsu.parsers.util.medianOrNull import java.io.InputStream import java.util.zip.ZipFile diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt index 9abebf0fb..2125d044c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BasePreferenceFragment.kt @@ -6,14 +6,18 @@ import androidx.annotation.CallSuper import androidx.annotation.StringRes import androidx.core.graphics.Insets import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView import org.koin.android.ext.android.inject import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.SettingsHeadersFragment -abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(), +abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : + PreferenceFragmentCompat(), WindowInsetsDelegate.WindowInsetsListener, RecyclerViewOwner { @@ -39,16 +43,20 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : Pre override fun onResume() { super.onResume() if (titleId != 0) { - activity?.setTitle(titleId) + setTitle(getString(titleId)) } } @CallSuper override fun onWindowInsetsChanged(insets: Insets) { listView.updatePadding( - left = insets.left, - right = insets.right, bottom = insets.bottom ) } -} + + @Suppress("UsePropertyAccessSyntax") + protected fun setTitle(title: CharSequence) { + (parentFragment as? SettingsHeadersFragment)?.setTitle(title) + ?: activity?.setTitle(title) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt index c31307a24..8b3498e3c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TagsDao.kt @@ -12,7 +12,7 @@ abstract class TagsDao { @Query( """SELECT tags.* FROM tags LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id - GROUP BY manga_tags.tag_id + GROUP BY tags.title ORDER BY COUNT(manga_id) DESC LIMIT :limit""" ) @@ -22,7 +22,7 @@ abstract class TagsDao { """SELECT tags.* FROM tags LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id WHERE tags.source = :source - GROUP BY manga_tags.tag_id + GROUP BY tags.title ORDER BY COUNT(manga_id) DESC LIMIT :limit""" ) @@ -32,7 +32,7 @@ abstract class TagsDao { """SELECT tags.* FROM tags LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id WHERE tags.source = :source AND title LIKE :query - GROUP BY manga_tags.tag_id + GROUP BY tags.title ORDER BY COUNT(manga_id) DESC LIMIT :limit""" ) @@ -42,7 +42,7 @@ abstract class TagsDao { """SELECT tags.* FROM tags LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id WHERE title LIKE :query - GROUP BY manga_tags.tag_id + GROUP BY tags.title ORDER BY COUNT(manga_id) DESC LIMIT :limit""" ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt index ad80b0beb..524fe906d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaWithTags.kt @@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.db.entity import androidx.room.Embedded import androidx.room.Junction import androidx.room.Relation -import org.koitharu.kotatsu.utils.ext.mapToSet +import org.koitharu.kotatsu.parsers.util.mapToSet class MangaWithTags( @Embedded val manga: MangaEntity, @@ -15,7 +15,5 @@ class MangaWithTags( val tags: List ) { - fun toManga() = manga.toManga(tags.mapToSet { - it.toMangaTag() - }) + fun toManga() = manga.toManga(tags.mapToSet { it.toMangaTag() }) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt index 263e91938..037b22ad8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TrackLogWithManga.kt @@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity import androidx.room.Embedded import androidx.room.Junction import androidx.room.Relation -import org.koitharu.kotatsu.core.model.TrackingLogItem -import org.koitharu.kotatsu.utils.ext.mapToSet import java.util.* +import org.koitharu.kotatsu.core.model.TrackingLogItem +import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.utils.ext.mapToSet class TrackLogWithManga( @Embedded val trackLog: TrackLogEntity, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt index 7b74c329c..67bb80044 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/Parcelable.kt @@ -4,7 +4,7 @@ import android.os.Parcel import androidx.core.os.ParcelCompat import org.koitharu.kotatsu.parsers.model.* -fun Manga.writeToParcel(out: Parcel, flags: Int) { +fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) { out.writeLong(id) out.writeString(title) out.writeString(altTitle) @@ -18,7 +18,11 @@ fun Manga.writeToParcel(out: Parcel, flags: Int) { out.writeParcelable(ParcelableMangaTags(tags), flags) out.writeSerializable(state) out.writeString(author) - out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags) + if (withChapters) { + out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags) + } else { + out.writeString(null) + } out.writeSerializable(source) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt index 44c0ae8f8..fd9b6cfa1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt @@ -2,24 +2,34 @@ package org.koitharu.kotatsu.core.model.parcelable import android.os.Parcel import android.os.Parcelable -import android.util.Log -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.parsers.model.Manga +// Limits to avoid TransactionTooLargeException +private const val MAX_SAFE_SIZE = 1024 * 512 // Assume that 512 kb is safe parcel size +private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe + class ParcelableManga( val manga: Manga, ) : Parcelable { constructor(parcel: Parcel) : this(parcel.readManga()) - init { - if (BuildConfig.DEBUG && manga.chapters != null) { - Log.w("ParcelableManga", "Passing manga with chapters as Parcelable is dangerous!") - } - } - override fun writeToParcel(parcel: Parcel, flags: Int) { - manga.writeToParcel(parcel, flags) + val chapters = manga.chapters + if (chapters == null || chapters.size <= MAX_SAFE_CHAPTERS_COUNT) { + // fast path + manga.writeToParcel(parcel, flags, withChapters = true) + return + } + val tempParcel = Parcel.obtain() + manga.writeToParcel(tempParcel, flags, withChapters = true) + val size = tempParcel.dataSize() + if (size < MAX_SAFE_SIZE) { + parcel.appendFrom(tempParcel, 0, size) + } else { + manga.writeToParcel(parcel, flags, withChapters = false) + } + tempParcel.recycle() } override fun describeContents(): Int { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt deleted file mode 100644 index e5bf2deca..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/CurlLoggingInterceptor.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.koitharu.kotatsu.core.network - -import android.util.Log -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import okio.Buffer -import java.io.IOException -import java.nio.charset.StandardCharsets - -private const val TAG = "CURL" - -class CurlLoggingInterceptor( - private val extraCurlOptions: String? = null, -) : Interceptor { - - @Throws(IOException::class) - override fun intercept(chain: Interceptor.Chain): Response { - val request: Request = chain.request() - var compressed = false - val curlCmd = StringBuilder("curl") - if (extraCurlOptions != null) { - curlCmd.append(" ").append(extraCurlOptions) - } - curlCmd.append(" -X ").append(request.method) - val headers = request.headers - var i = 0 - val count = headers.size - while (i < count) { - val name = headers.name(i) - val value = headers.value(i) - if ("Accept-Encoding".equals(name, ignoreCase = true) && "gzip".equals(value, - ignoreCase = true) - ) { - compressed = true - } - curlCmd.append(" -H " + "\"").append(name).append(": ").append(value).append("\"") - i++ - } - val requestBody = request.body - if (requestBody != null) { - val buffer = Buffer() - requestBody.writeTo(buffer) - val contentType = requestBody.contentType() - val charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 - curlCmd.append(" --data $'") - .append(buffer.readString(charset).replace("\n", "\\n")) - .append("'") - } - curlCmd.append(if (compressed) " --compressed " else " ").append(request.url) - Log.d(TAG, "╭--- cURL (" + request.url + ")") - Log.d(TAG, curlCmd.toString()) - Log.d(TAG, "╰--- (copy and paste the above line to a terminal)") - return chain.proceed(request) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt index 4e7f0eb1c..48b009a33 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -5,7 +5,6 @@ import okhttp3.CookieJar import okhttp3.OkHttpClient import org.koin.dsl.bind import org.koin.dsl.module -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.parsers.MangaLoaderContext @@ -22,9 +21,6 @@ val networkModule cache(get().createHttpCache()) addInterceptor(UserAgentInterceptor()) addInterceptor(CloudFlareInterceptor()) - if (BuildConfig.DEBUG) { - addNetworkInterceptor(CurlLoggingInterceptor()) - } }.build() } single { MangaLoaderContextImpl(get(), get(), get()) } 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 bbffd9b7e..de191e304 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 @@ -165,6 +165,18 @@ class AppSettings(context: Context) { else -> SimpleDateFormat(format, Locale.getDefault()) } + fun getSuggestionsTagsBlacklistRegex(): Regex? { + val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',') + if (string.isNullOrEmpty()) { + return null + } + val tags = string.split(',') + val regex = tags.joinToString(prefix = "(", separator = "|", postfix = ")") { tag -> + Regex.escape(tag.trim()) + } + return Regex(regex, RegexOption.IGNORE_CASE) + } + fun getMangaSources(includeHidden: Boolean): List { val list = MangaSource.values().toMutableList() list.remove(MangaSource.LOCAL) @@ -247,17 +259,16 @@ class AppSettings(context: Context) { const val KEY_PAGES_PRELOAD = "pages_preload" const val KEY_SUGGESTIONS = "suggestions" const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw" + const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags" const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source" // About const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_APP_TRANSLATION = "about_app_translation" - const val KEY_APP_GRATITUDES = "about_gratitudes" const val KEY_FEEDBACK_4PDA = "about_feedback_4pda" const val KEY_FEEDBACK_DISCORD = "about_feedback_discord" const val KEY_FEEDBACK_GITHUB = "about_feedback_github" - const val KEY_SUPPORT_DEVELOPER = "about_support_developer" private const val NETWORK_NEVER = 0 private const val NETWORK_ALWAYS = 1 diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 53546ea6a..7c5fe8574 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -24,11 +24,11 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository 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 org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.iterator -import org.koitharu.kotatsu.utils.ext.mapToSet import java.io.IOException class DetailsViewModel( diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt index 5cee213a7..2ca602df9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/BranchesAdapter.kt @@ -6,7 +6,7 @@ import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.TextView import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.replaceWith +import org.koitharu.kotatsu.parsers.util.replaceWith class BranchesAdapter : BaseAdapter() { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt index c16896205..1a86d0153 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.download.domain.DownloadState +import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.progress.ProgressJob diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index aef7a1667..dd3fa3035 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -16,8 +16,8 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.utils.PendingIntentCompat -import org.koitharu.kotatsu.utils.ext.format import org.koitharu.kotatsu.utils.ext.getDisplayMessage import com.google.android.material.R as materialR diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index 27a993a91..a005346f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -35,7 +35,6 @@ import org.koitharu.kotatsu.utils.ext.throttle import org.koitharu.kotatsu.utils.ext.toArraySet import org.koitharu.kotatsu.utils.progress.ProgressJob import java.util.concurrent.TimeUnit -import kotlin.collections.set class DownloadService : BaseService() { @@ -71,7 +70,6 @@ class DownloadService : BaseService() { return if (manga != null) { jobs[startId] = downloadManga(startId, manga, chapters) jobCount.value = jobs.size - Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show() START_REDELIVER_INTENT } else { stopSelf(startId) @@ -185,6 +183,7 @@ class DownloadService : BaseService() { intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) } ContextCompat.startForegroundService(context, intent) + Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt index 2202ab606..c7d0b0191 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt @@ -24,9 +24,10 @@ class ForegroundNotificationSwitcher( @Synchronized fun notify(startId: Int, notification: Notification) { if (notifications.isEmpty()) { - handler.postDelayed(StartForegroundRunnable(startId, notification), DEFAULT_DELAY) + StartForegroundRunnable(startId, notification) + } else { + notificationManager.notify(startId, notification) } - notificationManager.notify(startId, notification) notifications[startId] = notification } 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 db334b38b..49156d9ce 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,8 +13,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.utils.ext.mapItems -import org.koitharu.kotatsu.utils.ext.mapToSet class FavouritesRepository(private val db: MangaDatabase) { diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt index 0b973aa64..a2527f59e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity - @Dao abstract class HistoryDao { @@ -23,8 +22,15 @@ abstract class HistoryDao { @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") abstract suspend fun findAllManga(): List - @Query("SELECT * FROM tags WHERE tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_id IN (SELECT manga_id FROM history))") - abstract suspend fun findAllTags(): List + @Query( + """SELECT tags.* FROM tags + LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id + INNER JOIN history ON history.manga_id = manga_tags.manga_id + GROUP BY manga_tags.tag_id + ORDER BY COUNT(manga_tags.manga_id) DESC + LIMIT :limit""" + ) + abstract suspend fun findPopularTags(limit: Int): List @Query("SELECT * FROM history WHERE manga_id = :id") abstract suspend fun find(id: Long): HistoryEntity? @@ -60,5 +66,4 @@ abstract class HistoryDao { true } else false } - } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt index cd1ad25bd..99fa176f4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/domain/HistoryRepository.kt @@ -11,9 +11,9 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.mapItems -import org.koitharu.kotatsu.utils.ext.mapToSet class HistoryRepository( private val db: MangaDatabase, @@ -99,7 +99,7 @@ class HistoryRepository( } } - suspend fun getAllTags(): Set { - return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() } + suspend fun getPopularTags(limit: Int): List { + return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index 0c18d24ab..6a363cf4b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -44,6 +44,7 @@ import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet +import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.utils.GridTouchHelper import org.koitharu.kotatsu.utils.ScreenOrientationHelper import org.koitharu.kotatsu.utils.ShareHelper @@ -158,7 +159,7 @@ class ReaderActivity : ) } R.id.action_settings -> { - startActivity(SimpleSettingsActivity.newReaderSettingsIntent(this)) + startActivity(SettingsActivity.newReaderSettingsIntent(this)) } R.id.action_chapters -> { ChaptersBottomSheet.show( diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt deleted file mode 100644 index 049a9de5b..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.koitharu.kotatsu.reader.ui - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.graphics.Insets -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding -import androidx.fragment.app.commit -import com.google.android.material.appbar.AppBarLayout -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseActivity -import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding -import org.koitharu.kotatsu.main.ui.AppBarOwner -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.settings.* - -class SimpleSettingsActivity : BaseActivity(), AppBarOwner { - - override val appBar: AppBarLayout - get() = binding.appbar - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivitySettingsSimpleBinding.inflate(layoutInflater)) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportFragmentManager.commit { - replace( - R.id.container, - when (intent?.action) { - Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment() - ACTION_READER -> ReaderSettingsFragment() - ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() - ACTION_SOURCE -> SourceSettingsFragment.newInstance( - intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL - ) - else -> MainSettingsFragment() - } - ) - } - } - - override fun onWindowInsetsChanged(insets: Insets) { - with(binding.toolbar) { - updatePadding( - left = insets.left, - right = insets.right - ) - updateLayoutParams { - topMargin = insets.top - } - } - } - - companion object { - - private const val ACTION_READER = - "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" - private const val ACTION_SUGGESTIONS = - "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS" - private const val ACTION_SOURCE = - "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" - private const val EXTRA_SOURCE = "source" - - fun newReaderSettingsIntent(context: Context) = - Intent(context, SimpleSettingsActivity::class.java) - .setAction(ACTION_READER) - - fun newSuggestionsSettingsIntent(context: Context) = - Intent(context, SimpleSettingsActivity::class.java) - .setAction(ACTION_SUGGESTIONS) - - fun newSourceSettingsIntent(context: Context, source: MangaSource) = - Intent(context, SimpleSettingsActivity::class.java) - .setAction(ACTION_SOURCE) - .putExtra(EXTRA_SOURCE, source) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 12402fcbc..16a3b16a6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -10,7 +10,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.filter.FilterBottomSheet import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity +import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.utils.ext.serializableArgument import org.koitharu.kotatsu.utils.ext.withArgs @@ -35,10 +35,7 @@ class RemoteListFragment : MangaListFragment() { return when (item.itemId) { R.id.action_source_settings -> { startActivity( - SimpleSettingsActivity.newSourceSettingsIntent( - context ?: return false, - source, - ) + SettingsActivity.newSourceSettingsIntent(context ?: return false, source) ) true } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index a60ee96ac..c8ed656e7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -84,7 +84,7 @@ class MangaSearchRepository( return when { query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit) query.isNotEmpty() -> db.tagsDao.findTags("%$query%", limit) - source != null -> db.tagsDao.findTags(source.name, limit) + source != null -> db.tagsDao.findPopularTags(source.name, limit) else -> db.tagsDao.findPopularTags(limit) }.map { it.toMangaTag() diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt index 1b89b32d3..341c89ac8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.utils.ext.areItemsEquals +import org.koitharu.kotatsu.parsers.util.areItemsEquals sealed interface SearchSuggestionItem { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt new file mode 100644 index 000000000..aca880ea1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -0,0 +1,100 @@ +package org.koitharu.kotatsu.settings + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.TwoStatePreference +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity +import org.koitharu.kotatsu.settings.utils.SliderPreference +import org.koitharu.kotatsu.utils.ext.names +import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat +import java.util.* + +class AppearanceSettingsFragment : + BasePreferenceFragment(R.string.appearance), + SharedPreferences.OnSharedPreferenceChangeListener { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_appearance) + findPreference(AppSettings.KEY_GRID_SIZE)?.run { + summary = "%d%%".format(value) + setOnPreferenceChangeListener { preference, newValue -> + preference.summary = "%d%%".format(newValue) + true + } + } + preferenceScreen?.findPreference(AppSettings.KEY_LIST_MODE)?.run { + entryValues = ListMode.values().names() + setDefaultValueCompat(ListMode.GRID.name) + } + findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = AppSettings.isDynamicColorAvailable + findPreference(AppSettings.KEY_DATE_FORMAT)?.run { + entryValues = resources.getStringArray(R.array.date_formats) + val now = Date().time + entries = entryValues.map { value -> + val formattedDate = settings.getDateFormat(value.toString()).format(now) + if (value == "") { + "${context.getString(R.string.system_default)} ($formattedDate)" + } else { + formattedDate + } + }.toTypedArray() + setDefaultValueCompat("") + summary = "%s" + } + findPreference(AppSettings.KEY_PROTECT_APP) + ?.isChecked = !settings.appPassword.isNullOrEmpty() + } + + 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_THEME -> { + AppCompatDelegate.setDefaultNightMode(settings.theme) + } + AppSettings.KEY_DYNAMIC_THEME -> { + findPreference(key)?.setSummary(R.string.restart_required) + } + AppSettings.KEY_THEME_AMOLED -> { + findPreference(key)?.setSummary(R.string.restart_required) + } + AppSettings.KEY_APP_PASSWORD -> { + findPreference(AppSettings.KEY_PROTECT_APP) + ?.isChecked = !settings.appPassword.isNullOrEmpty() + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + AppSettings.KEY_PROTECT_APP -> { + val pref = (preference as? TwoStatePreference ?: return false) + if (pref.isChecked) { + pref.isChecked = false + startActivity(Intent(preference.context, ProtectSetupActivity::class.java)) + } else { + settings.appPassword = null + } + true + } + else -> super.onPreferenceTreeClick(preference) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt new file mode 100644 index 000000000..5e4c7555b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt @@ -0,0 +1,96 @@ +package org.koitharu.kotatsu.settings + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import java.io.File +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.base.ui.dialog.StorageSelectDialog +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.utils.ext.getStorageName +import org.koitharu.kotatsu.utils.ext.viewLifecycleScope + +class ContentSettingsFragment : + BasePreferenceFragment(R.string.content), + SharedPreferences.OnSharedPreferenceChangeListener, + StorageSelectDialog.OnStorageSelectListener { + + private val storageManager by inject() + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_content) + + findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( + if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled + ) + bindRemoteSourcesSummary() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + findPreference(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName() + settings.subscribe(this) + } + + override fun onDestroyView() { + settings.unsubscribe(this) + super.onDestroyView() + } + + override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_LOCAL_STORAGE -> { + findPreference(key)?.bindStorageName() + } + AppSettings.KEY_SUGGESTIONS -> { + findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( + if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled + ) + } + AppSettings.KEY_SOURCES_HIDDEN -> { + bindRemoteSourcesSummary() + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + AppSettings.KEY_LOCAL_STORAGE -> { + val ctx = context ?: return false + StorageSelectDialog.Builder(ctx, storageManager, this) + .setTitle(preference.title ?: "") + .setNegativeButton(android.R.string.cancel) + .create() + .show() + true + } + else -> super.onPreferenceTreeClick(preference) + } + } + + override fun onStorageSelected(file: File) { + settings.mangaStorageDir = file + } + + private fun Preference.bindStorageName() { + viewLifecycleScope.launch { + val storage = storageManager.getDefaultWriteableDir() + summary = storage?.getStorageName(context) ?: getString(R.string.not_available) + } + } + + private fun bindRemoteSourcesSummary() { + findPreference(AppSettings.KEY_REMOTE_SOURCES)?.run { + val total = MangaSource.values().size - 1 + summary = getString( + R.string.enabled_d_of_d, total - settings.hiddenSources.size, total + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt deleted file mode 100644 index b0a9518dd..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt +++ /dev/null @@ -1,182 +0,0 @@ -package org.koitharu.kotatsu.settings - -import android.content.ComponentName -import android.content.Intent -import android.content.SharedPreferences -import android.os.Bundle -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import androidx.appcompat.app.AppCompatDelegate -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceScreen -import androidx.preference.TwoStatePreference -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.base.ui.dialog.StorageSelectDialog -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.ListMode -import org.koitharu.kotatsu.local.data.LocalStorageManager -import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity -import org.koitharu.kotatsu.settings.utils.SliderPreference -import org.koitharu.kotatsu.utils.ext.getStorageName -import org.koitharu.kotatsu.utils.ext.names -import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -import java.io.File -import java.util.* - - -class MainSettingsFragment : BasePreferenceFragment(R.string.settings), - SharedPreferences.OnSharedPreferenceChangeListener, - StorageSelectDialog.OnStorageSelectListener { - - private val storageManager by inject() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - addPreferencesFromResource(R.xml.pref_main) - findPreference(AppSettings.KEY_GRID_SIZE)?.run { - summary = "%d%%".format(value) - setOnPreferenceChangeListener { preference, newValue -> - preference.summary = "%d%%".format(newValue) - true - } - } - preferenceScreen?.findPreference(AppSettings.KEY_LIST_MODE)?.run { - entryValues = ListMode.values().names() - setDefaultValueCompat(ListMode.GRID.name) - } - findPreference(AppSettings.KEY_DYNAMIC_THEME)?.isVisible = - AppSettings.isDynamicColorAvailable - findPreference(AppSettings.KEY_DATE_FORMAT)?.run { - entryValues = resources.getStringArray(R.array.date_formats) - val now = Date().time - entries = entryValues.map { value -> - val formattedDate = settings.getDateFormat(value.toString()).format(now) - if (value == "") { - "${context.getString(R.string.system_default)} ($formattedDate)" - } else { - formattedDate - } - }.toTypedArray() - setDefaultValueCompat("") - summary = "%s" - } - findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( - if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - findPreference(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName() - findPreference(AppSettings.KEY_PROTECT_APP)?.isChecked = - !settings.appPassword.isNullOrEmpty() - settings.subscribe(this) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.opt_settings, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_leaks -> { - val intent = Intent() - intent.component = ComponentName(requireContext(), "leakcanary.internal.activity.LeakActivity") - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(intent) - true - } - else -> super.onOptionsItemSelected(item) - } - } - - override fun onDestroyView() { - settings.unsubscribe(this) - super.onDestroyView() - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - when (key) { - AppSettings.KEY_THEME -> { - AppCompatDelegate.setDefaultNightMode(settings.theme) - } - AppSettings.KEY_DYNAMIC_THEME -> { - findPreference(key)?.setSummary(R.string.restart_required) - } - AppSettings.KEY_THEME_AMOLED -> { - findPreference(key)?.setSummary(R.string.restart_required) - } - AppSettings.KEY_LOCAL_STORAGE -> { - findPreference(key)?.bindStorageName() - } - AppSettings.KEY_APP_PASSWORD -> { - findPreference(AppSettings.KEY_PROTECT_APP) - ?.isChecked = !settings.appPassword.isNullOrEmpty() - } - AppSettings.KEY_SUGGESTIONS -> { - findPreference(AppSettings.KEY_SUGGESTIONS)?.setSummary( - if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled - ) - } - } - } - - override fun onResume() { - super.onResume() - findPreference(AppSettings.KEY_REMOTE_SOURCES)?.run { - val total = MangaSource.values().size - 1 - summary = getString( - R.string.enabled_d_of_d, total - settings.hiddenSources.size, total - ) - } - } - - override fun onPreferenceTreeClick(preference: Preference): Boolean { - return when (preference.key) { - AppSettings.KEY_LOCAL_STORAGE -> { - val ctx = context ?: return false - StorageSelectDialog.Builder(ctx, storageManager, this) - .setTitle(preference.title ?: "") - .setNegativeButton(android.R.string.cancel) - .create() - .show() - true - } - AppSettings.KEY_PROTECT_APP -> { - val pref = (preference as? TwoStatePreference ?: return false) - if (pref.isChecked) { - pref.isChecked = false - startActivity(Intent(preference.context, ProtectSetupActivity::class.java)) - } else { - settings.appPassword = null - } - true - } - else -> super.onPreferenceTreeClick(preference) - } - } - - override fun onStorageSelected(file: File) { - settings.mangaStorageDir = file - } - - private fun Preference.bindStorageName() { - viewLifecycleScope.launch { - val storage = storageManager.getDefaultWriteableDir() - summary = storage?.getStorageName(context) ?: getString(R.string.not_available) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt similarity index 61% rename from app/src/main/java/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt rename to app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt index 8bfb2c3d6..ad02acf64 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/NetworkSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/RootSettingsFragment.kt @@ -4,9 +4,9 @@ import android.os.Bundle import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -class NetworkSettingsFragment : BasePreferenceFragment(R.string.settings) { +class RootSettingsFragment : BasePreferenceFragment(R.string.settings) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - //TODO https://developer.android.com/training/basics/network-ops/managing + addPreferencesFromResource(R.xml.pref_root) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt index 04afdd246..8b82b327c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -1,9 +1,13 @@ package org.koitharu.kotatsu.settings +import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import androidx.core.graphics.Insets +import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction @@ -11,6 +15,7 @@ import androidx.fragment.app.commit import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.appbar.AppBarLayout +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner @@ -34,9 +39,7 @@ class SettingsActivity : supportActionBar?.setDisplayHomeAsUpEnabled(true) if (supportFragmentManager.findFragmentById(R.id.container) == null) { - supportFragmentManager.commit { - replace(R.id.container, MainSettingsFragment()) - } + openDefaultFragment() } } @@ -55,6 +58,22 @@ class SettingsActivity : super.onStop() } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.opt_settings, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.action_leaks -> { + val intent = Intent() + intent.component = ComponentName(this, "leakcanary.internal.activity.LeakActivity") + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + true + } + else -> super.onOptionsItemSelected(item) + } + override fun onBackStackChanged() { val fragment = supportFragmentManager.findFragmentById(R.id.container) as? RecyclerViewOwner ?: return val recyclerView = fragment.recyclerView @@ -70,32 +89,66 @@ class SettingsActivity : val fm = supportFragmentManager val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false) fragment.arguments = pref.extras - fragment.setTargetFragment(caller, 0) + // fragment.setTargetFragment(caller, 0) openFragment(fragment) return true } - fun openMangaSourceSettings(mangaSource: MangaSource) { - openFragment(SourceSettingsFragment.newInstance(mangaSource)) + override fun onWindowInsetsChanged(insets: Insets) { + binding.appbar.updatePadding( + left = insets.left, + right = insets.right, + ) + binding.container.updatePadding( + left = insets.left, + right = insets.right, + ) } - fun openNotificationSettingsLegacy() { - openFragment(NotificationSettingsLegacyFragment()) - } - - private fun openFragment(fragment: Fragment) { + fun openFragment(fragment: Fragment) { supportFragmentManager.commit { - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - replace(R.id.container, fragment) setReorderingAllowed(true) + replace(R.id.container, fragment) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) addToBackStack(null) } } - override fun onWindowInsetsChanged(insets: Insets) = Unit + private fun openDefaultFragment() { + val fragment = when (intent?.action) { + ACTION_READER -> ReaderSettingsFragment() + ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() + ACTION_SOURCE -> SourceSettingsFragment.newInstance( + intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL + ) + else -> SettingsHeadersFragment() + } + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.container, fragment) + } + } companion object { + private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" + private const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS" + private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" + private const val EXTRA_SOURCE = "source" + fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java) + + fun newReaderSettingsIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_READER) + + fun newSuggestionsSettingsIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_SUGGESTIONS) + + fun newSourceSettingsIntent(context: Context, source: MangaSource) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_SOURCE) + .putExtra(EXTRA_SOURCE, source) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt new file mode 100644 index 000000000..bec03f017 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsHeadersFragment.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.settings + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction +import androidx.fragment.app.commit +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceHeaderFragmentCompat +import androidx.slidingpanelayout.widget.SlidingPaneLayout +import org.koitharu.kotatsu.R + +class SettingsHeadersFragment : PreferenceHeaderFragmentCompat(), SlidingPaneLayout.PanelSlideListener { + + private var currentTitle: CharSequence? = null + + override fun onCreatePreferenceHeader(): PreferenceFragmentCompat = RootSettingsFragment() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + slidingPaneLayout.addPanelSlideListener(this) + } + + override fun onPanelSlide(panel: View, slideOffset: Float) = Unit + + override fun onPanelOpened(panel: View) { + activity?.title = currentTitle ?: getString(R.string.settings) + } + + override fun onPanelClosed(panel: View) { + activity?.setTitle(R.string.settings) + } + + fun setTitle(title: CharSequence?) { + currentTitle = title + if (slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) { + activity?.title = title + } + } + + fun openFragment(fragment: Fragment) { + childFragmentManager.commit { + setReorderingAllowed(true) + replace(androidx.preference.R.id.preferences_detail, fragment) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + addToBackStack(null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index fc670874a..384905df2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -25,7 +25,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { override fun onResume() { super.onResume() - activity?.title = source.title + setTitle(source.title) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt index 02467f1d6..46aced8a0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt @@ -4,10 +4,13 @@ import android.content.SharedPreferences import android.os.Bundle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch +import org.koin.android.ext.android.get 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.settings.utils.MultiAutoCompleteTextViewPreference +import org.koitharu.kotatsu.settings.utils.TagsAutoCompleteProvider import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker @@ -23,6 +26,11 @@ class SuggestionsSettingsFragment : BasePreferenceFragment(R.string.suggestions) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_suggestions) + + findPreference(AppSettings.KEY_SUGGESTIONS_EXCLUDE_TAGS)?.run { + autoCompleteProvider = TagsAutoCompleteProvider(get()) + summaryProvider = MultiAutoCompleteTextViewPreference.SimpleSummaryProvider(summary) + } } override fun onDestroy() { 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 a7191a756..1e993f845 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/TrackerSettingsFragment.kt @@ -31,7 +31,6 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch append(getString(R.string.read_more)) } } - warningPreference } } @@ -43,10 +42,10 @@ class TrackerSettingsFragment : BasePreferenceFragment(R.string.check_for_new_ch .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) .putExtra(Settings.EXTRA_CHANNEL_ID, TrackWorker.CHANNEL_ID) startActivity(intent) + true } else { - (activity as? SettingsActivity)?.openNotificationSettingsLegacy() + super.onPreferenceTreeClick(preference) } - true } else -> super.onPreferenceTreeClick(preference) } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt index a88856b15..8e149f83b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt @@ -1,13 +1,13 @@ package org.koitharu.kotatsu.settings.about +import android.content.Intent import android.os.Bundle -import android.view.View +import androidx.core.net.toUri import androidx.preference.Preference import kotlinx.coroutines.launch import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment -import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.utils.ext.viewLifecycleScope @@ -16,20 +16,16 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_about) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + val isUpdateSupported = AppUpdateChecker.isUpdateSupported(requireContext()) findPreference(AppSettings.KEY_APP_UPDATE_AUTO)?.run { - isVisible = AppUpdateChecker.isUpdateSupported(context) + isVisible = isUpdateSupported } findPreference(AppSettings.KEY_APP_VERSION)?.run { title = getString(R.string.app_version, BuildConfig.VERSION_NAME) - isEnabled = AppUpdateChecker.isUpdateSupported(context) + isEnabled = isUpdateSupported } } - override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { AppSettings.KEY_APP_VERSION -> { @@ -37,39 +33,19 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { true } AppSettings.KEY_APP_TRANSLATION -> { - startActivity(context?.let { BrowserActivity.newIntent(it, - "https://hosted.weblate.org/engage/kotatsu", - resources.getString(R.string.about_app_translation)) }) + openLink(getString(R.string.url_weblate), preference.title) true } AppSettings.KEY_FEEDBACK_4PDA -> { - startActivity(context?.let { BrowserActivity.newIntent(it, - "https://4pda.to/forum/index.php?showtopic=697669", - resources.getString(R.string.about_feedback_4pda)) }) + openLink(getString(R.string.url_forpda), preference.title) true } AppSettings.KEY_FEEDBACK_DISCORD -> { - startActivity(context?.let { BrowserActivity.newIntent(it, - "https://discord.gg/NNJ5RgVBC5", - "Discord") }) + openLink(getString(R.string.url_discord), preference.title) true } AppSettings.KEY_FEEDBACK_GITHUB -> { - startActivity(context?.let { BrowserActivity.newIntent(it, - "https://github.com/nv95/Kotatsu/issues", - "GitHub") }) - true - } - AppSettings.KEY_SUPPORT_DEVELOPER -> { - startActivity(context?.let { BrowserActivity.newIntent(it, - "https://yoomoney.ru/to/410012543938752", - resources.getString(R.string.about_support_developer)) }) - true - } - AppSettings.KEY_APP_GRATITUDES -> { - startActivity(context?.let { BrowserActivity.newIntent(it, - "https://github.com/nv95/Kotatsu/graphs/contributors", - resources.getString(R.string.about_gratitudes)) }) + openLink(getString(R.string.url_github_issues), preference.title) true } else -> super.onPreferenceTreeClick(preference) @@ -95,4 +71,16 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { } } } -} + + private fun openLink(url: String, title: CharSequence?) { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = url.toUri() + startActivity( + if (title != null) { + Intent.createChooser(intent, title) + } else { + intent + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/LicenseFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/LicenseFragment.kt deleted file mode 100644 index 8d25e3ca9..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/LicenseFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.koitharu.kotatsu.settings.about - -import android.os.Bundle -import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.graphics.Insets -import androidx.core.text.HtmlCompat -import androidx.core.text.parseAsHtml -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.databinding.FragmentCopyrightBinding - -class LicenseFragment : BaseFragment() { - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.textView.apply { - text = - SpannableStringBuilder(resources.openRawResource(R.raw.copyright).bufferedReader() - .readText() - .parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST)) - movementMethod = LinkMovementMethod.getInstance() - } - } - - override fun onInflateView( - inflater: LayoutInflater, - container: ViewGroup? - ) = FragmentCopyrightBinding.inflate(inflater, container, false) - - override fun onResume() { - super.onResume() - activity?.setTitle(R.string.about_license) - } - - override fun onWindowInsetsChanged(insets: Insets) = Unit - -} \ No newline at end of file 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 4e336c181..b9f97f37d 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.settings.onboard.model.SourceLocale import org.koitharu.kotatsu.utils.ext.map diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 200d8c31f..60a5b6ff9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -15,6 +15,8 @@ import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.SettingsHeadersFragment +import org.koitharu.kotatsu.settings.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem @@ -87,7 +89,9 @@ class SourcesSettingsFragment : BaseFragment(), } override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) { - (activity as? SettingsActivity)?.openMangaSourceSettings(item.source) + val fragment = SourceSettingsFragment.newInstance(item.source) + (parentFragment as? SettingsHeadersFragment)?.openFragment(fragment) + ?: (activity as? SettingsActivity)?.openFragment(fragment) } override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt new file mode 100644 index 000000000..bccaaa942 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt @@ -0,0 +1,118 @@ +package org.koitharu.kotatsu.settings.utils + +import android.content.Context +import android.util.AttributeSet +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.Filter +import android.widget.MultiAutoCompleteTextView +import androidx.annotation.AttrRes +import androidx.annotation.MainThread +import androidx.annotation.StyleRes +import androidx.annotation.WorkerThread +import androidx.preference.EditTextPreference +import kotlinx.coroutines.runBlocking +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.parsers.util.replaceWith + +class MultiAutoCompleteTextViewPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = R.attr.multiAutoCompleteTextViewPreferenceStyle, + @StyleRes defStyleRes: Int = R.style.Preference_MultiAutoCompleteTextView, +) : EditTextPreference(context, attrs, defStyleAttr, defStyleRes) { + + private val autoCompleteBindListener = AutoCompleteBindListener() + + var autoCompleteProvider: AutoCompleteProvider? = null + + init { + super.setOnBindEditTextListener(autoCompleteBindListener) + } + + override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) { + autoCompleteBindListener.delegate = onBindEditTextListener + } + + private inner class AutoCompleteBindListener : OnBindEditTextListener { + + var delegate: OnBindEditTextListener? = null + + override fun onBindEditText(editText: EditText) { + delegate?.onBindEditText(editText) + if (editText !is MultiAutoCompleteTextView) { + return + } + editText.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer()) + editText.setAdapter( + autoCompleteProvider?.let { + CompletionAdapter(editText.context, it, ArrayList()) + } + ) + editText.threshold = 1 + } + } + + interface AutoCompleteProvider { + + suspend fun getSuggestions(query: String): List + } + + class SimpleSummaryProvider( + private val emptySummary: CharSequence?, + ) : SummaryProvider { + + override fun provideSummary(preference: MultiAutoCompleteTextViewPreference): CharSequence? { + return if (preference.text.isNullOrEmpty()) { + emptySummary + } else { + preference.text?.trimEnd(' ', ',') + } + } + } + + private class CompletionAdapter( + context: Context, + private val completionProvider: AutoCompleteProvider, + private val dataset: MutableList, + ) : ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, dataset) { + + override fun getFilter(): Filter { + return CompletionFilter(this, completionProvider) + } + + fun publishResults(results: List) { + dataset.replaceWith(results) + notifyDataSetChanged() + } + } + + private class CompletionFilter( + private val adapter: CompletionAdapter, + private val provider: AutoCompleteProvider, + ) : Filter() { + + @WorkerThread + override fun performFiltering(constraint: CharSequence?): FilterResults { + val query = constraint?.toString().orEmpty() + val suggestions = runBlocking { provider.getSuggestions(query) } + return CompletionResults(suggestions) + } + + @MainThread + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + val completions = (results as CompletionResults).completions + adapter.publishResults(completions) + } + + private class CompletionResults( + val completions: List, + ) : FilterResults() { + + init { + values = completions + count = completions.size + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt index 472467a1d..0933fe1d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/SliderPreference.kt @@ -30,18 +30,22 @@ class SliderPreference @JvmOverloads constructor( set(value) = setValueInternal(value, notifyChanged = true) private val sliderListener = Slider.OnChangeListener { _, value, fromUser -> - if (fromUser) { - syncValueInternal(value.toInt()) - } + if (fromUser) { + syncValueInternal(value.toInt()) } + } init { - context.withStyledAttributes(attrs, + context.withStyledAttributes( + attrs, R.styleable.SliderPreference, defStyleAttr, - defStyleRes) { - valueFrom = getFloat(R.styleable.SliderPreference_android_valueFrom, - valueFrom.toFloat()).toInt() + defStyleRes + ) { + valueFrom = getFloat( + R.styleable.SliderPreference_android_valueFrom, + valueFrom.toFloat() + ).toInt() valueTo = getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt() stepSize = diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt new file mode 100644 index 000000000..53998e48b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.settings.utils + +import org.koitharu.kotatsu.core.db.MangaDatabase + +class TagsAutoCompleteProvider( + private val db: MangaDatabase, +) : MultiAutoCompleteTextViewPreference.AutoCompleteProvider { + + override suspend fun getSuggestions(query: String): List { + if (query.isEmpty()) { + return emptyList() + } + val tags = db.tagsDao.findTags(query = "$query%", limit = 6) + val set = HashSet() + val result = ArrayList(tags.size) + for (tag in tags) { + if (set.add(tag.title)) { + result.add(tag.title) + } + } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt index a7bc69ace..17dd71840 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -6,9 +6,9 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.utils.ext.mapItems -import org.koitharu.kotatsu.utils.ext.mapToSet class SuggestionRepository( private val db: MangaDatabase, diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index 46f36bef0..ceb873f49 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -9,7 +9,7 @@ import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.list.ui.MangaListFragment -import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity +import org.koitharu.kotatsu.settings.SettingsActivity class SuggestionsFragment : MangaListFragment() { @@ -38,7 +38,7 @@ class SuggestionsFragment : MangaListFragment() { true } R.id.action_settings -> { - startActivity(SimpleSettingsActivity.newSuggestionsSettingsIntent(requireContext())) + startActivity(SettingsActivity.newSuggestionsSettingsIntent(requireContext())) true } else -> super.onOptionsItemSelected(item) diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 4420d4481..7433e689b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -1,16 +1,29 @@ package org.koitharu.kotatsu.suggestions.ui +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.os.Build +import androidx.annotation.FloatRange +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import androidx.work.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.domain.HistoryRepository -import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository +import org.koitharu.kotatsu.utils.ext.asArrayList +import org.koitharu.kotatsu.utils.ext.trySetForeground import java.util.concurrent.TimeUnit import kotlin.math.pow @@ -21,11 +34,41 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : private val historyRepository by inject() private val appSettings by inject() - override suspend fun doWork(): Result = try { + override suspend fun doWork(): Result { val count = doWorkImpl() - Result.success(workDataOf(DATA_COUNT to count)) - } catch (t: Throwable) { - Result.failure() + val outputData = workDataOf(DATA_COUNT to count) + return Result.success(outputData) + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val title = applicationContext.getString(R.string.suggestions_updating) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + WORKER_CHANNEL_ID, + title, + NotificationManager.IMPORTANCE_LOW + ) + channel.setShowBadge(false) + channel.enableVibration(false) + channel.setSound(null, null) + channel.enableLights(false) + manager.createNotificationChannel(channel) + } + + val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID) + .setContentTitle(title) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setDefaults(0) + .setColor(ContextCompat.getColor(applicationContext, R.color.blue_primary_dark)) + .setSilent(true) + .setProgress(0, 0, true) + .setSmallIcon(android.R.drawable.stat_notify_sync) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) + .setOngoing(true) + .build() + + return ForegroundInfo(WORKER_NOTIFICATION_ID, notification) } private suspend fun doWorkImpl(): Int { @@ -33,47 +76,75 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : suggestionRepository.clear() return 0 } - val rawResults = ArrayList() - val allTags = historyRepository.getAllTags() + val blacklistTagRegex = appSettings.getSuggestionsTagsBlacklistRegex() + val allTags = historyRepository.getPopularTags(TAGS_LIMIT).filterNot { + blacklistTagRegex?.containsMatchIn(it.title) ?: false + } if (allTags.isEmpty()) { return 0 } + if (TAG in tags) { // not expedited + trySetForeground() + } val tagsBySources = allTags.groupBy { x -> x.source } - for ((source, tags) in tagsBySources) { - val repo = MangaRepository(source) - tags.flatMapTo(rawResults) { tag -> - repo.getList( - offset = 0, - sortOrder = SortOrder.UPDATED, - tags = setOf(tag), - ) - } + val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) + val rawResults = coroutineScope { + tagsBySources.flatMap { (source, tags) -> + val repo = MangaRepository(source) + tags.map { tag -> + async(dispatcher) { + repo.getList( + offset = 0, + sortOrder = SortOrder.UPDATED, + tags = setOf(tag), + ) + } + } + }.awaitAll().flatten().asArrayList() } if (appSettings.isSuggestionsExcludeNsfw) { rawResults.removeAll { it.isNsfw } } + if (blacklistTagRegex != null) { + rawResults.removeAll { + it.tags.any { x -> blacklistTagRegex.containsMatchIn(x.title) } + } + } if (rawResults.isEmpty()) { return 0 } val suggestions = rawResults.distinctBy { manga -> manga.id }.map { manga -> - val jointTags = manga.tags intersect allTags MangaSuggestion( manga = manga, - relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(), + relevance = computeRelevance(manga.tags, allTags) ) }.sortedBy { it.relevance }.take(LIMIT) suggestionRepository.replace(suggestions) return suggestions.size } + @FloatRange(from = 0.0, to = 1.0) + private fun computeRelevance(mangaTags: Set, allTags: List): Float { + val maxWeight = (allTags.size + allTags.size + 1 - mangaTags.size) * mangaTags.size / 2.0 + val weight = mangaTags.sumOf { tag -> + val index = allTags.indexOf(tag) + if (index < 0) 0 else allTags.size - index + } + return (weight / maxWeight).pow(2.0).toFloat() + } + companion object { private const val TAG = "suggestions" private const val TAG_ONESHOT = "suggestions_oneshot" private const val LIMIT = 140 + private const val TAGS_LIMIT = 20 + private const val MAX_PARALLELISM = 4 private const val DATA_COUNT = "count" + private const val WORKER_CHANNEL_ID = "suggestion_worker" + private const val WORKER_NOTIFICATION_ID = 36 fun setup(context: Context) { val constraints = Constraints.Builder() @@ -96,6 +167,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) .addTag(TAG_ONESHOT) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() WorkManager.getInstance(context) .enqueue(request) 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 04ad1695b..a61b25c47 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 @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.toBitmapOrNull +import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.progress.Progress import java.util.concurrent.TimeUnit @@ -53,7 +54,9 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : if (tracks.isEmpty()) { return Result.success() } - setForeground(createForegroundInfo()) + if (TAG in tags) { // not expedited + trySetForeground() + } var success = 0 val workData = Data.Builder() .putInt(DATA_TOTAL, tracks.size) @@ -201,7 +204,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : } } - private fun createForegroundInfo(): ForegroundInfo { + override suspend fun getForegroundInfo(): ForegroundInfo { val title = applicationContext.getString(R.string.check_for_new_chapters) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( @@ -281,6 +284,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) .addTag(TAG_ONESHOT) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() WorkManager.getInstance(context) .enqueue(request) 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 2fc663c9b..21140be00 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 @@ -5,6 +5,7 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkRequest import android.net.Uri +import androidx.work.CoroutineWorker import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine @@ -28,4 +29,9 @@ suspend fun ConnectivityManager.waitForNetwork(): Network { } } -fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) \ No newline at end of file +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 diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt index 5ba3d0aa2..916d83491 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CollectionExt.kt @@ -3,46 +3,12 @@ package org.koitharu.kotatsu.utils.ext import androidx.collection.ArraySet import java.util.* -fun MutableCollection.replaceWith(subject: Iterable) { - clear() - addAll(subject) -} - -fun List.medianOrNull(): T? = when { - isEmpty() -> null - else -> get((size / 2).coerceIn(indices)) -} - -inline fun Collection.mapToSet(transform: (T) -> R): Set { - return mapTo(ArraySet(size), transform) -} - fun LongArray.toArraySet(): Set = createSet(size) { i -> this[i] } fun > Array.names() = Array(size) { i -> this[i].name } -fun Collection.isDistinct(): Boolean { - val set = HashSet(size) - for (item in this) { - if (!set.add(item)) { - return false - } - } - return set.size == size -} - -fun Collection.isDistinctBy(selector: (T) -> K): Boolean { - val set = HashSet(size) - for (item in this) { - if (!set.add(selector(item))) { - return false - } - } - return set.size == size -} - fun MutableList.move(sourceIndex: Int, targetIndex: Int) { if (sourceIndex <= targetIndex) { Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) @@ -51,20 +17,6 @@ fun MutableList.move(sourceIndex: Int, targetIndex: Int) { } } -inline fun List.areItemsEquals(other: List, equals: (T, T) -> Boolean): Boolean { - if (size != other.size) { - return false - } - for (i in indices) { - val a = this[i] - val b = other[i] - if (!equals(a, b)) { - return false - } - } - return true -} - @Suppress("FunctionName") inline fun MutableSet(size: Int, init: (index: Int) -> T): MutableSet { val set = ArraySet(size) @@ -82,4 +34,10 @@ inline fun createList(size: Int, init: (index: Int) -> T): List = when (s 0 -> emptyList() 1 -> Collections.singletonList(init(0)) else -> MutableList(size, init) +} + +fun List.asArrayList(): ArrayList = if (this is ArrayList<*>) { + this as ArrayList +} else { + ArrayList(this) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt index 7e104e0f4..67ca43114 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CommonExt.kt @@ -2,14 +2,15 @@ package org.koitharu.kotatsu.utils.ext import android.content.res.Resources import android.util.Log +import java.io.FileNotFoundException +import java.net.SocketTimeoutException import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException -import java.io.FileNotFoundException -import java.net.SocketTimeoutException +import org.koitharu.kotatsu.parsers.util.format fun Throwable.getDisplayMessage(resources: Resources) = when (this) { is AuthRequiredException -> resources.getString(R.string.auth_required) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt index 14e7cbb9d..a8359f5a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/PrimitiveExt.kt @@ -1,27 +1,3 @@ package org.koitharu.kotatsu.utils.ext -import java.text.DecimalFormat -import java.text.NumberFormat -import java.util.* - -fun Number.format(decimals: Int = 0, decPoint: Char = '.', thousandsSep: Char? = ' '): String { - val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat - val symbols = formatter.decimalFormatSymbols - if (thousandsSep != null) { - symbols.groupingSeparator = thousandsSep - formatter.isGroupingUsed = true - } else { - formatter.isGroupingUsed = false - } - symbols.decimalSeparator = decPoint - formatter.decimalFormatSymbols = symbols - formatter.minimumFractionDigits = decimals - formatter.maximumFractionDigits = decimals - return when (this) { - is Float, - is Double -> formatter.format(this.toDouble()) - else -> formatter.format(this.toLong()) - } -} - inline fun Int.ifZero(defaultValue: () -> Int): Int = if (this == 0) defaultValue() else this \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_appearance.xml b/app/src/main/res/drawable/ic_appearance.xml new file mode 100644 index 000000000..b5aa6519c --- /dev/null +++ b/app/src/main/res/drawable/ic_appearance.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_copyleft.xml b/app/src/main/res/drawable/ic_copyleft.xml deleted file mode 100644 index 47ac8bbca..000000000 --- a/app/src/main/res/drawable/ic_copyleft.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/app/src/main/res/layout-w600dp/activity_settings.xml b/app/src/main/res/layout-w600dp/activity_settings.xml index c381476a0..642380efc 100644 --- a/app/src/main/res/layout-w600dp/activity_settings.xml +++ b/app/src/main/res/layout-w600dp/activity_settings.xml @@ -1,5 +1,5 @@ - + app:layout_constraintTop_toTopOf="parent" + app:liftOnScroll="false"> - + - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_simple.xml b/app/src/main/res/layout/activity_settings_simple.xml deleted file mode 100644 index fa9c5d762..000000000 --- a/app/src/main/res/layout/activity_settings_simple.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_copyright.xml b/app/src/main/res/layout/fragment_copyright.xml deleted file mode 100644 index fe9c3e4e1..000000000 --- a/app/src/main/res/layout/fragment_copyright.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml b/app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml new file mode 100644 index 000000000..bcf067dc3 --- /dev/null +++ b/app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_slider.xml b/app/src/main/res/layout/preference_slider.xml similarity index 100% rename from app/src/main/res/layout/pref_slider.xml rename to app/src/main/res/layout/preference_slider.xml diff --git a/app/src/main/res/raw/copyright b/app/src/main/res/raw/copyright deleted file mode 100644 index bf4212730..000000000 --- a/app/src/main/res/raw/copyright +++ /dev/null @@ -1,24 +0,0 @@ -

Kotatsu is a free and open source manga reader for Android.

-

Copyright (C) 2020 by Koitharu

-

This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version.

-

This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details.

-

You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/.

-

Open Source Licenses

- \ 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 0f0e6134e..0e8ab0bad 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -217,13 +217,7 @@ Гэтая глава адсутнічае на вашай прыладзе. Спампуйце ціпрачытайце яе онлайн. На дадзены момант няма актыўных спамповак У чарзе - Ліцэнзія - Аўтарскія правы і ліцэнзіі - Гэтыя людзі робяць Kotatsu лепш - Падзякі - Калі вам падабаецца гэтая праграма, вы можаце дапамагчы фінансава з дапамогай ЮMoney (был. Яндекс.Деньги) - Падтрымаць распрацоўшчыка - Тэма на 4PDA + Тэма на 4PDA Зваротная сувязь Дапамагчы з перакладам праграмы Пераклад diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 966ee5667..d25f7be80 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -217,13 +217,7 @@ Dieses Kapitel fehlt auf deinem Gerät. Lade ihn herunter oder lese ihn online. Zurzeit sind keine aktiven Datenübertragungen vorhanden In Warteschlange - Wenn diese Anwendung dir gefällt, kannst du über Yoomoney (Yandex.Money) finanziell helfen - Lizenz - Urheberrecht und Lizenzen - Diese Leute machen Kotatsu besser - Danksagung - Den Entwickler unterstützen - Thema auf 4PDA + Thema auf 4PDA Rückmeldung Übersetzung Diese Anwendung übersetzen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1b64f874e..44bb53494 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -205,14 +205,8 @@ Encuentra qué leer en el menú lateral. Lo que leas se mostrará aquí Está un poco vacío aquí… - Agradecimientos - Si te gusta esta aplicación, puedes ayudar económicamente a través de Yoomoney (ex. Yandex.Money) - Apoyar al desarrollador - Buscar sólo en %s - Todas estas personas hicieron que Kotatsu fuera mejor - Licencia - Derechos de autor y licencias - Falta un capítulo + Buscar sólo en %s + Falta un capítulo Traducir esta aplicación Comentarios Traducción diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 022d894ce..69ef71847 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -4,13 +4,7 @@ Tyylilajit %s -valtuutusta ei tueta Valtuutus valmis - Lisenssi - Tekijänoikeudet ja lisenssit - Nämä ihmiset tekevät Kotatsusta paremman - Kiitokset - Jos pidät tästä sovelluksesta, voit auttaa taloudellisesti Yoomoneyn (ent. Yandex.Money) kautta - Tue kehittäjää - Aihe 4PDA: ssa + Aihe 4PDA: ssa Palaute Käännös Käännä tämä sovellus diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a20bce18d..c76fb6027 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -217,13 +217,7 @@ Téléchargez ou lisez ce chapitre manquant en ligne. Aucun téléchargement actif En file d\'attente - Si vous aimez cette application, vous pouvez envoyer de l\'argent via Yoomoney (ex. Yandex.Money) - Soutenir le concepteur - Licence - Droits d\'auteur et licences - Ces personnes ont toutes rendu Kotatsu meilleur - Remerciements - Sujet sur 4PDA + Sujet sur 4PDA Remarques Traduction Traduire cette application diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4245f4355..a1586ed71 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -217,13 +217,7 @@ Questo capitolo manca sul tuo dispositivo. Scaricalo o leggilo in linea. Attualmente non ci sono scaricamenti attivi In coda - Licenza - Diritti d\'autore e licenze - Queste persone rendono Kotatsu migliore - Ringraziamenti - Se ti piace questa applicazione, puoi aiutare finanziariamente via Yoomoney (Yandex.Money) - Sostieni lo sviluppatore - Argomento su 4PDA + Argomento su 4PDA Commenti Traduzione Traduci questa applicazione diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index dbf16817d..4e3fdfcd5 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -163,10 +163,7 @@ 右から左(←)の読書を好む フィードバック 4PDAに関する話題 - 開発者をサポートします(Yoomoneyが開きます) - このアプリが気に入ったら、Yoomoney(Yandex.Moneyなど)から開発者をサポートできます - ライセンス - 承認済み + 承認済み %sへのログインはサポートされていません 完成 進行中 @@ -212,12 +209,10 @@ このアプリを翻訳 全てのデータが復元されました 復元 - 感謝の気持ち - 準備中… + 準備中… 開始時に維持 バックアップと復元 - 著作権とライセンス - リバース + リバース 選択した構成はこの漫画のために記憶されます クッキーを削除 GitHubでissueを作成する(GitHubのアカウントが必要です) @@ -235,8 +230,7 @@ スケールモード 名前を入力する必要があります 今日 - この人達のお陰でKotatsuがより良くなりました - Kotatsuを翻訳する(Weblateのサイトに移動します) + Kotatsuを翻訳する(Weblateのサイトに移動します) 次のページ サイレント サインイン diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 04a7d2dd5..74c07b980 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -223,13 +223,7 @@ Oversettelse Tilbakemelding Emne på 4PDA - Støtt utvikleren - Hvis du liker programmet kan du kronerulle det på Yoomoney (tidligere Yandex.Money) - Takk rettes til - Folk som gjorde Kotatsu enda bedre - Opphavsrett og lisenser - Lisens - Identitetsbekreftet + Identitetsbekreftet Innlogging på %s støttes ikke Du vil bli utlogget fra alle kilder Sjangere diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index afa68d7a0..25bc9f483 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -183,12 +183,7 @@ Tradução Comentar Tópico no 4PDA - Apoie o desenvolvedor - Se você gosta deste aplicativo, você pode enviar dinheiro através do Yoomoney (ex. Yandex.Money) - Gratidão - Todas essas pessoas tornaram o Kotatsu melhor - Licença - Autorizado + Autorizado O login em %s não é suportado Você será desconectado de todas as fontes Gêneros @@ -266,8 +261,7 @@ Os dados foram restaurados, mas há erros Criar problema no GitHub Arquivo não encontrado - Direitos autorais e licenças - Localizar capítulo + Localizar capítulo Não há capítulos neste mangá %1$s%% \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 24d687be3..0f0d99fc0 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -185,13 +185,7 @@ Traduzir esta aplicação Comentar Tópico no 4PDA - Apoiar o desenvolvedor - Se você gosta deste aplicativo, você pode enviar dinheiro através do Yoomoney (ex. Yandex.Money) - Agradecimentos - Todas essas pessoas tornaram o Kotatsu melhor - Direitos de autor e licenças - Licença - Baixe ou leia este capítulo perdido online. + Baixe ou leia este capítulo perdido online. O capítulo está em falta Autorizado O login em %s não é suportado diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4801b3498..6a432f53a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -220,13 +220,7 @@ Перевод Тема на 4PDA Обратная связь - Поддержать разработчика - Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) - Благодарности - Все эти люди сделали Kotatsu лучше - Авторские права и лицензии - Лицензия - Авторизация выполнена + Авторизация выполнена Вход в %s не поддерживается Вы выйдете из всех источников Жанры @@ -269,4 +263,9 @@ Разные языки Найти главу В этой манге нет глав + Оформление + Контент + Обновление рекомендаций + Исключить жанры + Укажите жанры, которые Вы не хотите видеть в рекомендациях \ No newline at end of file diff --git a/app/src/main/res/values-sw360dp/bools.xml b/app/src/main/res/values-sw360dp/bools.xml new file mode 100644 index 000000000..927afe2f8 --- /dev/null +++ b/app/src/main/res/values-sw360dp/bools.xml @@ -0,0 +1,6 @@ + + + + false + + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c9154f6a5..eb03a51d6 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -131,8 +131,7 @@ Yedekten geri yükle Güncelle Oturum aç - Lisans - Bitti + Bitti Hakkında Bu içeriği görüntülemek için oturum açın Onayla @@ -185,9 +184,7 @@ Çeviri Yalnızca %s içinde ara 4PDA\'daki konu - Teşekkürler - Tüm bu insanlar Kotatsu\'yu daha iyi hale getirdi - Devam ediyor + Devam ediyor Tüm kaynaklardaki oturumunuz kapatılacak Kullanılan kaynaklar Kullanılabilir kaynaklar @@ -226,16 +223,13 @@ CAPTCHA gerekli Tüm çerezler kaldırıldı Seçilen yapılandırma bu manga için hatırlanacak - Bu uygulamayı beğendiyseniz Yoomoney (eski Yandex.Money) üzerinden para gönderebilirsiniz - Tüm güncelleme geçmişi kalıcı olarak silinsin mi\? + Tüm güncelleme geçmişi kalıcı olarak silinsin mi\? …ve %1$d daha fazlası Uygulamayı başlatmak için bir parola girin Tüm son arama sorguları kalıcı olarak kaldırılsın mı\? Geri bildirim Yedek kaydedildi - Geliştiriciyi destekleyin - Telif Hakkı ve Lisanslar - Türler + Türler Tarih biçimi Öntanımlı Bir ad girmelisiniz diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 42d3174b2..449fc3a29 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -2,6 +2,7 @@ + diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml index 967cf9dce..6cb53be9e 100644 --- a/app/src/main/res/values/bools.xml +++ b/app/src/main/res/values/bools.xml @@ -1,5 +1,5 @@ - + false true false diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 2e676d010..5132610e6 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -1,5 +1,9 @@ + https://github.com/nv95/Kotatsu/issues + https://discord.gg/NNJ5RgVBC5 + https://4pda.to/forum/index.php?showtopic=697669 + https://hosted.weblate.org/engage/kotatsu -1 1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a825c48a6..a44dfec15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -222,13 +222,7 @@ Translation Feedback Topic on 4PDA - Support the developer - If you like this app, you can send money through Yoomoney (ex. Yandex.Money) - Gratitudes - These people all made Kotatsu better - Copyright and Licenses - License - Authorized + Authorized Logging in on %s is not supported You will be logged out from all sources Genres @@ -270,6 +264,13 @@ Find chapter No chapters in this manga %1$s%% + Appearance + Content + GitHub + Discord + Suggestions updating + Exclude genres + Specify genres that you do not want to see in the suggestions Delete selected items from device permanently? Removal completed Are you sure you want to download all selected manga with all its chapters? This action can consume a lot of traffic and storage diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2acf59e35..2ad34b167 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - + @@ -149,7 +149,11 @@ + + diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index 7be3e5c8a..bde3d1904 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -1,84 +1,44 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> - + + android:key="app_version" + android:persistent="false" + android:summary="@string/check_for_updates" /> - - - - - - + android:defaultValue="true" + android:key="app_update_auto" + android:summary="@string/show_notification_app_update" + android:title="@string/application_update" /> - + + android:key="about_feedback_github" + android:persistent="false" + android:summary="@string/url_github_issues" + android:title="@string/github" /> + android:key="about_feedback_4pda" + android:summary="@string/url_forpda" + android:title="@string/about_feedback_4pda" /> - - - - + android:key="about_feedback_discord" + android:summary="@string/url_discord" + android:title="@string/discord" /> + android:key="about_app_translation" + android:summary="@string/about_app_translation_summary" + android:title="@string/about_app_translation" /> diff --git a/app/src/main/res/xml/pref_appearance.xml b/app/src/main/res/xml/pref_appearance.xml new file mode 100644 index 000000000..eac08e162 --- /dev/null +++ b/app/src/main/res/xml/pref_appearance.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_backup.xml b/app/src/main/res/xml/pref_backup.xml index 7a61eb336..7fc94bcba 100644 --- a/app/src/main/res/xml/pref_backup.xml +++ b/app/src/main/res/xml/pref_backup.xml @@ -6,14 +6,12 @@ + android:title="@string/create_backup" /> + android:title="@string/restore_backup" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_history.xml b/app/src/main/res/xml/pref_history.xml index 50061d04a..6b7d82c50 100644 --- a/app/src/main/res/xml/pref_history.xml +++ b/app/src/main/res/xml/pref_history.xml @@ -1,51 +1,42 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> + android:title="@string/clear_search_history" /> + android:title="@string/clear_updates_feed" /> + android:title="@string/exclude_nsfw_from_history" /> - + + android:title="@string/clear_thumbs_cache" /> + android:title="@string/clear_pages_cache" /> + android:title="@string/clear_cookies" /> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml deleted file mode 100644 index dd72fe7c1..000000000 --- a/app/src/main/res/xml/pref_main.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 c1a70a6fd..b3c349d72 100644 --- a/app/src/main/res/xml/pref_notifications.xml +++ b/app/src/main/res/xml/pref_notifications.xml @@ -1,23 +1,19 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> + android:title="@string/notification_sound" /> + android:title="@string/vibration" /> + android:title="@string/light_indicator" /> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index 8fa00ef0e..e152d7054 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -7,14 +7,13 @@ android:defaultValue="false" android:key="reader_prefer_rtl" android:summary="@string/prefer_rtl_reader_summary" - android:title="@string/prefer_rtl_reader" - app:iconSpaceReserved="false" /> + android:title="@string/prefer_rtl_reader" /> + app:allowDividerAbove="true" /> + android:title="@string/pages_animation" /> + android:title="@string/show_pages_numbers" /> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_root.xml b/app/src/main/res/xml/pref_root.xml new file mode 100644 index 000000000..a7ed841c0 --- /dev/null +++ b/app/src/main/res/xml/pref_root.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml index d4be72273..51a669888 100644 --- a/app/src/main/res/xml/pref_source.xml +++ b/app/src/main/res/xml/pref_source.xml @@ -5,10 +5,9 @@ + app:allowDividerAbove="true" /> \ 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 aa6f20132..224185a67 100644 --- a/app/src/main/res/xml/pref_suggestions.xml +++ b/app/src/main/res/xml/pref_suggestions.xml @@ -7,14 +7,18 @@ android:defaultValue="false" android:key="suggestions" android:summary="@string/suggestions_summary" - android:title="@string/suggestions_enable" - app:iconSpaceReserved="false" /> + android:title="@string/suggestions_enable" /> + + + android:title="@string/exclude_nsfw_from_suggestions" /> + android:title="@string/track_sources" /> + android:title="@string/notifications" /> + android:title="@string/notifications_settings" /> -Main Features: -
    -
  • Online manga catalogues
  • -
  • Search manga by name and genre
  • -
  • Reading history
  • -
  • Favourites organized by user-defined categories
  • -
  • Downloading manga and reading it offline. Third-party CBZ archives also supported
  • -
  • Tablet-optimized material design UI
  • -
  • Standard and Webtoon-optimized reader
  • -
  • Notifications about new chapters with updates feed
  • -
+**Main Features:** + +- Online manga catalogues +- Search manga by name and genre +- Reading history +- Favourites organized by user-defined categories +- Downloading manga and reading it offline. Third-party CBZ archives also supported +- Tablet-optimized material design UI +- Standard and Webtoon-optimized reader +- Notifications about new chapters with updates feed +- Available in multiple languages +- Password protect access to the app diff --git a/metadata/ru/full_description.txt b/metadata/ru/full_description.txt index 327bdf3b7..87944e3da 100644 --- a/metadata/ru/full_description.txt +++ b/metadata/ru/full_description.txt @@ -1,12 +1,11 @@ Kotatsu - приложения для чтения манги с открытым исходным кодом.
-Основные возможности: -
    -
  • Онлайн каталоги с мангой
  • -
  • Поиск манги по имени и жанрам
  • -
  • История чтения
  • -
  • Избранное с пользовательскими категориями
  • -
  • Возможность сохранять мангу и читать её оффлайн. Поддержка сторонних комиксов в формате CBZ
  • -
  • Интерфейс также оптимизирован для планшетов
  • -
  • Поддержка манхвы (Webtoon)
  • -
  • Уведомления о новых главах и лента обновлений
  • -
+**Основные возможности:** + +- Онлайн каталоги с мангой +- Поиск манги по имени и жанрам +- История чтения +- Избранное с пользовательскими категориями +- Возможность сохранять мангу и читать её оффлайн. Поддержка сторонних комиксов в формате CBZ +- Интерфейс также оптимизирован для планшетов +- Поддержка манхвы (Webtoon) +- Уведомления о новых главах и лента обновлений