Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Jay 2020-01-03 12:05:11 -08:00
commit 8d5037ce56
37 changed files with 238 additions and 173 deletions

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -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.

View File

@ -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)

View File

@ -2,8 +2,8 @@ dist: trusty
language: android language: android
android: android:
components: components:
- build-tools-28.0.3 - build-tools-29.0.2
- android-27 - android-28
- extra-android-m2repository - extra-android-m2repository
- extra-google-m2repository - extra-google-m2repository
- extra-android-support - extra-android-support
@ -11,7 +11,7 @@ android:
licenses: licenses:
- android-sdk-license-.+ - android-sdk-license-.+
before_install: 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 - 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; 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; tar xf secrets.tar;

View File

@ -11,8 +11,8 @@ Tachiyomi is a free and open source manga reader for Android.
Features of Tachiyomi include: Features of Tachiyomi include:
* Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions) * Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Local reading of downloaded manga * Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings * A 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 * [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 * Categories to organize your library
* Light and dark themes * Light and dark themes
* Schedule updating your library for new chapters * Schedule updating your library for new chapters

View File

@ -102,7 +102,6 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
} }
dependencies { dependencies {
@ -200,9 +199,6 @@ dependencies {
// Crash reports // Crash reports
implementation 'ch.acra:acra:4.9.2' implementation 'ch.acra:acra:4.9.2'
// Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
// UI // UI
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'

View File

@ -19,6 +19,7 @@
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -28,7 +28,9 @@ class DownloadProvider(private val context: Context) {
* The root directory for downloads. * The root directory for downloads.
*/ */
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let { 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 { init {

View File

@ -132,7 +132,7 @@ class DownloadService : Service() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state) .subscribe({ state -> onNetworkStateChanged(state)
}, { _ -> }, {
toast(R.string.download_queue_error) toast(R.string.download_queue_error)
stopSelf() stopSelf()
}) })

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.ImageUtil import eu.kanade.tachiyomi.util.ImageUtil
import eu.kanade.tachiyomi.util.RetryWithDelay import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.launchNow import eu.kanade.tachiyomi.util.launchNow
@ -431,6 +432,8 @@ class Downloader(
if (download.status == Download.DOWNLOADED) { if (download.status == Download.DOWNLOADED) {
tmpDir.renameTo(dirname) tmpDir.renameTo(dirname)
cache.addChapter(dirname, mangaDir, download.manga) cache.addChapter(dirname, mangaDir, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context)
} }
} }

View File

@ -45,8 +45,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
prefs.edit().putString(key, value).apply() prefs.edit().putString(key, value).apply()
} }
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> { override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
return prefs.getStringSet(key, defValues)!! return prefs.getStringSet(key, defValues)
} }
override fun putStringSet(key: String?, values: MutableSet<String>?) { override fun putStringSet(key: String?, values: MutableSet<String>?) {

View File

@ -22,13 +22,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull() private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> { fun addLibManga(track: Track): Observable<Track> {
val query = """ val query = """
mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
{ id status } } | id
""" | status
|}
|}
|""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"mangaId" to track.media_id, "mangaId" to track.media_id,
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
@ -59,14 +61,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun updateLibManga(track: Track): Observable<Track> { fun updateLibManga(track: Track): Observable<Track> {
val query = """ val query = """
mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { |mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { |SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
id |id
status |status
progress |progress
} |}
} |}
""" |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"listId" to track.library_id, "listId" to track.library_id,
"progress" to track.last_chapter_read, "progress" to track.last_chapter_read,
@ -91,29 +93,29 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val query = """ val query = """
query Search(${'$'}query: String) { |query Search(${'$'}query: String) {
Page (perPage: 50) { |Page (perPage: 50) {
media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
id |id
title { |title {
romaji |romaji
} |}
coverImage { |coverImage {
large |large
} |}
type |type
status |status
chapters |chapters
description |description
startDate { |startDate {
year |year
month |month
day |day
} |}
} |}
} |}
} |}
""" |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"query" to search "query" to search
) )
@ -143,37 +145,37 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
fun findLibManga(track: Track, userid: Int) : Observable<Track?> { fun findLibManga(track: Track, userid: Int): Observable<Track?> {
val query = """ val query = """
query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
Page { |Page {
mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
id |id
status |status
scoreRaw: score(format: POINT_100) |scoreRaw: score(format: POINT_100)
progress |progress
media{ |media {
id |id
title { |title {
romaji |romaji
} |}
coverImage { |coverImage {
large |large
} |}
type |type
status |status
chapters |chapters
description |description
startDate { |startDate {
year |year
month |month
day |day
} |}
} |}
} |}
} |}
} |}
""" |""".trimMargin()
val variables = jsonObject( val variables = jsonObject(
"id" to userid, "id" to userid,
"manga_id" to track.media_id "manga_id" to track.media_id
@ -215,16 +217,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
fun getCurrentUser(): Observable<Pair<Int, String>> { fun getCurrentUser(): Observable<Pair<Int, String>> {
val query = """ val query = """
query User |query User {
{ |Viewer {
Viewer { |id
id |mediaListOptions {
mediaListOptions { |scoreFormat
scoreFormat |}
} |}
} |}
} |""".trimMargin()
"""
val payload = jsonObject( val payload = jsonObject(
"query" to query "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 = try {
val date = Calendar.getInstance() val date = Calendar.getInstance()
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1, 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) date, struct["chapters"].nullInt ?: 0)
} }
fun jsonToALUserManga(struct: JsonObject): ALUserManga{ private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj) ) return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
} }
companion object { companion object {
private const val clientId = "385" private const val clientId = "385"
private const val clientUrl = "tachiyomi://anilist-auth" private const val clientUrl = "tachiyomi://anilist-auth"

View File

@ -18,13 +18,12 @@ class KitsuSearchManga(obj: JsonObject) {
private val synopsis by obj.byString private val synopsis by obj.byString
private var startDate = obj.get("startDate").nullString?.let { private var startDate = obj.get("startDate").nullString?.let {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) 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 private val endDate = obj.get("endDate").nullString
@CallSuper @CallSuper
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
media_id = this@KitsuSearchManga.id media_id = this@KitsuSearchManga.id
title = canonicalTitle title = canonicalTitle
total_chapters = chapterCount ?: 0 total_chapters = chapterCount ?: 0
@ -55,7 +54,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
val progress by obj["attributes"].byInt val progress by obj["attributes"].byInt
open fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply { fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
media_id = libraryId media_id = libraryId
title = canonicalTitle title = canonicalTitle
total_chapters = chapterCount ?: 0 total_chapters = chapterCount ?: 0

View File

@ -113,11 +113,7 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
.toCompletable() .toCompletable()
} }
// Attempt to login again if cookies have been cleared but credentials are still filled fun refreshLogin() {
fun ensureLoggedIn() {
if (isAuthorized) return
if (!isLogged) throw Exception("MAL Login Credentials not found")
val username = getUsername() val username = getUsername()
val password = getPassword() val password = getPassword()
logout() 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() { override fun logout() {
super.logout() super.logout()
preferences.trackToken(this).delete() preferences.trackToken(this).delete()

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.Response import okhttp3.Response
import okio.Buffer import okio.Buffer
@ -11,18 +12,27 @@ class MyAnimeListInterceptor(private val myanimelist: Myanimelist): Interceptor
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
myanimelist.ensureLoggedIn() myanimelist.ensureLoggedIn()
var request = chain.request() val request = chain.request()
request.body?.let { 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 contentType = it.contentType().toString()
val updatedBody = when { val updatedBody = when {
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it) contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
contentType.contains("json") -> updateJsonBody(it) contentType.contains("json") -> updateJsonBody(it)
else -> it else -> it
} }
request = request.newBuilder().post(updatedBody).build() request.newBuilder().post(updatedBody).build()
} } ?: request
return chain.proceed(request)
} }
private fun bodyToString(requestBody: RequestBody): String { private fun bodyToString(requestBody: RequestBody): String {

View File

@ -7,6 +7,10 @@ import android.content.IntentFilter
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.util.launchNow 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 * 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 { private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
val pkgName = getPackageNameFromIntent(intent) ?: val pkgName = getPackageNameFromIntent(intent) ?:
return LoadResult.Error("Package name not found") return LoadResult.Error("Package name not found")
return ExtensionLoader.loadExtensionFromPkgName(context, pkgName) return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT, { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }).await()
} }
/** /**

View File

@ -2,20 +2,24 @@ package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.R 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.ChapterRecognition
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.EpubFile import eu.kanade.tachiyomi.util.EpubFile
import eu.kanade.tachiyomi.util.ImageUtil import eu.kanade.tachiyomi.util.ImageUtil
import junrar.Archive import junrar.Archive
import junrar.rarfile.FileHeader import junrar.rarfile.FileHeader
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.util.Comparator
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry 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 fetchMangaDetails(manga: SManga) = Observable.just(manga)
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
val chapters = getBaseDirectories(context) val chapters = getBaseDirectories(context)
.mapNotNull { File(it, manga.url).listFiles()?.toList() } .mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten() .flatten()
@ -146,7 +149,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
.sortedWith(Comparator { c1, c2 -> .sortedWith(Comparator { c1, c2 ->
val c = c2.chapter_number.compareTo(c1.chapter_number) 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) return Observable.just(chapters)
@ -189,20 +192,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
private fun updateCover(chapter: SChapter, manga: SManga): File? { private fun updateCover(chapter: SChapter, manga: SManga): File? {
val format = getFormat(chapter) val format = getFormat(chapter)
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return when (format) { return when (format) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry = format.file.listFiles()
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) }) .sortedWith(Comparator<File> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name, { FileInputStream(it) }) } .find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream())} entry?.let { updateCover(context, manga, it.inputStream())}
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile(format.file).use { zip ->
val entry = zip.entries().toList() val entry = zip.entries().toList()
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) }) .sortedWith(Comparator<ZipEntry> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name, { zip.getInputStream(it) }) } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { updateCover(context, manga, 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 -> { is Format.Rar -> {
Archive(format.file).use { archive -> Archive(format.file).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) }) .sortedWith(Comparator<FileHeader> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) })
.find { !it.isDirectory && ImageUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) } .find { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
entry?.let { updateCover(context, manga, archive.getInputStream(it) )} entry?.let { updateCover(context, manga, archive.getInputStream(it) )}
} }

View File

@ -11,6 +11,9 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout 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.f2prateek.rx.preferences.Preference
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -207,10 +210,11 @@ open class BrowseCatalogueController(bundle: Bundle) :
} }
val recycler = if (presenter.isListMode) { val recycler = if (presenter.isListMode) {
androidx.recyclerview.widget.RecyclerView(view.context).apply { RecyclerView(view.context).apply {
id = R.id.recycler id = R.id.recycler
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(context, androidx.recyclerview.widget.DividerItemDecoration.VERTICAL)) layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
} }
} else { } else {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { (catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
@ -377,9 +381,8 @@ open class BrowseCatalogueController(bundle: Bundle) :
adapter.onLoadMoreComplete(null) adapter.onLoadMoreComplete(null)
hideProgressBar() hideProgressBar()
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
snack?.dismiss() 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) { snack = catalouge_layout?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
// If not the first page, show bottom progress bar. // If not the first page, show bottom progress bar.
@ -388,11 +391,11 @@ open class BrowseCatalogueController(bundle: Bundle) :
adapter.addScrollableFooterWithDelay(item, 0, true) adapter.addScrollableFooterWithDelay(item, 0, true)
} else { } else {
showProgressBar() showProgressBar()
}
presenter.requestNext() presenter.requestNext()
} }
} }
} }
}
/** /**
* Sets a new progress item and reenables the scroll listener. * Sets a new progress item and reenables the scroll listener.

View File

@ -28,7 +28,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
val view = inflate(R.layout.catalogue_drawer_content) val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler) ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view) addView(view)
title.text = context?.getString(R.string.source_search_options) title.text = context.getString(R.string.source_search_options)
search_btn.setOnClickListener { onSearchClicked() } search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() } reset_btn.setOnClickListener { onResetClicked() }
} }

View File

@ -19,8 +19,8 @@ class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
*/ */
private var bundle = Bundle() private var bundle = Bundle()
override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: androidx.recyclerview.widget.RecyclerView.ViewHolder, position: Int, payloads: List<Any?>) {
super.onBindViewHolder(holder, position) super.onBindViewHolder(holder, position, payloads)
restoreHolderState(holder) restoreHolderState(holder)
} }

View File

@ -213,7 +213,6 @@ class MangaController : RxController, TabbedController {
} }
companion object { companion object {
const val FROM_CATALOGUE_EXTRA = "from_catalogue" const val FROM_CATALOGUE_EXTRA = "from_catalogue"
const val MANGA_EXTRA = "manga" const val MANGA_EXTRA = "manga"
@ -225,5 +224,4 @@ class MangaController : RxController, TabbedController {
.apply { isAccessible = true } .apply { isAccessible = true }
} }
} }

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.catalogue_main_controller_card.* import kotlinx.android.synthetic.main.catalogue_main_controller_card.*
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import kotlinx.android.synthetic.main.catalogue_main_controller_card.title
/** /**
* Item that contains the selection header. * Item that contains the selection header.
@ -38,7 +39,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
class Holder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>) : BaseFlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>) : BaseFlexibleViewHolder(view, adapter) {
init { init {
title.text = "Please select a source to migrate from" title.text = view.context.getString(R.string.migration_selection_prompt)
} }
} }

View File

@ -507,7 +507,7 @@ class ReaderPresenter(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeFirst( .subscribeFirst(
{ view, file -> view.onShareImageResult(file) }, { view, file -> view.onShareImageResult(file) },
{ view, error -> /* Empty */ } { _, _ -> /* Empty */ }
) )
} }

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.ImageUtil import eu.kanade.tachiyomi.util.ImageUtil
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable import rx.Observable
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -18,11 +18,9 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
* comparator. * comparator.
*/ */
override fun getPages(): Observable<List<ReaderPage>> { override fun getPages(): Observable<List<ReaderPage>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return file.listFiles() return file.listFiles()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) }) .sortedWith(Comparator<File> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.mapIndexed { i, file -> .mapIndexed { i, file ->
val streamFn = { FileInputStream(file) } val streamFn = { FileInputStream(file) }
ReaderPage(i).apply { ReaderPage(i).apply {

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.InputStream
/** /**
* Loader used to load a chapter from the downloaded chapters. * Loader used to load a chapter from the downloaded chapters.

View File

@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.ImageUtil import eu.kanade.tachiyomi.util.ImageUtil
import junrar.Archive import junrar.Archive
import junrar.rarfile.FileHeader import junrar.rarfile.FileHeader
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable import rx.Observable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -42,11 +42,9 @@ class RarPageLoader(file: File) : PageLoader() {
* comparator. * comparator.
*/ */
override fun getPages(): Observable<List<ReaderPage>> { override fun getPages(): Observable<List<ReaderPage>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return archive.fileHeaders return archive.fileHeaders
.filter { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.fileNameString) { archive.getInputStream(it) } }
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) }) .sortedWith(Comparator<FileHeader> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.fileNameString, f2.fileNameString) })
.mapIndexed { i, header -> .mapIndexed { i, header ->
val streamFn = { getStream(header) } val streamFn = { getStream(header) }

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.ComparatorUtil.CaseInsensitiveNaturalComparator
import eu.kanade.tachiyomi.util.ImageUtil import eu.kanade.tachiyomi.util.ImageUtil
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import rx.Observable import rx.Observable
import java.io.File import java.io.File
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -32,11 +32,9 @@ class ZipPageLoader(file: File) : PageLoader() {
* comparator. * comparator.
*/ */
override fun getPages(): Observable<List<ReaderPage>> { override fun getPages(): Observable<List<ReaderPage>> {
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
return zip.entries().toList() return zip.entries().toList()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) }) .sortedWith(Comparator<ZipEntry> { f1, f2 -> CaseInsensitiveNaturalComparator.compare(f1.name, f2.name) })
.mapIndexed { i, entry -> .mapIndexed { i, entry ->
val streamFn = { zip.getInputStream(entry) } val streamFn = { zip.getInputStream(entry) }
ReaderPage(i).apply { ReaderPage(i).apply {

View File

@ -50,6 +50,7 @@ class PagerTransitionHolder(
private var textView = TextView(context).apply { private var textView = TextView(context).apply {
//if (Build.VERSION.SDK_INT >= 23) //if (Build.VERSION.SDK_INT >= 23)
//setTextColor(context.getResourceColor(R.attr.)) //setTextColor(context.getResourceColor(R.attr.))
textSize = 17.5F
wrapContent() wrapContent()
} }

View File

@ -101,7 +101,7 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer {
recycler.longTapListener = f@ { event -> recycler.longTapListener = f@ { event ->
if (activity.menuVisible || config.longTapEnabled) { if (activity.menuVisible || config.longTapEnabled) {
val child = recycler.findChildViewUnder(event.x, event.y) val child = recycler.findChildViewUnder(event.x, event.y)
if(child != null) { if (child != null) {
val position = recycler.getChildAdapterPosition(child) val position = recycler.getChildAdapterPosition(child)
val item = adapter.items.getOrNull(position) val item = adapter.items.getOrNull(position)
if (item is ReaderPage) { if (item is ReaderPage) {

View File

@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.getFilePicker import eu.kanade.tachiyomi.util.getFilePicker
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -45,15 +44,6 @@ class SettingsDownloadController : SettingsController() {
.subscribeUntilDestroy { path -> .subscribeUntilDestroy { path ->
val dir = UniFile.fromUri(context, Uri.parse(path)) val dir = UniFile.fromUri(context, Uri.parse(path))
summary = dir.filePath ?: 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 { switchPreference {

View File

@ -91,7 +91,7 @@ object ChapterRecognition {
* @param chapter chapter object * @param chapter chapter object
* @return true if volume is found * @return true if volume is found
*/ */
fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean { private fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean {
match?.let { match?.let {
val initial = it.groups[1]?.value?.toFloat()!! val initial = it.groups[1]?.value?.toFloat()!!
val subChapterDecimal = it.groups[2]?.value val subChapterDecimal = it.groups[2]?.value
@ -109,12 +109,12 @@ object ChapterRecognition {
* @param alpha alpha value of regex * @param alpha alpha value of regex
* @return decimal/alpha float value * @return decimal/alpha float value
*/ */
fun checkForDecimal(decimal: String?, alpha: String?): Float { private fun checkForDecimal(decimal: String?, alpha: String?): Float {
if (!decimal.isNullOrEmpty()) if (!decimal.isNullOrEmpty())
return decimal?.toFloat()!! return decimal.toFloat()
if (!alpha.isNullOrEmpty()) { if (!alpha.isNullOrEmpty()) {
if (alpha!!.contains("extra")) if (alpha.contains("extra"))
return .99f return .99f
if (alpha.contains("omake")) if (alpha.contains("omake"))
@ -138,7 +138,7 @@ object ChapterRecognition {
* x.a -> x.1, x.b -> x.2, etc * x.a -> x.1, x.b -> x.2, etc
*/ */
private fun parseAlphaPostFix(alpha: Char): Float { private fun parseAlphaPostFix(alpha: Char): Float {
return ("0." + Integer.toString(alpha.toInt() - 96)).toFloat() return ("0." + (alpha.toInt() - 96).toString()).toFloat()
} }
} }

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.util
object ComparatorUtil {
val CaseInsensitiveNaturalComparator = compareBy<String, String>(String.CASE_INSENSITIVE_ORDER) { it }.then(naturalOrder())
}

View File

@ -172,11 +172,11 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
*/ */
fun Context.openInBrowser(url: String) { fun Context.openInBrowser(url: String) {
try { try {
val url = Uri.parse(url) val parsedUrl = Uri.parse(url)
val intent = CustomTabsIntent.Builder() val intent = CustomTabsIntent.Builder()
.setToolbarColor(getResourceColor(R.attr.colorPrimary)) .setToolbarColor(getResourceColor(R.attr.colorPrimary))
.build() .build()
intent.launchUrl(this, url) intent.launchUrl(this, parsedUrl)
} catch (e: Exception) { } catch (e: Exception) {
toast(e.message) toast(e.message)
} }

View File

@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job = 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 = fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.Main,CoroutineStart.UNDISPATCHED,block) GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)

View File

@ -7,6 +7,7 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.EnvironmentCompat import androidx.core.os.EnvironmentCompat
import com.hippo.unifile.UniFile
import java.io.File import java.io.File
object DiskUtil { object DiskUtil {
@ -54,6 +55,19 @@ object DiskUtil {
return directories 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. * Scans the given file so that it can be shown in gallery apps, for example.
*/ */

View File

@ -343,6 +343,7 @@
<string name="select_source">Select a source</string> <string name="select_source">Select a source</string>
<string name="no_valid_sources">Please enable at least one valid source</string> <string name="no_valid_sources">Please enable at least one valid source</string>
<string name="no_more_results">No more results</string> <string name="no_more_results">No more results</string>
<string name="no_results_found">No results found</string>
<string name="local_source">Local manga</string> <string name="local_source">Local manga</string>
<string name="other_source">Other</string> <string name="other_source">Other</string>
<string name="invalid_combination">Default can\'t be selected with other categories</string> <string name="invalid_combination">Default can\'t be selected with other categories</string>
@ -477,6 +478,7 @@
<!-- Source migration screen --> <!-- Source migration screen -->
<string name="migration_info">Tap to select the source to migrate from</string> <string name="migration_info">Tap to select the source to migrate from</string>
<string name="migration_dialog_what_to_include">Select data to include</string> <string name="migration_dialog_what_to_include">Select data to include</string>
<string name="migration_selection_prompt">Select a source to migrate from</string>
<string name="select">Select</string> <string name="select">Select</string>
<string name="migrate">Migrate</string> <string name="migrate">Migrate</string>
<string name="copy">Copy</string> <string name="copy">Copy</string>

View File

@ -136,8 +136,9 @@
<style name="Theme.Widget" /> <style name="Theme.Widget" />
<style name="Theme.Widget.FAB"> <style name="Theme.Widget.FAB">
<item name="android:layout_height">@dimen/fab_size</item> <item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">@dimen/fab_size</item> <item name="android:layout_width">wrap_content</item>
<item name="fabCustomSize">@dimen/fab_size</item>
<item name="android:layout_gravity">bottom|end</item> <item name="android:layout_gravity">bottom|end</item>
<item name="android:layout_margin">@dimen/fab_margin</item> <item name="android:layout_margin">@dimen/fab_margin</item>
<item name="android:scaleType">fitCenter</item> <item name="android:scaleType">fitCenter</item>

View File

@ -10,7 +10,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.22.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.22.0'
classpath 'com.github.zellius:android-shortcut-gradle-plugin:0.1.2' classpath 'com.github.zellius:android-shortcut-gradle-plugin:0.1.2'
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }