Skip to content

Commit 10e1a37

Browse files
authored
Merge pull request #201 from timusus/media-import-compose
Add MediaProviderSelectionScreen
2 parents 24a16ac + 1260749 commit 10e1a37

File tree

27 files changed

+546
-2
lines changed

27 files changed

+546
-2
lines changed

android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,6 @@ class OnboardingParentFragment :
215215
// Static
216216

217217
companion object {
218-
const val REQUEST_CODE_READ_STORAGE = 100
219-
220218
const val TAG = "OnboardingParentFragment"
221219

222220
fun newInstance(args: OnboardingParentFragmentArgs) = OnboardingParentFragment().apply {
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
package com.simplecityapps.shuttle.ui.screens.onboarding.mediaprovider
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.compose.animation.AnimatedVisibility
8+
import androidx.compose.foundation.background
9+
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.layout.Arrangement
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxSize
14+
import androidx.compose.foundation.layout.fillMaxWidth
15+
import androidx.compose.foundation.layout.height
16+
import androidx.compose.foundation.layout.padding
17+
import androidx.compose.foundation.layout.size
18+
import androidx.compose.foundation.layout.wrapContentWidth
19+
import androidx.compose.foundation.lazy.LazyColumn
20+
import androidx.compose.foundation.lazy.items
21+
import androidx.compose.foundation.shape.RoundedCornerShape
22+
import androidx.compose.material.icons.Icons
23+
import androidx.compose.material.icons.filled.MoreVert
24+
import androidx.compose.material3.AlertDialog
25+
import androidx.compose.material3.ColorScheme
26+
import androidx.compose.material3.DropdownMenu
27+
import androidx.compose.material3.DropdownMenuItem
28+
import androidx.compose.material3.Icon
29+
import androidx.compose.material3.IconButton
30+
import androidx.compose.material3.ListItem
31+
import androidx.compose.material3.MaterialTheme
32+
import androidx.compose.material3.OutlinedButton
33+
import androidx.compose.material3.Text
34+
import androidx.compose.material3.TextButton
35+
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.ui.Alignment
38+
import androidx.compose.ui.Modifier
39+
import androidx.compose.ui.draw.clip
40+
import androidx.compose.ui.graphics.Color
41+
import androidx.compose.ui.platform.ComposeView
42+
import androidx.compose.ui.res.painterResource
43+
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.tooling.preview.Preview
45+
import androidx.compose.ui.tooling.preview.PreviewParameter
46+
import androidx.compose.ui.unit.dp
47+
import androidx.core.view.postDelayed
48+
import androidx.fragment.app.Fragment
49+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
50+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
51+
import com.simplecityapps.mediaprovider.iconResId
52+
import com.simplecityapps.shuttle.R
53+
import com.simplecityapps.shuttle.model.MediaProviderType
54+
import com.simplecityapps.shuttle.ui.screens.onboarding.OnboardingChild
55+
import com.simplecityapps.shuttle.ui.screens.onboarding.OnboardingPage
56+
import com.simplecityapps.shuttle.ui.screens.onboarding.OnboardingParent
57+
import com.simplecityapps.shuttle.ui.snapshot.Snapshot
58+
import com.simplecityapps.shuttle.ui.theme.AppTheme
59+
import com.simplecityapps.shuttle.ui.theme.ColorSchemePreviewParameterProvider
60+
61+
class MediaProviderSelectionScreenFragment :
62+
Fragment(),
63+
OnboardingChild {
64+
override fun onCreateView(
65+
inflater: LayoutInflater,
66+
container: ViewGroup?,
67+
savedInstanceState: Bundle?
68+
): View {
69+
val onboardingParent = parentFragment as OnboardingParent
70+
return ComposeView(requireContext()).apply {
71+
postDelayed(50) {
72+
onboardingParent.hideBackButton()
73+
onboardingParent.toggleNextButton(true)
74+
onboardingParent.showNextButton(getString(R.string.onboarding_button_next))
75+
}
76+
setContent {
77+
AppTheme {
78+
MediaProviderSelectionScreen()
79+
}
80+
}
81+
}
82+
}
83+
84+
override val page = OnboardingPage.MediaProviderSelector
85+
86+
override fun getParent() = parentFragment as? OnboardingParent
87+
88+
override fun handleNextButtonClick() {
89+
getParent()?.goToNext()
90+
}
91+
}
92+
93+
@Composable
94+
private fun MediaProviderSelectionScreen(
95+
modifier: Modifier = Modifier,
96+
viewModel: MediaProviderViewModel = hiltViewModel()
97+
) {
98+
val mediaProviders by viewModel.mediaProviders.collectAsStateWithLifecycle()
99+
val unAddedMediaProviders by viewModel.unAddedMediaProviders.collectAsStateWithLifecycle()
100+
val showAddProviderDialog by viewModel.showAddMediaProviderDialog.collectAsStateWithLifecycle()
101+
val showProviderOverflowMenu by viewModel.showProviderOverflowMenu.collectAsStateWithLifecycle()
102+
MediaProviderSelectionScreen(
103+
modifier = modifier,
104+
mediaProviders = mediaProviders,
105+
showAddProviderDialog = showAddProviderDialog,
106+
unAddedMediaProviders = unAddedMediaProviders,
107+
showProviderOverflowMenu = showProviderOverflowMenu,
108+
onMediaProviderTypeClick = viewModel::onAddMediaProvider,
109+
onRemoveProviderClick = viewModel::onRemoveMediaProvider,
110+
onAddMediaProviderClick = viewModel::onAddMediaProviderClicked,
111+
onProviderOverflowMenuClick = viewModel::onMediaProviderOverflowMenuClicked,
112+
onDismissOverflowMenuRequest = viewModel::onDismissMediaProviderOverflowMenu,
113+
onDismissAddMediaProviderRequest = viewModel::onDismissAddMediaProviderRequest
114+
)
115+
}
116+
117+
@Composable
118+
private fun MediaProviderSelectionScreen(
119+
showAddProviderDialog: Boolean,
120+
mediaProviders: List<MediaProviderType>,
121+
unAddedMediaProviders: List<MediaProviderType>,
122+
showProviderOverflowMenu: MediaProviderType?,
123+
onRemoveProviderClick: () -> Unit,
124+
onAddMediaProviderClick: () -> Unit,
125+
onDismissOverflowMenuRequest: () -> Unit,
126+
onDismissAddMediaProviderRequest: () -> Unit,
127+
onMediaProviderTypeClick: (MediaProviderType) -> Unit,
128+
onProviderOverflowMenuClick: (MediaProviderType) -> Unit,
129+
modifier: Modifier = Modifier
130+
) {
131+
if (showAddProviderDialog) {
132+
AddMediaProviderDialog(
133+
providers = unAddedMediaProviders,
134+
onDismissRequest = onDismissAddMediaProviderRequest,
135+
onMediaProviderTypeClick = onMediaProviderTypeClick
136+
)
137+
}
138+
Column(
139+
modifier = modifier
140+
.fillMaxSize()
141+
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
142+
.padding(24.dp)
143+
) {
144+
Text(
145+
color = MaterialTheme.colorScheme.onSurface,
146+
style = MaterialTheme.typography.headlineSmall,
147+
text = stringResource(R.string.media_provider_toolbar_title_onboarding)
148+
)
149+
Spacer(modifier = Modifier.height(8.dp))
150+
151+
Text(
152+
style = MaterialTheme.typography.bodySmall,
153+
color = MaterialTheme.colorScheme.onSurface,
154+
text = stringResource(R.string.onboarding_media_selection_subtitle)
155+
)
156+
Spacer(modifier = Modifier.height(24.dp))
157+
158+
LazyMediaProviderTypeColumn(
159+
providers = mediaProviders,
160+
modifier = Modifier.weight(1f),
161+
onRemoveProviderClick = onRemoveProviderClick,
162+
onOverflowMenuClick = onProviderOverflowMenuClick,
163+
showProviderOverflowMenu = showProviderOverflowMenu,
164+
onDismissOverflowMenuRequest = onDismissOverflowMenuRequest
165+
)
166+
167+
AnimatedVisibility(visible = mediaProviders.size < MediaProviderType.entries.size) {
168+
OutlinedButton(
169+
onClick = onAddMediaProviderClick,
170+
modifier = Modifier
171+
.fillMaxWidth()
172+
.wrapContentWidth(Alignment.CenterHorizontally)
173+
) {
174+
Text(text = stringResource(R.string.media_provider_add))
175+
}
176+
}
177+
}
178+
}
179+
180+
@Composable
181+
private fun AddMediaProviderDialog(
182+
onDismissRequest: () -> Unit,
183+
providers: List<MediaProviderType>,
184+
onMediaProviderTypeClick: (MediaProviderType) -> Unit,
185+
modifier: Modifier = Modifier
186+
) {
187+
AlertDialog(
188+
modifier = modifier,
189+
onDismissRequest = onDismissRequest,
190+
title = { Text(text = stringResource(R.string.media_provider_add)) },
191+
text = {
192+
LazyMediaProviderTypeColumn(
193+
providers = providers,
194+
onMediaProviderTypeClick = onMediaProviderTypeClick
195+
)
196+
},
197+
confirmButton = {
198+
TextButton(onDismissRequest) {
199+
Text(text = stringResource(R.string.dialog_button_close))
200+
}
201+
}
202+
)
203+
}
204+
205+
@Composable
206+
private fun LazyMediaProviderTypeColumn(
207+
providers: List<MediaProviderType>,
208+
onMediaProviderTypeClick: (MediaProviderType) -> Unit,
209+
modifier: Modifier = Modifier
210+
) {
211+
LazyColumn(
212+
modifier = modifier,
213+
verticalArrangement = Arrangement.spacedBy(4.dp)
214+
) {
215+
providers
216+
.groupBy(MediaProviderType::remote)
217+
.forEach { (remote, providers) ->
218+
item {
219+
MediaProviderItemHeader(
220+
remote = remote,
221+
modifier = Modifier.padding(vertical = 16.dp)
222+
)
223+
}
224+
items(providers) { provider ->
225+
MediaProviderTypeItem(
226+
provider = provider,
227+
modifier = Modifier.clickable(onClick = { onMediaProviderTypeClick(provider) })
228+
)
229+
}
230+
}
231+
}
232+
}
233+
234+
@Composable
235+
private fun LazyMediaProviderTypeColumn(
236+
providers: List<MediaProviderType>,
237+
showProviderOverflowMenu: MediaProviderType?,
238+
onRemoveProviderClick: () -> Unit,
239+
onDismissOverflowMenuRequest: () -> Unit,
240+
onOverflowMenuClick: (MediaProviderType) -> Unit,
241+
modifier: Modifier = Modifier
242+
) {
243+
LazyColumn(
244+
modifier = modifier,
245+
verticalArrangement = Arrangement.spacedBy(16.dp)
246+
) {
247+
providers
248+
.groupBy(MediaProviderType::remote)
249+
.forEach { (remote, providers) ->
250+
item { MediaProviderItemHeader(remote = remote) }
251+
items(providers) { provider ->
252+
MediaProviderTypeItem(
253+
provider = provider,
254+
onRemoveProviderClick = onRemoveProviderClick,
255+
onOverflowMenuClick = { onOverflowMenuClick(provider) },
256+
onDismissOverflowMenuRequest = onDismissOverflowMenuRequest,
257+
showProviderOverflowMenu = showProviderOverflowMenu == provider
258+
)
259+
}
260+
}
261+
}
262+
}
263+
264+
@Composable
265+
private fun MediaProviderItemHeader(
266+
remote: Boolean,
267+
modifier: Modifier = Modifier
268+
) {
269+
val headerTitle = if (remote) {
270+
stringResource(R.string.media_provider_type_remote)
271+
} else {
272+
stringResource(R.string.media_provider_type_local)
273+
}
274+
Text(
275+
text = headerTitle,
276+
modifier = modifier,
277+
style = MaterialTheme.typography.labelLarge,
278+
color = MaterialTheme.colorScheme.onSurface
279+
)
280+
}
281+
282+
@Composable
283+
private fun MediaProviderTypeItem(
284+
provider: MediaProviderType,
285+
showProviderOverflowMenu: Boolean,
286+
onOverflowMenuClick: () -> Unit,
287+
onRemoveProviderClick: () -> Unit,
288+
onDismissOverflowMenuRequest: () -> Unit,
289+
modifier: Modifier = Modifier
290+
) {
291+
MediaProviderTypeItem(
292+
modifier = modifier,
293+
provider = provider,
294+
trailingContent = {
295+
IconButton(onClick = onOverflowMenuClick) {
296+
Icon(
297+
contentDescription = null,
298+
imageVector = Icons.Default.MoreVert,
299+
tint = MaterialTheme.colorScheme.primary
300+
)
301+
}
302+
DropdownMenu(
303+
expanded = showProviderOverflowMenu,
304+
onDismissRequest = onDismissOverflowMenuRequest
305+
) {
306+
DropdownMenuItem(
307+
onClick = onRemoveProviderClick,
308+
text = { Text(stringResource(R.string.menu_title_remove)) }
309+
)
310+
}
311+
}
312+
)
313+
}
314+
315+
@Composable
316+
private fun MediaProviderTypeItem(
317+
provider: MediaProviderType,
318+
modifier: Modifier = Modifier,
319+
trailingContent: @Composable (() -> Unit)? = null
320+
) {
321+
val description: String = when (provider) {
322+
MediaProviderType.Shuttle -> stringResource(com.simplecityapps.mediaprovider.R.string.media_provider_description_s2)
323+
MediaProviderType.MediaStore -> stringResource(com.simplecityapps.mediaprovider.R.string.media_provider_description_media_store)
324+
MediaProviderType.Jellyfin -> stringResource(com.simplecityapps.mediaprovider.R.string.media_provider_description_jellyfin)
325+
MediaProviderType.Emby -> stringResource(com.simplecityapps.mediaprovider.R.string.media_provider_description_emby)
326+
MediaProviderType.Plex -> stringResource(com.simplecityapps.mediaprovider.R.string.media_provider_description_plex)
327+
}
328+
ListItem(
329+
trailingContent = trailingContent,
330+
headlineContent = { Text(text = provider.name) },
331+
supportingContent = { Text(text = description) },
332+
modifier = modifier.clip(RoundedCornerShape(8.dp)),
333+
leadingContent = {
334+
Icon(
335+
tint = Color.Unspecified,
336+
contentDescription = null,
337+
modifier = Modifier.size(24.dp),
338+
painter = painterResource(provider.iconResId())
339+
)
340+
}
341+
)
342+
}
343+
344+
@Snapshot
345+
@Preview
346+
@Composable
347+
private fun Preview(@PreviewParameter(ColorSchemePreviewParameterProvider::class) colorScheme: ColorScheme) {
348+
MaterialTheme(colorScheme = colorScheme) {
349+
MediaProviderSelectionScreen(
350+
onRemoveProviderClick = {},
351+
onAddMediaProviderClick = {},
352+
onMediaProviderTypeClick = {},
353+
onProviderOverflowMenuClick = {},
354+
onDismissOverflowMenuRequest = {},
355+
onDismissAddMediaProviderRequest = {},
356+
showAddProviderDialog = false,
357+
showProviderOverflowMenu = null,
358+
unAddedMediaProviders = emptyList(),
359+
mediaProviders = MediaProviderType.entries
360+
)
361+
}
362+
}
363+
364+
@Snapshot
365+
@Preview
366+
@Composable
367+
private fun AddMediaProviderDialog(@PreviewParameter(ColorSchemePreviewParameterProvider::class) colorScheme: ColorScheme) {
368+
MaterialTheme(colorScheme = colorScheme) {
369+
MediaProviderSelectionScreen(
370+
onRemoveProviderClick = {},
371+
onAddMediaProviderClick = {},
372+
onMediaProviderTypeClick = {},
373+
onProviderOverflowMenuClick = {},
374+
onDismissOverflowMenuRequest = {},
375+
onDismissAddMediaProviderRequest = {},
376+
showAddProviderDialog = true,
377+
showProviderOverflowMenu = null,
378+
mediaProviders = listOf(MediaProviderType.MediaStore),
379+
unAddedMediaProviders = MediaProviderType.entries - MediaProviderType.MediaStore
380+
)
381+
}
382+
}

0 commit comments

Comments
 (0)