feat: reviews

This commit is contained in:
rebelonion 2024-05-12 03:37:41 -05:00
parent 831b99ae40
commit a0fabd3ca6
16 changed files with 642 additions and 34 deletions

View file

@ -198,6 +198,12 @@
<activity <activity
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity
android:name=".media.ReviewActivity"
android:parentActivityName=".media.MediaDetailsActivity" />
<activity
android:name=".media.ReviewViewActivity"
android:parentActivityName=".media.ReviewActivity" />
<activity <activity
android:name=".media.SearchActivity" android:name=".media.SearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />

View file

@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
class AnilistMutations { class AnilistMutations {
@ -69,4 +70,10 @@ class AnilistMutations {
val variables = """{"id":"$listId"}""" val variables = """{"id":"$listId"}"""
executeQuery<JsonObject>(query, variables) executeQuery<JsonObject>(query, variables)
} }
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? {
val query = "mutation{RateReview(reviewId:$reviewId,rating:$rating){id mediaId mediaType summary body(asHtml:true)rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}"
return executeQuery<Query.RateReviewResponse>(query)
}
} }

View file

@ -1504,6 +1504,13 @@ Page(page:$page,perPage:50) {
return author return author
} }
suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "UPDATED_AT_DESC"): Query.ReviewsResponse? {
return executeQuery<Query.ReviewsResponse>(
"""{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""",
force = true
)
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? { suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>( return executeQuery<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}""" """mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""

View file

@ -299,6 +299,70 @@ class Query {
val following: List<ani.dantotsu.connections.anilist.api.User>? val following: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable ) : java.io.Serializable
@Serializable
data class ReviewsResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ReviewPage?
) : java.io.Serializable
}
@Serializable
data class ReviewPage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
@SerialName("reviews")
val reviews: List<Review>?
) : java.io.Serializable
@Serializable
data class RateReviewResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("RateReview")
val rateReview: Review
) : java.io.Serializable
}
@Serializable
data class Review(
@SerialName("id")
val id: Int,
@SerialName("mediaId")
val mediaId: Int,
@SerialName("mediaType")
val mediaType: String,
@SerialName("summary")
val summary: String,
@SerialName("body")
val body: String,
@SerialName("rating")
var rating: Int,
@SerialName("ratingAmount")
var ratingAmount: Int,
@SerialName("userRating")
var userRating: String,
@SerialName("score")
val score: Int,
@SerialName("private")
val private: Boolean,
@SerialName("siteUrl")
val siteUrl: String,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("updatedAt")
val updatedAt: Int?,
@SerialName("user")
val user: ani.dantotsu.connections.anilist.api.User?,
) : java.io.Serializable
@Serializable @Serializable
data class UserProfile( data class UserProfile(
@SerialName("id") @SerialName("id")

View file

@ -226,7 +226,7 @@ class AnimeDownloaderService : Service() {
task.episode task.episode
) ?: throw Exception("Failed to create output directory") ) ?: throw Exception("Failed to create output directory")
outputDir.findFile("${task.getTaskName()}.mp4")?.delete() outputDir.findFile("${task.getTaskName()}.mkv")?.delete()
val outputFile = outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv") val outputFile = outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv")
?: throw Exception("Failed to create output file") ?: throw Exception("Failed to create output file")
@ -245,7 +245,7 @@ class AnimeDownloaderService : Service() {
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'") .append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
} }
val probeRequest = val probeRequest =
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"" "-headers $headersStringBuilder -i \"${task.video.file.url}\" -show_entries format=duration -v quiet -of csv=\"p=0\""
ffExtension.executeFFProbe( ffExtension.executeFFProbe(
probeRequest probeRequest
) { ) {
@ -256,7 +256,7 @@ class AnimeDownloaderService : Service() {
val headers = headersStringBuilder.toString() val headers = headersStringBuilder.toString()
var request = "-headers $headers " var request = "-headers $headers "
request += "-i ${task.video.file.url} -c copy -map 0:v -map 0:a -map 0:s?" + request += "-i \"${task.video.file.url}\" -c copy -map 0:v -map 0:a -map 0:s?" +
" -f matroska -timeout 600 -reconnect 1" + " -f matroska -timeout 600 -reconnect 1" +
" -reconnect_streamed 1 -allowed_extensions ALL " + " -reconnect_streamed 1 -allowed_extensions ALL " +
"-tls_verify 0 $path -v trace" "-tls_verify 0 $path -v trace"

View file

@ -517,26 +517,24 @@ class MediaInfoFragment : Fragment() {
} }
parent.addView(root) parent.addView(root)
} }
}
ItemTitleSearchBinding.inflate( ItemTitleSearchBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
false false
).apply { ).apply {
titleSearchImage.loadImage(media.banner ?: media.cover) titleSearchImage.loadImage(media.banner ?: media.cover)
titleSearchText.text = titleSearchText.text =
getString(R.string.search_title, media.mainName()) getString(R.string.reviews)
titleSearchCard.setSafeOnClickListener { titleSearchCard.setSafeOnClickListener {
val query = Intent(requireContext(), SearchActivity::class.java) val query = Intent(requireContext(), ReviewActivity::class.java)
.putExtra("type", "ANIME") .putExtra("mediaId", media.id)
.putExtra("query", media.mainName()) ContextCompat.startActivity(requireContext(), query, null)
.putExtra("search", true)
ContextCompat.startActivity(requireContext(), query, null)
}
parent.addView(root)
} }
parent.addView(root)
} }
ItemTitleRecyclerBinding.inflate( ItemTitleRecyclerBinding.inflate(

View file

@ -0,0 +1,149 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.SpannableString
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ActivityFollowBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.FollowerItem
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityFollowBinding
val adapter = GroupieAdapter()
private val reviews = mutableListOf<Query.Review>()
var mediaId = 0
private var currentPage: Int = 1
private var hasNextPage: Boolean = true
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityFollowBinding.inflate(layoutInflater)
binding.listToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
binding.listFrameLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
setContentView(binding.root)
mediaId = intent.getIntExtra("mediaId", -1)
if (mediaId == -1) {
finish()
return
}
binding.followerGrid.visibility = View.GONE
binding.followerList.visibility = View.GONE
binding.followFilterButton.visibility = View.GONE
binding.listTitle.text = getString(R.string.reviews)
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.VERTICAL,
false
)
binding.listProgressBar.visibility = View.VISIBLE
binding.listBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.query.getReviews(mediaId)
withContext(Dispatchers.Main) {
binding.listProgressBar.visibility = View.GONE
binding.listRecyclerView.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_UP) {
if (hasNextPage && !binding.listRecyclerView.canScrollVertically(1) && !binding.followRefresh.isVisible
&& binding.listRecyclerView.adapter!!.itemCount != 0 &&
(binding.listRecyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() == (binding.listRecyclerView.adapter!!.itemCount - 1)
) {
binding.followRefresh.visibility = ViewGroup.VISIBLE
loadPage(++currentPage) {
binding.followRefresh.visibility = ViewGroup.GONE
}
}
}
false
}
currentPage = response?.data?.page?.pageInfo?.currentPage ?: 1
hasNextPage = response?.data?.page?.pageInfo?.hasNextPage ?: false
response?.data?.page?.reviews?.let {
reviews.addAll(it)
fillList()
}
}
}
}
private fun loadPage(page: Int, callback: () -> Unit) {
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.query.getReviews(mediaId, page)
currentPage = response?.data?.page?.pageInfo?.currentPage ?: 1
hasNextPage = response?.data?.page?.pageInfo?.hasNextPage ?: false
withContext(Dispatchers.Main) {
response?.data?.page?.reviews?.let {
reviews.addAll(it)
fillList()
}
callback()
}
}
}
private fun fillList() {
adapter.clear()
reviews.forEach {
val username = it.user?.name ?: "Unknown"
val name = SpannableString(username + " - " + it.score)
//change the size of the score
name.setSpan(
android.text.style.RelativeSizeSpan(0.9f),
0,
name.length,
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
//give the text an underline
name.setSpan(
android.text.style.UnderlineSpan(),
username.length + 3,
name.length,
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
adapter.add(
FollowerItem(
it.id,
name,
it.user?.avatar?.medium,
it.user?.bannerImage,
it.summary,
this::onUserClick
)
)
}
}
private fun onUserClick(userId: Int) {
val review = reviews.find { it.id == userId }
if (review != null) {
startActivity(Intent(this, ReviewViewActivity::class.java).putExtra("review", review))
}
}
}

View file

@ -0,0 +1,178 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ActivityReviewViewBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import ani.dantotsu.util.AniMarkdown
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ReviewViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityReviewViewBinding
private lateinit var review: Query.Review
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityReviewViewBinding.inflate(layoutInflater)
binding.userContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
binding.reviewContent.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin += navBarHeight
}
setContentView(binding.root)
review = intent.getSerializableExtra("review") as Query.Review
binding.userName.text = review.user?.name
binding.userAvatar.loadImage(review.user?.avatar?.medium)
binding.userTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
binding.profileUserBio.settings.loadWithOverviewMode = true
binding.profileUserBio.settings.useWideViewPort = true
binding.profileUserBio.setInitialScale(1)
val styledHtml = AniMarkdown.getFullAniHTML(
review.body,
ContextCompat.getColor(this, R.color.bg_opp)
)
binding.profileUserBio.loadDataWithBaseURL(
null,
styledHtml,
"text/html",
"utf-8",
null
)
binding.profileUserBio.setBackgroundColor(
ContextCompat.getColor(
this,
android.R.color.transparent
)
)
binding.profileUserBio.setLayerType(View.LAYER_TYPE_HARDWARE, null)
binding.profileUserBio.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
binding.profileUserBio.setBackgroundColor(
ContextCompat.getColor(
this@ReviewViewActivity,
android.R.color.transparent
)
)
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return true
}
}
userVote(review.userRating)
enableVote()
binding.voteCount.text = review.rating.toString()
binding.voteText.text = getString(
R.string.vote_out_of_total,
review.rating.toString(),
review.ratingAmount.toString()
)
}
private fun userVote(type: String) {
val selectedColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val unselectedColor = getThemeColor(androidx.appcompat.R.attr.colorControlNormal)
when (type) {
"NO_VOTE" -> {
binding.upvote.setColorFilter(unselectedColor)
binding.downvote.setColorFilter(unselectedColor)
}
"UP_VOTE" -> {
binding.upvote.setColorFilter(selectedColor)
binding.downvote.setColorFilter(unselectedColor)
}
"DOWN_VOTE" -> {
binding.upvote.setColorFilter(unselectedColor)
binding.downvote.setColorFilter(selectedColor)
}
}
}
private fun rateReview(rating: String) {
disableVote()
lifecycleScope.launch {
val result = Anilist.mutation.rateReview(review.id, rating)
if (result != null) {
withContext(Dispatchers.Main) {
val res = result.data.rateReview
review.rating = res.rating
review.ratingAmount = res.ratingAmount
review.userRating = res.userRating
userVote(review.userRating)
binding.voteCount.text = review.rating.toString()
binding.voteText.text = getString(
R.string.vote_out_of_total,
review.rating.toString(),
review.ratingAmount.toString()
)
userVote(review.userRating)
enableVote()
}
} else {
withContext(Dispatchers.Main) {
toast(
getString(R.string.error_message, "response is null")
)
enableVote()
}
}
}
}
private fun disableVote() {
binding.upvote.setOnClickListener(null)
binding.downvote.setOnClickListener(null)
binding.upvote.isEnabled = false
binding.downvote.isEnabled = false
}
private fun enableVote() {
binding.upvote.setOnClickListener {
if (review.userRating == "UP_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("UP_VOTE")
}
disableVote()
}
binding.downvote.setOnClickListener {
if (review.userRating == "DOWN_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("DOWN_VOTE")
}
disableVote()
}
binding.upvote.isEnabled = true
binding.downvote.isEnabled = true
}
}

View file

@ -2,6 +2,7 @@ package ani.dantotsu.profile
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.widget.ImageButton import android.widget.ImageButton
@ -54,7 +55,7 @@ class FollowActivity : AppCompatActivity() {
) )
binding.listRecyclerView.adapter = adapter binding.listRecyclerView.adapter = adapter
binding.listProgressBar.visibility = View.VISIBLE binding.listProgressBar.visibility = View.VISIBLE
binding.listBack.setOnClickListener { finish() } binding.listBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
val title = intent.getStringExtra("title") val title = intent.getStringExtra("title")
val userID = intent.getIntExtra("userId", 0) val userID = intent.getIntExtra("userId", 0)
@ -97,10 +98,11 @@ class FollowActivity : AppCompatActivity() {
} }
users?.forEach { user -> users?.forEach { user ->
if (getLayoutType(selected) == 0) { if (getLayoutType(selected) == 0) {
val username = SpannableString(user.name ?: "Unknown")
adapter.add( adapter.add(
FollowerItem( FollowerItem(
user.id, user.id,
user.name ?: "Unknown", username,
user.avatar?.medium, user.avatar?.medium,
user.bannerImage ?: user.avatar?.medium user.bannerImage ?: user.avatar?.medium
) { onUserClick(it) }) ) { onUserClick(it) })

View file

@ -1,6 +1,7 @@
package ani.dantotsu.profile package ani.dantotsu.profile
import android.text.SpannableString
import android.view.View import android.view.View
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.blurImage import ani.dantotsu.blurImage
@ -10,9 +11,10 @@ import com.xwray.groupie.viewbinding.BindableItem
class FollowerItem( class FollowerItem(
private val id: Int, private val id: Int,
private val name: String, private val name: SpannableString,
private val avatar: String?, private val avatar: String?,
private val banner: String?, private val banner: String?,
private val altText: String? = null,
val clickCallback: (Int) -> Unit val clickCallback: (Int) -> Unit
) : BindableItem<ItemFollowerBinding>() { ) : BindableItem<ItemFollowerBinding>() {
private lateinit var binding: ItemFollowerBinding private lateinit var binding: ItemFollowerBinding
@ -21,6 +23,10 @@ class FollowerItem(
binding = viewBinding binding = viewBinding
binding.profileUserName.text = name binding.profileUserName.text = name
avatar?.let { binding.profileUserAvatar.loadImage(it) } avatar?.let { binding.profileUserAvatar.loadImage(it) }
altText?.let {
binding.altText.visibility = View.VISIBLE
binding.altText.text = it
}
blurImage(binding.profileBannerImage, banner ?: avatar) blurImage(binding.profileBannerImage, banner ?: avatar)
binding.root.setOnClickListener { clickCallback(id) } binding.root.setOnClickListener { clickCallback(id) }
} }

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7.24,11V20H5.63C4.73,20 4.01,19.28 4.01,18.39V12.62C4.01,11.73 4.74,11 5.63,11H7.24ZM18.5,9.5H13.72V6C13.72,4.9 12.82,4 11.73,4H11.64C11.24,4 10.88,4.24 10.72,4.61L7.99,11V20H17.19C17.92,20 18.54,19.48 18.67,18.76L19.99,11.26C20.15,10.34 19.45,9.5 18.51,9.5H18.5Z" />
</vector>

View file

@ -0,0 +1,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurfaceVariant" />
<corners android:radius="16dp" />
</shape>

View file

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/reviewContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/userContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
android:id="@+id/userAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_margin="12dp"
android:backgroundTint="@color/transparent"
app:cardCornerRadius="64dp"
app:strokeColor="@color/transparent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/userAvatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_round_add_circle_24"
tools:ignore="ContentDescription,ImageContrastCheck"
tools:tint="@color/bg_black_50" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/userName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:fontFamily="@font/poppins_bold"
android:paddingTop="1dp"
android:paddingBottom="0dp"
android:singleLine="true"
android:text="Username"
android:textColor="@color/bg_opp"
android:textSize="18sp"
tools:ignore="HardcodedText,RtlSymmetry" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:alpha="0.6"
android:fontFamily="@font/poppins_semi_bold"
android:text="•"
android:textSize="18sp"
tools:ignore="HardcodedText,RtlSymmetry" />
<TextView
android:id="@+id/userTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:alpha="0.6"
android:fontFamily="@font/poppins_semi_bold"
android:text="Time"
android:textSize="14sp"
tools:ignore="HardcodedText,RtlSymmetry" />
</LinearLayout>
<WebView
android:id="@+id/profileUserBio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:nestedScrollingEnabled="true"
android:padding="16dp"
android:textAlignment="textStart"
tools:text="@string/slogan" />
<LinearLayout
android:id="@+id/reviewContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:background="@drawable/surface_rounded_bg"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/downvote"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
android:scaleX="-1"
android:scaleY="-1"
android:src="@drawable/ic_thumbs"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/voteCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:fontFamily="@font/poppins_semi_bold"
android:text="0"
android:textSize="18sp"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/upvote"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="8dp"
android:src="@drawable/ic_thumbs"
tools:ignore="ContentDescription" />
</LinearLayout>
<TextView
android:id="@+id/voteText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/vote_out_of_total"
android:textSize="14sp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -52,14 +52,34 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<TextView <LinearLayout
android:id="@+id/profileUserName" android:id="@+id/profileUserInfoContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="start|center_vertical" android:layout_gravity="start|center_vertical"
android:layout_marginStart="120dp" android:layout_marginStart="100dp"
android:fontFamily="@font/poppins_semi_bold" android:gravity="center_vertical"
android:text="@string/username" android:orientation="vertical">
android:textSize="18sp" />
<TextView
android:id="@+id/profileUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/username"
android:textSize="18sp" />
<TextView
android:id="@+id/altText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:ellipsize="end"
android:fontFamily="@font/poppins_semi_bold"
android:maxLines="2"
android:text="@string/lorem_ipsum"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</FrameLayout> </FrameLayout>

View file

@ -50,7 +50,7 @@
android:gravity="center" android:gravity="center"
android:maxLines="2" android:maxLines="2"
android:padding="4dp" android:padding="4dp"
android:text="@string/search" android:text="@string/reviews"
android:textAllCaps="true" android:textAllCaps="true"
android:textColor="@color/bg_white" android:textColor="@color/bg_white"
android:textSize="14sp" /> android:textSize="14sp" />

View file

@ -430,7 +430,7 @@
<string name="error_message">Error: %1$s</string> <string name="error_message">Error: %1$s</string>
<string name="install_step">Step: %1$s</string> <string name="install_step">Step: %1$s</string>
<string name="review">Review</string> <string name="review">Review</string>
<string name="reviews">Reviews</string>
<string name="discord_nothing_button">Display only the first button</string> <string name="discord_nothing_button">Display only the first button</string>
<string name="discord_dantotsu_button">Display dantotsu in the second button</string> <string name="discord_dantotsu_button">Display dantotsu in the second button</string>
<string name="discord_anilist_button">Display your AniList profile instead</string> <string name="discord_anilist_button">Display your AniList profile instead</string>
@ -850,8 +850,6 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="do_it">Do it!</string> <string name="do_it">Do it!</string>
<string name="password">Password</string> <string name="password">Password</string>
<string name="search_title">Search %1$s</string>
<string name="profile_stats_widget">Track progress directly from your home screen</string> <string name="profile_stats_widget">Track progress directly from your home screen</string>
<string name="anime_watched">Anime\nWatched</string> <string name="anime_watched">Anime\nWatched</string>
<string name="manga_read">Manga\nRead</string> <string name="manga_read">Manga\nRead</string>
@ -981,4 +979,5 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="download_subtitle">Download Subtitle</string> <string name="download_subtitle">Download Subtitle</string>
<string name="no_video_selected">No video selected</string> <string name="no_video_selected">No video selected</string>
<string name="no_subtitles_available">No subtitles available</string> <string name="no_subtitles_available">No subtitles available</string>
<string name="vote_out_of_total">(%1$s out of %2$s liked this review)</string>
</resources> </resources>