Skip to content

Commit 9802b2b

Browse files
authored
Create VersionCard.kt
1 parent 22e67b0 commit 9802b2b

1 file changed

Lines changed: 326 additions & 0 deletions

File tree

  • source-code/app/src/main/java/com/hackeros/app/ui/components
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
package com.hackeros.app.ui.components
2+
3+
import androidx.compose.animation.core.*
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.border
6+
import androidx.compose.foundation.layout.*
7+
import androidx.compose.foundation.shape.CircleShape
8+
import androidx.compose.foundation.shape.RoundedCornerShape
9+
import androidx.compose.material.icons.Icons
10+
import androidx.compose.material.icons.filled.*
11+
import androidx.compose.material3.*
12+
import androidx.compose.runtime.*
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.draw.clip
16+
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.text.font.FontFamily
18+
import androidx.compose.ui.text.font.FontWeight
19+
import androidx.compose.ui.unit.dp
20+
import androidx.compose.ui.unit.sp
21+
import com.hackeros.app.data.model.ReleaseInfo
22+
import com.hackeros.app.ui.theme.LocalAppTheme
23+
import com.hackeros.app.ui.theme.cardColor
24+
import com.hackeros.app.ui.theme.mutedColor
25+
import com.hackeros.app.ui.theme.primaryColor
26+
import com.hackeros.app.ui.theme.textColor
27+
import com.hackeros.app.utils.Translations
28+
29+
@Composable
30+
fun VersionCard(
31+
release: ReleaseInfo,
32+
isLatest: Boolean,
33+
translations: Translations
34+
) {
35+
val theme = LocalAppTheme.current
36+
val t = translations
37+
38+
fun parseEditionLine(line: String): Pair<String, String> {
39+
val idx = line.indexOf(':')
40+
return if (idx != -1) {
41+
line.substring(0, idx).trim() to line.substring(idx + 1).trim()
42+
} else line to ""
43+
}
44+
45+
val editionsList = if (release.editions.isNotEmpty())
46+
release.editions.split("\n").map { parseEditionLine(it) }
47+
else emptyList()
48+
49+
// Ping animation for latest indicator
50+
val infiniteTransition = rememberInfiniteTransition(label = "ping")
51+
val pingAlpha by infiniteTransition.animateFloat(
52+
initialValue = 0.8f,
53+
targetValue = 0f,
54+
animationSpec = infiniteRepeatable(
55+
animation = tween(1200, easing = LinearEasing),
56+
repeatMode = RepeatMode.Restart
57+
),
58+
label = "pingAlpha"
59+
)
60+
61+
val cardBorderColor = if (isLatest) theme.primaryColor().copy(alpha = 0.4f)
62+
else Color.White.copy(alpha = 0.05f)
63+
val cardBg = if (isLatest) theme.cardColor().copy(alpha = 0.7f)
64+
else theme.cardColor().copy(alpha = 0.35f)
65+
66+
Box(
67+
modifier = Modifier
68+
.fillMaxWidth()
69+
.clip(RoundedCornerShape(20.dp))
70+
.background(cardBg)
71+
.border(1.dp, cardBorderColor, RoundedCornerShape(20.dp))
72+
) {
73+
Column {
74+
// Latest banner
75+
if (isLatest) {
76+
Row(
77+
modifier = Modifier
78+
.fillMaxWidth()
79+
.background(theme.primaryColor().copy(alpha = 0.1f))
80+
.padding(horizontal = 16.dp, vertical = 8.dp),
81+
verticalAlignment = Alignment.CenterVertically,
82+
horizontalArrangement = Arrangement.SpaceBetween
83+
) {
84+
Row(verticalAlignment = Alignment.CenterVertically) {
85+
// Ping dot
86+
Box(contentAlignment = Alignment.Center) {
87+
Box(
88+
modifier = Modifier
89+
.size(10.dp)
90+
.clip(CircleShape)
91+
.background(theme.primaryColor().copy(alpha = pingAlpha))
92+
)
93+
Box(
94+
modifier = Modifier
95+
.size(6.dp)
96+
.clip(CircleShape)
97+
.background(theme.primaryColor())
98+
)
99+
}
100+
Spacer(Modifier.width(8.dp))
101+
Text(
102+
text = t.latest_build.uppercase(),
103+
fontSize = 9.sp,
104+
fontFamily = FontFamily.Monospace,
105+
fontWeight = FontWeight.Bold,
106+
color = theme.primaryColor(),
107+
letterSpacing = 1.sp
108+
)
109+
}
110+
}
111+
Divider(color = theme.primaryColor().copy(alpha = 0.1f), thickness = 1.dp)
112+
}
113+
114+
Column(modifier = Modifier.padding(20.dp)) {
115+
// Version header
116+
Row(
117+
modifier = Modifier.fillMaxWidth(),
118+
horizontalArrangement = Arrangement.spacedBy(16.dp)
119+
) {
120+
Box(
121+
modifier = Modifier
122+
.clip(RoundedCornerShape(12.dp))
123+
.background(
124+
if (isLatest) theme.primaryColor()
125+
else Color.White.copy(alpha = 0.05f)
126+
)
127+
.padding(12.dp),
128+
contentAlignment = Alignment.Center
129+
) {
130+
Icon(
131+
imageVector = Icons.Default.Terminal,
132+
contentDescription = null,
133+
tint = if (isLatest) theme.backgroundColor() else theme.mutedColor(),
134+
modifier = Modifier.size(24.dp)
135+
)
136+
}
137+
Column(modifier = Modifier.weight(1f)) {
138+
Text(
139+
text = release.version,
140+
fontFamily = FontFamily.Monospace,
141+
fontWeight = FontWeight.Bold,
142+
fontSize = 22.sp,
143+
color = theme.textColor()
144+
)
145+
if (release.description.isNotEmpty()) {
146+
Spacer(Modifier.height(6.dp))
147+
Row(
148+
horizontalArrangement = Arrangement.spacedBy(6.dp),
149+
modifier = Modifier.fillMaxWidth()
150+
) {
151+
Icon(
152+
Icons.Default.Info,
153+
contentDescription = null,
154+
tint = theme.mutedColor().copy(alpha = 0.7f),
155+
modifier = Modifier.size(14.dp).padding(top = 2.dp)
156+
)
157+
Text(
158+
text = release.description,
159+
fontSize = 13.sp,
160+
color = theme.mutedColor().copy(alpha = 0.9f),
161+
lineHeight = 18.sp
162+
)
163+
}
164+
}
165+
}
166+
}
167+
168+
// Editions / Roadmap
169+
if (editionsList.isNotEmpty()) {
170+
Spacer(Modifier.height(20.dp))
171+
Column(
172+
modifier = Modifier
173+
.fillMaxWidth()
174+
.clip(RoundedCornerShape(14.dp))
175+
.background(theme.backgroundColor().copy(alpha = 0.5f))
176+
.border(1.dp, Color.White.copy(alpha = 0.05f), RoundedCornerShape(14.dp))
177+
) {
178+
Row(
179+
modifier = Modifier
180+
.fillMaxWidth()
181+
.background(Color.White.copy(alpha = 0.04f))
182+
.padding(horizontal = 14.dp, vertical = 10.dp),
183+
verticalAlignment = Alignment.CenterVertically,
184+
horizontalArrangement = Arrangement.spacedBy(8.dp)
185+
) {
186+
Icon(Icons.Default.CalendarToday, null,
187+
tint = theme.primaryColor(), modifier = Modifier.size(12.dp))
188+
Text(
189+
text = t.roadmap.uppercase(),
190+
fontSize = 9.sp,
191+
fontFamily = FontFamily.Monospace,
192+
fontWeight = FontWeight.Bold,
193+
color = theme.mutedColor(),
194+
letterSpacing = 1.sp
195+
)
196+
}
197+
Divider(color = Color.White.copy(alpha = 0.05f))
198+
editionsList.forEachIndexed { idx, (name, date) ->
199+
if (idx > 0) Divider(color = Color.White.copy(alpha = 0.05f))
200+
Row(
201+
modifier = Modifier
202+
.fillMaxWidth()
203+
.padding(horizontal = 14.dp, vertical = 12.dp),
204+
verticalAlignment = Alignment.CenterVertically,
205+
horizontalArrangement = Arrangement.SpaceBetween
206+
) {
207+
Row(
208+
verticalAlignment = Alignment.CenterVertically,
209+
horizontalArrangement = Arrangement.spacedBy(10.dp)
210+
) {
211+
Box(
212+
modifier = Modifier
213+
.size(6.dp)
214+
.clip(CircleShape)
215+
.background(theme.primaryColor().copy(alpha = 0.5f))
216+
)
217+
Text(
218+
text = name,
219+
fontSize = 12.sp,
220+
fontWeight = FontWeight.Medium,
221+
color = theme.textColor()
222+
)
223+
}
224+
if (date.isNotEmpty()) {
225+
Row(
226+
modifier = Modifier
227+
.clip(RoundedCornerShape(6.dp))
228+
.background(theme.backgroundColor().copy(alpha = 0.7f))
229+
.border(
230+
1.dp,
231+
theme.primaryColor().copy(alpha = 0.15f),
232+
RoundedCornerShape(6.dp)
233+
)
234+
.padding(horizontal = 8.dp, vertical = 4.dp),
235+
verticalAlignment = Alignment.CenterVertically,
236+
horizontalArrangement = Arrangement.spacedBy(4.dp)
237+
) {
238+
Icon(
239+
Icons.Default.Schedule, null,
240+
tint = theme.primaryColor(),
241+
modifier = Modifier.size(10.dp)
242+
)
243+
Text(
244+
text = date,
245+
fontSize = 10.sp,
246+
fontFamily = FontFamily.Monospace,
247+
color = theme.primaryColor()
248+
)
249+
}
250+
}
251+
}
252+
}
253+
}
254+
}
255+
256+
// Changelog
257+
Spacer(Modifier.height(20.dp))
258+
Row(
259+
verticalAlignment = Alignment.CenterVertically,
260+
horizontalArrangement = Arrangement.spacedBy(8.dp),
261+
modifier = Modifier.padding(start = 4.dp, bottom = 12.dp)
262+
) {
263+
Icon(Icons.Default.ShowChart, null,
264+
tint = theme.mutedColor().copy(alpha = 0.5f), modifier = Modifier.size(12.dp))
265+
Text(
266+
text = t.changelog.uppercase(),
267+
fontSize = 9.sp,
268+
fontFamily = FontFamily.Monospace,
269+
fontWeight = FontWeight.Bold,
270+
color = theme.mutedColor().copy(alpha = 0.5f),
271+
letterSpacing = 1.sp
272+
)
273+
}
274+
275+
Box {
276+
// Timeline line
277+
Box(
278+
modifier = Modifier
279+
.padding(start = 6.dp)
280+
.width(1.dp)
281+
.fillMaxHeight()
282+
.background(Color.White.copy(alpha = 0.1f))
283+
)
284+
Column(
285+
modifier = Modifier.padding(start = 22.dp),
286+
verticalArrangement = Arrangement.spacedBy(14.dp)
287+
) {
288+
if (release.news.isNotEmpty()) {
289+
release.news.split("\n").forEach { line ->
290+
Row(
291+
horizontalArrangement = Arrangement.spacedBy(10.dp)
292+
) {
293+
Box(
294+
modifier = Modifier
295+
.padding(top = 6.dp)
296+
.offset(x = (-28).dp)
297+
.size(8.dp)
298+
.clip(CircleShape)
299+
.background(theme.backgroundColor())
300+
.border(1.dp, theme.cardColor(), CircleShape)
301+
)
302+
Text(
303+
text = line,
304+
fontSize = 12.sp,
305+
fontFamily = FontFamily.Monospace,
306+
color = theme.textColor().copy(alpha = 0.8f),
307+
lineHeight = 18.sp,
308+
modifier = Modifier.offset(x = (-28).dp)
309+
)
310+
}
311+
}
312+
} else {
313+
Text(
314+
text = t.no_changes,
315+
fontSize = 12.sp,
316+
color = theme.mutedColor(),
317+
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
318+
modifier = Modifier.offset(x = (-28).dp)
319+
)
320+
}
321+
}
322+
}
323+
}
324+
}
325+
}
326+
}

0 commit comments

Comments
 (0)