diff --git a/.gitignore b/.gitignore index d73350954..961881ece 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ .cxx /app/src/main/java/com/axiel7/moelist/private/ClientId.kt /app/release +*.salive diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19389179e..03334c71a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,6 +55,8 @@ android { ) buildConfigField("String", "CLIENT_ID", privateProps.getProperty("CLIENT_ID")) resValue("string", "app_name", "MoeList Debug") + buildConfigField("String", "ANILIST_CLIENT_ID", privateProps.getProperty("ANILIST_CLIENT_ID")) + buildConfigField("String", "ANILIST_CLIENT_SECRET", privateProps.getProperty("ANILIST_CLIENT_SECRET")) } release { isDebuggable = false @@ -65,6 +67,8 @@ android { "proguard-rules.pro" ) buildConfigField("String", "CLIENT_ID", privateProps.getProperty("CLIENT_ID")) + buildConfigField("String", "ANILIST_CLIENT_ID", privateProps.getProperty("ANILIST_CLIENT_ID")) + buildConfigField("String", "ANILIST_CLIENT_SECRET", privateProps.getProperty("ANILIST_CLIENT_SECRET")) } } splits { @@ -170,4 +174,10 @@ dependencies { implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") + + //kache + // For in-memory cache + implementation("com.mayakapps.kache:kache:2.1.0") + // For persistent cache + implementation("com.mayakapps.kache:file-kache:2.1.0") } diff --git a/app/src/main/java/com/axiel7/moelist/App.kt b/app/src/main/java/com/axiel7/moelist/App.kt index bb007de25..62abc6b24 100644 --- a/app/src/main/java/com/axiel7/moelist/App.kt +++ b/app/src/main/java/com/axiel7/moelist/App.kt @@ -8,6 +8,7 @@ import coil3.disk.DiskCache import coil3.disk.directory import coil3.memory.MemoryCache import coil3.request.crossfade +import com.axiel7.moelist._GitHubPRs.Anilist.AnilistQuery import com.axiel7.moelist.data.model.media.TitleLanguage import com.axiel7.moelist.di.dataStoreModule import com.axiel7.moelist.di.databaseModule @@ -15,6 +16,9 @@ import com.axiel7.moelist.di.networkModule import com.axiel7.moelist.di.repositoryModule import com.axiel7.moelist.di.viewModelModule import com.axiel7.moelist.di.workerModule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.androidx.workmanager.koin.workManagerFactory @@ -40,6 +44,11 @@ class App : Application(), KoinComponent, KoinStartup, SingletonImageLoader.Fact workerModule, databaseModule, ) + + GlobalScope.launch(Dispatchers.IO) { + AnilistQuery.cache = AnilistQuery.New_ObjectKache() + } + } override fun newImageLoader(context: PlatformContext) = diff --git a/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/ALNextAiringEpisode.kt b/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/ALNextAiringEpisode.kt new file mode 100644 index 000000000..8c1227c30 --- /dev/null +++ b/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/ALNextAiringEpisode.kt @@ -0,0 +1,141 @@ +package com.axiel7.moelist._GitHubPRs.Anilist + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import com.axiel7.moelist.R +import com.axiel7.moelist.data.model.anime.AnimeNode +import com.axiel7.moelist.data.model.anime.Broadcast +import com.axiel7.moelist.data.model.media.BaseMediaNode +import com.axiel7.moelist.data.model.media.BaseUserMediaList +import com.axiel7.moelist.utils.StringExtensions.toStringOrEmpty +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName + +@Serializable +data class ALNextAiringEpisode( + val data: Data, +) +@Serializable +data class Data( + @SerialName("Page" ) val Page: Page, +) +@Serializable +data class Page( + val pageInfo: PageInfo, + val media: List, +) +@Serializable +data class PageInfo( + val total: Long, + val currentPage: Long, + val lastPage: Long, + val hasNextPage: Boolean, + val perPage: Long, +) +@Serializable +data class Media( + val id: Long, + val idMal: Long, + val nextAiringEpisode: NextAiringEpisode?, + val title: Title, +) +@Serializable +data class NextAiringEpisode( + val episode: Long, + val timeUntilAiring: Long, +) +{ + fun EpN_in_Mdays_ToString():String + { + var days = secondsToDays_AsString(timeUntilAiring) + var str = """Ep ${episode} in ${days}""" + return str + } + + +} +@Serializable +data class Title( + val english: String?, +) +{ + +} + +/** + * ALl Funcs Below are Helper. they Makes date Human Readable. + */ + +/** + * Supports Days , Hours , Minutes. less than a minute will be 0 + */ +fun secondsToDays_AsString(seconds: Long): String { + val _1month :Long = 30 *24 * 60 * 60 + val _1day :Long = 24 * 60 * 60 + val _1hour :Long = 60 * 60 + val _1min :Long = 60 + + var HumanReadbleTime ="" + + HumanReadbleTime = + GetNLDatesString_OrNull(seconds, _1month , "months" ,"month" ) + ?: GetNLDatesString_OrNull(seconds, _1day , "days" ,"day" ) + ?: GetNLDatesString_OrNull(seconds, _1hour , "hours" ,"hour" ) + ?: GetNLDatesString_OrNull(seconds, _1min , "mins" ,"min" ) + ?: "? sec" ; + + return HumanReadbleTime; +} + +private fun GetNLDatesString_OrNull( + seconds: Long, + _1Period: Long, + PluaralText:String, + SingularText:String, +): String? { + var HumanReadbleTime: String? = null + + if (seconds > _1Period) { + val days = seconds / _1Period; HumanReadbleTime = "${days} ${PluaralText}" + } else if (seconds == _1Period) { + val days = seconds / _1Period; HumanReadbleTime = "${days} ${SingularText}" + } + return HumanReadbleTime; +} + + +@Composable +fun AiringEpN_in_Ndays_ToString( + broadcast: Broadcast?, + item: BaseUserMediaList +): String { + val isAiring = remember { item.isAiring } + +// var textCompact = broadcast?.airingInString() ?: stringResource(R.string.airing) + var text = + if (isAiring ) broadcast?.airingInString() ?: stringResource(R.string.airing) + else item.node.mediaFormat?.localized().orEmpty() + + if (item.node is AnimeNode) + text = (item.node as AnimeNode)?.al_nextAiringEpisode.toStringOrEmpty() + return text +} + +/** + * For Grid - ie: 8d + */ +@Composable +fun AiringEpN_in_Ndays_ToShortString( + broadcast: Broadcast?, + item: BaseUserMediaList +): String { + val isAiring = remember { item.isAiring } + + var text = broadcast?.airingInShortString() ?: stringResource(R.string.airing) + + if (item.node is AnimeNode) + text = (item.node as AnimeNode)?.al_nextAiringEpisode.toStringOrEmpty() + return text +} + diff --git a/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/AnilistQuery.kt b/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/AnilistQuery.kt new file mode 100644 index 000000000..1a50af96d --- /dev/null +++ b/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/AnilistQuery.kt @@ -0,0 +1,200 @@ +package com.axiel7.moelist._GitHubPRs.Anilist + +import android.app.Application +import com.axiel7.moelist.data.model.anime.UserAnimeList +import com.axiel7.moelist.data.model.media.ListStatus +import com.axiel7.moelist.utils.StringExtensions.toStringOrEmpty +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlin.concurrent.thread +import kotlin.system.measureTimeMillis + +import kotlin.time.Duration.Companion.hours + +class AnilistQuery { + + //static Holder + companion object { + lateinit var appThis: CoroutineScope + lateinit var appContext: Application + lateinit var cache : InMemoryKache,List> + + @JvmStatic + suspend fun New_ObjectKache() : InMemoryKache,List> { + //GlobalScope.launch (Dispatchers.Main) {} + val cache = InMemoryKache,List>(500) { + strategy = KacheStrategy.LRU + expireAfterAccessDuration = 1.hours + } + return cache + } + + + + //---------------Normal + /** + * Uses withCache + */ + suspend fun AddNextAiringEpInfo_withMeasureTime( + result: com.axiel7.moelist.data.model.Response>) { + result.data?.let { + val timeInMillis: Long = measureTimeMillis { + AddNextAiringEpInfo(it) + } + println("AddNextAiringEpInfo : elapsedTime(ms):" + timeInMillis) + }; + } + + //AnimeRepository + /** + * Uses withCache + */ + suspend fun AddNextAiringEpInfo( userAnimeList :List + ):List? + { + fun _isAiring(it: UserAnimeList) = + ( + (it.listStatus?.status == ListStatus.WATCHING + || it.listStatus?.status == ListStatus.PLAN_TO_WATCH) + && it.isAiring + ) + + var airingAnimes = userAnimeList.filter{ _isAiring(it) } + + val airingAnimes_idlist = airingAnimes.map{ it.node.id } + if (airingAnimes_idlist.isEmpty()) + return null + + var al_mediaList = GetAiringInfo_ToPoco_FromCache(airingAnimes_idlist) + if (al_mediaList?.isEmpty() == true) + return null + + userAnimeList.filter { _isAiring(it) }.forEach { it -> + var _id = it.node.id.toLong(); + // it.node.al_nextAiringEpisode = "test success"; + var it_AirInfo = al_mediaList?.firstOrNull { it.idMal == _id }?.nextAiringEpisode + + it.node.al_nextAiringEpisode = it_AirInfo?.EpN_in_Mdays_ToString() + } + return userAnimeList; + } + + + + suspend fun GetAiringInfo_ToPoco_FromCache( airingAnimes_id_list: List): List? + { + val key = airingAnimes_id_list; + + val data = cache.getOrPut(key) { + try { + GetAiringInfo_ToPoco(key) + } catch (ex: Throwable) { + println(ex.message ) + println(ex.cause ) + println("GetAiringInfo_ToPoco_FromCache" ) + null // returning null, The value (null) will not be cached + } + } + return data; + } + + fun GetAiringInfo_ToPoco( airingAnimes_id_list: List ): List? + { + var resp1 = getAiringInfo(airingAnimes_id_list) + var resp1_bodSTR = resp1.body?.string().toStringOrEmpty() + val al_AirDataList = Json.decodeFromString(resp1_bodSTR) + var al_mediaList = al_AirDataList.data?.Page?.media; + return al_mediaList + } + + fun getAiringInfo (mal_id_list: List): Response + { + var url = "https://com.example/graphql"; + var query = Build_query_AiringInfo(mal_id_list); +// // HAVE TO MAKE graphql query one liner. - otherwise its not valid json, will fail. +// query = query.replace("\n", " " ).replace(" ", " ") + + var resp1 = makeRequest_POST_JSON(ANILIST_GRAPHQL_URL,query); + + if(resp1.code != 200) { + println("response NOT200:" + resp1.code + " - " + resp1.body) + var errorMessage = resp1.body?.string() + println("response_body?_string:" + errorMessage) + } + + return resp1; + } + + private fun Build_query_AiringInfo( mal_id_list: List ): String + { + val mal_ids_asSTR = mal_id_list.joinToString(separator = ", ") + + //validJSON + val query_AiringInfo_oneliner = """ query Al_AiringInfo { Page (page: 0, perPage: 50) { pageInfo { total currentPage lastPage hasNextPage perPage } media(idMal_in: [$mal_ids_asSTR], type: ANIME) { id idMal nextAiringEpisode { episode timeUntilAiring } title { english } } } } """; + return query_AiringInfo_oneliner; + +// val query_AiringInfo = +// """ +//query { +// Page (page: 0, perPage: 50) { +// pageInfo { +// total +// currentPage +// lastPage +// hasNextPage +// perPage +// } +// +// media(idMal_in: [$mal_ids_asSTR], type: ANIME) +// { +// id +// idMal +// nextAiringEpisode { +// episode +// timeUntilAiring +// } +// title { +// english +// } +// } +// } +//} +// +//""" + + } + + private fun makeRequest_POST_JSON(url: String, query: String ): Response + { + var jsonQuery = """ {"query":" $query ", "variables":null, "operationName":null} """ + val client = OkHttpClient() + +// val json = JSONObject() +// json.put("query",query) +// val requestBody = json.toString().toRequestBody(null) + val requestBody = jsonQuery.toRequestBody() + val request = + Request.Builder() + .url(url) + .post(requestBody) + .addHeader("Content-type", "application/json") + .addHeader("Accept", "application/json") + .build() + + return client.newCall(request).execute() + } + + + } + + +} + diff --git a/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/Constants.kt b/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/Constants.kt new file mode 100644 index 000000000..15751ed75 --- /dev/null +++ b/app/src/main/java/com/axiel7/moelist/_GitHubPRs/Anilist/Constants.kt @@ -0,0 +1,11 @@ +@file:Suppress("unused") + +package com.axiel7.moelist._GitHubPRs.Anilist +import com.axiel7.moelist.BuildConfig + +const val ANILIST_GRAPHQL_URL = "https://graphql.anilist.co" +const val ANILIST_GRAPHQL = "https://graphql.anilist.co/graphql" +const val ANILIST_URL = "https://anilist.co" + +//const val ANILIST_API_URL = "$ANILIST_URL/api/v2" + diff --git a/app/src/main/java/com/axiel7/moelist/data/model/anime/AnimeNode.kt b/app/src/main/java/com/axiel7/moelist/data/model/anime/AnimeNode.kt index 0b7b37976..878096799 100644 --- a/app/src/main/java/com/axiel7/moelist/data/model/anime/AnimeNode.kt +++ b/app/src/main/java/com/axiel7/moelist/data/model/anime/AnimeNode.kt @@ -35,4 +35,8 @@ data class AnimeNode( override val mean: Float? = null, @SerialName("my_list_status") override val myListStatus: BasicMyListStatus? = null, + + /* Anilist NextEp Airing in */ + var al_nextAiringEpisode: String? = null, + ) : BaseMediaNode() \ No newline at end of file diff --git a/app/src/main/java/com/axiel7/moelist/data/repository/AnimeRepository.kt b/app/src/main/java/com/axiel7/moelist/data/repository/AnimeRepository.kt index b3e750d0c..1216702c2 100644 --- a/app/src/main/java/com/axiel7/moelist/data/repository/AnimeRepository.kt +++ b/app/src/main/java/com/axiel7/moelist/data/repository/AnimeRepository.kt @@ -1,6 +1,7 @@ package com.axiel7.moelist.data.repository import androidx.annotation.IntRange +import com.axiel7.moelist._GitHubPRs.Anilist.AnilistQuery import com.axiel7.moelist.data.model.Response import com.axiel7.moelist.data.model.anime.AnimeDetails import com.axiel7.moelist.data.model.anime.AnimeList @@ -133,6 +134,10 @@ class AnimeRepository( fields = USER_ANIME_LIST_FIELDS ) else api.getUserAnimeList(page) + + //inject my logic ,here. since i couldnt update ui. + AnilistQuery.AddNextAiringEpInfo_withMeasureTime(result) + val retry = result.error?.let { handleResponseError(it) } return if (retry == true) getUserAnimeList(status, sort, page) else result } catch (e: Exception) { diff --git a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/CompactUserMediaListItem.kt b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/CompactUserMediaListItem.kt index c8a38534a..de81936dd 100644 --- a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/CompactUserMediaListItem.kt +++ b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/CompactUserMediaListItem.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.axiel7.moelist._GitHubPRs.Anilist.AiringEpN_in_Ndays_ToString import com.axiel7.moelist.R import com.axiel7.moelist.data.model.anime.AnimeNode import com.axiel7.moelist.data.model.anime.exampleUserAnimeList @@ -122,7 +123,7 @@ fun CompactUserMediaListItem( if (isAiring) { Text( - text = broadcast?.airingInString() ?: stringResource(R.string.airing), + text = AiringEpN_in_Ndays_ToString(broadcast, item), modifier = Modifier.padding(horizontal = 16.dp), color = MaterialTheme.colorScheme.primary, fontSize = 16.sp, diff --git a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/GridUserMediaListItem.kt b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/GridUserMediaListItem.kt index 5f658585d..7006967a7 100644 --- a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/GridUserMediaListItem.kt +++ b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/GridUserMediaListItem.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.axiel7.moelist._GitHubPRs.Anilist.AiringEpN_in_Ndays_ToShortString import com.axiel7.moelist.R import com.axiel7.moelist.data.model.anime.AnimeNode import com.axiel7.moelist.data.model.anime.exampleUserAnimeList @@ -121,7 +122,8 @@ fun GridUserMediaListItem( tint = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = broadcast?.airingInShortString() ?: stringResource(R.string.airing), + //text = broadcast?.airingInShortString() ?: stringResource(R.string.airing), + text = AiringEpN_in_Ndays_ToShortString(broadcast, item), modifier = Modifier.padding(end = 8.dp), fontSize = 15.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/MinimalUserMediaListItem.kt b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/MinimalUserMediaListItem.kt index ff595aec0..408ced02c 100644 --- a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/MinimalUserMediaListItem.kt +++ b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/MinimalUserMediaListItem.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.axiel7.moelist._GitHubPRs.Anilist.AiringEpN_in_Ndays_ToString import com.axiel7.moelist.R import com.axiel7.moelist.data.model.anime.AnimeNode import com.axiel7.moelist.data.model.anime.exampleUserAnimeList @@ -78,7 +79,7 @@ fun MinimalUserMediaListItem( if (isAiring) { Text( - text = broadcast?.airingInString() ?: stringResource(R.string.airing), + text = AiringEpN_in_Ndays_ToString(broadcast, item), color = MaterialTheme.colorScheme.primary, fontSize = 16.sp, lineHeight = 19.sp, diff --git a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/StandardUserMediaListItem.kt b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/StandardUserMediaListItem.kt index 61c0e9293..481587b41 100644 --- a/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/StandardUserMediaListItem.kt +++ b/app/src/main/java/com/axiel7/moelist/ui/userlist/composables/StandardUserMediaListItem.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.axiel7.moelist._GitHubPRs.Anilist.AiringEpN_in_Ndays_ToString import com.axiel7.moelist.R import com.axiel7.moelist.data.model.anime.AnimeNode import com.axiel7.moelist.data.model.anime.exampleUserAnimeList @@ -128,9 +129,7 @@ fun StandardUserMediaListItem( maxLines = 2 ) Text( - text = if (isAiring && broadcast != null) broadcast.airingInString() - else if (isAiring) stringResource(R.string.airing) - else item.node.mediaFormat?.localized().orEmpty(), + text = AiringEpN_in_Ndays_ToString(broadcast, item), modifier = Modifier.padding(horizontal = 16.dp), color = if (isAiring) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant