diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d7527cf..a2d2590 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -101,6 +101,7 @@
+
@@ -469,34 +470,1180 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/org/dokiteam/doki/core/parser/MangaLinkResolver.kt b/app/src/main/kotlin/org/dokiteam/doki/core/parser/MangaLinkResolver.kt
index 030d0de..8c03647 100644
--- a/app/src/main/kotlin/org/dokiteam/doki/core/parser/MangaLinkResolver.kt
+++ b/app/src/main/kotlin/org/dokiteam/doki/core/parser/MangaLinkResolver.kt
@@ -118,7 +118,9 @@ class MangaLinkResolver @Inject constructor(
companion object {
fun isValidLink(str: String): Boolean {
- return str.isHttpUrl() || str.startsWith("doki://", ignoreCase = true)
+ return str.isHttpUrl() ||
+ str.startsWith("doki://", ignoreCase = true) ||
+ str.startsWith("kotatsu://", ignoreCase = true)
}
}
}
diff --git a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/anilist/data/AniListRepository.kt b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/anilist/data/AniListRepository.kt
index cfddde7..b8c2c97 100644
--- a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/anilist/data/AniListRepository.kt
+++ b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/anilist/data/AniListRepository.kt
@@ -28,7 +28,7 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.roundToInt
-private const val REDIRECT_URI = "doki://anilist-auth"
+private const val REDIRECT_URI = "kotatsu://anilist-auth"
private const val BASE_URL = "https://anilist.co/api/v2/"
private const val ENDPOINT = "https://graphql.anilist.co"
private const val MANGA_PAGE_SIZE = 10
diff --git a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/kitsu/ui/KitsuAuthActivity.kt b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/kitsu/ui/KitsuAuthActivity.kt
index f9ce976..968f330 100644
--- a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/kitsu/ui/KitsuAuthActivity.kt
+++ b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/kitsu/ui/KitsuAuthActivity.kt
@@ -103,7 +103,7 @@ class KitsuAuthActivity : BaseActivity(),
private fun continueAuth() {
val email = viewBinding.editEmail.text?.toString()?.trim().orEmpty()
val password = viewBinding.editPassword.text?.toString()?.trim().orEmpty()
- val url = "doki://kitsu-auth?code=" + "$email;$password".urlEncoded()
+ val url = "kotatsu://kitsu-auth?code=" + "$email;$password".urlEncoded()
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
finishAfterTransition()
diff --git a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/mal/data/MALRepository.kt b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/mal/data/MALRepository.kt
index 0b5f467..19b64dc 100644
--- a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/mal/data/MALRepository.kt
+++ b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/mal/data/MALRepository.kt
@@ -26,7 +26,7 @@ import java.security.SecureRandom
import javax.inject.Inject
import javax.inject.Singleton
-private const val REDIRECT_URI = "doki://mal-auth"
+private const val REDIRECT_URI = "kotatsu://mal-auth"
private const val BASE_WEB_URL = "https://myanimelist.net"
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
diff --git a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/shikimori/data/ShikimoriRepository.kt b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/shikimori/data/ShikimoriRepository.kt
index 3aae7d6..9cc527e 100644
--- a/app/src/main/kotlin/org/dokiteam/doki/scrobbling/shikimori/data/ShikimoriRepository.kt
+++ b/app/src/main/kotlin/org/dokiteam/doki/scrobbling/shikimori/data/ShikimoriRepository.kt
@@ -28,7 +28,7 @@ import javax.inject.Inject
import javax.inject.Singleton
private const val DOMAIN = "shikimori.one"
-private const val REDIRECT_URI = "doki://shikimori-auth"
+private const val REDIRECT_URI = "kotatsu://shikimori-auth"
private const val BASE_URL = "https://$DOMAIN/"
private const val MANGA_PAGE_SIZE = 10
diff --git a/app/src/main/kotlin/org/dokiteam/doki/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/dokiteam/doki/tracker/work/TrackWorker.kt
index f15a619..951985e 100644
--- a/app/src/main/kotlin/org/dokiteam/doki/tracker/work/TrackWorker.kt
+++ b/app/src/main/kotlin/org/dokiteam/doki/tracker/work/TrackWorker.kt
@@ -33,9 +33,8 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
@@ -95,7 +94,7 @@ class TrackWorker @AssistedInject constructor(
doWorkImpl(isFullRun = isForeground && TAG_ONESHOT in tags)
} catch (e: CancellationException) {
throw e
- } catch (e: Throwable) {
+ } catch (e: Throwable) {
e.printStackTraceDebug()
Result.failure()
} finally {
@@ -105,74 +104,102 @@ class TrackWorker @AssistedInject constructor(
}
}
- private suspend fun doWorkImpl(isFullRun: Boolean): Result {
- if (!settings.isTrackerEnabled) {
- return Result.success()
- }
- val tracks = getTracksUseCase(if (isFullRun) Int.MAX_VALUE else BATCH_SIZE)
- if (tracks.isEmpty()) {
- return Result.success()
- }
+ private suspend fun doWorkImpl(isFullRun: Boolean): Result {
+ if (!settings.isTrackerEnabled) {
+ return Result.success()
+ }
+ val tracks = getTracksUseCase(if (isFullRun) Int.MAX_VALUE else BATCH_SIZE)
+ if (tracks.isEmpty()) {
+ return Result.success()
+ }
- val notifications = checkUpdatesAsync(tracks)
- if (notifications.isNotEmpty() && applicationContext.checkNotificationPermission(null)) {
- val groupNotification = notificationHelper.createGroupNotification(notifications)
- notifications.forEach { notificationManager.notify(it.tag, it.id, it.notification) }
- if (groupNotification != null) {
- notificationManager.notify(TAG, TrackerNotificationHelper.GROUP_NOTIFICATION_ID, groupNotification)
- }
- }
- return Result.success()
- }
+ checkUpdatesAsync(tracks)
+ return Result.success()
+ }
- @CheckResult
- private suspend fun checkUpdatesAsync(tracks: List): List {
- val semaphore = Semaphore(MAX_PARALLELISM)
- return channelFlow {
- for (track in tracks) {
- launch {
- semaphore.withPermit {
- send(
- runCatchingCancellable {
- checkNewChaptersUseCase.invoke(track)
- }.getOrElse { error ->
- MangaUpdates.Failure(
- manga = track.manga,
- error = error,
- )
- },
- )
- }
- }
- }
- }.onEachIndexed { index, it ->
- if (applicationContext.checkNotificationPermission(WORKER_CHANNEL_ID)) {
- notificationManager.notify(WORKER_NOTIFICATION_ID, createWorkerNotification(tracks.size, index + 1))
- }
- when (it) {
- is MangaUpdates.Failure -> {
- val e = it.error
- if (e is CloudFlareException) {
- captchaHandler.handle(e)
- }
- }
+ @CheckResult
+ private suspend fun checkUpdatesAsync(tracks: List) {
+ val semaphore = Semaphore(MAX_PARALLELISM)
+ val groupNotifications = mutableListOf()
- is MangaUpdates.Success -> processDownload(it)
- }
- }.mapNotNull {
- when (it) {
- is MangaUpdates.Failure -> null
- is MangaUpdates.Success -> if (it.isValid && it.isNotEmpty()) {
- notificationHelper.createNotification(
- manga = it.manga,
- newChapters = it.newChapters,
- )
- } else {
- null
- }
- }
- }.toList()
- }
+ try {
+ channelFlow {
+ for (track in tracks) {
+ launch {
+ semaphore.withPermit {
+ send(
+ runCatchingCancellable {
+ checkNewChaptersUseCase.invoke(track)
+ }.getOrElse { error ->
+ MangaUpdates.Failure(
+ manga = track.manga,
+ error = error,
+ )
+ },
+ )
+ }
+ }
+ }
+ }.onEachIndexed { index, it ->
+ if (applicationContext.checkNotificationPermission(WORKER_CHANNEL_ID)) {
+ notificationManager.notify(
+ WORKER_NOTIFICATION_ID,
+ createWorkerNotification(tracks.size, index + 1)
+ )
+ }
+
+ when (it) {
+ is MangaUpdates.Failure -> {
+ val e = it.error
+ if (e is CloudFlareException) {
+ captchaHandler.handle(e)
+ }
+ }
+
+ is MangaUpdates.Success -> {
+ processDownload(it)
+
+ if (it.isValid && it.isNotEmpty()) {
+ val notificationInfo = notificationHelper.createNotification(
+ manga = it.manga,
+ newChapters = it.newChapters,
+ )
+
+ if (notificationInfo != null &&
+ applicationContext.checkNotificationPermission(TrackerNotificationHelper.CHANNEL_ID)) {
+ notificationManager.notify(
+ notificationInfo.tag,
+ notificationInfo.id,
+ notificationInfo.notification
+ )
+
+ synchronized(groupNotifications) {
+ groupNotifications.add(notificationInfo)
+ }
+ }
+ }
+ }
+ }
+ }.collect()
+
+ } catch (e: CancellationException) {
+ e.printStackTraceDebug()
+ } finally {
+ withContext(NonCancellable) {
+ if (groupNotifications.size > 1 &&
+ applicationContext.checkNotificationPermission(TrackerNotificationHelper.CHANNEL_ID)) {
+ val groupNotification = notificationHelper.createGroupNotification(groupNotifications)
+ if (groupNotification != null) {
+ notificationManager.notify(
+ TAG,
+ TrackerNotificationHelper.GROUP_NOTIFICATION_ID,
+ groupNotification
+ )
+ }
+ }
+ }
+ }
+ }
override suspend fun getForegroundInfo(): ForegroundInfo {
val channel = NotificationChannelCompat.Builder(