Skip to content

Commit fedb5bc

Browse files
style(profile): Migrate Profile to Composable
AI-assistant: Claude Code v2.1.72 (Claude Sonnet 4.6) Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent cf33e64 commit fedb5bc

10 files changed

Lines changed: 1043 additions & 859 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.talk.profile
8+
9+
import android.widget.ImageView
10+
import androidx.compose.foundation.layout.Arrangement
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Row
13+
import androidx.compose.foundation.layout.Spacer
14+
import androidx.compose.foundation.layout.height
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.layout.width
18+
import androidx.compose.foundation.shape.CircleShape
19+
import androidx.compose.foundation.shape.RoundedCornerShape
20+
import androidx.compose.material3.FilledTonalIconButton
21+
import androidx.compose.material3.Icon
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.Text
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.key
26+
import androidx.compose.ui.Alignment
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.draw.clip
29+
import androidx.compose.ui.res.painterResource
30+
import androidx.compose.ui.res.stringResource
31+
import androidx.compose.ui.text.style.TextOverflow
32+
import androidx.compose.ui.unit.dp
33+
import androidx.compose.ui.viewinterop.AndroidView
34+
import com.nextcloud.talk.R
35+
import com.nextcloud.talk.utils.DisplayUtils
36+
37+
@Composable
38+
internal fun AvatarSection(
39+
state: ProfileUiState,
40+
callbacks: ProfileCallbacks,
41+
isLandscape: Boolean,
42+
modifier: Modifier = Modifier
43+
) {
44+
val avatarSize = if (isLandscape) 72.dp else 96.dp
45+
46+
@Composable
47+
fun AvatarImage() {
48+
key(state.currentUser?.userId, state.avatarRefreshKey) {
49+
AndroidView(
50+
factory = { ctx ->
51+
ImageView(ctx).apply {
52+
transitionName = "userAvatar.transitionTag"
53+
contentDescription = ctx.getString(R.string.avatar)
54+
}.also { imageView ->
55+
DisplayUtils.loadAvatarImage(
56+
state.currentUser,
57+
imageView,
58+
state.avatarRefreshKey > 0
59+
)
60+
}
61+
},
62+
modifier = Modifier
63+
.size(avatarSize)
64+
.clip(CircleShape)
65+
)
66+
}
67+
}
68+
69+
if (isLandscape) {
70+
// Landscape: avatar on the left, name + base URL inline to its right
71+
Column(modifier = modifier.padding(16.dp)) {
72+
Row(verticalAlignment = Alignment.Top) {
73+
AvatarImage()
74+
Spacer(modifier = Modifier.width(16.dp))
75+
Column {
76+
if (state.displayName.isNotEmpty()) {
77+
Text(
78+
text = state.displayName,
79+
style = MaterialTheme.typography.titleLarge,
80+
maxLines = 1,
81+
overflow = TextOverflow.Ellipsis
82+
)
83+
}
84+
if (state.baseUrl.isNotEmpty()) {
85+
Text(
86+
text = state.baseUrl,
87+
style = MaterialTheme.typography.bodyMedium,
88+
color = MaterialTheme.colorScheme.onSurfaceVariant,
89+
maxLines = 1,
90+
overflow = TextOverflow.Ellipsis,
91+
modifier = Modifier.padding(top = 2.dp)
92+
)
93+
}
94+
}
95+
}
96+
if (state.showAvatarButtons) {
97+
AvatarButtonsRow(
98+
callbacks = callbacks,
99+
modifier = Modifier.padding(top = 8.dp, start = 40.dp)
100+
)
101+
}
102+
if (state.showProfileEnabledCard) {
103+
ProfileEnabledCard(
104+
isEnabled = state.isProfileEnabled,
105+
onCheckedChange = callbacks.onProfileEnabledChange,
106+
modifier = Modifier.padding(vertical = 8.dp)
107+
)
108+
}
109+
}
110+
} else {
111+
// Portrait: everything stacked and centred
112+
Column(
113+
modifier = modifier.padding(top = 16.dp),
114+
horizontalAlignment = Alignment.CenterHorizontally
115+
) {
116+
AvatarImage()
117+
118+
if (state.displayName.isNotEmpty()) {
119+
Spacer(modifier = Modifier.height(8.dp))
120+
Text(
121+
text = state.displayName,
122+
style = MaterialTheme.typography.titleLarge,
123+
maxLines = 1,
124+
overflow = TextOverflow.Ellipsis,
125+
modifier = Modifier.padding(horizontal = 16.dp)
126+
)
127+
}
128+
129+
if (state.baseUrl.isNotEmpty()) {
130+
Text(
131+
text = state.baseUrl,
132+
style = MaterialTheme.typography.bodyMedium,
133+
color = MaterialTheme.colorScheme.onSurfaceVariant,
134+
maxLines = 2,
135+
overflow = TextOverflow.Ellipsis,
136+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp)
137+
)
138+
}
139+
140+
if (state.showAvatarButtons) {
141+
AvatarButtonsRow(
142+
callbacks = callbacks,
143+
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
144+
)
145+
}
146+
147+
if (state.showProfileEnabledCard) {
148+
ProfileEnabledCard(
149+
isEnabled = state.isProfileEnabled,
150+
onCheckedChange = callbacks.onProfileEnabledChange,
151+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
152+
)
153+
}
154+
}
155+
}
156+
}
157+
158+
@Composable
159+
private fun AvatarButtonsRow(callbacks: ProfileCallbacks, modifier: Modifier = Modifier) {
160+
val buttonShape = RoundedCornerShape(12.dp)
161+
Row(
162+
modifier = modifier,
163+
horizontalArrangement = Arrangement.spacedBy(8.dp)
164+
) {
165+
FilledTonalIconButton(
166+
onClick = callbacks.onAvatarUploadClick,
167+
modifier = Modifier.size(40.dp),
168+
shape = buttonShape
169+
) {
170+
Icon(
171+
painter = painterResource(R.drawable.upload),
172+
contentDescription = stringResource(R.string.upload_new_avatar_from_device)
173+
)
174+
}
175+
FilledTonalIconButton(
176+
onClick = callbacks.onAvatarChooseClick,
177+
modifier = Modifier.size(40.dp),
178+
shape = buttonShape
179+
) {
180+
Icon(
181+
painter = painterResource(R.drawable.ic_folder),
182+
contentDescription = stringResource(R.string.choose_avatar_from_cloud)
183+
)
184+
}
185+
FilledTonalIconButton(
186+
onClick = callbacks.onAvatarCameraClick,
187+
modifier = Modifier.size(40.dp),
188+
shape = buttonShape
189+
) {
190+
Icon(
191+
painter = painterResource(R.drawable.ic_baseline_photo_camera_24),
192+
contentDescription = stringResource(R.string.set_avatar_from_camera)
193+
)
194+
}
195+
FilledTonalIconButton(
196+
onClick = callbacks.onAvatarDeleteClick,
197+
modifier = Modifier.size(40.dp),
198+
shape = buttonShape
199+
) {
200+
Icon(
201+
painter = painterResource(R.drawable.trashbin),
202+
contentDescription = stringResource(R.string.delete_avatar)
203+
)
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)