diff --git a/anilist/src/commonMain/graphql/MediumQuery.graphql b/anilist/src/commonMain/graphql/MediumQuery.graphql index 8ae4626..1cef425 100644 --- a/anilist/src/commonMain/graphql/MediumQuery.graphql +++ b/anilist/src/commonMain/graphql/MediumQuery.graphql @@ -41,6 +41,7 @@ query MediumQuery($id: Int, $statusVersion: Int, $html: Boolean) { genres, characters(sort: [FAVOURITES_DESC,RELEVANCE]) { nodes { + id, name { first, middle diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt index 9002e01..75584e7 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayStateMachine.kt @@ -46,7 +46,7 @@ class AiringTodayStateMachine( }.mapError { val query = fallbackClient.query(state.snapshot.query) - query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { val wantedContent = if (!state.snapshot.adultContent) { val content = it.Page?.airingSchedulesFilterNotNull() ?: emptyList() diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt index bb15642..0accbdb 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterStateMachine.kt @@ -3,6 +3,7 @@ package dev.datlag.aniflow.anilist import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import com.freeletics.flowredux.dsl.State import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.firebase.FirebaseFactory import dev.datlag.aniflow.model.CatchResult @@ -15,21 +16,17 @@ import kotlin.time.Duration.Companion.seconds class CharacterStateMachine( private val client: ApolloClient, private val fallbackClient: ApolloClient, - private val crashlytics: FirebaseFactory.Crashlytics? + private val crashlytics: FirebaseFactory.Crashlytics?, + private val id: Int ) : FlowReduxStateMachine( - initialState = currentState + initialState = State.Loading(id) ) { + var currentState: State = State.Loading(id) + private set + init { spec { - inState { - onEnterEffect { - currentState = it - } - on { action, state -> - state.override { State.Loading(action.id) } - } - } inState { onEnterEffect { currentState = it @@ -46,10 +43,12 @@ class CharacterStateMachine( }.mapError { val query = fallbackClient.query(state.snapshot.query) - query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { it.Character?.let { data -> - State.Success(state.snapshot.query, Character(data)) + Character(data)?.let { char -> + State.Success(state.snapshot.query, char) + } } } @@ -57,7 +56,7 @@ class CharacterStateMachine( response.asSuccess { crashlytics?.log(it) - State.Error + State.Error(query) } } } @@ -67,27 +66,26 @@ class CharacterStateMachine( Cache.setCharacter(it.query, it.character) currentState = it } - on { action, state -> - state.override { State.Loading(action.id) } - } } inState { onEnterEffect { currentState = it } - on { action, state -> - state.override { State.Loading(action.id) } + on { _, state -> + state.override { + State.Loading(state.snapshot.query) + } } } } } sealed interface State { - data object Waiting : State data class Loading(internal val query: CharacterQuery) : State { constructor(id: Int) : this( query = CharacterQuery( - id = Optional.present(id) + id = Optional.present(id), + html = Optional.present(true) ) ) } @@ -95,18 +93,12 @@ class CharacterStateMachine( internal val query: CharacterQuery, val character: Character ) : State - data object Error : State + data class Error( + internal val query: CharacterQuery, + ) : State } sealed interface Action { - data class Load(val id: Int) : Action - } - - companion object { - var currentState: State - get() = StateSaver.character - set(value) { - StateSaver.character = value - } + data object Retry : Action } } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt index b18e59b..de03cd1 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt @@ -39,7 +39,7 @@ class MediumStateMachine( }.mapError { val query = fallbackClient.query(state.snapshot.query) - query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { it.Media?.let { data -> State.Success(state.snapshot.query, Medium.Full(data)) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt index 8d7e1c0..ec6ef85 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonStateMachine.kt @@ -39,7 +39,7 @@ class PopularNextSeasonStateMachine( }.mapError { val query = fallbackClient.query(state.snapshot.query) - query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { SeasonState.Success(state.snapshot.query, it) } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt index 01fb68c..60ac0bb 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonStateMachine.kt @@ -50,7 +50,7 @@ class PopularSeasonStateMachine( }.mapError { val query = fallbackClient.query(state.snapshot.query) - query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { SeasonState.Success(state.snapshot.query, it) } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt index a63634c..70cea9c 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/StateSaver.kt @@ -17,5 +17,4 @@ internal object StateSaver { year = nextYear ) } - var character: CharacterStateMachine.State = CharacterStateMachine.State.Waiting } \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt index 8136a39..b51e10e 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingAnimeStateMachine.kt @@ -40,7 +40,7 @@ class TrendingAnimeStateMachine( }.mapError { val query = fallbackClient.query(state.snapshot.query) - query.execute().data ?: query.toFlow().saveFirstOrNull()?.dataOrThrow() + query.execute().data ?: query.toFlow().saveFirstOrNull()?.data }.mapSuccess { State.Success(state.snapshot.query, it) } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt index 67e6c63..3133328 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Character.kt @@ -1,17 +1,204 @@ package dev.datlag.aniflow.anilist.model import dev.datlag.aniflow.anilist.CharacterQuery +import dev.datlag.aniflow.anilist.MediumQuery import kotlinx.serialization.Serializable @Serializable open class Character( + /** + * The id of the character + */ open val id: Int, + + /** + * The names of the character + */ + open val name: Name, + + /** + * Character images + */ + open val image: Image, + + /** + * The character's gender. + * Usually Male, Female, or Non-binary but can be any string. + */ open val gender: String?, + + /** + * The characters blood type + */ open val bloodType: String?, + + /** + * The character's birthdate + */ + open val birthDate: Character.BirthDate?, + + /** + * A general description of the character + */ + open val description: String?, ) { - constructor(char: CharacterQuery.Character) : this( - id = char.id, - gender = char.gender?.ifBlank { null }, - bloodType = char.bloodType?.ifBlank { null }, - ) -} + + @Serializable + data class Name( + /** + * The character's given name + */ + val first: String?, + + /** + * The character's middle name + */ + val middle: String?, + + /** + * The character's surname + */ + val last: String?, + + /** + * The character's first and last name + */ + val full: String?, + + /** + * The character's full name in their native language + */ + val native: String?, + + /** + * The currently authenticated users preferred name language. Default romaji for + * non-authenticated + */ + val userPreferred: String? + ) { + constructor(name: MediumQuery.Name) : this( + first = name.first?.ifBlank { null }, + middle = name.middle?.ifBlank { null }, + last = name.last?.ifBlank { null }, + full = name.full?.ifBlank { null }, + native = name.native?.ifBlank { null }, + userPreferred = name.userPreferred?.ifBlank { null } + ) + + constructor(name: CharacterQuery.Name) : this( + first = name.first?.ifBlank { null }, + middle = name.middle?.ifBlank { null }, + last = name.last?.ifBlank { null }, + full = name.full?.ifBlank { null }, + native = name.native?.ifBlank { null }, + userPreferred = name.userPreferred?.ifBlank { null } + ) + } + + @Serializable + data class Image( + val large: String?, + val medium: String? + ) { + constructor(image: MediumQuery.Image) : this( + large = image.large?.ifBlank { null }, + medium = image.medium?.ifBlank { null } + ) + + constructor(image: CharacterQuery.Image) : this( + large = image.large?.ifBlank { null }, + medium = image.medium?.ifBlank { null }, + ) + } + + @Serializable + data class BirthDate( + val day: Int?, + val month: Int?, + val year: Int? + ) { + fun format(): String { + return buildString { + if (day != null) { + if (day <= 9) { + append("0$day") + } else { + append(day) + } + append(". ") + } + if (month != null) { + when (month) { + 1 -> append("Jan") + 2 -> append("Feb") + 3 -> append("Mar") + 4 -> append("Apr") + 5 -> append("May") + 6 -> append("Jun") + 7 -> append("Jul") + 8 -> append("Aug") + 9 -> append("Sep") + 10 -> append("Oct") + 11 -> append("Nov") + 12 -> append("Dec") + else -> if (month <= 9) { + append("0$month.") + } else { + append("$month.") + } + } + append(' ') + } + if (year != null) { + append(year) + } + }.trim() + } + + companion object { + operator fun invoke(birth: CharacterQuery.DateOfBirth) : BirthDate? { + if (birth.day == null && birth.month == null && birth.year == null) { + return null + } + + return BirthDate( + day = birth.day, + month = birth.month, + year = birth.year + ) + } + } + } + + companion object { + operator fun invoke(character: MediumQuery.Node) : Character? { + val name = character.name?.let(::Name) ?: return null + val image = character.image?.let(::Image) ?: return null + + return Character( + id = character.id, + name = name, + image = image, + gender = null, + bloodType = null, + birthDate = null, + description = null + ) + } + + operator fun invoke(character: CharacterQuery.Character) : Character? { + val name = character.name?.let(::Name) ?: return null + val image = character.image?.let(::Image) ?: return null + + return Character( + id = character.id, + name = name, + image = image, + gender = character.gender?.ifBlank { null }, + bloodType = character.bloodType?.ifBlank { null }, + birthDate = character.dateOfBirth?.let { BirthDate(it) }, + description = character.description?.ifBlank { null } + ) + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt index f052a61..729fa2e 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/model/Medium.kt @@ -166,86 +166,6 @@ open class Medium( val medium: String?, ) - @Serializable - data class Character( - /** - * The names of the character - */ - val name: Name, - - /** - * Character images - */ - val image: Image - ) { - - @Serializable - data class Name( - /** - * The character's given name - */ - val first: String?, - - /** - * The character's middle name - */ - val middle: String?, - - /** - * The character's surname - */ - val last: String?, - - /** - * The character's first and last name - */ - val full: String?, - - /** - * The character's full name in their native language - */ - val native: String?, - - /** - * The currently authenticated users preferred name language. Default romaji for - * non-authenticated - */ - val userPreferred: String? - ) { - constructor(name: MediumQuery.Name) : this( - first = name.first?.ifBlank { null }, - middle = name.middle?.ifBlank { null }, - last = name.last?.ifBlank { null }, - full = name.full?.ifBlank { null }, - native = name.native?.ifBlank { null }, - userPreferred = name.userPreferred?.ifBlank { null } - ) - } - - @Serializable - data class Image( - val large: String?, - val medium: String? - ) { - constructor(image: MediumQuery.Image) : this( - large = image.large?.ifBlank { null }, - medium = image.medium?.ifBlank { null } - ) - } - - companion object { - operator fun invoke(character: MediumQuery.Node) : Character? { - val name = character.name?.let(::Name) ?: return null - val image = character.image?.let(::Image) ?: return null - - return Character( - name = name, - image = image - ) - } - } - } - @Serializable data class Ranking( /** diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt index 9a583a7..a5e464f 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.android.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.common import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.model.Character import java.util.Locale actual fun Medium.Title.preferred(): String { @@ -24,7 +25,7 @@ actual fun Medium.Title.preferred(): String { } ?: "" } -actual fun Medium.Character.Name.preferred(): String { +actual fun Character.Name.preferred(): String { return this.userPreferred?.ifBlank { null } ?: run { val locale = Locale.getDefault() diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt index cd6b448..ac3fe74 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt @@ -16,6 +16,7 @@ import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.DataStoreUserSettings import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.UserSettingsSerializer +import io.github.aakira.napier.Napier import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.* @@ -69,7 +70,20 @@ actual object PlatformModule { projectId = Sekret.firebaseProject(BuildKonfig.packageName), applicationId = Sekret.firebaseAndroidApplication(BuildKonfig.packageName)!!, apiKey = Sekret.firebaseAndroidApiKey(BuildKonfig.packageName)!!, - googleAuthProvider = instanceOrNull() + googleAuthProvider = instanceOrNull(), + localLogger = object : FirebaseFactory.Crashlytics.LocalLogger { + override fun warn(message: String?) { + message?.let { Napier.w(it) } + } + + override fun error(message: String?) { + message?.let { Napier.e(it) } + } + + override fun error(throwable: Throwable?) { + throwable?.let { Napier.e("", it) } + } + } ) } else { FirebaseFactory.Empty diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt index 6c5f067..68b46a8 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.android.kt @@ -84,8 +84,8 @@ actual class BurningSeriesResolver( } actual fun resolveByName(english: String?, romaji: String?) { - val englishTrimmed = english?.trim()?.ifBlank { null } - val romajiTrimmed = romaji?.trim()?.ifBlank { null } + val englishTrimmed = english?.trim()?.ifBlank { null }?.replace("'", "") + val romajiTrimmed = romaji?.trim()?.ifBlank { null }?.replace("'", "") if (seriesClient == null || (englishTrimmed == null && romajiTrimmed == null)) { return diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt index adb5582..589b819 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt @@ -54,7 +54,7 @@ actual fun TranslateButton( .requireWifi() .build() } - var enabled by remember { mutableStateOf(true) } + var enabled by remember(text) { mutableStateOf(text.isNotBlank()) } var translated by remember { mutableStateOf(false) } var progress by remember { mutableStateOf(false) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt index 99ab95f..d42f45d 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.common import androidx.compose.runtime.Composable import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaRankType @@ -120,9 +121,9 @@ fun Collection.popular(): Medium.Ranking? { fun Medium.Full.popular(): Medium.Ranking? = this.ranking.popular() -expect fun Medium.Character.Name.preferred(): String +expect fun Character.Name.preferred(): String -fun Medium.Character.preferredName(): String = this.name.preferred() +fun Character.preferredName(): String = this.name.preferred() private fun SearchResponse.Result.AniList.Title?.asMediumTitle(): Medium.Title { return Medium.Title( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 94ba901..5c319af 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -120,13 +120,6 @@ data object NetworkModule { crashlytics = nullableFirebaseInstance()?.crashlytics ) } - bindProvider { - CharacterStateMachine( - client = instance(Constants.AniList.APOLLO_CLIENT), - fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), - crashlytics = nullableFirebaseInstance()?.crashlytics - ) - } bindSingleton(Constants.AniList.Auth.CLIENT) { OpenIdConnectClient { endpoints { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt new file mode 100644 index 0000000..70f41f9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/DialogConfig.kt @@ -0,0 +1,13 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium + +import dev.datlag.aniflow.anilist.model.Character as Char +import kotlinx.serialization.Serializable + +@Serializable +sealed class DialogConfig { + + @Serializable + data class Character( + val initial: Char + ) : DialogConfig() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt index e1c93e4..4b05f52 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt @@ -1,10 +1,14 @@ package dev.datlag.aniflow.ui.navigation.screen.medium +import com.arkivanov.decompose.router.slot.ChildSlot +import com.arkivanov.decompose.value.Value +import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.ContentHolderComponent +import dev.datlag.aniflow.ui.navigation.DialogComponent import kotlinx.coroutines.flow.StateFlow interface MediumComponent : ContentHolderComponent { @@ -26,13 +30,15 @@ interface MediumComponent : ContentHolderComponent { val popular: StateFlow val score: StateFlow - val characters: StateFlow> + val characters: StateFlow> val rating: StateFlow val alreadyAdded: StateFlow val trailer: StateFlow val bsAvailable: Boolean + val dialog: Value> + fun back() override fun dismissContent() { back() @@ -40,4 +46,5 @@ interface MediumComponent : ContentHolderComponent { fun rate(onLoggedIn: () -> Unit) fun rate(value: Int) fun descriptionTranslation(text: String?) + fun showCharacter(character: Character) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt index d4ad6a5..2c82c91 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter +import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.maxkeppeker.sheets.core.models.base.Header import com.maxkeppeker.sheets.core.models.base.IconSource import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState @@ -79,6 +80,9 @@ fun MediumScreen(component: MediumComponent) { } val ratingState = rememberUseCaseState() val userRating by component.rating.collectAsStateWithLifecycle() + val dialogState by component.dialog.subscribeAsState() + + dialogState.child?.instance?.render() RatingDialog( state = ratingState, @@ -491,7 +495,7 @@ fun MediumScreen(component: MediumComponent) { item { Text( modifier = Modifier.padding(top = 16.dp).padding(horizontal = 16.dp), - text = "Characters", + text = stringResource(SharedRes.strings.characters), style = MaterialTheme.typography.headlineSmall ) } @@ -507,7 +511,7 @@ fun MediumScreen(component: MediumComponent) { char = char, modifier = Modifier.width(96.dp).height(200.dp) ) { - // ToDo("character dialog") + component.showCharacter(char) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index 317e659..bb864f1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -6,11 +6,14 @@ import androidx.compose.runtime.DisposableEffect import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.Optional import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.slot.* +import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackCallback import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import dev.chrisbanes.haze.HazeState import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.anilist.* +import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaStatus @@ -23,6 +26,8 @@ import dev.datlag.aniflow.other.BurningSeriesResolver import dev.datlag.aniflow.other.Constants import dev.datlag.aniflow.other.TokenRefreshHandler import dev.datlag.aniflow.settings.Settings +import dev.datlag.aniflow.ui.navigation.DialogComponent +import dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character.CharacterDialogComponent import dev.datlag.tooling.alsoTrue import dev.datlag.tooling.async.suspendCatching import dev.datlag.tooling.compose.ioDispatcher @@ -239,7 +244,7 @@ class MediumScreenComponent( } ) - override val characters: StateFlow> = mediumSuccessState.mapNotNull { + override val characters: StateFlow> = mediumSuccessState.mapNotNull { it?.data?.characters }.flowOn( context = ioDispatcher() @@ -306,6 +311,21 @@ class MediumScreenComponent( override val bsAvailable: Boolean get() = burningSeriesResolver.isAvailable + private val dialogNavigation = SlotNavigation() + override val dialog: Value> = childSlot( + source = dialogNavigation, + serializer = DialogConfig.serializer() + ) { config, context -> + when (config) { + is DialogConfig.Character -> CharacterDialogComponent( + componentContext = context, + di = di, + initialChar = config.initial, + onDismiss = dialogNavigation::dismiss + ) + } + } + init { launchIO { title.mapNotNull { it.english to it.romaji }.collect { (english, romaji) -> @@ -400,8 +420,10 @@ class MediumScreenComponent( } override fun descriptionTranslation(text: String?) { - launchIO { - translatedDescription.emit(text) - } + translatedDescription.update { text } + } + + override fun showCharacter(character: Character) { + dialogNavigation.activate(DialogConfig.Character(character)) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt index b227716..9014dc1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter +import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.preferredName import dev.datlag.aniflow.common.shimmerPainter @@ -25,9 +26,9 @@ import dev.datlag.tooling.compose.ifTrue @Composable fun CharacterCard( - char: Medium.Character, + char: Character, modifier: Modifier = Modifier, - onClick: (Medium.Character) -> Unit + onClick: (Character) -> Unit ) { Card( modifier = modifier, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt new file mode 100644 index 0000000..aba9a65 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterComponent.kt @@ -0,0 +1,23 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character + +import dev.datlag.aniflow.anilist.CharacterStateMachine +import dev.datlag.aniflow.anilist.model.Character +import dev.datlag.aniflow.ui.navigation.DialogComponent +import kotlinx.coroutines.flow.StateFlow + +interface CharacterComponent : DialogComponent { + val initialChar: Character + + val state: StateFlow + + val image: StateFlow + val name: StateFlow + val gender: StateFlow + val bloodType: StateFlow + val birthDate: StateFlow + val description: StateFlow + val translatedDescription: StateFlow + + fun descriptionTranslation(text: String?) + fun retry() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt new file mode 100644 index 0000000..02056be --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -0,0 +1,192 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bloodtype +import androidx.compose.material.icons.filled.Cake +import androidx.compose.material.icons.filled.Man4 +import androidx.compose.material.icons.filled.Translate +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.compose.rememberAsyncImagePainter +import dev.datlag.aniflow.SharedRes +import dev.datlag.aniflow.anilist.CharacterStateMachine +import dev.datlag.aniflow.common.htmlToAnnotatedString +import dev.datlag.aniflow.common.preferred +import dev.datlag.aniflow.common.preferredName +import dev.datlag.aniflow.ui.navigation.screen.medium.component.TranslateButton +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.stringResource + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun CharacterDialog(component: CharacterComponent) { + AlertDialog( + onDismissRequest = component::dismiss, + icon = { + val image by component.image.collectAsStateWithLifecycle() + + AsyncImage( + modifier = Modifier.size(80.dp).clip(CircleShape), + model = image.large, + error = rememberAsyncImagePainter( + model = image.medium, + contentScale = ContentScale.Crop, + ), + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + contentDescription = component.initialChar.preferredName() + ) + }, + title = { + val name by component.name.collectAsStateWithLifecycle() + + Text( + text = name.preferred(), + style = MaterialTheme.typography.headlineMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.SemiBold, + softWrap = true + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val description by component.description.collectAsStateWithLifecycle() + val translatedDescription by component.translatedDescription.collectAsStateWithLifecycle() + + FlowRow( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + horizontalArrangement = Arrangement.SpaceAround + ) { + val gender by component.gender.collectAsStateWithLifecycle() + val bloodType by component.bloodType.collectAsStateWithLifecycle() + val birthDate by component.birthDate.collectAsStateWithLifecycle() + + bloodType?.let { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Filled.Bloodtype, + contentDescription = null + ) + Text( + text = stringResource(SharedRes.strings.blood_type), + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + } + + gender?.let { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Filled.Man4, + contentDescription = null + ) + Text( + text = stringResource(SharedRes.strings.gender), + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + } + + birthDate?.let { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Filled.Cake, + contentDescription = null + ) + Text( + text = stringResource(SharedRes.strings.birth_date), + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = it.format(), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + } + } + + (translatedDescription ?: description)?.let { + Text( + modifier = Modifier.verticalScroll(rememberScrollState()), + text = it.htmlToAnnotatedString() + ) + } ?: run { + val state by component.state.collectAsStateWithLifecycle() + + if (state is CharacterStateMachine.State.Loading) { + CircularProgressIndicator() + } + // ToDo("Display something went wrong") + } + } + }, + dismissButton = { + val state by component.state.collectAsStateWithLifecycle() + val description by component.description.collectAsStateWithLifecycle() + + description?.let { + TranslateButton( + text = it, + ) { text -> + component.descriptionTranslation(text) + } + } ?: run { + if (state is CharacterStateMachine.State.Error) { + TextButton( + onClick = component::retry + ) { + Text(text = stringResource(SharedRes.strings.retry)) + } + } + } + }, + confirmButton = { + TextButton( + onClick = component::dismiss + ) { + Text(text = stringResource(SharedRes.strings.close)) + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt new file mode 100644 index 0000000..ad1e37c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialogComponent.kt @@ -0,0 +1,133 @@ +package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character + +import androidx.compose.runtime.Composable +import com.apollographql.apollo3.ApolloClient +import com.arkivanov.decompose.ComponentContext +import dev.datlag.aniflow.anilist.CharacterStateMachine +import dev.datlag.aniflow.anilist.model.Character +import dev.datlag.aniflow.common.nullableFirebaseInstance +import dev.datlag.aniflow.common.onRender +import dev.datlag.aniflow.other.Constants +import dev.datlag.tooling.compose.ioDispatcher +import dev.datlag.tooling.decompose.ioScope +import dev.datlag.tooling.safeCast +import kotlinx.coroutines.flow.* +import org.kodein.di.DI +import org.kodein.di.instance + +class CharacterDialogComponent( + componentContext: ComponentContext, + override val di: DI, + override val initialChar: Character, + private val onDismiss: () -> Unit +) : CharacterComponent, ComponentContext by componentContext { + + private val aniListClient by di.instance(Constants.AniList.APOLLO_CLIENT) + private val aniListFallbackClient by di.instance(Constants.AniList.FALLBACK_APOLLO_CLIENT) + private val characterStateMachine = CharacterStateMachine( + client = aniListClient, + fallbackClient = aniListFallbackClient, + crashlytics = di.nullableFirebaseInstance()?.crashlytics, + id = initialChar.id + ) + + override val state = characterStateMachine.state.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = characterStateMachine.currentState + ) + private val characterSuccessState = state.mapNotNull { + it.safeCast() + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + override val image: StateFlow = characterSuccessState.mapNotNull { + it?.character?.image + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = initialChar.image + ) + + override val name: StateFlow = characterSuccessState.mapNotNull { + it?.character?.name + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = initialChar.name + ) + + override val gender: StateFlow = characterSuccessState.mapNotNull { + it?.character?.gender + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = initialChar.gender + ) + + override val bloodType: StateFlow = characterSuccessState.mapNotNull { + it?.character?.bloodType + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = initialChar.bloodType + ) + + override val birthDate: StateFlow = characterSuccessState.mapNotNull { + it?.character?.birthDate + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = initialChar.birthDate + ) + + override val description: StateFlow = characterSuccessState.mapNotNull { + it?.character?.description + }.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = initialChar.description + ) + + override val translatedDescription: MutableStateFlow = MutableStateFlow(null) + + @Composable + override fun render() { + onRender { + CharacterDialog(this) + } + } + + override fun dismiss() { + onDismiss() + } + + override fun descriptionTranslation(text: String?) { + translatedDescription.update { text } + } + + override fun retry() { + launchIO { + characterStateMachine.dispatch(CharacterStateMachine.Action.Retry) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index c3db814..26144f4 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -24,4 +24,10 @@ Translate Description Burning-Series + Close + Characters + Retry + Blood Type + Gender + Birthdate diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.ios.kt index c8f6c76..96a9642 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.ios.kt @@ -2,6 +2,7 @@ package dev.datlag.aniflow.common import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium +import dev.datlag.aniflow.anilist.model.Character actual fun Medium.Title.preferred(): String { return this.userPreferred?.ifBlank { null } @@ -11,7 +12,7 @@ actual fun Medium.Title.preferred(): String { ?: "" } -actual fun Medium.Character.Name.preferred(): String { +actual fun Character.Name.preferred(): String { return this.userPreferred?.ifBlank { null } ?: this.full?.ifBlank { null } ?: buildString { diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt index da50e37..5cbadcf 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt @@ -47,7 +47,8 @@ actual object PlatformModule { projectId = Sekret.firebaseProject(BuildKonfig.packageName), applicationId = Sekret.firebaseIosApplication(BuildKonfig.packageName)!!, apiKey = Sekret.firebaseIosApiKey(BuildKonfig.packageName)!!, - googleAuthProvider = instanceOrNull() + googleAuthProvider = instanceOrNull(), + localLogger = instanceOrNull() ) } bindEagerSingleton { diff --git a/firebase/src/androidMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt b/firebase/src/androidMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt index 6615680..45a4899 100644 --- a/firebase/src/androidMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt +++ b/firebase/src/androidMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt @@ -1,6 +1,7 @@ package dev.datlag.aniflow.firebase import android.content.Context +import android.util.Log import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseOptions import dev.gitlive.firebase.initialize @@ -10,7 +11,8 @@ fun FirebaseFactory.Companion.initialize( projectId: String?, applicationId: String, apiKey: String, - googleAuthProvider: GoogleAuthProvider? + googleAuthProvider: GoogleAuthProvider?, + localLogger: FirebaseFactory.Crashlytics.LocalLogger? ): FirebaseFactory { return CommonFirebase( Firebase.initialize( @@ -21,6 +23,19 @@ fun FirebaseFactory.Companion.initialize( apiKey = apiKey ) ), - googleAuthProvider + googleAuthProvider, + localLogger = localLogger ?: object : FirebaseFactory.Crashlytics.LocalLogger { + override fun warn(message: String?) { + message?.let { Log.w(null, it) } + } + + override fun error(message: String?) { + message?.let { Log.e(null, it) } + } + + override fun error(throwable: Throwable?) { + throwable?.let { Log.e(null, null, throwable) } + } + } ) } \ No newline at end of file diff --git a/firebase/src/commonMain/kotlin/dev/datlag/aniflow/firebase/FirebaseFactory.kt b/firebase/src/commonMain/kotlin/dev/datlag/aniflow/firebase/FirebaseFactory.kt index 313f43a..bf0242e 100644 --- a/firebase/src/commonMain/kotlin/dev/datlag/aniflow/firebase/FirebaseFactory.kt +++ b/firebase/src/commonMain/kotlin/dev/datlag/aniflow/firebase/FirebaseFactory.kt @@ -28,6 +28,9 @@ interface FirebaseFactory { } interface Crashlytics { + val localLogger: LocalLogger? + get() = null + fun customKey(key: String, value: String) { } fun customKey(key: String, value: Boolean) { } fun customKey(key: String, value: Int) { } @@ -36,6 +39,12 @@ interface FirebaseFactory { fun customKey(key: String, value: Double) { } fun log(throwable: Throwable?) { } fun log(message: String?) { } + + interface LocalLogger { + fun warn(message: String?) + fun error(throwable: Throwable?) + fun error(message: String?) + } } companion object diff --git a/firebase/src/firebaseMain/kotlin/dev/datlag/aniflow/firebase/CommonFirebase.kt b/firebase/src/firebaseMain/kotlin/dev/datlag/aniflow/firebase/CommonFirebase.kt index 1ea6dc2..7699047 100644 --- a/firebase/src/firebaseMain/kotlin/dev/datlag/aniflow/firebase/CommonFirebase.kt +++ b/firebase/src/firebaseMain/kotlin/dev/datlag/aniflow/firebase/CommonFirebase.kt @@ -8,11 +8,12 @@ import dev.gitlive.firebase.auth.GoogleAuthProvider as FirebaseGoogleProvider open class CommonFirebase( private val app: FirebaseApp, - private val googleAuthProvider: GoogleAuthProvider? + private val googleAuthProvider: GoogleAuthProvider?, + localLogger: FirebaseFactory.Crashlytics.LocalLogger? ) : FirebaseFactory { override val auth: FirebaseFactory.Auth = Auth(app, googleAuthProvider) - override val crashlytics: FirebaseFactory.Crashlytics = Crashlytics(app) + override val crashlytics: FirebaseFactory.Crashlytics = Crashlytics(app, localLogger) data class Auth( private val app: FirebaseApp, @@ -66,7 +67,10 @@ open class CommonFirebase( } } - data class Crashlytics(private val app: FirebaseApp) : FirebaseFactory.Crashlytics { + data class Crashlytics( + private val app: FirebaseApp, + override val localLogger: FirebaseFactory.Crashlytics.LocalLogger? + ) : FirebaseFactory.Crashlytics { override fun customKey(key: String, value: String) { crashlyticsCustomKey(app, key, value) } @@ -86,9 +90,11 @@ open class CommonFirebase( crashlyticsCustomKey(app, key, value) } override fun log(throwable: Throwable?) { + localLogger?.error(throwable) crashlyticsLog(app, throwable) } override fun log(message: String?) { + localLogger?.warn(message) crashlyticsLog(app, message) } } diff --git a/firebase/src/iosMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt b/firebase/src/iosMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt index fa9c43d..e999eaa 100644 --- a/firebase/src/iosMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt +++ b/firebase/src/iosMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt @@ -8,7 +8,8 @@ fun FirebaseFactory.Companion.initialize( projectId: String?, applicationId: String, apiKey: String, - googleAuthProvider: GoogleAuthProvider? + googleAuthProvider: GoogleAuthProvider?, + localLogger: FirebaseFactory.Crashlytics.LocalLogger? ) : FirebaseFactory { return CommonFirebase( Firebase.initialize( @@ -19,6 +20,7 @@ fun FirebaseFactory.Companion.initialize( apiKey = apiKey ) ), - googleAuthProvider + googleAuthProvider, + localLogger ) } \ No newline at end of file diff --git a/firebase/src/jsMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt b/firebase/src/jsMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt index fa9c43d..80a3124 100644 --- a/firebase/src/jsMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt +++ b/firebase/src/jsMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt @@ -8,7 +8,8 @@ fun FirebaseFactory.Companion.initialize( projectId: String?, applicationId: String, apiKey: String, - googleAuthProvider: GoogleAuthProvider? + googleAuthProvider: GoogleAuthProvider?, + localLogger: FirebaseFactory.Crashlytics.LocalLogger? ) : FirebaseFactory { return CommonFirebase( Firebase.initialize( @@ -19,6 +20,19 @@ fun FirebaseFactory.Companion.initialize( apiKey = apiKey ) ), - googleAuthProvider + googleAuthProvider, + localLogger ?: object : FirebaseFactory.Crashlytics.LocalLogger { + override fun warn(message: String?) { + console.warn(message) + } + + override fun error(throwable: Throwable?) { + console.error(throwable) + } + + override fun error(message: String?) { + console.error(message) + } + } ) } \ No newline at end of file diff --git a/firebase/src/jvmMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt b/firebase/src/jvmMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt index 3ceb64b..44c14a7 100644 --- a/firebase/src/jvmMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt +++ b/firebase/src/jvmMain/kotlin/dev/datlag/aniflow/firebase/ExtendFirebase.kt @@ -4,11 +4,13 @@ import android.app.Application import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseOptions import dev.gitlive.firebase.initialize +import java.util.logging.Logger fun FirebaseFactory.Companion.initialize( projectId: String?, applicationId: String, - apiKey: String + apiKey: String, + localLogger: FirebaseFactory.Crashlytics.LocalLogger? ) : FirebaseFactory { return CommonFirebase( Firebase.initialize( @@ -19,6 +21,19 @@ fun FirebaseFactory.Companion.initialize( apiKey = apiKey ) ), - null + null, + localLogger ?: object : FirebaseFactory.Crashlytics.LocalLogger { + override fun warn(message: String?) { + Logger.getGlobal().warning(message) + } + + override fun error(message: String?) { + Logger.getGlobal().severe(message) + } + + override fun error(throwable: Throwable?) { + Logger.getGlobal().severe(throwable?.stackTraceToString() ?: throwable?.message) + } + } ) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0fe7e53..5830ab0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] app = "1.1.1" aboutlibraries = "11.1.3" -activity = "1.8.2" +activity = "1.9.0" android = "8.2.2" -android-core = "1.12.0" +android-core = "1.13.0" android-credentials = "1.3.0-alpha02" apollo = "4.0.0-beta.5" appcompat = "1.6.1" @@ -12,15 +12,15 @@ compose = "1.6.2" complete-kotlin = "1.1.0" coroutines = "1.8.0" crashlytics-plugin = "2.9.9" -datastore = "1.1.0-rc01" -datetime = "0.5.0" +datastore = "1.1.0" +datetime = "0.6.0-RC.2" decompose = "3.0.0-beta01" desugar = "2.0.4" firebase = "1.11.1" firebase-android = "20.4.3" -firebase-android-analytics = "21.6.1" +firebase-android-analytics = "21.6.2" firebase-android-auth = "22.3.1" -firebase-android-crashlytics = "18.6.3" +firebase-android-crashlytics = "18.6.4" flowredux = "1.2.1" google-identity = "1.1.0" haze = "0.7.0"