add reviews

This commit is contained in:
Owen LeJeune
2022-02-26 20:43:19 -05:00
parent 4f3f756d6b
commit 86bfa78590
20 changed files with 480 additions and 55 deletions

View File

@@ -64,6 +64,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}"
implementation "com.google.accompanist:accompanist-pager:${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-flowlayout:${Versions.compose_accompanist}"
// implementation "com.google.accompanist:accompanist-insets:${Versions.compose_accompanist}"
implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}" implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}"
implementation "androidx.paging:paging-compose:${Versions.compose_paging}" implementation "androidx.paging:paging-compose:${Versions.compose_paging}"
implementation "androidx.constraintlayout:constraintlayout-compose:${Versions.compose_constraint_layout}" implementation "androidx.constraintlayout:constraintlayout-compose:${Versions.compose_constraint_layout}"
@@ -85,11 +86,13 @@ dependencies {
implementation "me.onebone:toolbar-compose:2.3.1" implementation "me.onebone:toolbar-compose:2.3.1"
implementation 'com.google.android.exoplayer:exoplayer:2.16.1' // implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:v2.1.0' // implementation 'com.github.HaarigerHarald:android-youtubeExtractor:v2.1.0'
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:11.0.1' implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:11.0.1'
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:chromecast-sender:0.26' implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:chromecast-sender:0.26'
implementation "org.jetbrains:markdown:0.2.1"
testImplementation "junit:junit:${Versions.junit}" testImplementation "junit:junit:${Versions.junit}"
androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}" androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}"
androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}" androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}"

View File

@@ -16,7 +16,8 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.TVTime"> android:theme="@style/Theme.TVTime"
android:windowSoftInputMode="adjustResize">
<!-- android:screenOrientation="portrait">--> <!-- android:screenOrientation="portrait">-->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -21,6 +21,7 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
AppKeyboardFocusManager() AppKeyboardFocusManager()
val displayUnderStatusBar = remember { mutableStateOf(false) } val displayUnderStatusBar = remember { mutableStateOf(false) }
// WindowCompat.setDecorFitsSystemWindows(window, false)
// WindowCompat.setDecorFitsSystemWindows(window, !displayUnderStatusBar.value) // WindowCompat.setDecorFitsSystemWindows(window, !displayUnderStatusBar.value)
// val statusBarColor = if (displayUnderStatusBar.value) { // val statusBarColor = if (displayUnderStatusBar.value) {
// Color.Transparent // Color.Transparent

View File

@@ -15,4 +15,6 @@ interface DetailService {
suspend fun getVideos(id: Int): Response<VideoResponse> suspend fun getVideos(id: Int): Response<VideoResponse>
suspend fun getReviews(id: Int): Response<ReviewResponse>
} }

View File

@@ -38,4 +38,7 @@ interface MoviesApi {
@GET("movie/{id}/videos") @GET("movie/{id}/videos")
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse> suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
@GET("movie/{id}/reviews")
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
} }

View File

@@ -48,4 +48,8 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
return service.getVideos(id) return service.getVideos(id)
} }
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
return service.getReviews(id)
}
} }

View File

@@ -38,4 +38,7 @@ interface TvApi {
@GET("tv/{id}/videos") @GET("tv/{id}/videos")
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse> suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
@GET("movie/{id}/reviews")
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
} }

View File

@@ -47,4 +47,9 @@ class TvService: KoinComponent, DetailService, HomePageService {
override suspend fun getVideos(id: Int): Response<VideoResponse> { override suspend fun getVideos(id: Int): Response<VideoResponse> {
return service.getVideos(id) return service.getVideos(id)
} }
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
return service.getReviews(id)
}
} }

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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<Review>
)

View File

@@ -5,6 +5,9 @@ import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material3.MaterialTheme 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 @Composable
fun TwoLineImageTextCard( fun TwoLineImageTextCard(
title: String, title: String,

View File

@@ -5,10 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape 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.extensions.listItems
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
private val POSTER_WIDTH = 127.dp private val POSTER_WIDTH = 120.dp
private val POSTER_HEIGHT = 190.dp private val POSTER_HEIGHT = 190.dp
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -51,7 +48,8 @@ fun PosterGrid(
LazyVerticalGrid( LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = POSTER_WIDTH), cells = GridCells.Adaptive(minSize = POSTER_WIDTH),
contentPadding = PaddingValues(8.dp) contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
listItems(mediaList.value) { item -> listItems(mediaList.value) { item ->
PosterItem( PosterItem(

View File

@@ -1,15 +1,16 @@
package com.owenlejeune.tvtime.ui.components package com.owenlejeune.tvtime.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas import androidx.compose.foundation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* 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.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -22,16 +23,22 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor 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.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily 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.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.text.HtmlCompat
import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter
import coil.transform.CircleCropTransformation
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.owenlejeune.tvtime.R 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.YouTubePlayer
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView 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 @Composable
fun TopLevelSwitch( fun TopLevelSwitch(
@@ -334,6 +350,7 @@ fun RatingRing(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun RoundedTextField( fun RoundedTextField(
value: String, value: String,
@@ -352,15 +369,25 @@ fun RoundedTextField(
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default keyboardActions: KeyboardActions = KeyboardActions.Default,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
) { ) {
Surface( Surface(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(50.dp), shape = RoundedCornerShape(50.dp),
color = backgroundColor color = backgroundColor
) { ) {
Box( Row(Modifier.padding(horizontal = 12.dp),
modifier = Modifier.padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (leadingIcon != null) {
leadingIcon()
}
Box(modifier = Modifier
.fillMaxHeight()
.weight(1f),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
if (value.isEmpty() && placeHolder.isNotEmpty()) { if (value.isEmpty() && placeHolder.isNotEmpty()) {
@@ -370,13 +397,21 @@ fun RoundedTextField(
color = placeHolderTextColor color = placeHolderTextColor
) )
} }
Row( val bringIntoViewRequester = remember { BringIntoViewRequester() }
verticalAlignment = Alignment.CenterVertically val coroutineScope = rememberCoroutineScope()
) {
BasicTextField( BasicTextField(
modifier = Modifier modifier = Modifier
.weight(1f) .focusRequester(focusRequester)
.focusRequester(focusRequester), .fillMaxWidth()
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
delay(200)
bringIntoViewRequester.bringIntoView()
}
}
},
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
singleLine = singleLine, singleLine = singleLine,
@@ -386,9 +421,12 @@ fun RoundedTextField(
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
keyboardOptions = keyboardOptions, 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) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun FullScreenThumbnailVideoPlayer( fun FullScreenThumbnailVideoPlayer(
@@ -451,3 +511,89 @@ 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
)
}
}
}

View File

@@ -5,21 +5,21 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.IconButton import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Text import androidx.compose.runtime.*
import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import androidx.navigation.NavController import androidx.navigation.NavController
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
@@ -381,6 +381,8 @@ private fun ContentColumn(
SimilarContentCard(itemId = itemId, service = service, mediaType = mediaType, appNavController = appNavController) SimilarContentCard(itemId = itemId, service = service, mediaType = mediaType, appNavController = appNavController)
VideosCard(itemId = itemId, service = service) VideosCard(itemId = itemId, service = service)
ReviewsCard(itemId = itemId, service = service)
} }
} }
@@ -640,6 +642,152 @@ private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ReviewsCard(
itemId: Int?,
service: DetailService,
modifier: Modifier = Modifier
) {
val reviewsResponse = remember { mutableStateOf<ReviewResponse?>(null) }
itemId?.let {
if (reviewsResponse.value == null) {
fetchReviews(itemId, service, reviewsResponse)
}
}
// > 0
val hasReviews = reviewsResponse.value?.results?.size?.let { it > 0 }
val m = if (hasReviews == true) {
modifier.height(400.dp)
} else {
modifier.height(200.dp)
}
LazyListContentCard(
modifier = m
.fillMaxWidth(),
header = {
Text(
text = "Reviews",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
footer = {
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
var reviewTextState by remember { mutableStateOf("") }
RoundedTextField(
modifier = Modifier
.height(50.dp)
.padding(top = 4.dp)
.weight(1f),
value = reviewTextState,
onValueChange = { reviewTextState = it },
placeHolder = "Add a review",
backgroundColor = MaterialTheme.colorScheme.secondary,
placeHolderTextColor = MaterialTheme.colorScheme.background,
textColor = MaterialTheme.colorScheme.onSecondary
)
CircleBackgroundColorImage(
size = 40.dp,
backgroundColor = MaterialTheme.colorScheme.tertiary,
image = Icons.Filled.Send,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.surfaceVariant),
contentDescription = ""
)
}
}
) {
val reviews = reviewsResponse.value?.results ?: emptyList()
if (reviews.isNotEmpty()) {
items(reviews.size) { i ->
val review = reviews[i]
Row(
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AvatarImage(
size = 50.dp,
author = review.authorDetails
)
// todo - only show this for user's review
CircleBackgroundColorImage(
image = Icons.Filled.Delete,
size = 30.dp,
backgroundColor = MaterialTheme.colorScheme.error,
contentDescription = "",
imageHeight = 15.dp,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surfaceVariant)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = review.author,
color = MaterialTheme.colorScheme.secondary,
fontWeight = FontWeight.Bold
)
HtmlText(
text = review.content,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val createdAt = TmdbUtils.formatDate(review.createdAt)
val updatedAt = TmdbUtils.formatDate(review.updatedAt)
var timestamp = stringResource(id = R.string.created_at_label, createdAt)
if (updatedAt != createdAt) {
timestamp += "\n${stringResource(id = R.string.updated_at_label, updatedAt)}"
}
Text(
text = timestamp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
)
}
}
Divider(
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 50.dp, vertical = 6.dp)
)
}
} else {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 24.dp, vertical = 22.dp),
text = stringResource(R.string.no_reviews_label),
color = MaterialTheme.colorScheme.tertiary,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium
)
}
}
}
}
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) { private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id) val response = service.getById(id)
@@ -740,3 +888,14 @@ private fun fetchCredits(id: Int, credits: MutableState<PersonCreditsResponse?>)
} }
} }
} }
private fun fetchReviews(id: Int, service: DetailService, reviewResponse: MutableState<ReviewResponse?>) {
CoroutineScope(Dispatchers.IO).launch {
val result = service.getReviews(id)
if (result.isSuccessful) {
withContext(Dispatchers.Main) {
reviewResponse.value = result.body()
}
}
}
}

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -139,7 +141,14 @@ private fun SearchTopBar(
focusRequester = focusRequester, focusRequester = focusRequester,
value = textState, value = textState,
onValueChange = { textState = it }, onValueChange = { textState = it },
placeHolder = stringResource(id = R.string.search_placeholder, title.value) placeHolder = stringResource(id = R.string.search_placeholder, title.value),
trailingIcon = {
Image(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = stringResource(R.string.search_icon_content_descriptor),
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary)
)
}
) )
} }
} }
@@ -164,7 +173,9 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
NavigationBar { NavigationBar(
containerColor = MaterialTheme.colorScheme.primaryContainer
) {
BottomNavItem.Items.forEach { item -> BottomNavItem.Items.forEach { item ->
NavigationBarItem( NavigationBarItem(
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) }, icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
@@ -179,8 +190,8 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
}, },
colors = NavigationBarItemDefaults colors = NavigationBarItemDefaults
.colors( .colors(
selectedIconColor = MaterialTheme.colorScheme.onPrimary, selectedIconColor = MaterialTheme.colorScheme.secondary,
indicatorColor = MaterialTheme.colorScheme.primary indicatorColor = MaterialTheme.colorScheme.onSecondary
) )
) )
} }

View File

@@ -1,12 +1,8 @@
package com.owenlejeune.tvtime.utils package com.owenlejeune.tvtime.utils
import android.content.Context
import android.util.SparseArray
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import at.huber.youtubeExtractor.VideoMeta
import at.huber.youtubeExtractor.YouTubeExtractor
import at.huber.youtubeExtractor.YtFile
import com.owenlejeune.tvtime.api.tmdb.model.* import com.owenlejeune.tvtime.api.tmdb.model.*
import java.text.SimpleDateFormat
object TmdbUtils { object TmdbUtils {
@@ -48,6 +44,19 @@ object TmdbUtils {
return getFullPersonImagePath(person.profilePath) return getFullPersonImagePath(person.profilePath)
} }
fun getFullAvatarPath(path: String?): String? {
return path?.let {
if (path.contains("http")) {
return path.substring(startIndex = 1)
}
"https://www.themoviedb.org/t/p/w150_and_h150_face${path}"
}
}
fun getFullAvatarPath(author: AuthorDetails): String? {
return getFullAvatarPath(author.avatarPath)
}
fun getMovieReleaseYear(movie: DetailedMovie): String { fun getMovieReleaseYear(movie: DetailedMovie): String {
return movie.releaseDate.split("-")[0] return movie.releaseDate.split("-")[0]
} }
@@ -139,19 +148,18 @@ object TmdbUtils {
} }
fun getFullVideoUrl(video: Video): String { fun getFullVideoUrl(video: Video): String {
// object: YouTubeExtractor(context) {
// override fun onExtractionComplete(
// ytFiles: SparseArray<YtFile>?,
// videoMeta: VideoMeta?
// ) {
// if (ytFiles != null) {
// }
// }
// }
if (video.site == "YouTube") { if (video.site == "YouTube") {
return "http://www.youtube.com/watch?v=${video.key}" return "http://www.youtube.com/watch?v=${video.key}"
} }
return "" return ""
} }
fun formatDate(inDate: String): String {
val orig = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US)
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.US)
val date = orig.parse(inDate)//.replace("Z", "+0000"))
return formatter.format(date)
}
} }

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View File

@@ -25,6 +25,10 @@
<string name="search_placeholder">Search %1$s</string> <string name="search_placeholder">Search %1$s</string>
<string name="created_at_label">Created at: %1$s</string>
<string name="updated_at_label">Updated at: %1$s</string>
<string name="no_reviews_label">No reviews</string>
<!-- preferences --> <!-- preferences -->
<string name="preference_heading_search">Search</string> <string name="preference_heading_search">Search</string>
<string name="preferences_persistent_search_title">Persistent search bar</string> <string name="preferences_persistent_search_title">Persistent search bar</string>
@@ -40,4 +44,5 @@
<string name="video_type_behind_the_scenes">Behind the Scenes</string> <string name="video_type_behind_the_scenes">Behind the Scenes</string>
<string name="video_type_featureette">Featurettes</string> <string name="video_type_featureette">Featurettes</string>
<string name="content_description_back_button">Back</string> <string name="content_description_back_button">Back</string>
<string name="search_icon_content_descriptor">Search Icon</string>
</resources> </resources>

View File

@@ -2,6 +2,5 @@
<resources> <resources>
<style name="Theme.TVTime" parent="android:Theme.Material.Light.NoActionBar"> <style name="Theme.TVTime" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
</style> </style>
</resources> </resources>