From 5e0fddfc45bfcfce8130bec0d35333b34edf15e4 Mon Sep 17 00:00:00 2001 From: seiko Date: Tue, 17 Jan 2023 13:45:06 +0800 Subject: [PATCH 1/4] media viewModel to presenter --- .../twiderex/di/modules/ViewModelModule.kt | 2 - .../twidere/twiderex/scenes/MediaPresenter.kt | 110 ++++++++++++++++ .../com/twidere/twiderex/scenes/MediaScene.kt | 119 +++++++++--------- .../twiderex/viewmodel/MediaViewModel.kt | 97 -------------- .../twiderex/viewmodel/MediaViewModelTest.kt | 109 ---------------- 5 files changed, 172 insertions(+), 265 deletions(-) create mode 100644 common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt delete mode 100644 common/src/commonMain/kotlin/com/twidere/twiderex/viewmodel/MediaViewModel.kt delete mode 100644 common/src/commonTest/kotlin/com/twidere/twiderex/viewmodel/MediaViewModelTest.kt diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/di/modules/ViewModelModule.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/di/modules/ViewModelModule.kt index 6449e37ef..9bbb6ea50 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/di/modules/ViewModelModule.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/di/modules/ViewModelModule.kt @@ -24,7 +24,6 @@ import com.twidere.twiderex.extensions.viewModel import com.twidere.twiderex.model.MicroBlogKey import com.twidere.twiderex.viewmodel.ActiveAccountViewModel import com.twidere.twiderex.viewmodel.DraftViewModel -import com.twidere.twiderex.viewmodel.MediaViewModel import com.twidere.twiderex.viewmodel.PureMediaViewModel import com.twidere.twiderex.viewmodel.StatusViewModel import com.twidere.twiderex.viewmodel.compose.ComposeSearchUserViewModel @@ -49,7 +48,6 @@ import org.koin.dsl.module val viewModelModule = module { viewModel { (statusKey: MicroBlogKey) -> StatusViewModel(get(), get(), statusKey) } viewModel { (belongKey: MicroBlogKey) -> PureMediaViewModel(get(), belongKey) } - viewModel { (statusKey: MicroBlogKey) -> MediaViewModel(get(), get(), get(), statusKey) } viewModel { DraftViewModel(get(), get()) } viewModel { ActiveAccountViewModel(get()) } diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt new file mode 100644 index 000000000..342d8d83d --- /dev/null +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt @@ -0,0 +1,110 @@ +/* + * Twidere X + * + * Copyright (C) TwidereProject and Contributors + * + * This file is part of Twidere X. + * + * Twidere X is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Twidere X is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Twidere X. If not, see . + */ +package com.twidere.twiderex.scenes + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import com.twidere.twiderex.action.MediaAction +import com.twidere.twiderex.di.ext.get +import com.twidere.twiderex.model.MicroBlogKey +import com.twidere.twiderex.model.ui.UiMedia +import com.twidere.twiderex.model.ui.UiStatus +import com.twidere.twiderex.repository.StatusRepository +import kotlinx.coroutines.flow.Flow +import moe.tlaster.kfilepicker.FilePicker + +@Composable +internal fun MediaPresenter( + event: Flow, + statusKey: MicroBlogKey, + statusRepository: StatusRepository = get(), + mediaAction: MediaAction = get(), +): MediaState { + val accountState = CurrentAccountPresenter() + if (accountState !is CurrentAccountState.Account) { + return MediaState.Loading + } + val account = accountState.account + + LaunchedEffect(Unit) { + event.collect { event -> + when (event) { + is MediaEvent.SaveMedia -> { + val fileName = event.currentMedia.fileName ?: return@collect + val path = FilePicker.createFile(fileName)?.path ?: return@collect + event.currentMedia.mediaUrl?.let { + mediaAction.download( + accountKey = account.accountKey, + source = it, + target = path + ) + } + } + is MediaEvent.ShareMedia -> { + val fileName = event.currentMedia.fileName ?: return@collect + val mediaUrl = event.currentMedia.mediaUrl ?: return@collect + mediaAction.share( + source = mediaUrl, + fileName = fileName, + accountKey = account.accountKey, + extraText = event.extraText() + ) + } + } + } + } + val status by produceState( + initialValue = MediaState.Loading, + ) { + statusRepository.loadStatus( + statusKey = statusKey, + accountKey = account.accountKey, + ).collect { status -> + if (status != null) { + value = MediaState.Data(status) + } + } + } + return status +} + +internal sealed interface MediaEvent { + data class SaveMedia( + val currentMedia: UiMedia, + ) : MediaEvent + + data class ShareMedia( + val currentMedia: UiMedia, + val extraText: () -> String, + ) : MediaEvent +} + +internal sealed interface MediaState { + object Loading : MediaState + + @Immutable + data class Data( + val status: UiStatus + ) : MediaState +} diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt index 6ff0fa301..4187e5ff6 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt @@ -53,7 +53,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -89,9 +88,9 @@ import com.twidere.twiderex.component.status.renderContentAnnotatedString import com.twidere.twiderex.component.status.resolveLink import com.twidere.twiderex.component.stringResource import com.twidere.twiderex.component.topInsetsPadding -import com.twidere.twiderex.di.ext.getViewModel import com.twidere.twiderex.extensions.observeAsState import com.twidere.twiderex.extensions.playEnable +import com.twidere.twiderex.extensions.rememberPresenterState import com.twidere.twiderex.kmp.LocalPlatformWindow import com.twidere.twiderex.kmp.Platform import com.twidere.twiderex.kmp.currentPlatform @@ -108,14 +107,10 @@ import com.twidere.twiderex.preferences.model.DisplayPreferences import com.twidere.twiderex.ui.LocalVideoPlayback import com.twidere.twiderex.ui.TwidereDialog import com.twidere.twiderex.utils.video.CustomVideoControl -import com.twidere.twiderex.viewmodel.MediaViewModel -import kotlinx.coroutines.launch -import moe.tlaster.kfilepicker.FilePicker import moe.tlaster.precompose.navigation.Navigator import moe.tlaster.swiper.Swiper import moe.tlaster.swiper.SwiperState import moe.tlaster.swiper.rememberSwiperState -import org.koin.core.parameter.parametersOf import java.net.URLDecoder @Composable @@ -143,50 +138,60 @@ private fun StatusMediaScene( selectedIndex: Int, navigator: Navigator, ) { - val viewModel = getViewModel { - parametersOf(statusKey) + val (state, channel) = rememberPresenterState { + MediaPresenter(it, statusKey) } - val status by viewModel.status.observeAsState(null) - val loading by viewModel.loading.observeAsState(initial = false) TwidereDialog( requireDarkTheme = true, extendViewIntoStatusBar = true, extendViewIntoNavigationBar = true, ) { - if (loading && status == null) { - Scaffold { - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - LoadingProgress() + when (state) { + MediaState.Loading -> { + Scaffold { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + LoadingProgress() + } } } - } - status?.let { - CompositionLocalProvider( - LocalVideoPlayback provides DisplayPreferences.AutoPlayback.Always - ) { - val statusNavigationData = rememberStatusNavigationData(navigator) - StatusMediaScene( - status = it, - selectedIndex = selectedIndex.coerceIn(0, it.media.lastIndex), - viewModel = viewModel, - statusNavigationData = statusNavigationData, - ) + + is MediaState.Data -> { + CompositionLocalProvider( + LocalVideoPlayback provides DisplayPreferences.AutoPlayback.Always + ) { + val statusNavigationData = rememberStatusNavigationData(navigator) + StatusMediaScene( + status = state.status, + selectedIndex = selectedIndex.coerceIn(0, state.status.media.lastIndex), + statusNavigationData = statusNavigationData, + clickable = remember { + StatusMediaSceneClickable( + onSaveMediaClicked = { currentMedia -> + channel.trySend(MediaEvent.SaveMedia(currentMedia)) + }, + onShareMediaClicked = { currentMedia, extraText -> + channel.trySend(MediaEvent.ShareMedia(currentMedia, extraText)) + } + ) + } + ) + } } } } } @Composable -fun StatusMediaScene( +private fun StatusMediaScene( status: UiStatus, selectedIndex: Int, - viewModel: MediaViewModel, statusNavigationData: StatusNavigationData, + clickable: StatusMediaSceneClickable, ) { val window = LocalPlatformWindow.current var controlVisibility by remember { mutableStateOf(true) } @@ -214,7 +219,7 @@ fun StatusMediaScene( controlPanelColor = controlPanelColor, statusNavigationData = statusNavigationData, videoPlayerState = videoPlayerState.value, - viewModel = viewModel, + clickable = clickable, currentMedia = currentMedia, pagerState = pagerState ) @@ -319,7 +324,7 @@ private fun StatusMediaBottomContent( visible: Boolean, controlPanelColor: Color, videoPlayerState: VideoPlayerState?, - viewModel: MediaViewModel, + clickable: StatusMediaSceneClickable, currentMedia: UiMedia, pagerState: PagerState, statusNavigationData: StatusNavigationData @@ -356,7 +361,7 @@ private fun StatusMediaBottomContent( StatusMediaInfo( videoPlayerState, status, - viewModel, + clickable, currentMedia, statusNavigationData ) @@ -370,12 +375,10 @@ private fun StatusMediaBottomContent( private fun StatusMediaInfo( videoPlayerState: VideoPlayerState?, status: UiStatus, - viewModel: MediaViewModel, + clickable: StatusMediaSceneClickable, currentMedia: UiMedia, statusNavigationData: StatusNavigationData, ) { - val scope = rememberCoroutineScope() - val text = renderContentAnnotatedString( htmlText = status.htmlText, linkResolver = { status.resolveLink(it) }, @@ -387,7 +390,7 @@ private fun StatusMediaInfo( if (videoPlayerState != null) { CustomVideoControl(state = videoPlayerState) } - StatusText(status = status, maxLines = 2, showMastodonPoll = false, openLink = statusNavigationData.openLink,) + StatusText(status = status, maxLines = 2, showMastodonPoll = false, openLink = statusNavigationData.openLink) Spacer(modifier = Modifier.height(StatusMediaInfoDefaults.TextSpacing)) Row( verticalAlignment = Alignment.CenterVertically, @@ -423,12 +426,13 @@ private fun StatusMediaInfo( ShareButton(status = status) { callback -> DropdownMenuItem( onClick = { - scope.launch { - callback.invoke() - viewModel.saveFile(currentMedia, target = { - FilePicker.createFile(it)?.path - }) - } + clickable.onSaveMediaClicked(currentMedia) + // scope.launch { + // callback.invoke() + // viewModel.saveFile(currentMedia, target = { + // FilePicker.createFile(it)?.path + // }) + // } } ) { Text( @@ -438,17 +442,12 @@ private fun StatusMediaInfo( DropdownMenuItem( onClick = { callback.invoke() - currentMedia.fileName?.let { - scope.launch { - viewModel.shareMedia( - currentMedia = currentMedia, - extraText = buildString { - append(text) - append(System.lineSeparator()) - append(System.lineSeparator()) - append(status.generateShareLink()) - } - ) + clickable.onShareMediaClicked(currentMedia) { + buildString { + append(text) + append(System.lineSeparator()) + append(System.lineSeparator()) + append(status.generateShareLink()) } } } @@ -595,8 +594,14 @@ fun MediaView( } ) } + MediaType.other -> Unit } } } } + +private data class StatusMediaSceneClickable( + val onSaveMediaClicked: (UiMedia) -> Unit, + val onShareMediaClicked: (UiMedia, () -> String) -> Unit, +) diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/viewmodel/MediaViewModel.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/viewmodel/MediaViewModel.kt deleted file mode 100644 index d600c2f71..000000000 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/viewmodel/MediaViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Twidere X - * - * Copyright (C) TwidereProject and Contributors - * - * This file is part of Twidere X. - * - * Twidere X is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Twidere X is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Twidere X. If not, see . - */ -package com.twidere.twiderex.viewmodel - -import com.twidere.twiderex.action.MediaAction -import com.twidere.twiderex.model.MicroBlogKey -import com.twidere.twiderex.model.ui.UiMedia -import com.twidere.twiderex.repository.AccountRepository -import com.twidere.twiderex.repository.StatusRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.mapNotNull -import moe.tlaster.precompose.viewmodel.ViewModel - -class MediaViewModel( - private val repository: StatusRepository, - private val accountRepository: AccountRepository, - private val mediaAction: MediaAction, - private val statusKey: MicroBlogKey, -) : ViewModel() { - private val account by lazy { - accountRepository.activeAccount.mapNotNull { it } - } - - suspend fun saveFile(currentMedia: UiMedia, target: suspend (fileName: String) -> String?) { - val account = account.firstOrNull() ?: return - val fileName = currentMedia.fileName ?: return - val path = target.invoke(fileName) ?: return - currentMedia.mediaUrl?.let { - mediaAction.download( - accountKey = account.accountKey, - source = it, - target = path - ) - } - } - - suspend fun shareMedia(currentMedia: UiMedia, extraText: String = "") { - val account = account.firstOrNull() ?: return - currentMedia.mediaUrl?.let { mediaUrl -> - currentMedia.fileName?.let { fileName -> - mediaAction.share( - source = mediaUrl, - fileName = fileName, - accountKey = account.accountKey, - extraText = extraText - ) - } - } - } - - val loading = MutableStateFlow(false) - - @OptIn(ExperimentalCoroutinesApi::class) - val status by lazy { - account.flatMapLatest { - repository.loadStatus( - statusKey = statusKey, - accountKey = it.accountKey, - ) - } - } - - // init { - // viewModelScope.launch { - // try { - // repository.loadTweetFromNetwork( - // statusKey.id, - // account.accountKey, - // account.service as LookupService - // ) - // } catch (e: Throwable) { - // e.notify(inAppNotification) - // } - // } - // } -} diff --git a/common/src/commonTest/kotlin/com/twidere/twiderex/viewmodel/MediaViewModelTest.kt b/common/src/commonTest/kotlin/com/twidere/twiderex/viewmodel/MediaViewModelTest.kt deleted file mode 100644 index 3a807383e..000000000 --- a/common/src/commonTest/kotlin/com/twidere/twiderex/viewmodel/MediaViewModelTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Twidere X - * - * Copyright (C) TwidereProject and Contributors - * - * This file is part of Twidere X. - * - * Twidere X is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Twidere X is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Twidere X. If not, see . - */ -package com.twidere.twiderex.viewmodel - -import com.twidere.services.microblog.MicroBlogService -import com.twidere.twiderex.action.MediaAction -import com.twidere.twiderex.model.MicroBlogKey -import com.twidere.twiderex.repository.StatusRepository -import io.mockk.coEvery -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -internal class MediaViewModelTest : AccountViewModelTestBase() { - override val mockService: MicroBlogService = mockk() - private lateinit var viewModel: MediaViewModel - - @MockK - private lateinit var repository: StatusRepository - - @MockK - private lateinit var mediaAction: MediaAction - - override fun setUp() { - super.setUp() - viewModel = MediaViewModel( - repository, - mockAccountRepository, - mediaAction, - MicroBlogKey.twitter("123") - ) - coEvery { repository.loadStatus(any(), any()) }.returns( - flowOf( - mockk { - every { statusKey }.returns(MicroBlogKey.twitter("123")) - } - ) - ) - } - - @Test - fun load_status(): Unit = runBlocking { - viewModel.status.firstOrNull().let { - assertNotNull(it) - assertEquals(MicroBlogKey.twitter("123"), it.statusKey) - } - } - - @Test - fun saveFile_success(): Unit = runBlocking { - viewModel.saveFile( - mockk { - every { mediaUrl }.returns("123") - every { fileName }.returns("target") - } - ) { - it - } - verify(exactly = 1) { - mediaAction.download( - "123", - "target", - MicroBlogKey.twitter("123") - ) - } - } - - @Test - fun shareMedia_success(): Unit = runBlocking { - viewModel.shareMedia( - mockk { - every { mediaUrl }.returns("123") - every { fileName }.returns("target") - } - ) - verify(exactly = 1) { - mediaAction.share( - "123", - "target", - MicroBlogKey.twitter("123") - ) - } - } -} From 4a67952e89afde547dbadf38c12c8cc71099fd04 Mon Sep 17 00:00:00 2001 From: seiko Date: Tue, 17 Jan 2023 15:56:41 +0800 Subject: [PATCH 2/4] remove Pager --- .../twiderex/component/UserComponent.kt | 14 +- .../twiderex/component/foundation/Pager.kt | 341 ------------------ .../foundation/platform/PagerIndicator.kt | 111 ------ .../com/twidere/twiderex/scenes/HomeScene.kt | 26 +- .../com/twidere/twiderex/scenes/MediaScene.kt | 25 +- .../twidere/twiderex/scenes/PureMediaScene.kt | 5 +- .../home/mastodon/MastodonNotificationItem.kt | 17 +- .../twiderex/scenes/search/SearchScene.kt | 15 +- 8 files changed, 53 insertions(+), 501 deletions(-) delete mode 100644 common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/Pager.kt delete mode 100644 common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/platform/PagerIndicator.kt diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt index 14964941c..3ce2182ec 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt @@ -77,14 +77,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState import com.twidere.services.microblog.model.IRelationship import com.twidere.twiderex.component.foundation.DropdownMenu import com.twidere.twiderex.component.foundation.DropdownMenuItem import com.twidere.twiderex.component.foundation.HorizontalDivider import com.twidere.twiderex.component.foundation.NetworkImage -import com.twidere.twiderex.component.foundation.Pager import com.twidere.twiderex.component.foundation.SwipeToRefreshLayout -import com.twidere.twiderex.component.foundation.rememberPagerState import com.twidere.twiderex.component.lazy.ui.LazyUiStatusImageList import com.twidere.twiderex.component.lazy.ui.LazyUiStatusList import com.twidere.twiderex.component.status.HtmlText @@ -186,7 +186,7 @@ fun UserComponent( ) }, content = { - val pagerState = rememberPagerState(pageCount = tabs.size) + val pagerState = rememberPagerState() Column { val scope = rememberCoroutineScope() TabRow( @@ -206,7 +206,7 @@ fun UserComponent( selected = pagerState.currentPage == index, onClick = { scope.launch { - pagerState.currentPage = index + pagerState.animateScrollToPage(index) } }, content = { @@ -223,11 +223,11 @@ fun UserComponent( ) } } - Pager( + HorizontalPager( + count = tabs.size, modifier = Modifier.weight(1f), state = pagerState, - offscreenLimit = 0, - ) { + ) { page -> Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter, diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/Pager.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/Pager.kt deleted file mode 100644 index 043d386b0..000000000 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/Pager.kt +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Twidere X - * - * Copyright (C) TwidereProject and Contributors - * - * This file is part of Twidere X. - * - * Twidere X is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Twidere X is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Twidere X. If not, see . - */ -package com.twidere.twiderex.component.foundation - -import androidx.annotation.IntRange -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation -import androidx.compose.foundation.gestures.forEachGesture -import androidx.compose.foundation.gestures.horizontalDrag -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.positionChange -import androidx.compose.ui.input.pointer.util.VelocityTracker -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.Measurable -import androidx.compose.ui.layout.ParentDataModifier -import androidx.compose.ui.unit.Density -import kotlinx.coroutines.launch -import kotlin.math.absoluteValue -import kotlin.math.roundToInt -import kotlin.math.sign -import kotlin.math.withSign - -/** - * This is a modified version of: - * https://gist.github.com/adamp/07d468f4bcfe632670f305ce3734f511 - */ - -@Composable -fun rememberPagerState( - @IntRange(from = 0) pageCount: Int, - @IntRange(from = 0) initialPage: Int = 0, -): PagerState { - return rememberSaveable( - saver = PagerState.Saver(), - ) { - PagerState(pageCount = pageCount, currentPage = initialPage) - }.apply { - this.pageCount = pageCount - } -} - -@Stable -class PagerState( - @IntRange(from = 0) pageCount: Int, - @IntRange(from = 0) currentPage: Int = 0, -) { - private val velocityTracker = VelocityTracker() - private var _pageCount by mutableStateOf(pageCount) - private var _currentPage by mutableStateOf(currentPage) - - companion object { - fun Saver(): Saver = listSaver( - save = { listOf(it.pageCount, it.currentPage) }, - restore = { - PagerState( - pageCount = it[0], - currentPage = it[1], - ) - } - ) - } - - internal inline val firstPageIndex: Int - get() = 0 - - internal inline val lastPageIndex: Int - get() = (pageCount - 1).coerceAtLeast(0) - - @get:IntRange(from = 0) - var pageCount: Int - get() = _pageCount - set(@IntRange(from = 0) value) { - require(value >= 0) { "pageCount must be >= 0" } - _pageCount = value - currentPage = currentPage.coerceIn(firstPageIndex, lastPageIndex) - // updateLayoutPages(currentPage) - } - - private fun Int.floorMod(other: Int): Int { - return when (other) { - 0 -> this - else -> this - this.floorDiv(other) * other - } - } - - @get:IntRange(from = 0) - var currentPage: Int - get() = _currentPage - set(value) { - _currentPage = value.floorMod(pageCount) - } - - enum class SelectionState { Selected, Undecided } - - var selectionState by mutableStateOf(SelectionState.Selected) - - suspend inline fun selectPage(block: PagerState.() -> R): R = try { - selectionState = SelectionState.Undecided - block.invoke(this) - } finally { - selectPage() - } - - suspend fun selectPage() { - currentPage -= currentPageOffset.roundToInt() - snapToOffset(0f) - selectionState = SelectionState.Selected - } - - private var _currentPageOffset = Animatable(0f).apply { - updateBounds(-1f, 1f) - } - val currentPageOffset: Float - get() = _currentPageOffset.value - - suspend fun snapToOffset(offset: Float) { - val max = if (currentPage == firstPageIndex) 0f else 1f - val min = if (currentPage == lastPageIndex) 0f else -1f - _currentPageOffset.snapTo(offset.coerceIn(min, max)) - } - - suspend fun fling(velocity: Float) { - if (velocity < 0 && currentPage == lastPageIndex) return - if (velocity > 0 && currentPage == firstPageIndex) return - val currentOffset = _currentPageOffset.value - when { - currentOffset.sign == velocity.sign && - ( - velocity.absoluteValue > 1.5f || - currentOffset.absoluteValue > 0.5 && currentOffset.absoluteValue < 1f - ) -> { - _currentPageOffset.animateTo(1f.withSign(velocity)) - selectPage() - } - else -> { - _currentPageOffset.animateTo(0f) - selectPage() - } - } - } - - fun addPosition(uptimeMillis: Long, position: Offset) { - velocityTracker.addPosition(timeMillis = uptimeMillis, position = position) - } - - suspend fun dragEnd(pageSize: Int) { - val velocity = velocityTracker.calculateVelocity() - fling(velocity.x / pageSize) - } -} - -@Immutable -private data class PageData(val page: Int) : ParentDataModifier { - override fun Density.modifyParentData(parentData: Any?): Any? = this@PageData -} - -private val Measurable.page: Int - get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this") - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun Pager( - state: PagerState, - modifier: Modifier = Modifier, - offscreenLimit: Int = 2, - dragEnabled: Boolean = true, - content: @Composable PagerScope.() -> Unit -) { - val coroutineScope = rememberCoroutineScope() - var pageSize by remember { mutableStateOf(0) } - Layout( - content = { - val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.firstPageIndex) - val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.lastPageIndex) - - for (page in minPage..maxPage) { - val pageData = PageData(page) - val scope = PagerScope(state, page) - key(pageData) { - Box(contentAlignment = Alignment.Center, modifier = pageData) { - scope.content() - } - } - } - }, - modifier = modifier - .pointerInput(Unit) { - if (dragEnabled) { - detectHorizontalDrag( - onHorizontalDrag = { change, dragAmount -> - with(state) { - selectionState = PagerState.SelectionState.Undecided - val pos = pageSize * currentPageOffset - val max = - if (currentPage == firstPageIndex) 0 else pageSize * offscreenLimit - val min = - if (currentPage == lastPageIndex) 0 else -pageSize * offscreenLimit - val newPos = - (pos + dragAmount).coerceIn(min.toFloat(), max.toFloat()) - if (newPos != 0f) { - if (change.positionChange() != Offset.Zero) change.consume() - addPosition(change.uptimeMillis, change.position) - coroutineScope.launch { - snapToOffset(newPos / pageSize) - } - } - } - }, - onDragEnd = { - coroutineScope.launch { - state.dragEnd(pageSize) - } - }, - onDragCancel = { - coroutineScope.launch { - state.dragEnd(pageSize) - } - }, - ) - } - } - ) { measurables, constraints -> - layout(constraints.maxWidth, constraints.maxHeight) { - val currentPage = state.currentPage - val offset = state.currentPageOffset - val childConstraints = constraints.copy(minWidth = 0, minHeight = 0) - - measurables - .map { - it.measure(childConstraints) to it.page - } - .forEach { (placeable, page) -> - // TODO: current this centers each page. We should investigate reading - // gravity modifiers on the child, or maybe as a param to Pager. - val xCenterOffset = (constraints.maxWidth - placeable.width) / 2 - val yCenterOffset = (constraints.maxHeight - placeable.height) / 2 - - if (currentPage == page) { - pageSize = placeable.width - } - - val xItemOffset = ((page + offset - currentPage) * placeable.width).roundToInt() - - placeable.place( - x = xCenterOffset + xItemOffset, - y = yCenterOffset - ) - } - } - } -} - -/** - * Scope for [Pager] content. - */ -class PagerScope( - private val state: PagerState, - val page: Int -) { - /** - * Returns the current selected page - */ - val currentPage: Int - get() = state.currentPage - - /** - * Returns the current selected page offset - */ - val currentPageOffset: Float - get() = state.currentPageOffset - - /** - * Returns the current selection state - */ - val selectionState: PagerState.SelectionState - get() = state.selectionState -} - -private suspend fun PointerInputScope.detectHorizontalDrag( - onDragStart: (Offset) -> Unit = { }, - onDragEnd: () -> Unit = { }, - onDragCancel: () -> Unit = { }, - onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit -) { - forEachGesture { - awaitPointerEventScope { - val down = awaitFirstDown(requireUnconsumed = false) - val drag = awaitHorizontalTouchSlopOrCancellation(down.id, onHorizontalDrag) - if (drag != null) { - onDragStart.invoke(drag.position) - if ( - horizontalDrag(drag.id) { - onHorizontalDrag(it, it.positionChange().x) - } - ) { - onDragEnd() - } else { - onDragCancel() - } - } - } - } -} diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/platform/PagerIndicator.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/platform/PagerIndicator.kt deleted file mode 100644 index a0052f0f1..000000000 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/component/foundation/platform/PagerIndicator.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Twidere X - * - * Copyright (C) TwidereProject and Contributors - * - * This file is part of Twidere X. - * - * Twidere X is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Twidere X is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Twidere X. If not, see . - */ -package com.twidere.twiderex.component.foundation.platform - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ContentAlpha -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalContentColor -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import com.twidere.twiderex.component.foundation.PagerState - -/** - * An horizontally laid out indicator for a [HorizontalPager] or [VerticalPager], representing - * the currently active page and total pages drawn using a [Shape]. - * - * This element allows the setting of the [indicatorShape], which defines how the - * indicator is visually represented. - * - * @sample com.google.accompanist.sample.pager.HorizontalPagerIndicatorSample - * - * @param pagerState the state object of your [Pager] to be used to observe the list's state. - * @param modifier the modifier to apply to this layout. - * @param activeColor the color of the active Page indicator - * @param inactiveColor the color of page indicators that are inactive. This defaults to - * [activeColor] with the alpha component set to the [ContentAlpha.disabled]. - * @param indicatorWidth the width of each indicator in [Dp]. - * @param indicatorHeight the height of each indicator in [Dp]. Defaults to [indicatorWidth]. - * @param spacing the spacing between each indicator in [Dp]. - * @param indicatorShape the shape representing each indicator. This defaults to [CircleShape]. - */ -@Composable -fun HorizontalPagerIndicator( - pagerState: PagerState, - modifier: Modifier = Modifier, - activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), - inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), - indicatorWidth: Dp = 8.dp, - indicatorHeight: Dp = indicatorWidth, - spacing: Dp = indicatorWidth, - indicatorShape: Shape = CircleShape, -) { - val indicatorWidthPx = LocalDensity.current.run { indicatorWidth.roundToPx() } - val spacingPx = LocalDensity.current.run { spacing.roundToPx() } - - Box( - modifier = modifier, - contentAlignment = Alignment.CenterStart - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(spacing), - verticalAlignment = Alignment.CenterVertically, - ) { - val indicatorModifier = Modifier - .size(width = indicatorWidth, height = indicatorHeight) - .background(color = inactiveColor, shape = indicatorShape) - - repeat(pagerState.pageCount) { - Box(indicatorModifier) - } - } - - Box( - Modifier - .offset { - val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffset) - .coerceIn(0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat()) - IntOffset( - x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(), - y = 0 - ) - } - .size(width = indicatorWidth, height = indicatorHeight) - .background( - color = activeColor, - shape = indicatorShape, - ) - ) - } -} diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/HomeScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/HomeScene.kt index 86f53f046..324dc9d55 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/HomeScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/HomeScene.kt @@ -72,6 +72,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.rememberPagerState import com.twidere.twiderex.component.UserMetrics import com.twidere.twiderex.component.foundation.AppBar import com.twidere.twiderex.component.foundation.AppBarDefaults @@ -79,9 +83,6 @@ import com.twidere.twiderex.component.foundation.ApplyNotification import com.twidere.twiderex.component.foundation.IconTabsComponent import com.twidere.twiderex.component.foundation.InAppNotificationScaffold import com.twidere.twiderex.component.foundation.NestedScrollScaffold -import com.twidere.twiderex.component.foundation.Pager -import com.twidere.twiderex.component.foundation.PagerState -import com.twidere.twiderex.component.foundation.rememberPagerState import com.twidere.twiderex.component.lazy.divider import com.twidere.twiderex.component.navigation.openLink import com.twidere.twiderex.component.navigation.user @@ -107,6 +108,7 @@ import kotlinx.coroutines.launch import moe.tlaster.precompose.navigation.BackHandler import moe.tlaster.precompose.navigation.Navigator +@OptIn(ExperimentalPagerApi::class) @Composable fun HomeScene( navigator: Navigator, @@ -129,9 +131,7 @@ fun HomeScene( } } } - val pagerState = rememberPagerState( - pageCount = menus.size, - ) + val pagerState = rememberPagerState() val scaffoldState = rememberScaffoldState() if (scaffoldState.drawerState.isOpen) { BackHandler { @@ -179,9 +179,7 @@ fun HomeScene( } } scope.launch { - pagerState.selectPage { - pagerState.currentPage = it - } + pagerState.animateScrollToPage(it) } } ) @@ -222,9 +220,10 @@ fun HomeScene( .fillMaxSize() .padding(it) ) { - Pager( + HorizontalPager( + count = menus.size, state = pagerState, - ) { + ) { page -> menus[page].item.Content(navigator) } } @@ -288,6 +287,7 @@ private object EmptyColumnHomeContentDefaults { val VerticalPadding = 48.dp } +@OptIn(ExperimentalPagerApi::class) @Composable fun HomeAppBar( tabPosition: AppearancePreferences.TabPosition, @@ -356,9 +356,7 @@ fun HomeAppBar( } } scope.launch { - pagerState.selectPage { - pagerState.currentPage = it - } + pagerState.animateScrollToPage(it) } }, ) diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt index 4187e5ff6..f73a00a8d 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt @@ -18,6 +18,8 @@ * You should have received a copy of the GNU General Public License * along with Twidere X. If not, see . */ +@file:OptIn(ExperimentalPagerApi::class) + package com.twidere.twiderex.scenes import androidx.compose.animation.AnimatedVisibility @@ -62,18 +64,19 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.rememberPagerState +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.HorizontalPagerIndicator import com.mxalbert.zoomable.Zoomable import com.twidere.twiderex.component.bottomInsetsHeight import com.twidere.twiderex.component.bottomInsetsPadding import com.twidere.twiderex.component.foundation.DropdownMenuItem import com.twidere.twiderex.component.foundation.LoadingProgress import com.twidere.twiderex.component.foundation.NetworkImage -import com.twidere.twiderex.component.foundation.Pager -import com.twidere.twiderex.component.foundation.PagerState import com.twidere.twiderex.component.foundation.VideoPlayer import com.twidere.twiderex.component.foundation.VideoPlayerState -import com.twidere.twiderex.component.foundation.platform.HorizontalPagerIndicator -import com.twidere.twiderex.component.foundation.rememberPagerState import com.twidere.twiderex.component.foundation.rememberVideoPlayerState import com.twidere.twiderex.component.painterResource import com.twidere.twiderex.component.status.LikeButton @@ -186,6 +189,7 @@ private fun StatusMediaScene( } } +@OptIn(ExperimentalPagerApi::class) @Composable private fun StatusMediaScene( status: UiStatus, @@ -198,7 +202,6 @@ private fun StatusMediaScene( val controlPanelColor = MaterialTheme.colors.surface.copy(alpha = 0.6f) val pagerState = rememberPagerState( initialPage = selectedIndex, - pageCount = status.media.size, ) val currentMedia = status.media[pagerState.currentPage] @@ -427,12 +430,6 @@ private fun StatusMediaInfo( DropdownMenuItem( onClick = { clickable.onSaveMediaClicked(currentMedia) - // scope.launch { - // callback.invoke() - // viewModel.saveFile(currentMedia, target = { - // FilePicker.createFile(it)?.path - // }) - // } } ) { Text( @@ -526,7 +523,6 @@ fun MediaView( swiperState: SwiperState = rememberSwiperState(), pagerState: PagerState = rememberPagerState( initialPage = 0, - pageCount = media.size, ), onVideoPlayerStateSet: (VideoPlayerState?) -> Unit = {}, volume: Float = 1f, @@ -536,9 +532,10 @@ fun MediaView( modifier = modifier, state = swiperState, ) { - Pager( + HorizontalPager( + count = media.size, state = pagerState, - ) { + ) { page -> val data = media[page] when (data.type) { MediaType.photo -> diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PureMediaScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PureMediaScene.kt index 45c6ab43a..908901101 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PureMediaScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PureMediaScene.kt @@ -50,9 +50,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.rememberPagerState import com.twidere.twiderex.component.bottomInsetsPadding import com.twidere.twiderex.component.foundation.VideoPlayerState -import com.twidere.twiderex.component.foundation.rememberPagerState import com.twidere.twiderex.component.painterResource import com.twidere.twiderex.component.stringResource import com.twidere.twiderex.component.topInsetsPadding @@ -86,6 +87,7 @@ fun PureMediaScene( } } +@OptIn(ExperimentalPagerApi::class) @Composable private fun PureMediaScene( belongToKey: MicroBlogKey, @@ -110,7 +112,6 @@ private fun PureMediaScene( val controlPanelColor = MaterialTheme.colors.surface.copy(alpha = 0.6f) val pagerState = rememberPagerState( initialPage = selectedIndex, - pageCount = medias.size, ) val videoPlayerState = remember { mutableStateOf(null) } val swiperState = rememberSwiperState( diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/home/mastodon/MastodonNotificationItem.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/home/mastodon/MastodonNotificationItem.kt index e829c0eb3..ef778d061 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/home/mastodon/MastodonNotificationItem.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/home/mastodon/MastodonNotificationItem.kt @@ -27,12 +27,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.painter.Painter +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState import com.twidere.twiderex.component.foundation.AppBar import com.twidere.twiderex.component.foundation.AppBarNavigationButton import com.twidere.twiderex.component.foundation.InAppNotificationScaffold -import com.twidere.twiderex.component.foundation.Pager import com.twidere.twiderex.component.foundation.TextTabsComponent -import com.twidere.twiderex.component.foundation.rememberPagerState import com.twidere.twiderex.component.lazy.LazyListController import com.twidere.twiderex.component.painterResource import com.twidere.twiderex.component.stringResource @@ -104,6 +105,7 @@ fun MastodonNotificationScene( } } +@OptIn(ExperimentalPagerApi::class) @Composable fun MastodonNotificationSceneContent( navigator: Navigator, @@ -116,7 +118,7 @@ fun MastodonNotificationSceneContent( MentionItem() ) } - val pagerState = rememberPagerState(pageCount = tabs.size) + val pagerState = rememberPagerState() LaunchedEffect(pagerState.currentPage) { // FIXME: 2021/5/17 A little bit dirty setLazyListController?.invoke(tabs[pagerState.currentPage].lazyListController) @@ -129,15 +131,16 @@ fun MastodonNotificationSceneContent( selectedItem = pagerState.currentPage, onItemSelected = { scope.launch { - pagerState.selectPage { - pagerState.currentPage = it - } + pagerState.animateScrollToPage(it) } }, ) } ) { - Pager(state = pagerState) { + HorizontalPager( + count = tabs.size, + state = pagerState, + ) { page -> tabs[page].Content(navigator) } } diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/search/SearchScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/search/SearchScene.kt index 0b9d1c21b..0dba2a38e 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/search/SearchScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/search/SearchScene.kt @@ -46,12 +46,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState import com.twidere.twiderex.component.foundation.AppBar import com.twidere.twiderex.component.foundation.AppBarDefaults import com.twidere.twiderex.component.foundation.AppBarNavigationButton import com.twidere.twiderex.component.foundation.InAppNotificationScaffold -import com.twidere.twiderex.component.foundation.Pager -import com.twidere.twiderex.component.foundation.rememberPagerState import com.twidere.twiderex.component.painterResource import com.twidere.twiderex.component.stringResource import com.twidere.twiderex.extensions.rememberPresenterState @@ -69,6 +70,7 @@ import com.twidere.twiderex.ui.TwidereScene import kotlinx.coroutines.launch import moe.tlaster.precompose.navigation.Navigator +@OptIn(ExperimentalPagerApi::class) @Composable fun SearchScene( keyword: String, @@ -94,7 +96,7 @@ fun SearchScene( ) } } - val pagerState = rememberPagerState(pageCount = tabs.size) + val pagerState = rememberPagerState() val scope = rememberCoroutineScope() TwidereScene { InAppNotificationScaffold { @@ -177,7 +179,7 @@ fun SearchScene( selected = pagerState.currentPage == index, onClick = { scope.launch { - pagerState.currentPage = index + pagerState.animateScrollToPage(index) } }, content = { @@ -195,7 +197,10 @@ fun SearchScene( Box( modifier = Modifier.weight(1F), ) { - Pager(state = pagerState) { + HorizontalPager( + count = tabs.size, + state = pagerState, + ) { page -> tabs[page].Content(keyword = keyword, navigator = navigator) } } From cd2283f944ebf62c4a57028fd17af71ad3f502bc Mon Sep 17 00:00:00 2001 From: seiko Date: Wed, 18 Jan 2023 09:58:11 +0800 Subject: [PATCH 3/4] try pass userkey --- .../com/twidere/twiderex/scenes/PlatformMediaScene.kt | 2 ++ .../kotlin/com/twidere/twiderex/component/UserComponent.kt | 4 +++- .../twidere/twiderex/component/navigation/NavigatorExt.kt | 3 ++- .../twiderex/component/status/StatusMediaComponent.kt | 2 +- .../com/twidere/twiderex/navigation/NavigationEvent.kt | 6 +++--- .../kotlin/com/twidere/twiderex/navigation/Root.kt | 2 +- .../kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt | 1 + .../kotlin/com/twidere/twiderex/scenes/MediaScene.kt | 5 ++++- .../com/twidere/twiderex/scenes/PlatformMediaScene.kt | 1 + .../com/twidere/twiderex/scenes/PlatformMediaScene.kt | 2 ++ 10 files changed, 20 insertions(+), 8 deletions(-) diff --git a/common/src/androidMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt b/common/src/androidMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt index 55bf1281d..dffe17b5f 100644 --- a/common/src/androidMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt +++ b/common/src/androidMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt @@ -32,11 +32,13 @@ import java.net.URLDecoder actual fun PlatformStatusMediaScene( statusKey: String, selectedIndex: Int?, + userKey: String?, navigator: Navigator, ) { StatusMediaScene( statusKey = statusKey, selectedIndex = selectedIndex ?: 0, + userKey = userKey, navigator = navigator, ) } diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt index 3ce2182ec..1c317da4c 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/component/UserComponent.kt @@ -144,7 +144,9 @@ fun UserComponent( ) { UserMediaTimeline( state = state.userMediaTimelineState, - openMedia = userNavigationData.statusNavigation.toMediaWithIndex, + openMedia = { statusKey, index -> + userNavigationData.statusNavigation.toMediaWithIndex(statusKey, index, userKey) + }, ) }, ).let { diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/component/navigation/NavigatorExt.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/component/navigation/NavigatorExt.kt index 15f41fef6..f4a676e7a 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/component/navigation/NavigatorExt.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/component/navigation/NavigatorExt.kt @@ -74,9 +74,10 @@ fun Navigator.status( fun Navigator.media( statusKey: MicroBlogKey, selectedIndex: Int = 0, + userKey: MicroBlogKey? = null, navOptions: NavOptions? = null, ) { - navigate(Root.Media.Status(statusKey, selectedIndex), navOptions) + navigate(Root.Media.Status(statusKey, selectedIndex, userKey), navOptions) } fun Navigator.searchInput(initial: String? = null) { diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt index d5aa2c47a..988a23e0e 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt @@ -81,7 +81,7 @@ fun StatusMediaComponent( } val onItemClick = { it: UiMedia -> val index = media.indexOf(it) - statusNavigation.toMediaWithIndex(status.statusKey, index) + statusNavigation.toMediaWithIndex(status.statusKey, index, null) } val isAlwaysShowSensitiveMedia = LocalAccountPreferences.current.isAlwaysShowSensitiveMedia var sensitive by key(isAlwaysShowSensitiveMedia) { diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/NavigationEvent.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/NavigationEvent.kt index b547e70dc..4b8bda717 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/NavigationEvent.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/NavigationEvent.kt @@ -41,7 +41,7 @@ data class StatusNavigationData( val toUser: (UiUser) -> Unit = { }, val toStatus: (UiStatus) -> Unit = { }, val toMedia: (MicroBlogKey) -> Unit = { }, - val toMediaWithIndex: (MicroBlogKey, Int) -> Unit = { _: MicroBlogKey, _: Int -> }, + val toMediaWithIndex: (statusKey: MicroBlogKey, index: Int, userKey: MicroBlogKey?) -> Unit = { _, _, _ -> }, val openLink: (String) -> Unit = { }, val composeNavigationData: ComposeNavigationData = ComposeNavigationData(), val popBackStack: () -> Unit = {}, @@ -67,8 +67,8 @@ fun rememberStatusNavigationData( toMedia = { navigator.media(it) }, - toMediaWithIndex = { key, index -> - navigator.media(key, index) + toMediaWithIndex = { key, index, userKey -> + navigator.media(key, index, userKey) }, openLink = { navigator.openLink(it) diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/Root.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/Root.kt index 7adcb635f..4fcb440f5 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/Root.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/navigation/Root.kt @@ -161,7 +161,7 @@ object Root { object Media { object Status { const val route = "/Root/Media/Status/{statusKey}" - operator fun invoke(statusKey: MicroBlogKey, selectedIndex: Int?) = "/Root/Media/Status/$statusKey?selectedIndex=$selectedIndex" + operator fun invoke(statusKey: MicroBlogKey, selectedIndex: Int?, userKey: MicroBlogKey?) = "/Root/Media/Status/$statusKey?selectedIndex=$selectedIndex&userKey=$userKey" } object Raw { const val route = "/Root/Media/Raw/{type}/{url}" diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt index 342d8d83d..3280f11a2 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt @@ -38,6 +38,7 @@ import moe.tlaster.kfilepicker.FilePicker internal fun MediaPresenter( event: Flow, statusKey: MicroBlogKey, + userKey: MicroBlogKey?, statusRepository: StatusRepository = get(), mediaAction: MediaAction = get(), ): MediaState { diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt index f73a00a8d..8c7a66939 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaScene.kt @@ -120,6 +120,7 @@ import java.net.URLDecoder fun StatusMediaScene( statusKey: String, selectedIndex: Int, + userKey: String?, navigator: Navigator, ) { MicroBlogKey.valueOf(statusKey).let { key -> @@ -128,6 +129,7 @@ fun StatusMediaScene( StatusMediaScene( statusKey = key, selectedIndex = selectedIndex, + userKey = userKey?.let { MicroBlogKey.valueOf(it) }, navigator = navigator, ) } @@ -139,10 +141,11 @@ fun StatusMediaScene( private fun StatusMediaScene( statusKey: MicroBlogKey, selectedIndex: Int, + userKey: MicroBlogKey?, navigator: Navigator, ) { val (state, channel) = rememberPresenterState { - MediaPresenter(it, statusKey) + MediaPresenter(it, statusKey, userKey) } TwidereDialog( requireDarkTheme = true, diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt index 47a842e6d..b4c190b0c 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt @@ -35,6 +35,7 @@ import moe.tlaster.precompose.navigation.Navigator expect fun PlatformStatusMediaScene( @Path("statusKey") statusKey: String, @Query("selectedIndex") selectedIndex: Int?, + @Query("userKey") userKey: String?, navigator: Navigator, ) diff --git a/common/src/desktopMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt b/common/src/desktopMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt index a3a203375..c6e2bcc0f 100644 --- a/common/src/desktopMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt +++ b/common/src/desktopMain/kotlin/com/twidere/twiderex/scenes/PlatformMediaScene.kt @@ -33,12 +33,14 @@ import moe.tlaster.precompose.navigation.Navigator actual fun PlatformStatusMediaScene( statusKey: String, selectedIndex: Int?, + userKey: String?, navigator: Navigator, ) { MediaScene { StatusMediaScene( statusKey = statusKey, selectedIndex = selectedIndex ?: 0, + userKey = userKey, navigator = navigator, ) } From f55b14c717bf49e6c55cdaaefa258fb7132039d8 Mon Sep 17 00:00:00 2001 From: seiko Date: Wed, 18 Jan 2023 09:58:30 +0800 Subject: [PATCH 4/4] try load user medias in media presenter --- .../twidere/twiderex/scenes/MediaPresenter.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt index 3280f11a2..4d5a2b0d2 100644 --- a/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt +++ b/common/src/commonMain/kotlin/com/twidere/twiderex/scenes/MediaPresenter.kt @@ -23,14 +23,19 @@ package com.twidere.twiderex.scenes import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.twidere.services.microblog.TimelineService import com.twidere.twiderex.action.MediaAction import com.twidere.twiderex.di.ext.get import com.twidere.twiderex.model.MicroBlogKey import com.twidere.twiderex.model.ui.UiMedia import com.twidere.twiderex.model.ui.UiStatus import com.twidere.twiderex.repository.StatusRepository +import com.twidere.twiderex.repository.TimelineRepository import kotlinx.coroutines.flow.Flow import moe.tlaster.kfilepicker.FilePicker @@ -40,6 +45,7 @@ internal fun MediaPresenter( statusKey: MicroBlogKey, userKey: MicroBlogKey?, statusRepository: StatusRepository = get(), + timelineRepository: TimelineRepository = get(), mediaAction: MediaAction = get(), ): MediaState { val accountState = CurrentAccountPresenter() @@ -75,19 +81,28 @@ internal fun MediaPresenter( } } } - val status by produceState( - initialValue = MediaState.Loading, - ) { + val status by remember { statusRepository.loadStatus( statusKey = statusKey, accountKey = account.accountKey, - ).collect { status -> - if (status != null) { - value = MediaState.Data(status) - } - } + ) + }.collectAsState(null) + val userMedias = if (userKey != null) { + remember(account) { + timelineRepository.mediaTimeline( + userKey, + account.accountKey, + account.service as TimelineService + ) + }.collectAsLazyPagingItems() + } else null + return when (status) { + null -> MediaState.Loading + else -> MediaState.Data( + status!!, + userMedias, + ) } - return status } internal sealed interface MediaEvent { @@ -106,6 +121,7 @@ internal sealed interface MediaState { @Immutable data class Data( - val status: UiStatus + val status: UiStatus, + val userMedias: LazyPagingItems>? ) : MediaState }