diff --git a/app/src/main/java/io/github/akiomik/seiun/ui/user/UserScreen.kt b/app/src/main/java/io/github/akiomik/seiun/ui/user/UserScreen.kt index c6e699c..74f3c71 100644 --- a/app/src/main/java/io/github/akiomik/seiun/ui/user/UserScreen.kt +++ b/app/src/main/java/io/github/akiomik/seiun/ui/user/UserScreen.kt @@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider @@ -27,12 +29,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import io.github.akiomik.seiun.R import io.github.akiomik.seiun.model.app.bsky.actor.Profile import io.github.akiomik.seiun.ui.theme.Indigo800 +import io.github.akiomik.seiun.viewmodels.AppViewModel import io.github.akiomik.seiun.viewmodels.UserFeedViewModel import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.ScrollStrategy @@ -40,14 +47,15 @@ import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState @Composable private fun UserBanner(profile: Profile, height: Dp = 128.dp) { - if (profile.banner == null) { + Box { + // fallback Box( modifier = Modifier .background(color = Indigo800) .height(height) .fillMaxWidth() - ) {} - } else { + ) + AsyncImage( model = profile.banner, contentDescription = null, @@ -58,13 +66,12 @@ private fun UserBanner(profile: Profile, height: Dp = 128.dp) { } @Composable -private fun Avatar(profile: Profile) { +private fun Avatar(profile: Profile, modifier: Modifier = Modifier, size: Dp = 64.dp) { AsyncImage( model = profile.avatar, contentDescription = null, - modifier = Modifier - .width(60.dp) - .height(60.dp) + modifier = modifier + .size(size) .clip(CircleShape) ) } @@ -87,31 +94,90 @@ private fun NameAndHandle(profile: Profile) { } @Composable -fun Profile(profile: Profile) { +private fun StatRow(profile: Profile) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = profile.followsCount.toString(), + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.follows), + style = MaterialTheme.typography.labelMedium + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = profile.followersCount.toString(), + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.followers), + style = MaterialTheme.typography.labelMedium + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = profile.postsCount.toString(), + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.posts), + style = MaterialTheme.typography.labelMedium + ) + } + } +} + +@Composable +private fun Profile(profile: Profile) { + val viewModel: AppViewModel = viewModel() + val viewer by viewModel.profile.collectAsState() + Column( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(top = 8.dp, end = 16.dp, bottom = 16.dp, start = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Avatar(profile = profile) - NameAndHandle(profile = profile) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + // TODO: Implement edit and follow + Spacer(modifier = Modifier.height(32.dp)) +// if (profile.did == viewer?.did) { +// Button(onClick = {}) { +// Text(stringResource(R.string.edit)) +// } +// } else { +// Button(onClick = {}) { +// Text(stringResource(R.string.follow)) +// } +// } } + NameAndHandle(profile = profile) + Text(profile.description.orEmpty()) + + StatRow(profile = profile) } } @Composable private fun UserModalContent(profile: Profile, onProfileClick: (String) -> Unit) { + val bannerHeight = 128.dp + val avatarSize = 96.dp + // TODO: Use ExitUntilCollapsed CollapsingToolbarScaffold( state = rememberCollapsingToolbarScaffoldState(), toolbar = { Column { - UserBanner(profile) + Box(modifier = Modifier.zIndex(2f)) { + UserBanner(profile, height = bannerHeight) + Avatar( + profile = profile, + modifier = Modifier.offset(x = 16.dp, y = bannerHeight - (avatarSize / 2)), + size = avatarSize + ) + } Box( modifier = Modifier .background(color = MaterialTheme.colorScheme.surfaceVariant) diff --git a/app/src/main/java/io/github/akiomik/seiun/viewmodels/AppViewModel.kt b/app/src/main/java/io/github/akiomik/seiun/viewmodels/AppViewModel.kt index 85a647d..2858591 100644 --- a/app/src/main/java/io/github/akiomik/seiun/viewmodels/AppViewModel.kt +++ b/app/src/main/java/io/github/akiomik/seiun/viewmodels/AppViewModel.kt @@ -2,7 +2,6 @@ package io.github.akiomik.seiun.viewmodels import android.util.Log import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.lifecycle.viewModelScope import io.github.akiomik.seiun.SeiunApplication import io.github.akiomik.seiun.model.app.bsky.actor.Profile @@ -13,62 +12,63 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -class AppViewModel : ApplicationViewModel() { - private val _atpService = SeiunApplication.instance!!.atpService +object AppViewModel : ApplicationViewModel() { + private val innerAtpService = SeiunApplication.instance!!.atpService - private var _profile = MutableStateFlow(null) - private var _showDrawer = MutableStateFlow(false) - private var _showTopBar = MutableStateFlow(false) - private var _showBottomBar = MutableStateFlow(false) - private var _fab = MutableStateFlow<@Composable () -> Unit>({}) + private var innerProfile = MutableStateFlow(null) + private var innerShowDrawer = MutableStateFlow(false) + private var innerShowTopBar = MutableStateFlow(false) + private var innerShowBottomBar = MutableStateFlow(false) + private var innerFab = MutableStateFlow<@Composable () -> Unit>({}) - private val isDrawerAvailable = _atpService.combine(_profile) { atpService, profile -> + private val isDrawerAvailable = innerAtpService.combine(innerProfile) { atpService, profile -> atpService != null && profile != null } - val profile = _profile.asStateFlow() - val showDrawer = _showDrawer.combine(isDrawerAvailable) { showDrawer, isAvailable -> + val profile = innerProfile.asStateFlow() + val showDrawer = innerShowDrawer.combine(isDrawerAvailable) { showDrawer, isAvailable -> showDrawer && isAvailable }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) - val showTopBar = _showTopBar.asStateFlow() - val showBottomBar = _showBottomBar.asStateFlow() - val fab = _fab.asStateFlow() + val showTopBar = innerShowTopBar.asStateFlow() + val showBottomBar = innerShowBottomBar.asStateFlow() + val fab = innerFab.asStateFlow() private val userRepository = SeiunApplication.instance!!.userRepository fun updateProfile() { wrapError( run = { userRepository.getProfile() }, - onSuccess = { _profile.value = it }, + onSuccess = { innerProfile.value = it }, onError = { Log.d(SeiunApplication.TAG, "Failed to init ProfileViewModel: $it") } ) } fun onTimeline() { - _showDrawer.value = true - _showTopBar.value = true - _showBottomBar.value = true - _fab.value = { NewPostFab() } + innerShowDrawer.value = true + innerShowTopBar.value = true + innerShowBottomBar.value = true + innerFab.value = { NewPostFab() } } fun onNotification() { - _showDrawer.value = true - _showTopBar.value = true - _showBottomBar.value = true - _fab.value = {} + innerShowDrawer.value = true + innerShowTopBar.value = true + innerShowBottomBar.value = true + innerFab.value = {} } fun onUser() { - _showDrawer.value = false - _showTopBar.value = false - _showBottomBar.value = true - _fab.value = {} + innerShowDrawer.value = false + innerShowTopBar.value = false + innerShowBottomBar.value = true + innerFab.value = {} } fun onLoginOrRegistration() { - _showDrawer.value = false - _showTopBar.value = false - _showBottomBar.value = false - _fab.value = {} + innerProfile.value = null + innerShowDrawer.value = false + innerShowTopBar.value = false + innerShowBottomBar.value = false + innerFab.value = {} } } diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index adb5bd2..f914b57 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -15,6 +15,12 @@ ライセンス ログイン サービスプロバイダ + フォロー中 + フォロワー + 投稿 + 編集 + フォロー + フォロー解除 ハンドルネーム または メールアドレス jack.bsky.social または jack@example.com パスワード diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dde2801..f85256a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,12 @@ Mute License Service Provider + follows + followers + posts + Edit + Follow + Unfollow Login Handle or Email jack.bsky.social or jack@example.com