Merge branch 'rebelonion:dev' into dev

This commit is contained in:
tutel
2024-05-20 13:43:55 +03:00
committed by GitHub
44 changed files with 1737 additions and 148 deletions

View File

@@ -200,6 +200,7 @@
android:parentActivityName=".MainActivity" />
<activity
android:name=".util.MarkdownCreatorActivity"/>
<activity android:name=".parsers.ParserTestActivity" />
<activity
android:name=".media.ReviewActivity"
android:parentActivityName=".media.MediaDetailsActivity" />

View File

@@ -21,6 +21,20 @@ import eu.kanade.tachiyomi.util.system.getApplicationIcon
class AddonLoader {
companion object {
/**
* Load an extension from a package name with a specific class name
* @param context the context
* @param packageName the package name of the extension
* @param type the type of extension
* @return the loaded extension
* @throws IllegalStateException if the extension is not of the correct type
* @throws ClassNotFoundException if the extension class is not found
* @throws NoClassDefFoundError if the extension class is not found
* @throws Exception if any other error occurs
* @throws PackageManager.NameNotFoundException if the package is not found
* @throws IllegalStateException if the extension is not found
*/
fun loadExtension(
context: Context,
packageName: String,
@@ -70,11 +84,11 @@ class AddonLoader {
val loadedClass = try {
Class.forName(className, false, classLoader)
} catch (e: ClassNotFoundException) {
Logger.log("Extension load error: $extName ($className)")
Logger.log("ClassNotFoundException load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: NoClassDefFoundError) {
Logger.log("Extension load error: $extName ($className)")
Logger.log("NoClassDefFoundError load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: Exception) {
@@ -117,24 +131,43 @@ class AddonLoader {
}
}
/**
* Load an extension from a package name (class is determined by type)
* @param context the context
* @param packageName the package name of the extension
* @param type the type of extension
* @return the loaded extension
*/
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
return when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
return try {
when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
}
} catch (e: Exception) {
Logger.log("Error loading extension from package name: $packageName")
Logger.log(e)
null
}
}
/**
* Check if a package is an extension by comparing the package name
* @param type the type of extension
* @param pkgInfo the package info
* @return true if the package is an extension
*/
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
return pkgInfo.packageName.equals(type)
}

View File

@@ -75,7 +75,7 @@ class AnilistQueries {
media.cameFromContinue = false
val query =
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true)
@@ -211,6 +211,9 @@ class AnilistQueries {
}
}
}
if (fetchedMedia.reviews?.nodes != null){
media.review = fetchedMedia.reviews!!.nodes as ArrayList<Query.Review>
}
if (user?.mediaList?.isNotEmpty() == true) {
media.users = user.mediaList?.mapNotNull {
it.user?.let { user ->
@@ -1505,7 +1508,7 @@ Page(page:$page,perPage:50) {
return author
}
suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "UPDATED_AT_DESC"): Query.ReviewsResponse? {
suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "SCORE_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

View File

@@ -152,7 +152,7 @@ data class Media(
@SerialName("mediaListEntry") var mediaListEntry: MediaList?,
// User reviews of the media
// @SerialName("reviews") var reviews: ReviewConnection?,
@SerialName("reviews") var reviews: ReviewConnection?,
// User recommendations for similar media
@SerialName("recommendations") var recommendations: RecommendationConnection?,
@@ -537,4 +537,9 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?,
) : java.io.Serializable
) : java.io.Serializable
@Serializable
data class ReviewConnection(
@SerialName("nodes") var nodes: List<Query.Review>?,
)

View File

@@ -26,7 +26,8 @@ enum class NotificationType(val value: String) {
//custom
COMMENT_REPLY("COMMENT_REPLY"),
COMMENT_WARNING("COMMENT_WARNING"),
DANTOTSU_UPDATE("DANTOTSU_UPDATE");
DANTOTSU_UPDATE("DANTOTSU_UPDATE"),
SUBSCRIPTION("SUBSCRIPTION");
fun toFormattedString(): String {
return this.value.replace("_", " ").lowercase(Locale.ROOT)

View File

@@ -84,15 +84,15 @@ class Contributors {
"https://anilist.co/user/6244220"
),
Developer(
"Zaidsenior",
"Ziadsenior",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
"Comment Moderator",
"Comment Moderator and Arabic Translator",
"https://anilist.co/user/6049773"
),
Developer(
"hastsu",
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
"Comment Moderator",
"Comment Moderator and Arabic Translator",
"https://anilist.co/user/6183359"
),
)

View File

@@ -12,13 +12,14 @@ import ani.dantotsu.getThemeColor
class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var parts: Int = 3
private var gapAngle: Float = 9f
private var gapAngle: Float = 12f
private val path = Path()
private var isUser = false
private var booleanList = listOf<Boolean>()
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 6f
strokeCap = Paint.Cap.ROUND
}
@SuppressLint("DrawAllocation")

View File

@@ -42,6 +42,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
class Stories @JvmOverloads constructor(
@@ -264,29 +265,44 @@ class Stories @JvmOverloads constructor(
private var startClickTime = 0L
private var startX = 0f
private var startY = 0f
private var isLongPress = false
private val swipeThreshold = 100
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
val maxClickDuration = 200
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
startClickTime = Calendar.getInstance().timeInMillis
pause()
isLongPress = false
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - startX
val deltaY = event.y - startY
if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
isLongPress = true
}
}
MotionEvent.ACTION_UP -> {
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
if (clickDuration < maxClickDuration) {
//click occurred
view?.let {
if (it.id == R.id.leftTouchPanel) {
leftPanelTouch()
} else if (it.id == R.id.rightTouchPanel) {
rightPanelTouch()
}
if (clickDuration < maxClickDuration && !isLongPress) {
when (view?.id) {
R.id.leftTouchPanel -> leftPanelTouch()
R.id.rightTouchPanel -> rightPanelTouch()
}
} else {
//hold click occurred
resume()
}
val deltaX = event.x - startX
if (abs(deltaX) > swipeThreshold) {
if (deltaX > 0) onStoriesPrevious()
else onStoriesCompleted()
}
}
}
return true
@@ -359,16 +375,14 @@ class Stories @JvmOverloads constructor(
)
}
fun visible(isList: Boolean) {
val visible = if (isList) View.VISIBLE else View.GONE
val gone = if (isList) View.GONE else View.VISIBLE
binding.textActivity.visibility = gone
binding.textActivityContainer.visibility = gone
binding.infoText.visibility = visible
binding.coverImage.visibility = visible
binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList
binding.infoText.isVisible = isList
binding.coverImage.isVisible = isList
binding.infoText.visibility = if (isList) View.VISIBLE else View.INVISIBLE
binding.infoText.text = ""
binding.contentImageViewKen.visibility = visible
binding.contentImageView.visibility = visible
binding.contentImageViewKen.isVisible = isList
binding.contentImageView.isVisible = isList
}
when (story.typename) {
@@ -383,16 +397,15 @@ class Stories @JvmOverloads constructor(
}
}
} ${story.progress ?: story.media?.title?.userPreferred} " +
if (
story.status?.contains("completed") == false &&
!story.status.contains("plans") &&
!story.status.contains("repeating")
)
{
"of ${story.media?.title?.userPreferred}"
} else {
""
}
if (
story.status?.contains("completed") == false &&
!story.status.contains("plans") &&
!story.status.contains("repeating")
) {
"of ${story.media?.title?.userPreferred}"
} else {
""
}
binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
@@ -404,18 +417,16 @@ class Stories @JvmOverloads constructor(
ContextCompat.startActivity(
context,
Intent(context, MediaDetailsActivity::class.java).putExtra(
"mediaId",
story.media?.id
),
"mediaId",
story.media?.id
),
ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle()
)
}
}
"TextActivity" -> {
@@ -445,11 +456,13 @@ class Stories @JvmOverloads constructor(
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id).show(activity.supportFragmentManager, "replies")
RepliesBottomDialog.newInstance(story.id)
.show(activity.supportFragmentManager, "replies")
}
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.replyCount.text = story.replyCount.toString()
binding.activityLikeCount.text = story.likeCount.toString()
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityLikeContainer.setOnClickListener {
like()
}

View File

@@ -5,6 +5,7 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User
@@ -62,6 +63,7 @@ data class Media(
var timeUntilAiring: Long? = null,
var characters: ArrayList<Character>? = null,
var review: ArrayList<Query.Review>? = null,
var staff: ArrayList<Author>? = null,
var prequel: Media? = null,
var sequel: Media? = null,

View File

@@ -34,7 +34,6 @@ import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemQuelsBinding
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
import ani.dantotsu.databinding.ItemTitleSearchBinding
import ani.dantotsu.databinding.ItemTitleTextBinding
import ani.dantotsu.databinding.ItemTitleTrailerBinding
import ani.dantotsu.displayTimer
@@ -46,6 +45,7 @@ import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.xwray.groupie.GroupieAdapter
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
@@ -81,7 +81,8 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext())
val offline: Boolean =
PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext())
binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@@ -254,7 +255,8 @@ class MediaInfoFragment : Fragment() {
if (!media.users.isNullOrEmpty() && !offline) {
val users: ArrayList<User> = media.users ?: arrayListOf()
if (Anilist.token != null && media.userStatus != null) {
users.add(0,
users.add(
0,
User(
id = Anilist.userid!!,
name = getString(R.string.you),
@@ -263,7 +265,8 @@ class MediaInfoFragment : Fragment() {
status = media.userStatus,
score = media.userScore.toFloat(),
progress = media.userProgress,
totalEpisodes = media.anime?.totalEpisodes ?: media.manga?.totalChapters,
totalEpisodes = media.anime?.totalEpisodes
?: media.manga?.totalChapters,
nextAiringEpisode = media.anime?.nextAiringEpisode
)
)
@@ -519,22 +522,41 @@ class MediaInfoFragment : Fragment() {
}
}
ItemTitleSearchBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
titleSearchImage.loadImage(media.banner ?: media.cover)
titleSearchText.text =
getString(R.string.reviews)
titleSearchCard.setSafeOnClickListener {
val query = Intent(requireContext(), ReviewActivity::class.java)
.putExtra("mediaId", media.id)
ContextCompat.startActivity(requireContext(), query, null)
if (!media.review.isNullOrEmpty()) {
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
fun onUserClick(userId: Int) {
val review = media.review!!.find { i -> i.id == userId }
if (review != null) {
startActivity(
Intent(requireContext(), ReviewViewActivity::class.java)
.putExtra("review", review)
)
}
}
val adapter = GroupieAdapter()
media.review!!.forEach {
adapter.add(ReviewAdapter(it, ::onUserClick))
}
itemTitle.setText(R.string.reviews)
itemRecycler.adapter = adapter
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
itemMore.visibility = View.VISIBLE
itemMore.setSafeOnClickListener {
startActivity(
Intent(requireContext(), ReviewActivity::class.java)
.putExtra("mediaId", media.id)
)
}
parent.addView(root)
}
parent.addView(root)
}
ItemTitleRecyclerBinding.inflate(

View File

@@ -77,7 +77,7 @@ class ReviewActivity : AppCompatActivity() {
binding.listBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.query.getReviews(mediaId)
val response = Anilist.query.getReviews(mediaId)?.data?.page
withContext(Dispatchers.Main) {
binding.listProgressBar.visibility = View.GONE
binding.listRecyclerView.setOnTouchListener { _, event ->
@@ -94,9 +94,9 @@ class ReviewActivity : AppCompatActivity() {
}
false
}
currentPage = response?.data?.page?.pageInfo?.currentPage ?: 1
hasNextPage = response?.data?.page?.pageInfo?.hasNextPage ?: false
response?.data?.page?.reviews?.let {
currentPage = response?.pageInfo?.currentPage ?: 1
hasNextPage = response?.pageInfo?.hasNextPage ?: false
response?.reviews?.let {
reviews.addAll(it)
fillList()
}
@@ -122,29 +122,9 @@ class ReviewActivity : AppCompatActivity() {
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,
ReviewAdapter(
it,
this::onUserClick
)
)

View File

@@ -0,0 +1,125 @@
package ani.dantotsu.media
import android.view.View
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ItemReviewsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.toast
import com.xwray.groupie.viewbinding.BindableItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ReviewAdapter(
private var review: Query.Review,
val clickCallback: (Int) -> Unit
) : BindableItem<ItemReviewsBinding>() {
private lateinit var binding: ItemReviewsBinding
override fun bind(viewBinding: ItemReviewsBinding, position: Int) {
binding = viewBinding
binding.reviewUserName.text = review.user?.name
binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium)
binding.reviewText.text = review.summary
binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
val text = "[${review.score/ 10.0f}]"
binding.reviewTag.text = text
binding.root.setOnClickListener { clickCallback(review.id) }
userVote(review.userRating)
enableVote()
binding.reviewTotalVotes.text = review.rating.toString()
}
override fun getLayout(): Int {
return R.layout.item_reviews
}
override fun initializeViewBinding(view: View): ItemReviewsBinding {
return ItemReviewsBinding.bind(view)
}
private fun userVote(type: String) {
when (type) {
"NO_VOTE" -> {
binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewUpVote.alpha = 0.6f
binding.reviewDownVote.alpha = 0.6f
}
"UP_VOTE" -> {
binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewUpVote.alpha = 1f
binding.reviewDownVote.alpha = 0.6f
}
"DOWN_VOTE" -> {
binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
binding.reviewDownVote.alpha = 1f
binding.reviewUpVote.alpha = 0.6f
}
}
}
private fun rateReview(rating: String) {
disableVote()
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.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.reviewTotalVotes.text = review.rating.toString()
userVote(review.userRating)
enableVote()
}
} else {
withContext(Dispatchers.Main) {
toast(
binding.root.context.getString(R.string.error_message, "response is null")
)
enableVote()
}
}
}
}
private fun disableVote() {
binding.reviewUpVote.setOnClickListener(null)
binding.reviewDownVote.setOnClickListener(null)
binding.reviewUpVote.isEnabled = false
binding.reviewDownVote.isEnabled = false
}
private fun enableVote() {
binding.reviewUpVote.setOnClickListener {
if (review.userRating == "UP_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("UP_VOTE")
}
disableVote()
}
binding.reviewDownVote.setOnClickListener {
if (review.userRating == "DOWN_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("DOWN_VOTE")
}
disableVote()
}
binding.reviewUpVote.isEnabled = true
binding.reviewDownVote.isEnabled = true
}
}

View File

@@ -188,7 +188,7 @@ class CommentNotificationTask : Task {
null
) ?: listOf()
val newStore = notificationStore.toMutableList()
if (newStore.size > 10) {
if (newStore.size > 30) {
newStore.remove(newStore.minByOrNull { it.time })
}
if (newStore.any { it.content == notification.content }) {

View File

@@ -13,8 +13,6 @@ data class CommentStore(
val time: Long = System.currentTimeMillis(),
) : java.io.Serializable {
companion object {
@Suppress("INAPPROPRIATE_CONST_NAME")
private const val serialVersionUID = 2L
}
}

View File

@@ -118,6 +118,15 @@ class SubscriptionNotificationTask : Task {
if (ep != null) ep.number + " " + context.getString(R.string.just_released) to null
else null
} ?: return@map
addSubscriptionToStore(
SubscriptionStore(
media.name,
text.first,
media.id
)
)
PrefManager.setVal(PrefName.UnreadCommentNotifications,
PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) + 1)
val notification = createNotification(
context.applicationContext,
media,
@@ -219,4 +228,17 @@ class SubscriptionNotificationTask : Task {
}
)
}
private fun addSubscriptionToStore(notification: SubscriptionStore) {
val notificationStore = PrefManager.getNullableVal<List<SubscriptionStore>>(
PrefName.SubscriptionNotificationStore,
null
) ?: listOf()
val newStore = notificationStore.toMutableList()
if (newStore.size >= 100) {
newStore.remove(newStore.minByOrNull { it.time })
}
newStore.add(notification)
PrefManager.setVal(PrefName.SubscriptionNotificationStore, newStore)
}
}

View File

@@ -0,0 +1,16 @@
package ani.dantotsu.notifications.subscription
import kotlinx.serialization.Serializable
@Serializable
data class SubscriptionStore(
val title: String,
val content: String,
val mediaId: Int,
val type: String = "SUBSCRIPTION",
val time: Long = System.currentTimeMillis(),
) : java.io.Serializable {
companion object {
private const val serialVersionUID = 1L
}
}

View File

@@ -53,8 +53,11 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override val hostUrl =
(extension.sources.first() as? AnimeHttpSource)?.baseUrl ?: extension.sources.first().name
override val isNSFW = extension.isNsfw
override val icon = extension.icon
override var selectDub: Boolean
get() = getDub()
set(value) {
@@ -324,8 +327,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override val hostUrl =
(extension.sources.first() as? HttpSource)?.baseUrl ?: extension.sources.first().name
override val isNSFW = extension.isNsfw
override val icon = extension.icon
override suspend fun loadChapters(
mangaLink: String,

View File

@@ -1,17 +1,21 @@
package ani.dantotsu.parsers
import android.graphics.drawable.Drawable
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.media.Media
import ani.dantotsu.okHttpClient
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import okhttp3.Request
import java.io.Serializable
import java.net.URLDecoder
import java.net.URLEncoder
import kotlin.system.measureTimeMillis
abstract class BaseParser {
@@ -41,6 +45,11 @@ abstract class BaseParser {
* **/
open val language = "English"
/**
* Icon of the site, can be null
*/
open val icon: Drawable? = null
/**
* Search for Anime/Manga/Novel, returns a List of Responses
*
@@ -133,10 +142,39 @@ abstract class BaseParser {
return response
}
/**
* ping the site to check if it's working or not.
* @return Triple<Int, Int?, String> : First Int is the status code, Second Int is the response time in milliseconds, Third String is the response message.
*/
fun ping(): Triple<Int, Int?, String> {
val client = okHttpClient
var statusCode = 0
var responseTime: Int? = null
var responseMessage = ""
try {
val request = Request.Builder()
.url(hostUrl)
.build()
responseTime = measureTimeMillis {
client.newCall(request).execute().use { response ->
statusCode = response.code
responseMessage = response.message
}
}.toInt()
} catch (e: Exception) {
Logger.log("Failed to ping $name")
statusCode = -1
responseMessage = if (e.message.isNullOrEmpty()) "None" else e.message!!
Logger.log(e)
}
return Triple(statusCode, responseTime, responseMessage)
}
/**
* Used to get an existing Search Response which was selected by the user.
* **/
* @param mediaId : The mediaId of the Media object.
* @return ShowResponse? : The ShowResponse object if found, else null.
*/
open suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
checkIfVariablesAreEmpty()
return PrefManager.getNullableCustomVal(
@@ -148,7 +186,10 @@ abstract class BaseParser {
/**
* Used to save Shows Response using `saveName`.
* **/
* @param mediaId : The mediaId of the Media object.
* @param response : The ShowResponse object to save.
* @param selected : Boolean : If the ShowResponse was selected by the user or not.
*/
open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) {
if (response != null) {
checkIfVariablesAreEmpty()

View File

@@ -0,0 +1,38 @@
package ani.dantotsu.parsers
import android.graphics.drawable.Drawable
import android.view.View
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemExtensionSelectBinding
import com.xwray.groupie.viewbinding.BindableItem
class ExtensionSelectItem(
private val name: String,
private val image: Drawable?,
private var isSelected: Boolean,
val selectCallback: (String, Boolean) -> Unit
) : BindableItem<ItemExtensionSelectBinding>() {
private lateinit var binding: ItemExtensionSelectBinding
override fun bind(viewBinding: ItemExtensionSelectBinding, position: Int) {
binding = viewBinding
binding.extensionNameTextView.text = name
image?.let {
binding.extensionIconImageView.setImageDrawable(it)
}
binding.extensionCheckBox.setOnCheckedChangeListener(null)
binding.extensionCheckBox.isChecked = isSelected
binding.extensionCheckBox.setOnCheckedChangeListener { _, isChecked ->
isSelected = isChecked
selectCallback(name, isChecked)
}
}
override fun getLayout(): Int {
return R.layout.item_extension_select
}
override fun initializeViewBinding(view: View): ItemExtensionSelectBinding {
return ItemExtensionSelectBinding.bind(view)
}
}

View File

@@ -0,0 +1,367 @@
package ani.dantotsu.parsers
import android.content.Context
import android.view.View
import androidx.core.view.isVisible
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemExtensionTestBinding
import ani.dantotsu.getThemeColor
import com.xwray.groupie.viewbinding.BindableItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ExtensionTestItem(
private var extensionType: String,
private var testType: String,
private var extension: BaseParser,
private var searchString: String = "Chainsaw Man"
) : BindableItem<ItemExtensionTestBinding>() {
private lateinit var binding: ItemExtensionTestBinding
private lateinit var context: Context
private var job: Job? = null
private var isRunning = false
private var pingResult: Triple<Int, Int?, String>? = null
private var searchResultSize: Int? = null
private var episodeResultSize: Int? = null
private var serverResultSize: Int? = null
override fun bind(viewBinding: ItemExtensionTestBinding, position: Int) {
binding = viewBinding
context = binding.root.context
binding.extensionIconImageView.setImageDrawable(extension.icon)
binding.extensionNameTextView.text = extension.name
binding.extensionLoading.isVisible = isRunning
hideAllResults()
pingResult()
searchResult()
episodeResult()
serverResult()
}
override fun getLayout(): Int {
return R.layout.item_extension_test
}
override fun initializeViewBinding(view: View): ItemExtensionTestBinding {
return ItemExtensionTestBinding.bind(view)
}
private fun hideAllResults() {
if (::binding.isInitialized.not()) return
binding.searchResultText.isVisible = false
binding.episodeResultText.isVisible = false
binding.serverResultText.isVisible = false
}
fun cancelJob() {
job?.cancel()
job = null
binding.extensionLoading.isVisible = false
}
fun startTest() {
pingResult = null
searchResultSize = null
episodeResultSize = null
serverResultSize = null
isRunning = true
hideAllResults()
job?.cancel()
job = Job()
CoroutineScope(Dispatchers.IO + job!!).launch {
when (extensionType) {
"anime" -> {
val extension = extension as AnimeParser
runAnimeTest(extension)
}
"manga" -> {
val extension = extension as MangaParser
runMangaTest(extension)
}
"novel" -> {
val extension = extension as NovelParser
runNovelTest(extension)
}
}
}
}
private suspend fun runAnimeTest(extension: AnimeParser) {
pingResult = extension.ping()
withContext(Dispatchers.Main) {
pingResult()
}
if (testType == "ping") {
done()
return
}
val searchResult = extension.search(searchString)
searchResultSize = searchResult.size
withContext(Dispatchers.Main) {
searchResult()
}
if (searchResultSize == 0 || testType == "basic") {
done()
return
}
val episodeResult = extension.loadEpisodes("", null, searchResult.first().sAnime!!)
episodeResultSize = episodeResult.size
withContext(Dispatchers.Main) {
episodeResult()
}
if (episodeResultSize == 0) {
done()
return
}
val serverResult = extension.loadVideoServers("", null, episodeResult.first().sEpisode!!)
serverResultSize = serverResult.size
withContext(Dispatchers.Main) {
serverResult()
}
done()
}
private suspend fun runMangaTest(extension: MangaParser) {
pingResult = extension.ping()
withContext(Dispatchers.Main) {
pingResult()
}
if (testType == "ping") {
done()
return
}
val searchResult = extension.search(searchString)
searchResultSize = searchResult.size
withContext(Dispatchers.Main) {
searchResult()
}
if (searchResultSize == 0 || testType == "basic") {
done()
return
}
val chapterResult = extension.loadChapters("", null, searchResult.first().sManga!!)
episodeResultSize = chapterResult.size
withContext(Dispatchers.Main) {
episodeResult()
}
if (episodeResultSize == 0) {
done()
return
}
val serverResult = extension.loadImages("", chapterResult.first().sChapter)
serverResultSize = serverResult.size
withContext(Dispatchers.Main) {
serverResult()
}
withContext(Dispatchers.Main) {
if (::binding.isInitialized )
binding.extensionLoading.isVisible = false
isRunning = false
}
}
private suspend fun runNovelTest(extension: NovelParser) {
withContext(Dispatchers.Main) {
pingResult()
}
if (testType == "ping") {
done()
return
}
val searchResult = extension.search(searchString)
searchResultSize = searchResult.size
withContext(Dispatchers.Main) {
searchResult()
}
if (searchResultSize == 0 || testType == "basic") {
done()
return
}
val chapterResult = extension.loadBook(searchResult.first().link, null)
episodeResultSize = chapterResult.links.size
withContext(Dispatchers.Main) {
episodeResult()
serverResult()
}
withContext(Dispatchers.Main) {
if (::binding.isInitialized )
binding.extensionLoading.isVisible = false
isRunning = false
}
}
private fun done() {
if (::binding.isInitialized.not()) return
binding.extensionLoading.isVisible = false
isRunning = false
}
private fun pingResult() {
if (::binding.isInitialized.not()) return
if (extensionType == "novel") {
binding.pingResultText.isVisible = true
binding.pingResultText.text = context.getString(R.string.test_not_supported)
binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_round_info_24, 0, 0, 0
)
return
}
if (pingResult == null) {
binding.pingResultText.isVisible = false
return
} else {
binding.pingResultText.isVisible = true
}
binding.pingResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
)
val (code, time, message) = pingResult!!
if (code == 200) {
binding.pingResultText.text = context.getString(R.string.ping_success, time.toString())
binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_check, 0, 0, 0
)
return
}
binding.pingResultText.text =
context.getString(R.string.ping_error, code.toString(), message)
binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_cancel, 0, 0, 0
)
binding.pingResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorError)
)
}
private fun searchResult() {
if (::binding.isInitialized.not()) return
if (searchResultSize == null) {
binding.searchResultText.isVisible = false
return
}
binding.searchResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
)
binding.searchResultText.isVisible = true
if (searchResultSize == 0) {
val text = context.getString(R.string.title_search_test,
context.getString(R.string.no_results_found))
binding.searchResultText.text = text
binding.searchResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_cancel, 0, 0, 0
)
binding.searchResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorError)
)
return
}
val text = context.getString(R.string.title_search_test,
context.getString(R.string.results_found, searchResultSize.toString()))
binding.searchResultText.text = text
binding.searchResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_check, 0, 0, 0
)
}
private fun episodeResult() {
if (::binding.isInitialized.not()) return
if (episodeResultSize == null) {
binding.episodeResultText.isVisible = false
return
}
binding.episodeResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
)
binding.episodeResultText.isVisible = true
if (episodeResultSize == 0) {
val text = when(extensionType) {
"anime" -> context.getString(R.string.episode_search_test,
context.getString(R.string.no_results_found))
"manga" -> context.getString(R.string.chapter_search_test,
context.getString(R.string.no_results_found))
else -> context.getString(R.string.book_search_test,
context.getString(R.string.no_results_found))
}
binding.episodeResultText.text = text
binding.episodeResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_cancel, 0, 0, 0
)
binding.episodeResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorError)
)
return
}
val text = when(extensionType) {
"anime" -> context.getString(R.string.episode_search_test,
context.getString(R.string.results_found, episodeResultSize.toString()))
"manga" -> context.getString(R.string.chapter_search_test,
context.getString(R.string.results_found, episodeResultSize.toString()))
else -> context.getString(R.string.book_search_test,
context.getString(R.string.results_found, episodeResultSize.toString()))
}
binding.episodeResultText.text = text
binding.episodeResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_check, 0, 0, 0
)
}
private fun serverResult() {
if (::binding.isInitialized.not()) return
if (extensionType == "novel") {
binding.pingResultText.isVisible = true
binding.pingResultText.text = context.getString(R.string.test_not_supported)
binding.pingResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_round_info_24, 0, 0, 0
)
return
}
if (serverResultSize == null) {
binding.serverResultText.isVisible = false
return
}
binding.serverResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
)
binding.serverResultText.isVisible = true
if (serverResultSize == 0) {
val text = when(extensionType) {
"anime" -> context.getString(R.string.video_search_test,
context.getString(R.string.no_results_found))
"manga" -> context.getString(R.string.image_search_test,
context.getString(R.string.no_results_found))
else -> context.getString(R.string.book_search_test,
context.getString(R.string.no_results_found))
}
binding.serverResultText.text = text
binding.serverResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_cancel, 0, 0, 0
)
binding.serverResultText.setTextColor(
context.getThemeColor(com.google.android.material.R.attr.colorError)
)
return
}
val text = when(extensionType) {
"anime" -> context.getString(R.string.video_search_test,
context.getString(R.string.results_found, serverResultSize.toString()))
"manga" -> context.getString(R.string.image_search_test,
context.getString(R.string.results_found, serverResultSize.toString()))
else -> context.getString(R.string.book_search_test,
context.getString(R.string.results_found, serverResultSize.toString()))
}
binding.serverResultText.text = text
binding.serverResultText.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_circle_check, 0, 0, 0
)
}
}

View File

@@ -0,0 +1,117 @@
package ani.dantotsu.parsers
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.databinding.BottomSheetExtensionTestSettingsBinding
import ani.dantotsu.parsers.novel.NovelExtensionManager
import com.xwray.groupie.GroupieAdapter
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionTestSettingsBottomDialog : BottomSheetDialogFragment() {
private var _binding: BottomSheetExtensionTestSettingsBinding? = null
private val binding get() = _binding!!
private val adapter: GroupieAdapter = GroupieAdapter()
private val animeExtension: AnimeExtensionManager = Injekt.get()
private val mangaExtensions: MangaExtensionManager = Injekt.get()
private val novelExtensions: NovelExtensionManager = Injekt.get()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = BottomSheetExtensionTestSettingsBinding.inflate(inflater, container, false)
return _binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.extensionSelectionRecyclerView.adapter = adapter
binding.extensionSelectionRecyclerView.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.animeRadioButton.setOnCheckedChangeListener { _, b ->
if (b) {
extensionType = "anime"
extensionsToTest.clear()
setupAdapter()
}
}
binding.mangaRadioButton.setOnCheckedChangeListener { _, b ->
if (b) {
extensionType = "manga"
extensionsToTest.clear()
setupAdapter()
}
}
binding.novelsRadioButton.setOnCheckedChangeListener { _, b ->
if (b) {
extensionType = "novel"
extensionsToTest.clear()
setupAdapter()
}
}
binding.pingRadioButton.setOnCheckedChangeListener { _, b ->
if (b) {
testType = "ping"
}
}
binding.basicRadioButton.setOnCheckedChangeListener { _, b ->
if (b) {
testType = "basic"
}
}
binding.fullRadioButton.setOnCheckedChangeListener { _, b ->
if (b) {
testType = "full"
}
}
setupAdapter()
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
private fun setupAdapter() {
val namesAndUrls: Map<String,Drawable?> = when (extensionType) {
"anime" -> animeExtension.installedExtensionsFlow.value.associate { it.name to it.icon }
"manga" -> mangaExtensions.installedExtensionsFlow.value.associate { it.name to it.icon }
"novel" -> novelExtensions.installedExtensionsFlow.value.associate { it.name to it.icon }
else -> emptyMap()
}
adapter.clear()
namesAndUrls.forEach { (name, icon) ->
val isSelected = extensionsToTest.contains(name)
adapter.add(ExtensionSelectItem(name, icon, isSelected, ::selectedCallback))
}
}
private fun selectedCallback(name: String, isSelected: Boolean) {
if (isSelected) {
extensionsToTest.add(name)
} else {
extensionsToTest.remove(name)
}
}
companion object {
fun newInstance(): ExtensionTestSettingsBottomDialog {
return ExtensionTestSettingsBottomDialog()
}
var extensionType = "anime"
var testType = "ping"
var extensionsToTest: MutableList<String> = mutableListOf()
}
}

View File

@@ -0,0 +1,112 @@
package ani.dantotsu.parsers
import android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.databinding.ActivityParserTestBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import com.xwray.groupie.GroupieAdapter
class ParserTestActivity : AppCompatActivity() {
private lateinit var binding: ActivityParserTestBinding
val adapter = GroupieAdapter()
val extensionsToTest: MutableList<ExtensionTestItem> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityParserTestBinding.inflate(layoutInflater)
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
binding.extensionResultsRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
setContentView(binding.root)
binding.extensionResultsRecyclerView.adapter = adapter
binding.extensionResultsRecyclerView.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.VERTICAL,
false
)
binding.backButton.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.optionsLayout.setOnClickListener {
ExtensionTestSettingsBottomDialog.newInstance()
.show(supportFragmentManager, "extension_test_settings")
}
binding.startButton.setOnClickListener {
if (ExtensionTestSettingsBottomDialog.extensionsToTest.isEmpty()) {
toast(R.string.no_extensions_selected)
return@setOnClickListener
}
extensionsToTest.forEach {
it.cancelJob()
}
extensionsToTest.clear()
adapter.clear()
when (ExtensionTestSettingsBottomDialog.extensionType) {
"anime" -> {
ExtensionTestSettingsBottomDialog.extensionsToTest.forEach { name ->
val extension =
AnimeSources.list.find { source -> source.name == name }?.get?.value
extension?.let {
extensionsToTest.add(
ExtensionTestItem(
"anime",
ExtensionTestSettingsBottomDialog.testType,
it
)
)
}
}
}
"manga" -> {
ExtensionTestSettingsBottomDialog.extensionsToTest.forEach { name ->
val extension =
MangaSources.list.find { source -> source.name == name }?.get?.value
extension?.let {
extensionsToTest.add(
ExtensionTestItem(
"manga",
ExtensionTestSettingsBottomDialog.testType,
it
)
)
}
}
}
"novel" -> {
ExtensionTestSettingsBottomDialog.extensionsToTest.forEach { name ->
val extension =
NovelSources.list.find { source -> source.name == name }?.get?.value
extension?.let {
extensionsToTest.add(
ExtensionTestItem(
"novel",
ExtensionTestSettingsBottomDialog.testType,
it
)
)
}
}
}
}
extensionsToTest.forEach {
adapter.add(it)
it.startTest()
}
}
}
}

View File

@@ -92,6 +92,10 @@ class ActivityItemBuilder {
NotificationType.DANTOTSU_UPDATE -> {
notification.context ?: "You should not see this"
}
NotificationType.SUBSCRIPTION -> {
notification.context ?: "You should not see this"
}
}
}

View File

@@ -28,6 +28,7 @@ import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.comment.CommentStore
import ani.dantotsu.notifications.subscription.SubscriptionStore
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
@@ -41,6 +42,8 @@ import kotlinx.coroutines.withContext
class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityFollowBinding
private lateinit var commentStore: List<CommentStore>
private lateinit var subscriptionStore: List<SubscriptionStore>
private var adapter: GroupieAdapter = GroupieAdapter()
private var notificationList: List<Notification> = emptyList()
val filters = ArrayList<String>()
@@ -70,6 +73,15 @@ class NotificationActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed()
}
binding.listProgressBar.visibility = ViewGroup.VISIBLE
commentStore = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
subscriptionStore = PrefManager.getNullableVal<List<SubscriptionStore>>(
PrefName.SubscriptionNotificationStore,
null
) ?: listOf()
binding.followFilterButton.setOnClickListener {
val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
@@ -193,22 +205,39 @@ class NotificationActivity : AppCompatActivity() {
notifications
}.toMutableList()
}
if (activityId == -1 && currentPage == 1) {
val commentStore = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
if (activityId == -1) {
val furthestTime = newNotifications.minOfOrNull { it.createdAt } ?: 0
commentStore.forEach {
val notification = Notification(
it.type.toString(),
System.currentTimeMillis().toInt(),
commentId = it.commentId,
notificationType = it.type.toString(),
mediaId = it.mediaId,
context = it.title + "\n" + it.content,
createdAt = (it.time / 1000L).toInt(),
)
newNotifications += notification
if ((it.time > furthestTime * 1000L || !hasNextPage) && notificationList.none { notification ->
notification.commentId == it.commentId && notification.createdAt == (it.time / 1000L).toInt()
}) {
val notification = Notification(
it.type.toString(),
System.currentTimeMillis().toInt(),
commentId = it.commentId,
notificationType = it.type.toString(),
mediaId = it.mediaId,
context = it.title + "\n" + it.content,
createdAt = (it.time / 1000L).toInt(),
)
newNotifications += notification
}
}
subscriptionStore.forEach {
if ((it.time > furthestTime * 1000L || !hasNextPage) && notificationList.none { notification ->
notification.mediaId == it.mediaId && notification.createdAt == (it.time / 1000L).toInt()
}) {
val notification = Notification(
it.type,
System.currentTimeMillis().toInt(),
commentId = it.mediaId,
mediaId = it.mediaId,
notificationType = it.type,
context = it.content,
createdAt = (it.time / 1000L).toInt(),
)
newNotifications += notification
}
}
newNotifications.sortByDescending { it.createdAt }
}

View File

@@ -332,6 +332,20 @@ class NotificationItem(
NotificationType.DANTOTSU_UPDATE -> {
image(user = true)
}
NotificationType.SUBSCRIPTION -> {
image(user = true, commentNotification = true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.mediaId ?: 0, null, NotificationClickType.MEDIA
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.mediaId ?: 0, null, NotificationClickType.MEDIA
)
}
}
}
}

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.settings
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@@ -13,6 +14,7 @@ import android.view.inputmethod.EditorInfo
import android.widget.AutoCompleteTextView
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -28,6 +30,7 @@ import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.AndroidBug5497Workaround
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.ParserTestActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
@@ -70,6 +73,14 @@ class ExtensionsActivity : AppCompatActivity() {
bottomMargin = statusBarHeight + navBarHeight
}
binding.testButton.setOnClickListener {
ContextCompat.startActivity(
this,
Intent(this, ParserTestActivity::class.java),
null
)
}
val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
viewPager.offscreenPageLimit = 1

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.settings
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
@@ -9,6 +10,7 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.LinearLayoutManager
@@ -20,6 +22,7 @@ import ani.dantotsu.databinding.ItemRepositoryBinding
import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.ParserTestActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
@@ -232,6 +235,20 @@ class SettingsExtensionsActivity : AppCompatActivity() {
setExtensionOutput(it.attachView, MediaType.MANGA)
}
),
Settings(
type = 1,
name = getString(R.string.extension_test),
desc = getString(R.string.extension_test_desc),
icon = R.drawable.ic_round_search_sources_24,
isActivity = true,
onClick = {
ContextCompat.startActivity(
context,
Intent(context, ParserTestActivity::class.java),
null
)
}
),
Settings(
type = 1,
name = getString(R.string.user_agent),

View File

@@ -5,6 +5,7 @@ import android.view.View
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemSubscriptionBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import com.xwray.groupie.GroupieAdapter
@@ -25,7 +26,7 @@ class SubscriptionItem(
else
SubscriptionHelper.getMangaParser(media.id).name
val mediaName = media.name
val showName = "$mediaName - $parserName"
val showName = "$mediaName ($parserName)"
binding.subscriptionName.text = showName
binding.root.setOnClickListener {
ContextCompat.startActivity(
@@ -36,6 +37,7 @@ class SubscriptionItem(
null
)
}
binding.subscriptionCover.loadImage(media.image)
binding.deleteSubscription.setOnClickListener {
SubscriptionHelper.deleteSubscription(id, true)
adapter.remove(this)

View File

@@ -4,6 +4,7 @@ import android.graphics.Color
import ani.dantotsu.connections.comments.AuthResponse
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.notifications.comment.CommentStore
import ani.dantotsu.notifications.subscription.SubscriptionStore
import ani.dantotsu.settings.saving.internal.Location
import ani.dantotsu.settings.saving.internal.Pref
@@ -186,6 +187,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
LogToFile(Pref(Location.Irrelevant, Boolean::class, false)),
RecentGlobalNotification(Pref(Location.Irrelevant, Int::class, 0)),
CommentNotificationStore(Pref(Location.Irrelevant, List::class, listOf<CommentStore>())),
SubscriptionNotificationStore(Pref(Location.Irrelevant, List::class, listOf<SubscriptionStore>())),
UnreadCommentNotifications(Pref(Location.Irrelevant, Int::class, 0)),
DownloadsDir(Pref(Location.Irrelevant, String::class, "")),
RefreshStatus(Pref(Location.Irrelevant, Boolean::class, false)),

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.util
import android.app.DownloadManager
import android.app.ForegroundServiceStartNotAllowedException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -11,9 +12,11 @@ import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import ani.dantotsu.R
import ani.dantotsu.media.AddonType
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.Type
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
@@ -160,7 +163,16 @@ class ExtensionInstaller(private val context: Context) {
else -> {
val intent =
ExtensionInstallService.getIntent(context, type, downloadId, uri, installer)
ContextCompat.startForegroundService(context, intent)
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: RuntimeException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) {
toast(context.getString(R.string.error_msg, context.getString(R.string.foreground_service_not_allowed)))
} else {
toast(context.getString(R.string.error_msg, e.message))
}
Logger.log(e)
}
}
}
}

View File

@@ -41,6 +41,15 @@
android:layout_marginHorizontal="16dp"
android:orientation="horizontal">
<ImageButton
android:id="@+id/testButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/sort_by"
app:srcCompat="@drawable/ic_round_search_sources_24"
app:tint="?attr/colorOnBackground" />
<ImageButton
android:id="@+id/openSettingsButton"
android:layout_width="48dp"

View File

@@ -0,0 +1,109 @@
<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"
tools:context=".settings.SettingsNotificationActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/backButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_marginStart="12dp"
android:src="@drawable/ic_round_arrow_back_ios_new_24"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="44dp"
android:ellipsize="end"
android:fontFamily="@font/poppins_bold"
android:gravity="center|start"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
android:textColor="?attr/colorOnBackground"
android:textSize="18sp"
android:text="@string/extension_test" />
</FrameLayout>
<LinearLayout
android:id="@+id/optionsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:layout_marginVertical="16dp"
android:layout_marginEnd="24dp"
app:srcCompat="@drawable/ic_round_filter_24"
app:tint="?attr/colorPrimary"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/poppins_bold"
android:text="@string/view_options"
android:textColor="?attr/colorOnSurface"
android:textSize="16sp" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginVertical="16dp"
android:rotation="180"
android:visibility="gone"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
app:tint="?attr/colorPrimary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</LinearLayout>
<Button
android:id="@+id/startButton"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="16dp"
android:backgroundTint="?attr/colorPrimaryContainer"
android:fontFamily="@font/poppins_bold"
android:text="@string/start_test"
android:textColor="?attr/colorOnPrimaryContainer"
app:cornerRadius="12dp"
app:iconTint="?attr/colorOnPrimaryContainer" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/extensionResultsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -2,6 +2,7 @@
<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:scrollbars="none"
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:layout_marginVertical="16dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
android:fontFamily="@font/poppins_bold"
android:text="@string/extension_type"
android:textAlignment="center"
android:textSize="16sp" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/animeRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:checked="true"
android:text="@string/anime"
android:textSize="16sp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/mangaRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/manga"
android:textSize="16sp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/novelsRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/novels"
android:textSize="16sp" />
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
android:layout_marginTop="16dp"
android:fontFamily="@font/poppins_bold"
android:text="@string/test_type"
android:textAlignment="center"
android:textSize="16sp" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/pingRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:checked="true"
android:text="@string/ping"
android:textSize="16sp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/basicRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/basic"
android:textSize="16sp" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/fullRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/full"
android:textSize="16sp" />
</RadioGroup>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/extensionSelectionRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
tools:listitem="@layout/item_extension_select"/>
</LinearLayout>

View File

@@ -27,7 +27,6 @@
android:id="@+id/activityUserAvatar"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_round_add_circle_24"
tools:ignore="ContentDescription,ImageContrastCheck"
tools:tint="@color/transparent" />

View File

@@ -28,7 +28,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="0dp"
android:gravity="center_vertical"
@@ -45,7 +45,9 @@
android:id="@+id/devRole"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold" />
android:text="@string/lorem_ipsum"
android:maxLines="2"
android:fontFamily="@font/poppins_semi_bold" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/extensionCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<ImageView
android:id="@+id/extensionIconImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="3dp"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/extensionNameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/extension_name"
android:textSize="15sp" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/extensionCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" />
</LinearLayout>

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/extensionCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/extensionIconImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="3dp"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/extensionNameTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/extension_name"
android:textSize="15sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/resultLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/pingResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/placeholder"
android:textSize="13sp"
android:gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:drawableStartCompat="@drawable/ic_round_help_24" />
<TextView
android:id="@+id/searchResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/placeholder"
android:textSize="13sp"
android:gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:drawableStartCompat="@drawable/ic_round_help_24" />
<TextView
android:id="@+id/episodeResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/placeholder"
android:textSize="13sp"
android:gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:drawableStartCompat="@drawable/ic_round_help_24" />
<TextView
android:id="@+id/serverResultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/placeholder"
android:textSize="13sp"
android:gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:drawableStartCompat="@drawable/ic_round_help_24" />
</LinearLayout>
<ProgressBar
android:id="@+id/extensionLoading"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="10dp"
android:visibility="visible" />
</LinearLayout>

View File

@@ -74,11 +74,12 @@
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:ellipsize="end"
android:alpha="0.75"
android:maxLines="2"
android:text="@string/lorem_ipsum"
android:textSize="14sp"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -12,14 +12,16 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/bg_white"
android:backgroundTint="@color/transparent"
app:strokeColor="@color/transparent"
app:cardCornerRadius="124dp">
<ImageView
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profileUserAvatar"
android:layout_width="100dp"
android:layout_height="100dp"
tools:ignore="ContentDescription,ImageContrastCheck"
android:layout_width="92dp"
android:layout_height="92dp"
app:srcCompat="@drawable/ic_round_add_circle_24"
tools:ignore="ContentDescription"
tools:tint="@color/transparent" />
<LinearLayout

View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/commentsCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp">
<LinearLayout
android:id="@+id/linearLayout5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="0dp"
android:paddingEnd="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/reviewUserAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/transparent"
app:cardCornerRadius="64dp"
app:strokeColor="@color/transparent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/reviewUserAvatar"
android:layout_width="42dp"
android:layout_height="42dp"
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>
</LinearLayout>
<LinearLayout
android:id="@+id/reviewUserDetailsLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="@+id/reviewText"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/reviewUserName"
android:layout_width="wrap_content"
android:layout_height="match_parent"
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="?attr/colorPrimary"
android:textSize="15sp"
tools:ignore="HardcodedText,RtlSymmetry" />
<TextView
android:id="@+id/reviewTag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:alpha="0.8"
android:fontFamily="@font/poppins_bold"
android:text="[1]"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:alpha="0.6"
android:fontFamily="@font/poppins_semi_bold"
android:text="•"
android:textSize="16sp"
tools:ignore="HardcodedText,RtlSymmetry" />
<TextView
android:id="@+id/reviewPostTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:alpha="0.6"
android:fontFamily="@font/poppins_semi_bold"
android:text="Time"
android:textSize="12sp"
tools:ignore="HardcodedText,RtlSymmetry" />
</LinearLayout>
<TextView
android:id="@+id/reviewText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/poppins_semi_bold"
android:maxLines="2"
android:scrollHorizontally="false"
android:text="@string/slogan"
android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/linearLayout7"
app:layout_constraintHeight_max="200dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/linearLayout5"
app:layout_constraintTop_toBottomOf="@+id/reviewUserDetailsLayout" />
<LinearLayout
android:id="@+id/linearLayout6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="52dp"
android:orientation="horizontal"
app:layout_constraintStart_toEndOf="@+id/linearLayout5"
app:layout_constraintTop_toBottomOf="@+id/reviewText">
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageView
android:id="@+id/reviewUpVote"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:alpha="0.6"
android:padding="2dp"
app:srcCompat="@drawable/ic_round_upvote_inactive_24"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/reviewTotalVotes"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:alpha="0.6"
android:fontFamily="@font/poppins_semi_bold"
android:gravity="center"
android:text="100"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@+id/commentUpVote"
app:layout_constraintTop_toBottomOf="@+id/reviewText"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/reviewDownVote"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="4dp"
android:alpha="0.6"
android:padding="2dp"
android:rotation="180"
app:layout_constraintStart_toEndOf="@+id/commentTotalVotes"
app:layout_constraintTop_toBottomOf="@+id/reviewText"
app:srcCompat="@drawable/ic_round_upvote_inactive_24"
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -30,7 +30,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_circle_cancel"
android:src="@drawable/ic_round_close_24"
android:textSize="14sp"
app:tint="?attr/colorOnBackground"
tools:ignore="ContentDescription" />

View File

@@ -8,14 +8,35 @@
android:layout_marginHorizontal="8dp"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<com.google.android.material.card.MaterialCardView
android:id="@+id/subscriptionCoverContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_margin="16dp"
android:layout_marginStart="16dp"
android:backgroundTint="@color/bg_white"
app:cardCornerRadius="16dp"
app:strokeColor="@color/transparent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/subscriptionCover"
android:layout_width="108dp"
android:layout_height="160dp"
android:layout_gravity="center"
android:scaleType="centerCrop"
tools:ignore="ContentDescription,ImageContrastCheck"
tools:srcCompat="@tools:sample/backgrounds/scenic"
tools:tint="@color/transparent" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/subscriptionName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:fontFamily="@font/poppins_bold"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/placeholder"
android:textSize="16sp" />
@@ -26,7 +47,7 @@
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:contentDescription="@string/delete"
android:src="@drawable/ic_circle_cancel"
android:src="@drawable/ic_round_close_24"
app:tint="?attr/colorOnBackground" />
</LinearLayout>

View File

@@ -5,16 +5,35 @@
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/itemTitle"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:fontFamily="@font/poppins_bold"
android:text="@string/relations"
android:textSize="16sp" />
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<TextView
android:id="@+id/itemTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_weight="1"
android:fontFamily="@font/poppins_bold"
android:padding="8dp"
android:text="@string/relations"
android:textSize="16sp" />
<ImageView
android:id="@+id/itemMore"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
android:fontFamily="@font/poppins_bold"
android:padding="8dp"
android:visibility="gone"
android:src="@drawable/arrow_mark"
android:textSize="16sp"
tools:ignore="ContentDescription" />
</LinearLayout>
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/itemRecycler"