diff --git a/app/build.gradle b/app/build.gradle index fce5767..0a5db70 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,6 +64,7 @@ dependencies { implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" implementation "com.google.accompanist:accompanist-pager:${Versions.compose_accompanist}" implementation "com.google.accompanist:accompanist-flowlayout:${Versions.compose_accompanist}" +// implementation "com.google.accompanist:accompanist-insets:${Versions.compose_accompanist}" implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}" implementation "androidx.paging:paging-compose:${Versions.compose_paging}" implementation "androidx.constraintlayout:constraintlayout-compose:${Versions.compose_constraint_layout}" @@ -85,11 +86,13 @@ dependencies { implementation "me.onebone:toolbar-compose:2.3.1" - implementation 'com.google.android.exoplayer:exoplayer:2.16.1' - implementation 'com.github.HaarigerHarald:android-youtubeExtractor:v2.1.0' +// implementation 'com.google.android.exoplayer:exoplayer:2.16.1' +// implementation 'com.github.HaarigerHarald:android-youtubeExtractor:v2.1.0' implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:11.0.1' implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:chromecast-sender:0.26' + implementation "org.jetbrains:markdown:0.2.1" + testImplementation "junit:junit:${Versions.junit}" androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}" androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6efa218..3b256a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,8 @@ + android:theme="@style/Theme.TVTime" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index ff8eb09..c5550f2 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -21,6 +21,7 @@ class MainActivity : ComponentActivity() { setContent { AppKeyboardFocusManager() val displayUnderStatusBar = remember { mutableStateOf(false) } +// WindowCompat.setDecorFitsSystemWindows(window, false) // WindowCompat.setDecorFitsSystemWindows(window, !displayUnderStatusBar.value) // val statusBarColor = if (displayUnderStatusBar.value) { // Color.Transparent diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt index 5b546db..7eaff04 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt @@ -15,4 +15,6 @@ interface DetailService { suspend fun getVideos(id: Int): Response + suspend fun getReviews(id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt index d9bd8b4..026ad6b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt @@ -38,4 +38,7 @@ interface MoviesApi { @GET("movie/{id}/videos") suspend fun getVideos(@Path("id") id: Int): Response + @GET("movie/{id}/reviews") + suspend fun getReviews(@Path("id") id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt index 75ce209..7b22a94 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt @@ -48,4 +48,8 @@ class MoviesService: KoinComponent, DetailService, HomePageService { return service.getVideos(id) } + override suspend fun getReviews(id: Int): Response { + return service.getReviews(id) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt index 7e62d07..6b1196a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt @@ -38,4 +38,7 @@ interface TvApi { @GET("tv/{id}/videos") suspend fun getVideos(@Path("id") id: Int): Response + @GET("movie/{id}/reviews") + suspend fun getReviews(@Path("id") id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt index 89e18d4..8a2210e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt @@ -47,4 +47,9 @@ class TvService: KoinComponent, DetailService, HomePageService { override suspend fun getVideos(id: Int): Response { return service.getVideos(id) } + + override suspend fun getReviews(id: Int): Response { + return service.getReviews(id) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AuthorDetails.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AuthorDetails.kt new file mode 100644 index 0000000..694a076 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/AuthorDetails.kt @@ -0,0 +1,10 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class AuthorDetails( + @SerializedName("name") val name: String, + @SerializedName("username") val username: String, + @SerializedName("avatar_path") val avatarPath: String?, + @SerializedName("rating") val rating: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Review.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Review.kt new file mode 100644 index 0000000..a100d99 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Review.kt @@ -0,0 +1,12 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class Review( + @SerializedName("id") val id: String, + @SerializedName("author") val author: String, + @SerializedName("author_details") val authorDetails: AuthorDetails, + @SerializedName("content") val content: String, + @SerializedName("created_at") val createdAt: String, + @SerializedName("updated_at") val updatedAt: String +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/ReviewResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/ReviewResponse.kt new file mode 100644 index 0000000..53bdcc3 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/ReviewResponse.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class ReviewResponse( + @SerializedName("page") val page: Int, + @SerializedName("results") val results: List +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt index c05b664..9483cc3 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt @@ -5,6 +5,9 @@ import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material3.MaterialTheme @@ -92,6 +95,40 @@ fun ExpandableContentCard( } } +@Composable +fun LazyListContentCard( + modifier: Modifier = Modifier, + header: @Composable (() -> Unit)? = null, + footer: @Composable (() -> Unit)? = null, + backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant, + content: LazyListScope.() -> Unit +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(10.dp), + backgroundColor = backgroundColor, + elevation = 8.dp + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + header?.invoke() + val listState = rememberLazyListState() + LazyColumn( + content = content, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + state = listState + ) + footer?.invoke() + } + } +} + @Composable fun TwoLineImageTextCard( title: String, diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt index 3ba4fae..0e615d8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt @@ -5,10 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape @@ -37,7 +34,7 @@ import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.utils.TmdbUtils -private val POSTER_WIDTH = 127.dp +private val POSTER_WIDTH = 120.dp private val POSTER_HEIGHT = 190.dp @OptIn(ExperimentalFoundationApi::class) @@ -51,7 +48,8 @@ fun PosterGrid( LazyVerticalGrid( cells = GridCells.Adaptive(minSize = POSTER_WIDTH), - contentPadding = PaddingValues(8.dp) + contentPadding = PaddingValues(8.dp), + horizontalArrangement = Arrangement.SpaceBetween ) { listItems(mediaList.value) { item -> PosterItem( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 3cb2301..05fdaeb 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -1,15 +1,16 @@ package com.owenlejeune.tvtime.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.widget.TextView import android.widget.Toast import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -22,16 +23,22 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -48,12 +55,21 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.core.text.HtmlCompat import coil.compose.rememberImagePainter +import coil.transform.CircleCropTransformation import com.google.accompanist.flowlayout.FlowRow import com.owenlejeune.tvtime.R +import com.owenlejeune.tvtime.api.tmdb.model.AuthorDetails +import com.owenlejeune.tvtime.utils.TmdbUtils import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.html.HtmlGenerator +import org.intellij.markdown.parser.MarkdownParser @Composable fun TopLevelSwitch( @@ -334,6 +350,7 @@ fun RatingRing( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun RoundedTextField( value: String, @@ -352,31 +369,49 @@ fun RoundedTextField( enabled: Boolean = true, readOnly: Boolean = false, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default + keyboardActions: KeyboardActions = KeyboardActions.Default, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, ) { Surface( modifier = modifier, shape = RoundedCornerShape(50.dp), color = backgroundColor ) { - Box( - modifier = Modifier.padding(horizontal = 12.dp), - contentAlignment = Alignment.CenterStart + Row(Modifier.padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - if (value.isEmpty() && placeHolder.isNotEmpty()) { - Text( - text = placeHolder, - style = textStyle, - color = placeHolderTextColor - ) + if (leadingIcon != null) { + leadingIcon() } - Row( - verticalAlignment = Alignment.CenterVertically + Box(modifier = Modifier + .fillMaxHeight() + .weight(1f), + contentAlignment = Alignment.CenterStart ) { + if (value.isEmpty() && placeHolder.isNotEmpty()) { + Text( + text = placeHolder, + style = textStyle, + color = placeHolderTextColor + ) + } + val bringIntoViewRequester = remember { BringIntoViewRequester() } + val coroutineScope = rememberCoroutineScope() BasicTextField( modifier = Modifier - .weight(1f) - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .fillMaxWidth() + .bringIntoViewRequester(bringIntoViewRequester) + .onFocusEvent { + if (it.isFocused) { + coroutineScope.launch { + delay(200) + bringIntoViewRequester.bringIntoView() + } + } + }, value = value, onValueChange = onValueChange, singleLine = singleLine, @@ -386,9 +421,12 @@ fun RoundedTextField( enabled = enabled, readOnly = readOnly, keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions + keyboardActions = keyboardActions, ) } + if (trailingIcon != null) { + trailingIcon() + } } } @@ -399,6 +437,28 @@ fun RoundedTextField( } } +@Preview(widthDp = 300, heightDp = 40) +@Composable +private fun RoundedEditTextPreview() { + RoundedTextField( + value = "this is my value", + onValueChange = {}, + placeHolder = "this is my placeholder", + trailingIcon = { + Image( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = "" + ) + }, + leadingIcon = { + Image( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = "" + ) + } + ) +} + @OptIn(ExperimentalComposeUiApi::class) @Composable fun FullScreenThumbnailVideoPlayer( @@ -450,4 +510,90 @@ fun FullScreenThumbnailVideoPlayer( } } } +} + +@Composable +fun HtmlText(text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, parseMarkdownFirst: Boolean = true) { + val htmlString = if (parseMarkdownFirst) { + val flavour = CommonMarkFlavourDescriptor() + val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(text) + HtmlGenerator(text, parsedTree, flavour).generateHtml() + } else { + text + } + AndroidView( + modifier = modifier, + factory = { context -> TextView(context) }, + update = { textView -> + textView.textSize = 14f + textView.text = HtmlCompat.fromHtml(htmlString, HtmlCompat.FROM_HTML_MODE_COMPACT) + textView.setTextColor(color.toArgb()) + } + ) +} + +@Composable +fun CircleBackgroundColorImage( + size: Dp, + backgroundColor: Color, + image: ImageVector, + modifier: Modifier = Modifier, + imageHeight: Dp? = null, + imageAlignment: Alignment = Alignment.Center, + contentDescription: String? = null, + colorFilter: ColorFilter? = null +) { + Box( + modifier = modifier + .clip(CircleShape) + .size(size) + .background(color = backgroundColor) + ) { + val mod = if (imageHeight != null) { + Modifier.align(imageAlignment).height(height = imageHeight) + } else { + Modifier.align(imageAlignment) + } + Image( + imageVector = image, + contentDescription = contentDescription, + modifier = mod, + colorFilter = colorFilter + ) + } +} + +@Composable +fun AvatarImage( + size: Dp, + author: AuthorDetails, + modifier: Modifier = Modifier +) { + if (author.avatarPath != null) { + Image( + modifier = modifier.size(size), + painter = rememberImagePainter( + data = TmdbUtils.getFullAvatarPath(author), + builder = { + transformations(CircleCropTransformation()) + } + ), + contentDescription = "" + ) + } else { + Box( + modifier = Modifier + .clip(CircleShape) + .size(size) + .background(color = MaterialTheme.colorScheme.tertiary) + ) { + Text( + modifier = Modifier.fillMaxSize().padding(top = size/5), + text = author.name[0].uppercase(), + color = MaterialTheme.colorScheme.onTertiary, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt index 697d3f3..c48a2dd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt @@ -5,21 +5,21 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Send +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.navigation.NavController import com.owenlejeune.tvtime.R @@ -381,6 +381,8 @@ private fun ContentColumn( SimilarContentCard(itemId = itemId, service = service, mediaType = mediaType, appNavController = appNavController) VideosCard(itemId = itemId, service = service) + + ReviewsCard(itemId = itemId, service = service) } } @@ -640,6 +642,152 @@ private fun VideoGroup(results: List