Skip to content

Commit 3adb6a2

Browse files
ui setup working
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
1 parent e17417f commit 3adb6a2

File tree

6 files changed

+422
-3
lines changed

6 files changed

+422
-3
lines changed

app/build.gradle

+4-2
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,14 @@ dependencies {
5252
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
5353

5454
// Jetpack Compose
55-
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
55+
def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
5656

57-
implementation "androidx.activity:activity-compose:1.9.1"
57+
implementation "androidx.activity:activity-compose:1.9.3"
5858
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
5959
implementation (composeBom)
6060
implementation "androidx.compose.runtime:runtime"
6161
implementation "androidx.compose.ui:ui"
62+
implementation "androidx.compose.ui:ui-viewbinding"
6263
implementation "androidx.compose.ui:ui-graphics"
6364
implementation "androidx.compose.ui:ui-tooling"
6465
implementation "androidx.compose.foundation:foundation"
@@ -113,6 +114,7 @@ dependencies {
113114
testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
114115
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
115116
testImplementation 'com.facebook.soloader:soloader:0.10.5'
117+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.9.0"
116118
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
117119
debugImplementation("androidx.fragment:fragment-testing:1.6.2")
118120
testImplementation "commons-io:commons-io:2.6"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package fr.free.nrw.commons.fileusages
2+
3+
data class FileUsagesUiModel(
4+
val title: String,
5+
val link: String?
6+
)
7+
8+
fun FileUsage.toUiModel(): FileUsagesUiModel {
9+
return FileUsagesUiModel(title = title, link = "https://commons.wikimedia.org/wiki/$title")
10+
}
11+
12+
fun GlobalFileUsage.toUiModel(): FileUsagesUiModel {
13+
// link is associated with sub items under wiki group (which is not used ATM)
14+
return FileUsagesUiModel(title = wiki, link = null)
15+
}

app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt

+266-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,46 @@ import android.widget.LinearLayout
2424
import android.widget.Spinner
2525
import android.widget.TextView
2626
import android.widget.Toast
27+
import androidx.compose.foundation.clickable
28+
import androidx.compose.foundation.isSystemInDarkTheme
29+
import androidx.compose.foundation.layout.Arrangement
30+
import androidx.compose.foundation.layout.Column
31+
import androidx.compose.foundation.layout.Row
32+
import androidx.compose.foundation.layout.fillMaxWidth
33+
import androidx.compose.foundation.layout.padding
34+
import androidx.compose.material.icons.Icons
35+
import androidx.compose.material.icons.filled.KeyboardArrowDown
36+
import androidx.compose.material.icons.filled.KeyboardArrowUp
37+
import androidx.compose.material3.Icon
38+
import androidx.compose.material3.IconButton
39+
import androidx.compose.material3.LinearProgressIndicator
40+
import androidx.compose.material3.ListItem
41+
import androidx.compose.material3.MaterialTheme
42+
import androidx.compose.material3.Surface
43+
import androidx.compose.material3.Text
44+
import androidx.compose.material3.darkColorScheme
45+
import androidx.compose.material3.lightColorScheme
46+
import androidx.compose.runtime.Composable
47+
import androidx.compose.runtime.collectAsState
48+
import androidx.compose.runtime.getValue
49+
import androidx.compose.runtime.mutableStateOf
50+
import androidx.compose.runtime.saveable.rememberSaveable
51+
import androidx.compose.runtime.setValue
52+
import androidx.compose.ui.Alignment
53+
import androidx.compose.ui.Modifier
54+
import androidx.compose.ui.graphics.Color
55+
import androidx.compose.ui.platform.LocalUriHandler
56+
import androidx.compose.ui.platform.ViewCompositionStrategy
57+
import androidx.compose.ui.res.colorResource
58+
import androidx.compose.ui.res.stringResource
59+
import androidx.compose.ui.text.font.FontWeight
60+
import androidx.compose.ui.text.style.TextAlign
61+
import androidx.compose.ui.text.style.TextDecoration
62+
import androidx.compose.ui.unit.dp
63+
import androidx.compose.ui.unit.sp
2764
import androidx.fragment.app.Fragment
2865
import androidx.fragment.app.FragmentTransaction
66+
import androidx.fragment.app.viewModels
2967
import com.facebook.drawee.backends.pipeline.Fresco
3068
import com.facebook.drawee.controller.BaseControllerListener
3169
import com.facebook.drawee.controller.ControllerListener
@@ -104,6 +142,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
104142
private var isWikipediaButtonDisplayed: Boolean = false
105143
private val callback: Callback? = null
106144

145+
@Inject
146+
lateinit var mediaDetailViewModelFactory: MediaDetailViewModel.MediaDetailViewModelProviderFactory
147+
107148
@Inject
108149
lateinit var locationManager: LocationServiceManager
109150

@@ -145,6 +186,8 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
145186
@field:Named("default_preferences")
146187
lateinit var applicationKvStore: JsonKvStore
147188

189+
private val viewModel: MediaDetailViewModel by viewModels<MediaDetailViewModel> { mediaDetailViewModelFactory }
190+
148191
private var initialListTop: Int = 0
149192

150193
private var _binding: FragmentMediaDetailBinding? = null
@@ -316,6 +359,47 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
316359
binding.coordinateEdit.setOnClickListener { onUpdateCoordinatesClicked() }
317360
binding.copyWikicode.setOnClickListener { onCopyWikicodeClicked() }
318361

362+
binding.fileUsagesComposeView.apply {
363+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
364+
setContent {
365+
MaterialTheme(
366+
colorScheme = if (isSystemInDarkTheme()) darkColorScheme(
367+
primary = colorResource(R.color.primaryDarkColor),
368+
surface = colorResource(R.color.main_background_dark),
369+
background = colorResource(R.color.main_background_dark)
370+
) else lightColorScheme(
371+
primary = colorResource(R.color.primaryColor),
372+
surface = colorResource(R.color.main_background_light),
373+
background = colorResource(R.color.main_background_light)
374+
)
375+
) {
376+
377+
val commonsContainerState by viewModel.commonsContainerState.collectAsState()
378+
val globalContainerState by viewModel.globalContainerState.collectAsState()
379+
380+
Surface {
381+
Column {
382+
Text(
383+
text = stringResource(R.string.file_usages_container_heading),
384+
modifier = Modifier
385+
.fillMaxWidth()
386+
.padding(top = 6.dp),
387+
textAlign = TextAlign.Center,
388+
fontWeight = FontWeight.SemiBold,
389+
fontSize = 16.sp,
390+
)
391+
FileUsagesContainer(
392+
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
393+
commonsContainerState = commonsContainerState,
394+
globalContainerState = globalContainerState
395+
)
396+
}
397+
}
398+
399+
400+
}
401+
}
402+
}
319403

320404
/**
321405
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
@@ -346,6 +430,16 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
346430
}
347431
}
348432

433+
private fun fetchFileUsages(fileName: String) {
434+
if (viewModel.commonsContainerState.value == MediaDetailViewModel.FileUsagesContainerState.Initial) {
435+
viewModel.loadFileUsagesCommons(fileName)
436+
}
437+
438+
if (viewModel.globalContainerState.value == MediaDetailViewModel.FileUsagesContainerState.Initial) {
439+
viewModel.loadGlobalFileUsages(fileName)
440+
}
441+
}
442+
349443
/**
350444
* launch zoom acitivity after permission check
351445
* @param view as ImageView
@@ -422,6 +516,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
422516
oldWidthOfImageView = binding.mediaDetailScrollView.width
423517
if (media != null) {
424518
displayMediaDetails()
519+
fetchFileUsages(media?.filename!!)
425520
}
426521
}
427522
}
@@ -677,7 +772,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
677772
}
678773

679774
compositeDisposable.clear()
680-
_binding = null
775+
681776
super.onDestroyView()
682777
}
683778

@@ -1963,3 +2058,173 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
19632058
const val NOMINATING_FOR_DELETION_MEDIA: String = "Nominating for deletion %s"
19642059
}
19652060
}
2061+
2062+
@Composable
2063+
fun FileUsagesContainer(
2064+
modifier: Modifier = Modifier,
2065+
commonsContainerState: MediaDetailViewModel.FileUsagesContainerState,
2066+
globalContainerState: MediaDetailViewModel.FileUsagesContainerState,
2067+
) {
2068+
var isCommonsListExpanded by rememberSaveable { mutableStateOf(true) }
2069+
var isOtherWikisListExpanded by rememberSaveable { mutableStateOf(true) }
2070+
2071+
val uriHandle = LocalUriHandler.current
2072+
2073+
Column(modifier = modifier) {
2074+
2075+
Row(
2076+
modifier = Modifier.fillMaxWidth(),
2077+
verticalAlignment = Alignment.CenterVertically,
2078+
horizontalArrangement = Arrangement.SpaceBetween
2079+
) {
2080+
2081+
Text(
2082+
text = stringResource(R.string.usages_on_commons_heading),
2083+
textAlign = TextAlign.Center,
2084+
style = MaterialTheme.typography.titleSmall
2085+
)
2086+
2087+
IconButton(onClick = {
2088+
isCommonsListExpanded = !isCommonsListExpanded
2089+
}) {
2090+
Icon(
2091+
imageVector = if (isCommonsListExpanded) Icons.Default.KeyboardArrowUp
2092+
else Icons.Default.KeyboardArrowDown,
2093+
contentDescription = null
2094+
)
2095+
}
2096+
}
2097+
2098+
if (isCommonsListExpanded) {
2099+
when (commonsContainerState) {
2100+
MediaDetailViewModel.FileUsagesContainerState.Loading -> {
2101+
LinearProgressIndicator()
2102+
}
2103+
2104+
is MediaDetailViewModel.FileUsagesContainerState.Success -> {
2105+
2106+
val data = commonsContainerState.data
2107+
2108+
if (data.isNullOrEmpty()) {
2109+
ListItem(headlineContent = {
2110+
Text(
2111+
text = stringResource(R.string.no_usages_found),
2112+
style = MaterialTheme.typography.titleSmall
2113+
)
2114+
})
2115+
} else {
2116+
data.forEach { usage ->
2117+
ListItem(
2118+
leadingContent = {
2119+
Text(
2120+
text = stringResource(R.string.bullet_point),
2121+
fontWeight = FontWeight.Bold
2122+
)
2123+
},
2124+
headlineContent = {
2125+
Text(
2126+
modifier = Modifier.clickable {
2127+
uriHandle.openUri(usage.link!!)
2128+
},
2129+
text = usage.title,
2130+
style = MaterialTheme.typography.titleSmall.copy(
2131+
color = Color(0xFF5A6AEC),
2132+
textDecoration = TextDecoration.Underline
2133+
)
2134+
)
2135+
})
2136+
}
2137+
}
2138+
}
2139+
2140+
is MediaDetailViewModel.FileUsagesContainerState.Error -> {
2141+
ListItem(headlineContent = {
2142+
Text(
2143+
text = commonsContainerState.errorMessage,
2144+
color = Color.Red,
2145+
style = MaterialTheme.typography.titleSmall
2146+
)
2147+
})
2148+
}
2149+
2150+
MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
2151+
}
2152+
}
2153+
2154+
2155+
Row(
2156+
modifier = Modifier.fillMaxWidth(),
2157+
verticalAlignment = Alignment.CenterVertically,
2158+
horizontalArrangement = Arrangement.SpaceBetween
2159+
) {
2160+
Text(
2161+
text = stringResource(R.string.usages_on_other_wikis_heading),
2162+
textAlign = TextAlign.Center,
2163+
style = MaterialTheme.typography.titleSmall
2164+
)
2165+
2166+
IconButton(onClick = {
2167+
isOtherWikisListExpanded = !isOtherWikisListExpanded
2168+
}) {
2169+
Icon(
2170+
imageVector = if (isOtherWikisListExpanded) Icons.Default.KeyboardArrowUp
2171+
else Icons.Default.KeyboardArrowDown,
2172+
contentDescription = null
2173+
)
2174+
}
2175+
}
2176+
2177+
if (isOtherWikisListExpanded) {
2178+
when (globalContainerState) {
2179+
MediaDetailViewModel.FileUsagesContainerState.Loading -> {
2180+
LinearProgressIndicator()
2181+
}
2182+
2183+
is MediaDetailViewModel.FileUsagesContainerState.Success -> {
2184+
2185+
val data = globalContainerState.data
2186+
2187+
if (data.isNullOrEmpty()) {
2188+
ListItem(headlineContent = {
2189+
Text(
2190+
text = stringResource(R.string.no_usages_found),
2191+
style = MaterialTheme.typography.titleSmall
2192+
)
2193+
})
2194+
} else {
2195+
data.forEach { usage ->
2196+
ListItem(
2197+
leadingContent = {
2198+
Text(
2199+
text = stringResource(R.string.bullet_point),
2200+
fontWeight = FontWeight.Bold
2201+
)
2202+
},
2203+
headlineContent = {
2204+
Text(
2205+
text = usage.title,
2206+
style = MaterialTheme.typography.titleSmall.copy(
2207+
textDecoration = TextDecoration.Underline
2208+
)
2209+
)
2210+
})
2211+
}
2212+
}
2213+
}
2214+
2215+
is MediaDetailViewModel.FileUsagesContainerState.Error -> {
2216+
ListItem(headlineContent = {
2217+
Text(
2218+
text = globalContainerState.errorMessage,
2219+
color = Color.Red,
2220+
style = MaterialTheme.typography.titleSmall
2221+
)
2222+
})
2223+
}
2224+
2225+
MediaDetailViewModel.FileUsagesContainerState.Initial -> {}
2226+
}
2227+
}
2228+
2229+
}
2230+
}

0 commit comments

Comments
 (0)