mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-11 14:22:55 -05:00
add reviews
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.TVTime">
|
||||
android:theme="@style/Theme.TVTime"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- android:screenOrientation="portrait">-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,4 +15,6 @@ interface DetailService {
|
||||
|
||||
suspend fun getVideos(id: Int): Response<VideoResponse>
|
||||
|
||||
suspend fun getReviews(id: Int): Response<ReviewResponse>
|
||||
|
||||
}
|
||||
@@ -38,4 +38,7 @@ interface MoviesApi {
|
||||
@GET("movie/{id}/videos")
|
||||
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
||||
|
||||
@GET("movie/{id}/reviews")
|
||||
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
||||
|
||||
}
|
||||
@@ -48,4 +48,8 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
return service.getVideos(id)
|
||||
}
|
||||
|
||||
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
|
||||
return service.getReviews(id)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,4 +38,7 @@ interface TvApi {
|
||||
@GET("tv/{id}/videos")
|
||||
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
||||
|
||||
@GET("movie/{id}/reviews")
|
||||
suspend fun getReviews(@Path("id") id: Int): Response<ReviewResponse>
|
||||
|
||||
}
|
||||
@@ -47,4 +47,9 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
override suspend fun getVideos(id: Int): Response<VideoResponse> {
|
||||
return service.getVideos(id)
|
||||
}
|
||||
|
||||
override suspend fun getReviews(id: Int): Response<ReviewResponse> {
|
||||
return service.getReviews(id)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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?>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.getById(id)
|
||||
@@ -739,4 +887,15 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.owenlejeune.tvtime.ui.screens
|
||||
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -139,7 +141,14 @@ private fun SearchTopBar(
|
||||
focusRequester = focusRequester,
|
||||
value = textState,
|
||||
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 currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
NavigationBar {
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
BottomNavItem.Items.forEach { item ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(painter = painterResource(id = item.icon), contentDescription = null) },
|
||||
@@ -179,8 +190,8 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
|
||||
},
|
||||
colors = NavigationBarItemDefaults
|
||||
.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
indicatorColor = MaterialTheme.colorScheme.primary
|
||||
selectedIconColor = MaterialTheme.colorScheme.secondary,
|
||||
indicatorColor = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.owenlejeune.tvtime.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
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 java.text.SimpleDateFormat
|
||||
|
||||
object TmdbUtils {
|
||||
|
||||
@@ -48,6 +44,19 @@ object TmdbUtils {
|
||||
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 {
|
||||
return movie.releaseDate.split("-")[0]
|
||||
}
|
||||
@@ -139,19 +148,18 @@ object TmdbUtils {
|
||||
}
|
||||
|
||||
fun getFullVideoUrl(video: Video): String {
|
||||
// object: YouTubeExtractor(context) {
|
||||
// override fun onExtractionComplete(
|
||||
// ytFiles: SparseArray<YtFile>?,
|
||||
// videoMeta: VideoMeta?
|
||||
// ) {
|
||||
// if (ytFiles != null) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (video.site == "YouTube") {
|
||||
return "http://www.youtube.com/watch?v=${video.key}"
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
10
app/src/main/res/drawable/ic_search.xml
Normal file
10
app/src/main/res/drawable/ic_search.xml
Normal 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>
|
||||
@@ -25,6 +25,10 @@
|
||||
|
||||
<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 -->
|
||||
<string name="preference_heading_search">Search</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_featureette">Featurettes</string>
|
||||
<string name="content_description_back_button">Back</string>
|
||||
<string name="search_icon_content_descriptor">Search Icon</string>
|
||||
</resources>
|
||||
@@ -2,6 +2,5 @@
|
||||
<resources>
|
||||
|
||||
<style name="Theme.TVTime" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/purple_700</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user