Browse Source

Compose TV: add a new screen for video groups and folders

merge-requests/2332/head
Nicolas Pomepuy 8 months ago
parent
commit
f5e6c4a4ac
  1. 5
      application/television/src/main/AndroidManifest.xml
  2. 101
      application/television/src/main/java/org/videolan/television/ui/VideoGroupFolderActivity.kt
  3. 9
      application/television/src/main/java/org/videolan/television/ui/compose/composable/components/DisplaySettings.kt
  4. 4
      application/television/src/main/java/org/videolan/television/ui/compose/composable/components/MediaListSidePanel.kt
  5. 2
      application/television/src/main/java/org/videolan/television/ui/compose/composable/lists/AudioList.kt
  6. 13
      application/television/src/main/java/org/videolan/television/ui/compose/composable/lists/VideoList.kt
  7. 5
      application/television/src/main/java/org/videolan/television/ui/compose/composable/screens/MainScreen.kt
  8. 115
      application/television/src/main/java/org/videolan/television/ui/compose/composable/screens/VideoGroupScreen.kt

5
application/television/src/main/AndroidManifest.xml

@ -8,6 +8,11 @@
android:exported="false"
android:label="@string/app_name"
android:theme="@style/Theme.VLC" />
<activity
android:name=".ui.VideoGroupFolderActivity"
android:exported="false"
android:label="@string/app_name"
android:theme="@style/Theme.VLC" />
<activity
android:name=".ui.MainTvActivity"
android:label="@string/app_name"

101
application/television/src/main/java/org/videolan/television/ui/VideoGroupFolderActivity.kt

@ -0,0 +1,101 @@
/*
* ************************************************************************
* VideoGroupFolderActivity.kt
* *************************************************************************
* Copyright © 2025 VLC authors and VideoLAN
* Author: Nicolas POMEPUY
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* **************************************************************************
*
*
*/
package org.videolan.television.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.os.bundleOf
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.videolan.medialibrary.interfaces.media.Folder
import org.videolan.medialibrary.interfaces.media.VideoGroup
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.resources.KEY_FOLDER
import org.videolan.resources.KEY_GROUP
import org.videolan.resources.util.parcelable
import org.videolan.television.ui.compose.composable.screens.VideoGroupScreen
import org.videolan.television.ui.compose.theme.VlcTVTheme
import org.videolan.tools.KEY_SHOW_UPDATE
import org.videolan.tools.Settings
import org.videolan.vlc.gui.SecondaryActivity
import org.videolan.vlc.gui.dialogs.UPDATE_DATE
import org.videolan.vlc.gui.dialogs.UPDATE_URL
import org.videolan.vlc.gui.dialogs.UpdateDialog
import org.videolan.vlc.util.AutoUpdate
class VideoGroupFolderActivity : DefaultTvActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = false
}
val folder = intent?.parcelable<Folder>(KEY_FOLDER)
val parentGroup = intent?.parcelable<VideoGroup>(KEY_GROUP)
if (folder == null && parentGroup == null) throw IllegalStateException("No folder or group provided")
setContent {
VlcTVTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize()) {
if (folder != null)
VideoGroupScreen(folder = folder)
else
VideoGroupScreen(group = parentGroup)
}
}
}
lifecycleScope.launch {
AutoUpdate.clean(this@VideoGroupFolderActivity.application)
if (!Settings.getInstance(this@VideoGroupFolderActivity).getBoolean(KEY_SHOW_UPDATE, true)) return@launch
AutoUpdate.checkUpdate(this@VideoGroupFolderActivity.application) { url, date ->
val updateDialog = UpdateDialog().apply {
arguments = bundleOf(UPDATE_URL to url, UPDATE_DATE to date.time)
}
updateDialog.show(supportFragmentManager, "fragment_update")
}
}
}
}
fun Activity.openVideoGroupFolder(item: MediaLibraryItem) {
val i = Intent(this, VideoGroupFolderActivity::class.java)
if (item is Folder) i.putExtra(KEY_FOLDER, item)
else if (item is VideoGroup) i.putExtra(KEY_GROUP, item)
startActivityForResult(i, SecondaryActivity.ACTIVITY_RESULT_SECONDARY)
}

9
application/television/src/main/java/org/videolan/television/ui/compose/composable/components/DisplaySettings.kt

@ -51,7 +51,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@ -64,7 +63,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
@ -88,7 +86,7 @@ import org.videolan.vlc.viewmodels.mobile.VideoGroupingType
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DisplaySettings(viewModel: MainActivityViewModel = viewModel()) {
fun DisplaySettings(viewModel: MainActivityViewModel = viewModel(), inGrouping: Boolean = false) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
@ -165,7 +163,7 @@ fun DisplaySettings(viewModel: MainActivityViewModel = viewModel()) {
Checkbox(checked = onlyFavs, onCheckedChange = { onFavClicked() })
}, onClick = { onFavClicked() })
VideoGroupingItem(current!!) { newEntry, videoGroupingType ->
VideoGroupingItem(current!!, inGrouping) { newEntry, videoGroupingType ->
coroutineScope.launch {
DisplaySettingsEventManager.onGroupingChanged(newEntry)
}
@ -343,10 +341,11 @@ private fun PlaybackActionsItem(current: MediaListEntry, context: Context) {
@Composable
private fun VideoGroupingItem(
current: MediaListEntry,
inGrouping: Boolean,
onClick: (MediaListEntry, VideoGroupingType) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
if (current in arrayOf(MediaListEntry.VIDEO, MediaListEntry.VIDEO_GROUPS, MediaListEntry.VIDEO_FOLDER)) {
if (current in arrayOf(MediaListEntry.VIDEO, MediaListEntry.VIDEO_GROUPS, MediaListEntry.VIDEO_FOLDER) && !inGrouping) {
DisplaySettingsLine(painterResource(R.drawable.ic_group_display), stringResource(R.string.video_min_group_length_title), endView = {
Box(
modifier = Modifier

4
application/television/src/main/java/org/videolan/television/ui/compose/composable/components/MediaListSidePanel.kt

@ -52,11 +52,11 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import org.videolan.television.R
import org.videolan.vlc.util.MediaListEntry
import org.videolan.television.viewmodel.MainActivityViewModel
import org.videolan.vlc.util.MediaListEntry
@Composable
fun MediaListSidePanel(content: MediaListSidePanelContent, viewModel: MainActivityViewModel = viewModel(), listener: (MediaListSidePanelListenerKey, Any) -> Unit = { _, _ -> }) {
fun MediaListSidePanel(content: MediaListSidePanelContent, inGrouping: Boolean, viewModel: MainActivityViewModel = viewModel(), listener: (MediaListSidePanelListenerKey, Any) -> Unit = { _, _ -> }) {
val coroutineScope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
Column(

2
application/television/src/main/java/org/videolan/television/ui/compose/composable/lists/AudioList.kt

@ -303,7 +303,7 @@ fun MediaList(entry: MediaListEntry, index: Int, mainActivityViewModel: MainActi
showResumePlayback = entry != MediaListEntry.ALL_PLAYLISTS,
if (inCard) gridState else listState,
entry
)
), inGrouping = false
) { first, second ->
when (first) {
MediaListSidePanelListenerKey.DISPLAY_MODE -> {

13
application/television/src/main/java/org/videolan/television/ui/compose/composable/lists/VideoList.kt

@ -86,6 +86,7 @@ import org.videolan.television.ui.compose.composable.items.VideoItem
import org.videolan.television.ui.compose.composable.items.VideoItemList
import org.videolan.television.ui.compose.theme.White
import org.videolan.television.ui.compose.theme.WhiteTransparent50
import org.videolan.television.ui.openVideoGroupFolder
import org.videolan.television.viewmodel.MainActivityViewModel
import org.videolan.television.viewmodel.SnackbarContent
import org.videolan.tools.KEY_CASTING_AUDIO_ONLY
@ -177,7 +178,7 @@ fun VideoListScreen(onFocusExit: () -> Unit, onFocusEnter: () -> Unit, mainActiv
@Composable
fun VideoList(mainActivityViewModel: MainActivityViewModel = viewModel()) {
fun VideoList(modifier: Modifier = Modifier, folder: Folder? = null, group: VideoGroup? = null, mainActivityViewModel: MainActivityViewModel = viewModel()) {
val listState = rememberLazyListState()
val gridState = rememberLazyGridState()
val displaySettingsChange by mainActivityViewModel.currentDisplaySettingsChange.collectAsState()
@ -185,8 +186,8 @@ fun VideoList(mainActivityViewModel: MainActivityViewModel = viewModel()) {
val context = LocalContext.current
val extras = MutableCreationExtras().apply {
set(VideosViewModel.PARENT_GROUP_KEY, null)
set(VideosViewModel.PARENT_FOLDER_KEY, null)
set(VideosViewModel.PARENT_GROUP_KEY, group)
set(VideosViewModel.PARENT_FOLDER_KEY, folder)
set(APPLICATION_KEY, context.applicationContext as Application)
}
val viewModel: VideosViewModel = viewModel(
@ -213,7 +214,7 @@ fun VideoList(mainActivityViewModel: MainActivityViewModel = viewModel()) {
val activity = LocalActivity.current
val onClick:(MediaLibraryItem, Int) -> Unit = { video, position ->
if (video is Folder || video is VideoGroup) {
mainActivityViewModel.showSnackbar(SnackbarContent(activity!!.resources.getString(R.string.not_implemented)))
activity!!.openVideoGroupFolder(video)
} else {
if (video !is MediaWrapper) throw IllegalStateException("Wrong video type")
if (activity !is AppCompatActivity) throw IllegalStateException("Wrong activity type")
@ -242,7 +243,7 @@ fun VideoList(mainActivityViewModel: MainActivityViewModel = viewModel()) {
} else
TvUtil.showMediaDetail(activity!!, video as MediaWrapper, false)
}
Row {
Row(modifier = modifier) {
if (inCard) {
PaginatedGrid(
items = videos,
@ -276,7 +277,7 @@ fun VideoList(mainActivityViewModel: MainActivityViewModel = viewModel()) {
showResumePlayback = true,
if (inCard) gridState else listState,
entry
)
), group != null || folder!= null
) { first, second ->
when (first) {
MediaListSidePanelListenerKey.DISPLAY_MODE -> {

5
application/television/src/main/java/org/videolan/television/ui/compose/composable/screens/MainScreen.kt

@ -223,7 +223,10 @@ fun Tabs(modifier: Modifier = Modifier, viewModel: MainActivityViewModel = viewM
onSelected = { index ->
if (index == pagerState.currentPage) return@VLCTabRow
coroutineScope.launch {
pagerState.animateScrollToPage(index)
try {
pagerState.animateScrollToPage(index)
} catch (e: Exception) {
}
}
settings.edit { putInt(KEY_MAIN_TAB, index) }
},

115
application/television/src/main/java/org/videolan/television/ui/compose/composable/screens/VideoGroupScreen.kt

@ -0,0 +1,115 @@
/*
* ************************************************************************
* VideoGroupScreen.kt
* *************************************************************************
* Copyright © 2025 VLC authors and VideoLAN
* Author: Nicolas POMEPUY
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* **************************************************************************
*
*
*/
package org.videolan.television.ui.compose.composable.screens
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import org.videolan.medialibrary.interfaces.media.Folder
import org.videolan.medialibrary.interfaces.media.VideoGroup
import org.videolan.television.R
import org.videolan.television.ui.compose.composable.components.AudioPlayer
import org.videolan.television.ui.compose.composable.components.DisplaySettings
import org.videolan.television.ui.compose.composable.components.LabeledIconButton
import org.videolan.television.ui.compose.composable.lists.VideoList
import org.videolan.television.viewmodel.MainActivityViewModel
@Composable
fun VideoGroupScreen(folder: Folder? = null, group : VideoGroup? = null, viewModel: MainActivityViewModel = viewModel()) {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val snackbarContent by viewModel.snackBarFlow.collectAsState()
LaunchedEffect(snackbarContent) {
snackbarContent?.let { snackbarContent ->
scope.launch {
snackbarHostState.showSnackbar(snackbarContent.message, duration = snackbarContent.duration)
viewModel.showSnackbar(null)
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
) { contentPadding ->
Box {
VideoGroupScreenContent(Modifier.padding(contentPadding), folder, group)
DisplaySettings(inGrouping = true)
}
}
}
@Composable
fun VideoGroupScreenContent(modifier: Modifier, folder: Folder? = null, group : VideoGroup? = null) {
Column(modifier
.fillMaxHeight()
.padding(
top = 16.dp,
start = 24.dp,
end = 24.dp
)) {
Row(verticalAlignment = Alignment.CenterVertically) {
val activity = LocalActivity.current
LabeledIconButton(
label = stringResource(R.string.close),
vectorImage = Icons.AutoMirrored.Outlined.ArrowBack,
modifier = Modifier
// .focusRequester(focusRequester = focusRequester),
) {
activity?.finish()
}
Text(text = if (folder != null) folder.title else group!!.title)
}
Row(modifier) {
if (folder != null)
VideoList(modifier, folder= folder)
else
VideoList(modifier, group = group)
AudioPlayer()
Tabs(modifier = Modifier.weight(1f))
}
}
}
Loading…
Cancel
Save