diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..ac0f44ea80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: "🐞 Bug report" +about: Report a bug +title: "[Bug] Write short description here" +labels: "bug" + +--- + +### Device information +* Tachiyomi version: ? +* Android version: ? + +## Steps to reproduce +1. First step +2. Second step + +### Expected behavior +This should happen. + +### Actual behavior +This happened instead. + +### Other details +Additional details and attachments. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..d3c307acf7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,12 @@ +--- +name: "🌟 Feature request" +about: Suggest a feature to improve Tachiyomi +title: "[Feature Request] Write short description here" +labels: "feature" + +--- +### Why/User Benefit/User Problem +(explain why this feature should be added) + +### What/Requirements +(explain how this feature would behave) diff --git a/.travis.yml b/.travis.yml index 9667a53d82..edfc5a7c0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ dist: trusty language: android android: components: - - build-tools-28.0.3 - - android-27 + - build-tools-29.0.2 + - android-28 - extra-android-m2repository - extra-google-m2repository - extra-android-support @@ -11,7 +11,7 @@ android: licenses: - android-sdk-license-.+ before_install: -- yes | sdkmanager "platforms;android-27" # workaround for accepting the license +- yes | sdkmanager "platforms;android-28" # workaround for accepting the license - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d; tar xf secrets.tar; diff --git a/README.md b/README.md index 946fd8614a..27eb4f9ccc 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Tachiyomi is a free and open source manga reader for Android. Features of Tachiyomi include: * Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions) * Local reading of downloaded manga -* Configurable reader with multiple viewers, reading directions and other settings -* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support +* A configurable reader with multiple viewers, reading directions and other settings. +* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/explore/anime), and [Shikimori](https://shikimori.one) support * Categories to organize your library * Light and dark themes * Schedule updating your library for new chapters diff --git a/app/build.gradle b/app/build.gradle index 03e39b08dd..8818757d7d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,7 +102,6 @@ android { kotlinOptions { jvmTarget = "1.8" } - } dependencies { @@ -200,9 +199,6 @@ dependencies { // Crash reports implementation 'ch.acra:acra:4.9.2' - // Sort - implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' - // UI implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6801ed687b..3c89d541e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:allowBackup="true" android:fullBackupContent="@xml/backup_rules" android:hardwareAccelerated="true" + android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:label="@string/app_name" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index e38bb88e2c..bed8c5e369 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -28,7 +28,9 @@ class DownloadProvider(private val context: Context) { * The root directory for downloads. */ private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let { - UniFile.fromUri(context, Uri.parse(it)) + val dir = UniFile.fromUri(context, Uri.parse(it)) + DiskUtil.createNoMediaFile(dir, context) + dir } init { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt index 860dbf145d..75676fcb83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -132,7 +132,7 @@ class DownloadService : Service() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ state -> onNetworkStateChanged(state) - }, { _ -> + }, { toast(R.string.download_queue_error) stopSelf() }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index bdccdb9670..f773be4fe9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList +import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.ImageUtil import eu.kanade.tachiyomi.util.RetryWithDelay import eu.kanade.tachiyomi.util.launchNow @@ -431,6 +432,8 @@ class Downloader( if (download.status == Download.DOWNLOADED) { tmpDir.renameTo(dirname) cache.addChapter(dirname, mangaDir, download.manga) + + DiskUtil.createNoMediaFile(tmpDir, context) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt index 03a4d17187..7c3295f21f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/SharedPreferencesDataStore.kt @@ -45,11 +45,11 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere prefs.edit().putString(key, value).apply() } - override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet { - return prefs.getStringSet(key, defValues)!! + override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? { + return prefs.getStringSet(key, defValues) } override fun putStringSet(key: String?, values: MutableSet?) { prefs.edit().putStringSet(key, values).apply() } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index 92c01aac93..e7fa348a3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -22,13 +22,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() private val authClient = client.newBuilder().addInterceptor(interceptor).build() - fun addLibManga(track: Track): Observable { val query = """ - mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { - SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) - { id status } } - """ + |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { + |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { + | id + | status + |} + |} + |""".trimMargin() val variables = jsonObject( "mangaId" to track.media_id, "progress" to track.last_chapter_read, @@ -59,14 +61,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { fun updateLibManga(track: Track): Observable { val query = """ - mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { - SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { - id - status - progress - } - } - """ + |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { + |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { + |id + |status + |progress + |} + |} + |""".trimMargin() val variables = jsonObject( "listId" to track.library_id, "progress" to track.last_chapter_read, @@ -91,29 +93,29 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { fun search(search: String): Observable> { val query = """ - query Search(${'$'}query: String) { - Page (perPage: 50) { - media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { - id - title { - romaji - } - coverImage { - large - } - type - status - chapters - description - startDate { - year - month - day - } - } - } - } - """ + |query Search(${'$'}query: String) { + |Page (perPage: 50) { + |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { + |id + |title { + |romaji + |} + |coverImage { + |large + |} + |type + |status + |chapters + |description + |startDate { + |year + |month + |day + |} + |} + |} + |} + |""".trimMargin() val variables = jsonObject( "query" to search ) @@ -143,37 +145,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } - fun findLibManga(track: Track, userid: Int) : Observable { + fun findLibManga(track: Track, userid: Int): Observable { val query = """ - query (${'$'}id: Int!, ${'$'}manga_id: Int!) { - Page { - mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { - id - status - scoreRaw: score(format: POINT_100) - progress - media{ - id - title { - romaji - } - coverImage { - large - } - type - status - chapters - description - startDate { - year - month - day - } - } - } - } - } - """ + |query (${'$'}id: Int!, ${'$'}manga_id: Int!) { + |Page { + |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { + |id + |status + |scoreRaw: score(format: POINT_100) + |progress + |media { + |id + |title { + |romaji + |} + |coverImage { + |large + |} + |type + |status + |chapters + |description + |startDate { + |year + |month + |day + |} + |} + |} + |} + |} + |""".trimMargin() val variables = jsonObject( "id" to userid, "manga_id" to track.media_id @@ -215,16 +217,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { fun getCurrentUser(): Observable> { val query = """ - query User - { - Viewer { - id - mediaListOptions { - scoreFormat - } - } - } - """ + |query User { + |Viewer { + |id + |mediaListOptions { + |scoreFormat + |} + |} + |} + |""".trimMargin() val payload = jsonObject( "query" to query ) @@ -247,7 +248,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { } } - fun jsonToALManga(struct: JsonObject): ALManga{ + private fun jsonToALManga(struct: JsonObject): ALManga { val date = try { val date = Calendar.getInstance() date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1, @@ -262,11 +263,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { date, struct["chapters"].nullInt ?: 0) } - fun jsonToALUserManga(struct: JsonObject): ALUserManga{ - return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) ) + private fun jsonToALUserManga(struct: JsonObject): ALUserManga { + return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj)) } - companion object { private const val clientId = "385" private const val clientUrl = "tachiyomi://anilist-auth" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt index 5a3df974b3..5e709e810f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt @@ -18,13 +18,12 @@ class KitsuSearchManga(obj: JsonObject) { private val synopsis by obj.byString private var startDate = obj.get("startDate").nullString?.let { val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(Date(it!!.toLong() * 1000)) + outputDf.format(Date(it.toLong() * 1000)) } private val endDate = obj.get("endDate").nullString - @CallSuper - open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { + fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { media_id = this@KitsuSearchManga.id title = canonicalTitle total_chapters = chapterCount ?: 0 @@ -55,7 +54,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) { private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString val progress by obj["attributes"].byInt - open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { + fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { media_id = libraryId title = canonicalTitle total_chapters = chapterCount ?: 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 35d821111c..cf9c6a3867 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -113,11 +113,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { .toCompletable() } - // Attempt to login again if cookies have been cleared but credentials are still filled - fun ensureLoggedIn() { - if (isAuthorized) return - if (!isLogged) throw Exception("MAL Login Credentials not found") - + fun refreshLogin() { val username = getUsername() val password = getPassword() logout() @@ -132,6 +128,14 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) { } } + // Attempt to login again if cookies have been cleared but credentials are still filled + fun ensureLoggedIn() { + if (isAuthorized) return + if (!isLogged) throw Exception("MAL Login Credentials not found") + + refreshLogin() + } + override fun logout() { super.logout() preferences.trackToken(this).delete() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 9089c3a343..5ba163693a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.data.track.myanimelist import okhttp3.Interceptor +import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response import okio.Buffer @@ -11,18 +12,27 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor override fun intercept(chain: Interceptor.Chain): Response { myanimelist.ensureLoggedIn() - var request = chain.request() - request.body?.let { + val request = chain.request() + var response = chain.proceed(updateRequest(request)) + + if (response.code == 400){ + myanimelist.refreshLogin() + response = chain.proceed(updateRequest(request)) + } + + return response + } + + private fun updateRequest(request: Request): Request { + return request.body?.let { val contentType = it.contentType().toString() val updatedBody = when { contentType.contains("x-www-form-urlencoded") -> updateFormBody(it) contentType.contains("json") -> updateJsonBody(it) else -> it } - request = request.newBuilder().post(updatedBody).build() - } - - return chain.proceed(request) + request.newBuilder().post(updatedBody).build() + } ?: request } private fun bodyToString(requestBody: RequestBody): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index f1d50ce4c3..2a55ccd7d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -7,6 +7,10 @@ import android.content.IntentFilter import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.util.launchNow +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async /** * Broadcast receiver that listens for the system's packages installed, updated or removed, and only @@ -90,7 +94,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { val pkgName = getPackageNameFromIntent(intent) ?: return LoadResult.Error("Package name not found") - return ExtensionLoader.loadExtensionFromPkgName(context, pkgName) + return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT, { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }).await() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 4dd8d4936b..1630e723d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -2,20 +2,24 @@ package eu.kanade.tachiyomi.source import android.content.Context import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.ChapterRecognition +import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.EpubFile import eu.kanade.tachiyomi.util.ImageUtil import junrar.Archive import junrar.rarfile.FileHeader -import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import rx.Observable import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.InputStream -import java.util.Comparator import java.util.Locale import java.util.concurrent.TimeUnit import java.util.zip.ZipEntry @@ -125,7 +129,6 @@ class LocalSource(private val context: Context) : CatalogueSource { override fun fetchMangaDetails(manga: SManga) = Observable.just(manga) override fun fetchChapterList(manga: SManga): Observable> { - val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() val chapters = getBaseDirectories(context) .mapNotNull { File(it, manga.url).listFiles()?.toList() } .flatten() @@ -146,7 +149,7 @@ class LocalSource(private val context: Context) : CatalogueSource { } .sortedWith(Comparator { c1, c2 -> val c = c2.chapter_number.compareTo(c1.chapter_number) - if (c == 0) comparator.compare(c2.name, c1.name) else c + if (c == 0) CaseInsensitiveNaturalComparator.compare(c2.name, c1.name) else c }) return Observable.just(chapters) @@ -189,20 +192,19 @@ class LocalSource(private val context: Context) : CatalogueSource { private fun updateCover(chapter: SChapter, manga: SManga): File? { val format = getFormat(chapter) - val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() return when (format) { is Format.Directory -> { val entry = format.file.listFiles() - .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.name, f2.name) }) - .find { !it.isDirectory && ImageUtil.isImage(it.name, { FileInputStream(it) }) } + .sortedWith(Comparator { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) }) + .find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } entry?.let { updateCover(context, manga, it.inputStream())} } is Format.Zip -> { ZipFile(format.file).use { zip -> val entry = zip.entries().toList() - .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.name, f2.name) }) - .find { !it.isDirectory && ImageUtil.isImage(it.name, { zip.getInputStream(it) }) } + .sortedWith(Comparator { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) }) + .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } entry?.let { updateCover(context, manga, zip.getInputStream(it) )} } @@ -210,8 +212,8 @@ class LocalSource(private val context: Context) : CatalogueSource { is Format.Rar -> { Archive(format.file).use { archive -> val entry = archive.fileHeaders - .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) }) - .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) } + .sortedWith(Comparator { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) }) + .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } entry?.let { updateCover(context, manga, archive.getInputStream(it) )} } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt index 8f1136be04..fecb253e48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt @@ -11,6 +11,9 @@ import android.view.ViewGroup import androidx.appcompat.widget.SearchView import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.f2prateek.rx.preferences.Preference import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar @@ -207,10 +210,11 @@ open class BrowseCatalogueController(bundle: Bundle) : } val recycler = if (presenter.isListMode) { - androidx.recyclerview.widget.RecyclerView(view.context).apply { + RecyclerView(view.context).apply { id = R.id.recycler - layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) - addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(context, androidx.recyclerview.widget.DividerItemDecoration.VERTICAL)) + layoutManager = LinearLayoutManager(context) + layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) } } else { (catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { @@ -377,9 +381,8 @@ open class BrowseCatalogueController(bundle: Bundle) : adapter.onLoadMoreComplete(null) hideProgressBar() - val message = if (error is NoResultsException) "No results found" else (error.message ?: "") - snack?.dismiss() + val message = if (error is NoResultsException) catalogue_view.context.getString(R.string.no_results_found) else (error.message ?: "") snack = catalouge_layout?.snack(message, Snackbar.LENGTH_INDEFINITE) { setAction(R.string.action_retry) { // If not the first page, show bottom progress bar. @@ -388,8 +391,8 @@ open class BrowseCatalogueController(bundle: Bundle) : adapter.addScrollableFooterWithDelay(item, 0, true) } else { showProgressBar() - } presenter.requestNext() + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt index 1f06fc4072..d78d1fd7a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt @@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: val view = inflate(R.layout.catalogue_drawer_content) ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler) addView(view) - title.text = context?.getString(R.string.source_search_options) + title.text = context.getString(R.string.source_search_options) search_btn.setOnClickListener { onSearchClicked() } reset_btn.setOnClickListener { onResetClicked() } } @@ -37,4 +37,4 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: adapter.updateDataSet(items) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt index ec419c7641..4dc13436c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt @@ -19,8 +19,8 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) : */ private var bundle = Bundle() - override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) + override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int, payloads: List) { + super.onBindViewHolder(holder, position, payloads) restoreHolderState(holder) } @@ -71,4 +71,4 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) : private companion object { const val HOLDER_BUNDLE_KEY = "holder_bundle" } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 698b179e0c..f61e3e64cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -213,7 +213,6 @@ class MangaController : RxController, TabbedController { } companion object { - const val FROM_CATALOGUE_EXTRA = "from_catalogue" const val MANGA_EXTRA = "manga" @@ -225,5 +224,4 @@ class MangaController : RxController, TabbedController { .apply { isAccessible = true } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt index 65f3381c8f..01f5dbe922 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import kotlinx.android.synthetic.main.catalogue_main_controller_card.* import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.items.IFlexible +import kotlinx.android.synthetic.main.catalogue_main_controller_card.title /** * Item that contains the selection header. @@ -38,7 +39,7 @@ class SelectionHeader : AbstractHeaderItem() { class Holder(view: View, adapter: FlexibleAdapter>) : BaseFlexibleViewHolder(view, adapter) { init { - title.text = "Please select a source to migrate from" + title.text = view.context.getString(R.string.migration_selection_prompt) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 77877d52a9..d9211b5eab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -507,7 +507,7 @@ class ReaderPresenter( .observeOn(AndroidSchedulers.mainThread()) .subscribeFirst( { view, file -> view.onShareImageResult(file) }, - { view, error -> /* Empty */ } + { _, _ -> /* Empty */ } ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt index d42efcef06..8d70d4a760 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt @@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator import eu.kanade.tachiyomi.util.ImageUtil -import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import rx.Observable import java.io.File import java.io.FileInputStream @@ -18,11 +18,9 @@ class DirectoryPageLoader(val file: File) : PageLoader() { * comparator. */ override fun getPages(): Observable> { - val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() - return file.listFiles() .filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } - .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.name, f2.name) }) + .sortedWith(Comparator { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) }) .mapIndexed { i, file -> val streamFn = { FileInputStream(file) } ReaderPage(i).apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index c64947a321..a21993a787 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import rx.Observable import uy.kohesive.injekt.injectLazy -import java.io.InputStream /** * Loader used to load a chapter from the downloaded chapters. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index dcba332aaa..4a30378a6d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator import eu.kanade.tachiyomi.util.ImageUtil import junrar.Archive import junrar.rarfile.FileHeader -import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import rx.Observable import java.io.File import java.io.InputStream @@ -42,11 +42,9 @@ class RarPageLoader(file: File) : PageLoader() { * comparator. */ override fun getPages(): Observable> { - val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() - return archive.fileHeaders .filter { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } - .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) }) + .sortedWith(Comparator { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) }) .mapIndexed { i, header -> val streamFn = { getStream(header) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index 3cd1b541e8..78d7488847 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator import eu.kanade.tachiyomi.util.ImageUtil -import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import rx.Observable import java.io.File import java.util.zip.ZipEntry @@ -32,11 +32,9 @@ class ZipPageLoader(file: File) : PageLoader() { * comparator. */ override fun getPages(): Observable> { - val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance() - return zip.entries().toList() .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } - .sortedWith(Comparator { f1, f2 -> comparator.compare(f1.name, f2.name) }) + .sortedWith(Comparator { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) }) .mapIndexed { i, entry -> val streamFn = { zip.getInputStream(entry) } ReaderPage(i).apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 4a44965cc6..fa38abc4e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -50,6 +50,7 @@ class PagerTransitionHolder( private var textView = TextView(context).apply { //if (Build.VERSION.SDK_INT >= 23) //setTextColor(context.getResourceColor(R.attr.)) + textSize = 17.5F wrapContent() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 282258b7a8..4c89f62d9a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -101,7 +101,7 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer { recycler.longTapListener = f@ { event -> if (activity.menuVisible || config.longTapEnabled) { val child = recycler.findChildViewUnder(event.x, event.y) - if(child != null) { + if (child != null) { val position = recycler.getChildAdapterPosition(child) val item = adapter.items.getOrNull(position) if (item is ReaderPage) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 988e411490..20a56778c1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.getFilePicker import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -45,15 +44,6 @@ class SettingsDownloadController : SettingsController() { .subscribeUntilDestroy { path -> val dir = UniFile.fromUri(context, Uri.parse(path)) summary = dir.filePath ?: path - - // Don't display downloaded chapters in gallery apps creating .nomedia - if (dir != null && dir.exists()) { - val nomedia = dir.findFile(".nomedia") - if (nomedia == null) { - dir.createFile(".nomedia") - applicationContext?.let { DiskUtil.scanMedia(it, dir.uri) } - } - } } } switchPreference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt index 9ee6774a76..742fa3e1a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ChapterRecognition.kt @@ -91,7 +91,7 @@ object ChapterRecognition { * @param chapter chapter object * @return true if volume is found */ - fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean { + private fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean { match?.let { val initial = it.groups[1]?.value?.toFloat()!! val subChapterDecimal = it.groups[2]?.value @@ -109,12 +109,12 @@ object ChapterRecognition { * @param alpha alpha value of regex * @return decimal/alpha float value */ - fun checkForDecimal(decimal: String?, alpha: String?): Float { + private fun checkForDecimal(decimal: String?, alpha: String?): Float { if (!decimal.isNullOrEmpty()) - return decimal?.toFloat()!! + return decimal.toFloat() if (!alpha.isNullOrEmpty()) { - if (alpha!!.contains("extra")) + if (alpha.contains("extra")) return .99f if (alpha.contains("omake")) @@ -138,7 +138,7 @@ object ChapterRecognition { * x.a -> x.1, x.b -> x.2, etc */ private fun parseAlphaPostFix(alpha: Char): Float { - return ("0." + Integer.toString(alpha.toInt() - 96)).toFloat() + return ("0." + (alpha.toInt() - 96).toString()).toFloat() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ComparatorUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ComparatorUtil.kt new file mode 100644 index 0000000000..aff3603b65 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ComparatorUtil.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.util + +object ComparatorUtil { + val CaseInsensitiveNaturalComparator = compareBy(String.CASE_INSENSITIVE_ORDER) { it }.then(naturalOrder()) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index 57e178cf0e..ab3631f764 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -172,11 +172,11 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean { */ fun Context.openInBrowser(url: String) { try { - val url = Uri.parse(url) + val parsedUrl = Uri.parse(url) val intent = CustomTabsIntent.Builder() .setToolbarColor(getResourceColor(R.attr.colorPrimary)) .build() - intent.launchUrl(this, url) + intent.launchUrl(this, parsedUrl) } catch (e: Exception) { toast(e.message) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt index 729418d0b7..2a286e09cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CoroutinesExtensions.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.launch import kotlin.coroutines.EmptyCoroutineContext fun launchUI(block: suspend CoroutineScope.() -> Unit): Job = - GlobalScope.launch(Dispatchers.Main,CoroutineStart.DEFAULT,block) + GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block) fun launchNow(block: suspend CoroutineScope.() -> Unit): Job = - GlobalScope.launch(Dispatchers.Main,CoroutineStart.UNDISPATCHED,block) + GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt index a93bd9ee16..4f0375e423 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/DiskUtil.kt @@ -7,6 +7,7 @@ import android.os.Build import android.os.Environment import androidx.core.content.ContextCompat import androidx.core.os.EnvironmentCompat +import com.hippo.unifile.UniFile import java.io.File object DiskUtil { @@ -54,6 +55,19 @@ object DiskUtil { return directories } + /** + * Don't display downloaded chapters in gallery apps creating `.nomedia`. + */ + fun createNoMediaFile(dir: UniFile?, context: Context?) { + if (dir != null && dir.exists()) { + val nomedia = dir.findFile(".nomedia") + if (nomedia == null) { + dir.createFile(".nomedia") + context?.let { scanMedia(it, dir.uri) } + } + } + } + /** * Scans the given file so that it can be shown in gallery apps, for example. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1eef24d258..45bed46bc4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -343,6 +343,7 @@ Select a source Please enable at least one valid source No more results + No results found Local manga Other Default can\'t be selected with other categories @@ -477,6 +478,7 @@ Tap to select the source to migrate from Select data to include + Select a source to migrate from Select Migrate Copy diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index dbd4611730..140042be81 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -136,8 +136,9 @@