diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt index 378a1cc..54e2690 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt @@ -32,6 +32,7 @@ class App : MultiDexApplication(), DIAware { StrictMode.ThreadPolicy.Builder() .detectAll() .permitDiskReads() + .permitDiskWrites() .penaltyLog() .penaltyDialog() .build() 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 68b46a8..3757ec7 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 @@ -6,6 +6,7 @@ import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import androidx.core.database.getStringOrNull import dev.datlag.tooling.scopeCatching import io.github.aakira.napier.Napier @@ -69,8 +70,9 @@ actual class BurningSeriesResolver( progress = progress, length = length, number = number, - series = Episode.Series( - title = seriesHref + series = Series( + title = seriesHref, + href = seriesHref ) ) ) @@ -83,12 +85,12 @@ actual class BurningSeriesResolver( return episodes } - actual fun resolveByName(english: String?, romaji: String?) { + actual fun resolveByName(english: String?, romaji: String?): Set { val englishTrimmed = english?.trim()?.ifBlank { null }?.replace("'", "") val romajiTrimmed = romaji?.trim()?.ifBlank { null }?.replace("'", "") if (seriesClient == null || (englishTrimmed == null && romajiTrimmed == null)) { - return + return emptySet() } val selection = if (englishTrimmed != null && romajiTrimmed != null) { @@ -104,26 +106,36 @@ actual class BurningSeriesResolver( selection, null, null - ) ?: return + ) ?: return emptySet() + + val series = mutableSetOf() if (seriesCursor.moveToFirst()) { while (!seriesCursor.isAfterLast) { val titleIndex = seriesCursor.getColumnIndex("title") + val hrefIndex = seriesCursor.getColumnIndex("hrefPrimary") - if (titleIndex == -1) { + if (hrefIndex == -1) { seriesCursor.moveToNext() continue } - val title = seriesCursor.getString(titleIndex) - Napier.e("Series matching name: $title") + val title = seriesCursor.getStringOrNull(titleIndex) + val href = seriesCursor.getString(hrefIndex) + + series.add( + Series( + title = title ?: href, + href = href + ) + ) seriesCursor.moveToNext() } } seriesCursor.close() - return + return series } actual fun close() { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt index 024fd96..609fcba 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.kt @@ -3,7 +3,7 @@ package dev.datlag.aniflow.other expect class BurningSeriesResolver { val isAvailable: Boolean fun resolveWatchedEpisodes(): Set - fun resolveByName(english: String?, romaji: String?) + fun resolveByName(english: String?, romaji: String?): Set fun close() } @@ -13,8 +13,9 @@ data class Episode( val length: Long, val number: String, val series: Series -) { - data class Series( - val title: String - ) -} \ No newline at end of file +) + +data class Series( + val title: String, + val href: String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt index 210a93a..dd4aaa0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/EditFAB.kt @@ -19,8 +19,11 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.unit.dp import dev.datlag.aniflow.SharedRes +import dev.datlag.tooling.compose.withDefaultContext +import dev.datlag.tooling.compose.withIOContext import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay @Composable fun EditFAB( @@ -35,25 +38,24 @@ fun EditFAB( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically) ) { - var showOtherFABs by remember { mutableStateOf(false) } + var showOtherFABs by remember(expanded) { mutableStateOf(expanded) } AnimatedVisibility( visible = showOtherFABs, - enter = scaleIn( + enter = slideInVertically( animationSpec = bouncySpring(), - transformOrigin = TransformOrigin(1F, 0.5F) + initialOffsetY = { -(-it / 2) } ) + fadeIn( animationSpec = bouncySpring() ), - exit = scaleOut( - transformOrigin = TransformOrigin(1F, 0.5F) + exit = slideOutVertically( + animationSpec = bouncySpring(), + targetOffsetY = { -(-it / 2) } ) + fadeOut( animationSpec = bouncySpring() ) ) { LabelFAB( - label = "Progress", - expanded = expanded, onClick = { showOtherFABs = false onProgress() @@ -67,21 +69,20 @@ fun EditFAB( } AnimatedVisibility( visible = showOtherFABs, - enter = scaleIn( + enter = slideInVertically( animationSpec = bouncySpring(), - transformOrigin = TransformOrigin(1F, 0.5F) + initialOffsetY = { -(-it / 2) } ) + fadeIn( animationSpec = bouncySpring() ), - exit = scaleOut( - transformOrigin = TransformOrigin(1F, 0.5F) + exit = slideOutVertically( + animationSpec = bouncySpring(), + targetOffsetY = { -(-it / 2) } ) + fadeOut( animationSpec = bouncySpring() ) ) { LabelFAB( - label = "Rating", - expanded = expanded, onClick = { showOtherFABs = false onRate() @@ -95,21 +96,20 @@ fun EditFAB( } AnimatedVisibility( visible = showOtherFABs && bsAvailable, - enter = scaleIn( + enter = slideInVertically( animationSpec = bouncySpring(), - transformOrigin = TransformOrigin(1F, 0.5F) + initialOffsetY = { -(-it / 2) } ) + fadeIn( animationSpec = bouncySpring() ), - exit = scaleOut( - transformOrigin = TransformOrigin(1F, 0.5F) + exit = slideOutVertically( + animationSpec = bouncySpring(), + targetOffsetY = { -(-it / 2) } ) + fadeOut( animationSpec = bouncySpring() ) ) { LabelFAB( - label = stringResource(SharedRes.strings.bs), - expanded = expanded, onClick = { showOtherFABs = false onBS() @@ -156,34 +156,21 @@ fun EditFAB( @Composable private fun LabelFAB( - label: String, - expanded: Boolean, onClick: () -> Unit, icon: @Composable () -> Unit ) { + var showLabel by remember { mutableStateOf(false) } + + LaunchedEffect(showLabel) { + if (!showLabel) { + showLabel = true + } + } + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - AnimatedVisibility( - visible = expanded, - enter = fadeIn(), - exit = fadeOut() - ) { - Surface( - onClick = onClick, - tonalElevation = 8.dp, - shadowElevation = 4.dp, - shape = RoundedCornerShape(4.dp) - ) { - Text( - modifier = Modifier.padding(4.dp), - text = label, - maxLines = 1 - ) - } - } - SmallFloatingActionButton( modifier = Modifier.padding(end = 4.dp), onClick = onClick @@ -191,6 +178,12 @@ private fun LabelFAB( icon() } } + + DisposableEffect(showLabel) { + onDispose { + showLabel = false + } + } } private fun bouncySpring() = spring( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt index 2228344..af0b934 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/component/ScheduleOverview.kt @@ -47,16 +47,6 @@ fun ScheduleOverview( style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.weight(1f)) - IconButton( - onClick = onMoreClick, - enabled = state is AiringTodayRepository.State.Success - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos, - contentDescription = null - ) - } } when (val current = state) { 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 4317ca7..daea17a 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 @@ -7,6 +7,7 @@ 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.other.Series import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.ContentHolderComponent @@ -50,6 +51,7 @@ interface MediumComponent : ContentHolderComponent { val siteUrl: Flow val bsAvailable: Boolean + val bsOptions: Flow> val dialog: Value> 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 5c7a2e6..9ac14af 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 @@ -21,6 +21,11 @@ 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 +import com.maxkeppeler.sheets.option.OptionDialog +import com.maxkeppeler.sheets.option.models.DisplayMode +import com.maxkeppeler.sheets.option.models.Option +import com.maxkeppeler.sheets.option.models.OptionConfig +import com.maxkeppeler.sheets.option.models.OptionSelection import com.maxkeppeler.sheets.rating.RatingDialog import com.maxkeppeler.sheets.rating.models.RatingBody import com.maxkeppeler.sheets.rating.models.RatingConfig @@ -40,6 +45,8 @@ import dev.datlag.aniflow.other.UserHelper import dev.datlag.aniflow.ui.custom.EditFAB import dev.datlag.aniflow.ui.navigation.screen.medium.component.* import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle +import dev.icerock.moko.resources.compose.painterResource +import io.github.aakira.napier.Napier import kotlinx.coroutines.launch import org.kodein.di.instance @@ -87,6 +94,9 @@ fun MediumScreen(component: MediumComponent) { floatingActionButton = { val userRating by component.rating.collectAsStateWithLifecycle(-1) val ratingState = rememberUseCaseState() + val bsState = rememberUseCaseState() + + val bsOptions by component.bsOptions.collectAsStateWithLifecycle(emptySet()) val alreadyAdded by component.alreadyAdded.collectAsStateWithLifecycle( component.initialMedium.entry != null @@ -116,6 +126,29 @@ fun MediumScreen(component: MediumComponent) { ) ) + OptionDialog( + state = bsState, + selection = OptionSelection.Single( + options = bsOptions.map { + Option( + titleText = it.title + ) + }, + onSelectOption = { option, _ -> + Napier.e("Selected: ${bsOptions.toList()[option]}") + } + ), + header = Header.Default( + icon = IconSource( + painter = painterResource(SharedRes.images.bs) + ), + title = "Connect with BS" + ), + config = OptionConfig( + mode = DisplayMode.LIST + ) + ) + if (!notReleased) { val uriHandler = LocalUriHandler.current val userHelper by LocalDI.current.instance() @@ -125,7 +158,7 @@ fun MediumScreen(component: MediumComponent) { bsAvailable = component.bsAvailable, expanded = listState.isScrollingUp(), onBS = { - + bsState.show() }, onRate = { uriHandler.openUri(userHelper.loginUrl) 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 0ee9887..8fb607b 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 @@ -32,6 +32,7 @@ import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.compose.withMainContext import dev.datlag.tooling.decompose.ioScope import dev.datlag.tooling.safeCast +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -61,51 +62,62 @@ class MediumScreenComponent( it.safeCast() } - override val isAdult: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val isAdult: Flow = mediumSuccessState.mapLatest { it.medium.isAdult } override val isAdultAllowed: Flow = appSettings.adultContent - private val type: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + private val type: Flow = mediumSuccessState.mapLatest { it.medium.type } - override val bannerImage: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val bannerImage: Flow = mediumSuccessState.mapLatest { it.medium.bannerImage } - override val coverImage: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val coverImage: Flow = mediumSuccessState.mapLatest { it.medium.coverImage } - override val title: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val title: Flow = mediumSuccessState.mapLatest { it.medium.title } - override val description: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val description: Flow = mediumSuccessState.mapLatest { it.medium.description?.ifBlank { null } } override val translatedDescription: MutableStateFlow = MutableStateFlow(null) - override val genres: Flow> = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val genres: Flow> = mediumSuccessState.mapLatest { it.medium.genres }.mapNotEmpty() - override val format: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val format: Flow = mediumSuccessState.mapLatest { it.medium.format } - override val episodes: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val episodes: Flow = mediumSuccessState.mapLatest { it.medium.episodes }.distinctUntilChanged() - override val duration: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val duration: Flow = mediumSuccessState.mapLatest { it.medium.avgEpisodeDurationInMin }.distinctUntilChanged() - override val status: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val status: Flow = mediumSuccessState.mapLatest { it.medium.status }.distinctUntilChanged() @@ -121,13 +133,15 @@ class MediumScreenComponent( it.medium.averageScore.asNullIf(-1) }.distinctUntilChanged() - override val characters: Flow> = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val characters: Flow> = mediumSuccessState.mapLatest { it.medium.characters }.mapNotEmpty() private val changedRating: MutableStateFlow = MutableStateFlow(initialMedium.entry?.score?.toInt() ?: -1) + @OptIn(ExperimentalCoroutinesApi::class) override val rating: Flow = combine( - mediumSuccessState.map { + mediumSuccessState.mapLatest { it.medium.entry?.score?.toInt() }, changedRating @@ -139,23 +153,28 @@ class MediumScreenComponent( } } - override val trailer: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val trailer: Flow = mediumSuccessState.mapLatest { it.medium.trailer } - override val alreadyAdded: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val alreadyAdded: Flow = mediumSuccessState.mapLatest { it.medium.entry != null } - override val isFavorite: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val isFavorite: Flow = mediumSuccessState.mapLatest { it.medium.isFavorite } - override val isFavoriteBlocked: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val isFavoriteBlocked: Flow = mediumSuccessState.mapLatest { it.medium.isFavoriteBlocked } - override val siteUrl: Flow = mediumSuccessState.map { + @OptIn(ExperimentalCoroutinesApi::class) + override val siteUrl: Flow = mediumSuccessState.mapLatest { it.medium.siteUrl } @@ -164,6 +183,11 @@ class MediumScreenComponent( override val bsAvailable: Boolean get() = burningSeriesResolver.isAvailable + @OptIn(ExperimentalCoroutinesApi::class) + override val bsOptions = title.mapLatest { + burningSeriesResolver.resolveByName(it.english, it.romaji) + } + private val dialogNavigation = SlotNavigation() override val dialog: Value> = childSlot( source = dialogNavigation, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt index c828041..1a3ad50 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/nekos/NekosScreen.kt @@ -94,20 +94,20 @@ fun NekosScreen(component: NekosComponent) { selection = OptionSelection.Single( options = listOf( Option( - titleText = "Safe", + titleText = stringResource(SharedRes.strings.safe), selected = rating is Rating.Safe, ), Option( - titleText = "Suggestive", + titleText = stringResource(SharedRes.strings.suggestive), selected = rating is Rating.Suggestive, ), Option( - titleText = "Borderline", + titleText = stringResource(SharedRes.strings.borderline), selected = rating is Rating.Borderline, disabled = !adultContent ), Option( - titleText = "Explicit", + titleText = stringResource(SharedRes.strings.explicit), selected = rating is Rating.Explicit, disabled = !adultContent ) @@ -134,7 +134,7 @@ fun NekosScreen(component: NekosComponent) { ) }, text = { - Text(text = "Filter") + Text(text = stringResource(SharedRes.strings.filter)) } ) } @@ -181,7 +181,6 @@ fun NekosScreen(component: NekosComponent) { Card( modifier = Modifier.animateItemPlacement(), onClick = { - Napier.e(it.toString()) uriHandler.openUri(it.imageUrl ?: it.sampleUrl ?: "") }, ) { diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt index b028fd8..7417e41 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/settings/SettingsScreen.kt @@ -193,7 +193,9 @@ fun SettingsScreen(component: SettingsComponent) { AnimatedVisibility( visible = clicked >= 1, ) { - Badge { + Badge( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) { Text(text = "$clicked") } } diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml index a94486b..5de154c 100644 --- a/composeApp/src/commonMain/moko-resources/base/strings.xml +++ b/composeApp/src/commonMain/moko-resources/base/strings.xml @@ -58,4 +58,9 @@ Profile Favorites Nekos API + Filter + Safe + Suggestive + Borderline + Explicit diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt index c4ff0bc..4d7205e 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/BurningSeriesResolver.ios.kt @@ -10,7 +10,9 @@ actual class BurningSeriesResolver { return emptySet() } - actual fun resolveByName(english: String?, romaji: String?) { } + actual fun resolveByName(english: String?, romaji: String?): Set { + return emptySet() + } actual fun close() { } } \ No newline at end of file