Skip to content

Commit df8127e

Browse files
committed
Add offline support for chapters using cache
1 parent 0665366 commit df8127e

File tree

9 files changed

+168
-38
lines changed

9 files changed

+168
-38
lines changed

feature-manga-list/src/main/java/com/melonhead/feature_manga_list/MangaRepository.kt

+17-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.melonhead.lib_core.models.*
77
import com.melonhead.data_app_data.AppDataService
88
import com.melonhead.data_at_home.AtHomeService
99
import com.melonhead.data_user.services.UserService
10-
import com.melonhead.lib_chapter_cache.ChapterCacheMechanism
10+
import com.melonhead.lib_chapter_cache.ChapterCache
1111
import com.melonhead.feature_manga_list.services.MangaService
1212
import com.melonhead.lib_app_context.AppContext
1313
import com.melonhead.lib_app_events.AppEventsRepository
@@ -32,7 +32,7 @@ import org.koin.core.component.inject
3232
internal interface MangaRepository {
3333
val manga: Flow<List<UIManga>>
3434
val refreshStatus: Flow<MangaRefreshStatus>
35-
suspend fun getChapterData(chapterId: String): List<String>?
35+
suspend fun getChapterData(mangaId: String,chapterId: String): List<String>?
3636
}
3737

3838
internal class MangaRepositoryImpl(
@@ -44,7 +44,7 @@ internal class MangaRepositoryImpl(
4444
private val mangaDb: MangaDao,
4545
private val readMarkerDb: ReadMarkerDao,
4646
private val context: Context,
47-
private val chapterCache: ChapterCacheMechanism,
47+
private val chapterCache: ChapterCache,
4848
private val appEventsRepository: AppEventsRepository,
4949
private val newChapterNotificationChannel: NewChapterNotificationChannel,
5050
private val appContext: AppContext,
@@ -197,17 +197,18 @@ internal class MangaRepositoryImpl(
197197
appDataService.updateLastRefreshDate()
198198
}
199199

200-
private suspend fun notifyOfNewChapters() {
200+
private suspend fun handleUnreadChapters() {
201+
val manga = mangaDb.getAllSync()
202+
val newChapters = chapterDb.getAllSync().filter { readMarkerDb.isRead(it.mangaId, it.chapter) != true }
203+
chapterCache.cacheImagesForChapters(manga, newChapters)
204+
201205
if (appContext.isInForeground) return
202206
val notificationManager = NotificationManagerCompat.from(context)
203207
if (!notificationManager.areNotificationsEnabled()) return
204208
val installDateSeconds = appDataService.installDateSeconds.firstOrNull() ?: 0L
205-
Clog.i("notifyOfNewChapters")
209+
Clog.i("Posting notification for new chapters")
206210

207-
val newChapters = chapterDb.getAllSync().filter { readMarkerDb.isRead(it.mangaId, it.chapter) != true }
208-
val manga = mangaDb.getAllSync()
209211
val notifyChapters = generateUIManga(manga, newChapters)
210-
chapterCache.cacheImagesForChapters(newChapters)
211212
newChapterNotificationChannel.post(context, notifyChapters, installDateSeconds)
212213
}
213214

@@ -231,7 +232,7 @@ internal class MangaRepositoryImpl(
231232
}
232233

233234
if (chaptersToUpdate.isEmpty()) {
234-
notifyOfNewChapters()
235+
handleUnreadChapters()
235236
return
236237
}
237238

@@ -247,7 +248,7 @@ internal class MangaRepositoryImpl(
247248
readMarkerDb.update(*readMarkersToUpdate.toTypedArray())
248249

249250
// notify user of new chapters
250-
notifyOfNewChapters()
251+
handleUnreadChapters()
251252
}
252253

253254
private fun markChapterRead(uiManga: UIManga, uiChapter: UIChapter, read: Boolean) {
@@ -256,13 +257,18 @@ internal class MangaRepositoryImpl(
256257
val entity = readMarkerDb.getEntity(uiManga.id, uiChapter.chapter) ?: return@launch
257258
if (read) {
258259
newChapterNotificationChannel.dismissNotification(context, uiManga, uiChapter)
260+
chapterCache.clearChapterFromCache(uiManga.id, uiChapter.id)
259261
}
260262
readMarkerDb.update(entity.copy(readStatus = read))
261263
mangaService.changeReadStatus(token, uiManga, uiChapter, read)
262264
}
263265
}
264266

265-
override suspend fun getChapterData(chapterId: String): List<String>? {
267+
override suspend fun getChapterData(mangaId: String, chapterId: String): List<String>? {
268+
val chapterFiles = chapterCache.getChapterFromCache(mangaId, chapterId)
269+
if (chapterFiles.isNotEmpty()) return chapterFiles
270+
271+
Clog.e("Chapter not found in cache", RuntimeException())
266272
val token = appDataService.token.firstOrNull() ?: return null
267273
val chapterData = atHomeService.getChapterData(token, chapterId)
268274
return if (appDataService.useDataSaver) {

feature-manga-list/src/main/java/com/melonhead/feature_manga_list/viewmodels/MangaListViewModel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ internal class MangaListViewModel(
8888
viewModelScope.launch {
8989
val intent = when (userAppDataService.renderStyle) {
9090
RenderStyle.Native -> {
91-
val chapterData = mangaRepository.getChapterData(uiChapter.id)
91+
val chapterData = mangaRepository.getChapterData(uiManga.id, uiChapter.id)
9292
// use secondary render style
9393
if (chapterData.isNullOrEmpty()) {
9494
appEventsRepository.postEvent(UserEvent.SetUseWebView(uiManga, true))

feature-native-chapter-viewer/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ dependencies {
4141
implementation project(":lib-app-events")
4242
implementation project(":lib-logging")
4343
implementation project(":lib-navigation")
44+
implementation project(":lib-chapter-cache")
4445
implementation project(":data-app-data")
4546

4647
api project(":data-app-data")

feature-native-chapter-viewer/src/main/java/com/melonhead/feature_native_chapter_viewer/ui/scenes/ChapterScreen.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ internal fun ChapterScreen(
4444
val (width, height) = getWidthHeight()
4545

4646
fun preloadImage(url: String, currentPageIndex: Int) {
47+
if (!(url.contains("http") || url.contains("https"))) {
48+
// don't preload images stored on disk
49+
return
50+
}
4751
val request = url.preloadImageRequest(currentPageIndex, context, width, height)
4852
context.imageLoader.enqueue(request)
4953
}
@@ -156,7 +160,13 @@ private fun ChapterView(
156160
var retryHash by remember { mutableStateOf(false) }
157161
val (width, height) = getWidthHeight()
158162
SubcomposeAsyncImage(
159-
model = currentPageUrl.preloadImageRequest(pageIndex = currentPageIndex, LocalContext.current, width, height, retryHash) {
163+
model = currentPageUrl.preloadImageRequest(
164+
pageIndex = currentPageIndex,
165+
LocalContext.current,
166+
width,
167+
height,
168+
retryHash
169+
) {
160170
Clog.i("Retrying due to load failure")
161171
retryHash = !retryHash
162172
},

lib-chapter-cache/build.gradle

+4
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ dependencies {
3535
implementation libs.bundles.koin
3636

3737
implementation project(":lib-database")
38+
implementation project(":lib-logging")
39+
implementation project(":data-authentication")
40+
implementation project(":data-app-data")
41+
implementation project(":data-at-home")
3842
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3-
3+
<uses-permission
4+
android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
5+
<uses-permission
6+
android:name="android.permission.INTERNET" />
47
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.melonhead.lib_chapter_cache
2+
3+
import android.content.Context
4+
import com.melonhead.data_app_data.AppDataService
5+
import com.melonhead.data_at_home.AtHomeService
6+
import com.melonhead.lib_database.chapter.ChapterEntity
7+
import com.melonhead.lib_database.manga.MangaEntity
8+
import com.melonhead.lib_logging.Clog
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.flow.firstOrNull
12+
import kotlinx.coroutines.launch
13+
import java.io.*
14+
import java.net.URL
15+
16+
interface ChapterCache {
17+
fun cacheImagesForChapters(manga: List<MangaEntity>, chapters: List<ChapterEntity>)
18+
fun getChapterFromCache(mangaId: String, chapterId: String): List<String>
19+
fun clearChapterFromCache(mangaId: String, chapterId: String)
20+
}
21+
22+
internal class ChapterCacheImpl(
23+
private val appDataService: AppDataService,
24+
private val atHomeService: AtHomeService,
25+
private val externalScope: CoroutineScope,
26+
private val appContext: Context,
27+
) : ChapterCache {
28+
29+
private suspend fun getChapterData(chapterId: String): List<String>? {
30+
val token = appDataService.token.firstOrNull() ?: return null
31+
val chapterData = atHomeService.getChapterData(token, chapterId)
32+
return if (appDataService.useDataSaver) {
33+
chapterData?.pagesDataSaver()
34+
} else {
35+
chapterData?.pages()
36+
}
37+
}
38+
39+
override fun cacheImagesForChapters(manga: List<MangaEntity>, chapters: List<ChapterEntity>) {
40+
Clog.d("Caching images for ${chapters.size} chapters")
41+
for (chapter in chapters) {
42+
val mangaForChapter = manga.find { it.id == chapter.mangaId } ?: continue
43+
if (mangaForChapter.useWebview) continue
44+
externalScope.launch(Dispatchers.IO) {
45+
Clog.d("Caching images for manga ${mangaForChapter.chosenTitle} chapter ${chapter.chapterTitle}")
46+
val cacheDirectory = appContext.cacheDir
47+
val mangaDirectory = File(cacheDirectory, mangaForChapter.id)
48+
49+
val chapterData = getChapterData(chapter.id)
50+
if (chapterData.isNullOrEmpty()) return@launch
51+
52+
if (!mangaDirectory.exists()) {
53+
mangaDirectory.mkdir()
54+
}
55+
56+
val chapterDirectory = File(mangaDirectory, chapter.id)
57+
if (!chapterDirectory.exists()) {
58+
chapterDirectory.mkdir()
59+
}
60+
61+
val oldFiles = chapterDirectory.listFiles()?.size ?: 0
62+
if (oldFiles != chapterData.size) {
63+
for (file in chapterDirectory.listFiles()!!) {
64+
file.delete()
65+
}
66+
}
67+
68+
Clog.d("Downloading images to cache for ${mangaForChapter.chosenTitle} chapter ${chapter.chapterTitle}")
69+
for ((i, page) in chapterData.withIndex()) {
70+
Clog.d("Downloading page $i for ${mangaForChapter.chosenTitle} chapter ${chapter.chapterTitle} - $page")
71+
val fileExtension = page.substringAfterLast(".")
72+
val pageFile = File(chapterDirectory, "$i.$fileExtension")
73+
pageFile.createNewFile()
74+
75+
try {
76+
val oStream = FileOutputStream(pageFile)
77+
val inputStream = URL(page).openStream()
78+
copy(inputStream, oStream)
79+
oStream.flush()
80+
inputStream.close()
81+
oStream.close()
82+
} catch (e: Exception) {
83+
Clog.i("Error downloading page $i for ${mangaForChapter.chosenTitle} chapter ${chapter.chapterTitle} - $page")
84+
Clog.e("Error downloading page", e)
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
@Throws(IOException::class)
92+
private fun copy(source: InputStream, target: OutputStream) {
93+
val buf = ByteArray(8192)
94+
var length: Int
95+
while (source.read(buf).also { length = it } > 0) {
96+
target.write(buf, 0, length)
97+
}
98+
}
99+
100+
override fun getChapterFromCache(mangaId: String, chapterId: String): List<String> {
101+
val cacheDirectory = appContext.cacheDir
102+
val mangaDirectory = File(cacheDirectory, mangaId)
103+
val chapterDirectory = File(mangaDirectory, chapterId)
104+
return if (chapterDirectory.exists()) {
105+
chapterDirectory.listFiles()?.map { it.absolutePath } ?: emptyList()
106+
} else {
107+
emptyList()
108+
}
109+
}
110+
111+
override fun clearChapterFromCache(mangaId: String, chapterId: String) {
112+
try {
113+
Clog.d("Clearing cache for manga $mangaId chapter $chapterId")
114+
val cacheDirectory = appContext.cacheDir
115+
val mangaDirectory = File(cacheDirectory, mangaId)
116+
val chapterDirectory = File(mangaDirectory, chapterId)
117+
if (chapterDirectory.exists()) {
118+
val result = chapterDirectory.deleteRecursively()
119+
Clog.d("Cleared cache for manga $mangaId chapter $chapterId - $result")
120+
}
121+
} catch (e: Exception) {
122+
Clog.i("Error clearing cache for manga $mangaId chapter $chapterId")
123+
Clog.e("Error clearing cache for manga", e)
124+
}
125+
}
126+
}

lib-chapter-cache/src/main/java/com/melonhead/lib_chapter_cache/ChapterCacheMechanism.kt

-20
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.melonhead.lib_chapter_cache.di
22

3-
import com.melonhead.lib_chapter_cache.ChapterCacheMechanism
4-
import com.melonhead.lib_chapter_cache.ChapterCacheMechanismImpl
3+
import com.melonhead.lib_chapter_cache.ChapterCache
4+
import com.melonhead.lib_chapter_cache.ChapterCacheImpl
55
import com.melonhead.lib_database.di.LibDbModule
66
import org.koin.dsl.module
77

88
val LibChapterCacheModule = module {
99
includes(LibDbModule)
10-
single<ChapterCacheMechanism> {
11-
ChapterCacheMechanismImpl()
10+
single<ChapterCache> {
11+
ChapterCacheImpl(get(), get(), get(), get())
1212
}
1313
}

0 commit comments

Comments
 (0)