Skip to content

Commit ba40bad

Browse files
committed
chore(feat): Throw notification immediately while checking for new chapters
1 parent 9caf901 commit ba40bad

File tree

7 files changed

+1277
-101
lines changed

7 files changed

+1277
-101
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1175 additions & 28 deletions
Large diffs are not rendered by default.

app/src/main/kotlin/org/dokiteam/doki/core/parser/MangaLinkResolver.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ class MangaLinkResolver @Inject constructor(
118118
companion object {
119119

120120
fun isValidLink(str: String): Boolean {
121-
return str.isHttpUrl() || str.startsWith("doki://", ignoreCase = true)
121+
return str.isHttpUrl() ||
122+
str.startsWith("doki://", ignoreCase = true) ||
123+
str.startsWith("kotatsu://", ignoreCase = true)
122124
}
123125
}
124126
}

app/src/main/kotlin/org/dokiteam/doki/scrobbling/anilist/data/AniListRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import javax.inject.Inject
2828
import javax.inject.Singleton
2929
import kotlin.math.roundToInt
3030

31-
private const val REDIRECT_URI = "doki://anilist-auth"
31+
private const val REDIRECT_URI = "kotatsu://anilist-auth"
3232
private const val BASE_URL = "https://anilist.co/api/v2/"
3333
private const val ENDPOINT = "https://graphql.anilist.co"
3434
private const val MANGA_PAGE_SIZE = 10

app/src/main/kotlin/org/dokiteam/doki/scrobbling/kitsu/ui/KitsuAuthActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class KitsuAuthActivity : BaseActivity<ActivityKitsuAuthBinding>(),
103103
private fun continueAuth() {
104104
val email = viewBinding.editEmail.text?.toString()?.trim().orEmpty()
105105
val password = viewBinding.editPassword.text?.toString()?.trim().orEmpty()
106-
val url = "doki://kitsu-auth?code=" + "$email;$password".urlEncoded()
106+
val url = "kotatsu://kitsu-auth?code=" + "$email;$password".urlEncoded()
107107
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
108108
startActivity(intent)
109109
finishAfterTransition()

app/src/main/kotlin/org/dokiteam/doki/scrobbling/mal/data/MALRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import java.security.SecureRandom
2626
import javax.inject.Inject
2727
import javax.inject.Singleton
2828

29-
private const val REDIRECT_URI = "doki://mal-auth"
29+
private const val REDIRECT_URI = "kotatsu://mal-auth"
3030
private const val BASE_WEB_URL = "https://myanimelist.net"
3131
private const val BASE_API_URL = "https://api.myanimelist.net/v2"
3232

app/src/main/kotlin/org/dokiteam/doki/scrobbling/shikimori/data/ShikimoriRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import javax.inject.Inject
2828
import javax.inject.Singleton
2929

3030
private const val DOMAIN = "shikimori.one"
31-
private const val REDIRECT_URI = "doki://shikimori-auth"
31+
private const val REDIRECT_URI = "kotatsu://shikimori-auth"
3232
private const val BASE_URL = "https://$DOMAIN/"
3333
private const val MANGA_PAGE_SIZE = 10
3434

app/src/main/kotlin/org/dokiteam/doki/tracker/work/TrackWorker.kt

Lines changed: 95 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ import kotlinx.coroutines.CancellationException
3333
import kotlinx.coroutines.NonCancellable
3434
import kotlinx.coroutines.flow.Flow
3535
import kotlinx.coroutines.flow.channelFlow
36+
import kotlinx.coroutines.flow.collect
3637
import kotlinx.coroutines.flow.map
37-
import kotlinx.coroutines.flow.mapNotNull
38-
import kotlinx.coroutines.flow.toList
3938
import kotlinx.coroutines.launch
4039
import kotlinx.coroutines.sync.Semaphore
4140
import kotlinx.coroutines.sync.withPermit
@@ -95,7 +94,7 @@ class TrackWorker @AssistedInject constructor(
9594
doWorkImpl(isFullRun = isForeground && TAG_ONESHOT in tags)
9695
} catch (e: CancellationException) {
9796
throw e
98-
} catch (e: Throwable) {
97+
} catch (e: Throwable) {
9998
e.printStackTraceDebug()
10099
Result.failure()
101100
} finally {
@@ -105,74 +104,102 @@ class TrackWorker @AssistedInject constructor(
105104
}
106105
}
107106

108-
private suspend fun doWorkImpl(isFullRun: Boolean): Result {
109-
if (!settings.isTrackerEnabled) {
110-
return Result.success()
111-
}
112-
val tracks = getTracksUseCase(if (isFullRun) Int.MAX_VALUE else BATCH_SIZE)
113-
if (tracks.isEmpty()) {
114-
return Result.success()
115-
}
107+
private suspend fun doWorkImpl(isFullRun: Boolean): Result {
108+
if (!settings.isTrackerEnabled) {
109+
return Result.success()
110+
}
111+
val tracks = getTracksUseCase(if (isFullRun) Int.MAX_VALUE else BATCH_SIZE)
112+
if (tracks.isEmpty()) {
113+
return Result.success()
114+
}
116115

117-
val notifications = checkUpdatesAsync(tracks)
118-
if (notifications.isNotEmpty() && applicationContext.checkNotificationPermission(TrackerNotificationHelper.CHANNEL_ID)) {
119-
val groupNotification = notificationHelper.createGroupNotification(notifications)
120-
notifications.forEach { notificationManager.notify(it.tag, it.id, it.notification) }
121-
if (groupNotification != null) {
122-
notificationManager.notify(TAG, TrackerNotificationHelper.GROUP_NOTIFICATION_ID, groupNotification)
123-
}
124-
}
125-
return Result.success()
126-
}
116+
checkUpdatesAsync(tracks)
117+
return Result.success()
118+
}
127119

128-
@CheckResult
129-
private suspend fun checkUpdatesAsync(tracks: List<MangaTracking>): List<NotificationInfo> {
130-
val semaphore = Semaphore(MAX_PARALLELISM)
131-
return channelFlow {
132-
for (track in tracks) {
133-
launch {
134-
semaphore.withPermit {
135-
send(
136-
runCatchingCancellable {
137-
checkNewChaptersUseCase.invoke(track)
138-
}.getOrElse { error ->
139-
MangaUpdates.Failure(
140-
manga = track.manga,
141-
error = error,
142-
)
143-
},
144-
)
145-
}
146-
}
147-
}
148-
}.onEachIndexed { index, it ->
149-
if (applicationContext.checkNotificationPermission(WORKER_CHANNEL_ID)) {
150-
notificationManager.notify(WORKER_NOTIFICATION_ID, createWorkerNotification(tracks.size, index + 1))
151-
}
152-
when (it) {
153-
is MangaUpdates.Failure -> {
154-
val e = it.error
155-
if (e is CloudFlareException) {
156-
captchaHandler.handle(e)
157-
}
158-
}
120+
@CheckResult
121+
private suspend fun checkUpdatesAsync(tracks: List<MangaTracking>) {
122+
val semaphore = Semaphore(MAX_PARALLELISM)
123+
val groupNotifications = mutableListOf<NotificationInfo>()
159124

160-
is MangaUpdates.Success -> processDownload(it)
161-
}
162-
}.mapNotNull {
163-
when (it) {
164-
is MangaUpdates.Failure -> null
165-
is MangaUpdates.Success -> if (it.isValid && it.isNotEmpty()) {
166-
notificationHelper.createNotification(
167-
manga = it.manga,
168-
newChapters = it.newChapters,
169-
)
170-
} else {
171-
null
172-
}
173-
}
174-
}.toList()
175-
}
125+
try {
126+
channelFlow {
127+
for (track in tracks) {
128+
launch {
129+
semaphore.withPermit {
130+
send(
131+
runCatchingCancellable {
132+
checkNewChaptersUseCase.invoke(track)
133+
}.getOrElse { error ->
134+
MangaUpdates.Failure(
135+
manga = track.manga,
136+
error = error,
137+
)
138+
},
139+
)
140+
}
141+
}
142+
}
143+
}.onEachIndexed { index, it ->
144+
if (applicationContext.checkNotificationPermission(WORKER_CHANNEL_ID)) {
145+
notificationManager.notify(
146+
WORKER_NOTIFICATION_ID,
147+
createWorkerNotification(tracks.size, index + 1)
148+
)
149+
}
150+
151+
when (it) {
152+
is MangaUpdates.Failure -> {
153+
val e = it.error
154+
if (e is CloudFlareException) {
155+
captchaHandler.handle(e)
156+
}
157+
}
158+
159+
is MangaUpdates.Success -> {
160+
processDownload(it)
161+
162+
if (it.isValid && it.isNotEmpty()) {
163+
val notificationInfo = notificationHelper.createNotification(
164+
manga = it.manga,
165+
newChapters = it.newChapters,
166+
)
167+
168+
if (notificationInfo != null &&
169+
applicationContext.checkNotificationPermission(TrackerNotificationHelper.CHANNEL_ID)) {
170+
notificationManager.notify(
171+
notificationInfo.tag,
172+
notificationInfo.id,
173+
notificationInfo.notification
174+
)
175+
176+
synchronized(groupNotifications) {
177+
groupNotifications.add(notificationInfo)
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}.collect()
184+
185+
} catch (e: CancellationException) {
186+
e.printStackTraceDebug()
187+
} finally {
188+
withContext(NonCancellable) {
189+
if (groupNotifications.size > 1 &&
190+
applicationContext.checkNotificationPermission(TrackerNotificationHelper.CHANNEL_ID)) {
191+
val groupNotification = notificationHelper.createGroupNotification(groupNotifications)
192+
if (groupNotification != null) {
193+
notificationManager.notify(
194+
TAG,
195+
TrackerNotificationHelper.GROUP_NOTIFICATION_ID,
196+
groupNotification
197+
)
198+
}
199+
}
200+
}
201+
}
202+
}
176203

177204
override suspend fun getForegroundInfo(): ForegroundInfo {
178205
val channel = NotificationChannelCompat.Builder(

0 commit comments

Comments
 (0)