diff --git a/data/novel/build.gradle.kts b/data/novel/build.gradle.kts new file mode 100644 index 000000000..2b6edc225 --- /dev/null +++ b/data/novel/build.gradle.kts @@ -0,0 +1,20 @@ +import com.into.websoso.setNamespace + +plugins { + id("websoso.android.library") + alias(libs.plugins.kotlin.serialization) +} + +android { + setNamespace("data.novel") +} + +dependencies { + implementation(projects.core.network) + + implementation(libs.paging.runtime) + implementation(libs.retrofit) + implementation(libs.serialization.json) + + testImplementation(libs.junit) +} diff --git a/data/novel/src/main/AndroidManifest.xml b/data/novel/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/data/novel/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt new file mode 100644 index 000000000..8eeed5e32 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApi.kt @@ -0,0 +1,13 @@ +package com.into.websoso.data.novel + +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface NovelSearchApi { + @GET("novels") + suspend fun getNovels( + @Query("query") query: String, + @Query("page") page: Int, + @Query("size") size: Int, + ): NovelSearchResponseDto +} diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt new file mode 100644 index 000000000..f20c102a8 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchApiModule.kt @@ -0,0 +1,16 @@ +package com.into.websoso.data.novel + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NovelSearchApiModule { + @Provides + @Singleton + fun provideNovelSearchApi(retrofit: Retrofit): NovelSearchApi = retrofit.create(NovelSearchApi::class.java) +} diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt new file mode 100644 index 000000000..e1e756bbc --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchPagingSource.kt @@ -0,0 +1,37 @@ +package com.into.websoso.data.novel + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.into.websoso.data.novel.model.NovelSearchEntity + +internal class NovelSearchPagingSource( + private val query: String, + private val api: NovelSearchApi, +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult = + runCatching { + val page = params.key ?: INITIAL_PAGE + val response = api.getNovels( + query = query, + page = page, + size = params.loadSize, + ) + + LoadResult.Page( + data = response.novels.map(NovelSearchResponseDto.NovelDto::toData), + prevKey = page.takeIf { it > INITIAL_PAGE }?.minus(1), + nextKey = (page + 1).takeIf { response.isLoadable }, + ) + }.getOrElse(LoadResult::Error) + + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.let { page -> + page.prevKey?.plus(1) ?: page.nextKey?.minus(1) + } + } + + private companion object { + const val INITIAL_PAGE = 0 + } +} diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt new file mode 100644 index 000000000..baa0f49c4 --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchResponseDto.kt @@ -0,0 +1,33 @@ +package com.into.websoso.data.novel + +import com.into.websoso.data.novel.model.NovelSearchEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class NovelSearchResponseDto( + @SerialName("isLoadable") + val isLoadable: Boolean, + @SerialName("novels") + val novels: List, +) { + @Serializable + data class NovelDto( + @SerialName("novelId") + val novelId: Long, + @SerialName("title") + val title: String, + @SerialName("author") + val author: String, + @SerialName("novelImage") + val imageUrl: String, + ) { + fun toData(): NovelSearchEntity = + NovelSearchEntity( + novelId = novelId, + title = title, + author = author, + imageUrl = imageUrl, + ) + } +} diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt b/data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt new file mode 100644 index 000000000..f5ba6a98e --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/model/NovelSearchEntity.kt @@ -0,0 +1,8 @@ +package com.into.websoso.data.novel.model + +data class NovelSearchEntity( + val novelId: Long, + val title: String, + val author: String, + val imageUrl: String, +) diff --git a/settings.gradle.kts b/settings.gradle.kts index 81a0aa86b..e814f4de1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include( include( ":data:account", ":data:library", + ":data:novel", ":data:feed", )