feat: creating, deleting comments | markdown, spoiler comments
This commit is contained in:
parent
129adc5825
commit
aaf9bdd00c
15 changed files with 672 additions and 157 deletions
|
@ -126,7 +126,6 @@ dependencies {
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
|
||||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||||
|
@ -134,6 +133,21 @@ dependencies {
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||||
|
|
||||||
|
// Markwon
|
||||||
|
ext.markwon_version = '4.6.2'
|
||||||
|
implementation "io.noties.markwon:core:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:editor:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:html:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:image-glide:$markwon_version"
|
||||||
|
|
||||||
|
// Groupie
|
||||||
|
ext.groupie_version = '2.10.1'
|
||||||
|
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
|
||||||
|
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
|
||||||
|
|
||||||
// string matching
|
// string matching
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
android:name=".others.imagesearch.ImageSearchActivity"
|
android:name=".others.imagesearch.ImageSearchActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.CommentsFragment" />
|
android:name=".media.comments.CommentsFragment" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.SearchActivity"
|
android:name=".media.SearchActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
android:parentActivityName=".MainActivity" />
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.multidex.MultiDex
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||||
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.others.DisabledReports
|
import ani.dantotsu.others.DisabledReports
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
|
|
|
@ -42,6 +42,7 @@ class AnilistQueries {
|
||||||
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
||||||
|
|
||||||
Anilist.userid = user.id
|
Anilist.userid = user.id
|
||||||
|
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
|
||||||
Anilist.username = user.name
|
Anilist.username = user.name
|
||||||
Anilist.bg = user.bannerImage
|
Anilist.bg = user.bannerImage
|
||||||
Anilist.avatar = user.avatar?.medium
|
Anilist.avatar = user.avatar?.medium
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import ani.dantotsu.BuildConfig
|
import ani.dantotsu.BuildConfig
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
import ani.dantotsu.connections.discord.Discord
|
import ani.dantotsu.connections.discord.Discord
|
||||||
import ani.dantotsu.connections.mal.MAL
|
import ani.dantotsu.connections.mal.MAL
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
@ -36,6 +37,7 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||||
if (MAL.token != null && !MAL.query.getUserData())
|
if (MAL.token != null && !MAL.query.getUserData())
|
||||||
snackString(context.getString(R.string.error_loading_mal_user_data))
|
snackString(context.getString(R.string.error_loading_mal_user_data))
|
||||||
}
|
}
|
||||||
|
CommentsAPI.fetchAuthToken()
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
snackString(context.getString(R.string.error_loading_anilist_user_data))
|
snackString(context.getString(R.string.error_loading_anilist_user_data))
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
package ani.dantotsu.connections.comments
|
|
||||||
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import android.util.Base64
|
|
||||||
import ani.dantotsu.BuildConfig
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
|
||||||
import com.lagradost.nicehttp.Requests
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class Comments {
|
|
||||||
val address: String = "http://10.0.2.2:8081"
|
|
||||||
val appSecret = BuildConfig.APP_SECRET
|
|
||||||
val requestClient = Injekt.get<NetworkHelper>().client
|
|
||||||
var authToken: String? = null
|
|
||||||
fun run() {
|
|
||||||
runBlocking {
|
|
||||||
val request = Requests(
|
|
||||||
requestClient,
|
|
||||||
headerBuilder()
|
|
||||||
)
|
|
||||||
.get(address)
|
|
||||||
println("comments: $request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCommentsForId(id: Int) {
|
|
||||||
val url = "$address/comments/$id"
|
|
||||||
runBlocking {
|
|
||||||
val request = Requests(
|
|
||||||
requestClient,
|
|
||||||
headerBuilder()
|
|
||||||
)
|
|
||||||
.get(url)
|
|
||||||
println("comments: $request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchAuthToken() {
|
|
||||||
val url = "$address/authenticate"
|
|
||||||
//test user id = asdf
|
|
||||||
//test username = test
|
|
||||||
val user = User(generateUserId() ?: return, "rebel onion")
|
|
||||||
val body: FormBody = FormBody.Builder()
|
|
||||||
.add("user_id", user.id)
|
|
||||||
.add("username", user.username)
|
|
||||||
.build()
|
|
||||||
runBlocking {
|
|
||||||
val request = Requests(
|
|
||||||
requestClient,
|
|
||||||
headerBuilder()
|
|
||||||
)
|
|
||||||
val json = request.post(url, requestBody = body)
|
|
||||||
if (!json.text.startsWith("{")) return@runBlocking
|
|
||||||
val parsed = try {
|
|
||||||
Json.decodeFromString<Auth>(json.text)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@runBlocking
|
|
||||||
}
|
|
||||||
authToken = parsed.authToken
|
|
||||||
|
|
||||||
println("comments: $json")
|
|
||||||
println("comments: $authToken")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun headerBuilder(): Map<String, String> {
|
|
||||||
return if (authToken != null) {
|
|
||||||
mapOf(
|
|
||||||
"appauth" to appSecret,
|
|
||||||
"Authorization" to authToken!!
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
mapOf(
|
|
||||||
"appauth" to appSecret,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateUserId(): String? {
|
|
||||||
val anilistId = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return null
|
|
||||||
val userIdEncryptKey = BuildConfig.USER_ID_ENCRYPT_KEY
|
|
||||||
val keySpec = SecretKeySpec(userIdEncryptKey.toByteArray(), KeyProperties.KEY_ALGORITHM_AES)
|
|
||||||
val cipher = Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}")
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec)
|
|
||||||
val encrypted = cipher.doFinal(anilistId.toByteArray())
|
|
||||||
val base = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
|
||||||
val bytes = MessageDigest.getInstance("SHA-256").digest(base.toByteArray())
|
|
||||||
return bytes.joinToString("") { "%02x".format(it) }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Auth(
|
|
||||||
@SerialName("authToken")
|
|
||||||
val authToken: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class User(
|
|
||||||
@SerialName("user_id")
|
|
||||||
val id: String,
|
|
||||||
@SerialName("username")
|
|
||||||
val username: String
|
|
||||||
)
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
package ani.dantotsu.connections.comments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import ani.dantotsu.BuildConfig
|
||||||
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import com.lagradost.nicehttp.Requests
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object CommentsAPI {
|
||||||
|
val address: String = "http://10.0.2.2:8081"
|
||||||
|
val appSecret = BuildConfig.APP_SECRET
|
||||||
|
var authToken: String? = null
|
||||||
|
var userId: String? = null
|
||||||
|
var isBanned: Boolean = false
|
||||||
|
var isAdmin: Boolean = false
|
||||||
|
var isMod: Boolean = false
|
||||||
|
|
||||||
|
suspend fun getCommentsForId(id: Int, page: Int = 1): CommentResponse? {
|
||||||
|
val url = "$address/comments/$id/$page"
|
||||||
|
val request = requestBuilder()
|
||||||
|
val json = request.get(url)
|
||||||
|
if (!json.text.startsWith("{")) return null
|
||||||
|
val parsed = try {
|
||||||
|
Json.decodeFromString<CommentResponse>(json.text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("comments: $e")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
||||||
|
val url = "$address/comments/vote/$commentId/$voteType"
|
||||||
|
val request = requestBuilder()
|
||||||
|
val json = request.post(url)
|
||||||
|
val res = json.code == 200
|
||||||
|
if (!res) {
|
||||||
|
errorReason(json.code)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String): Comment? {
|
||||||
|
val url = "$address/comments"
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("user_id", userId ?: return null)
|
||||||
|
.add("media_id", mediaId.toString())
|
||||||
|
.add("content", content)
|
||||||
|
parentCommentId?.let {
|
||||||
|
body.add("parent_comment_id", it.toString())
|
||||||
|
}
|
||||||
|
val request = requestBuilder()
|
||||||
|
val json = request.post(url, requestBody = body.build())
|
||||||
|
val res = json.code == 200
|
||||||
|
if (!res) {
|
||||||
|
errorReason(json.code)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val parsed = try {
|
||||||
|
Json.decodeFromString<ReturnedComment>(json.text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("comment: $e")
|
||||||
|
snackString("Failed to parse comment")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return Comment(
|
||||||
|
parsed.id,
|
||||||
|
parsed.userId,
|
||||||
|
parsed.mediaId,
|
||||||
|
parsed.parentCommentId,
|
||||||
|
parsed.content,
|
||||||
|
parsed.timestamp,
|
||||||
|
parsed.deleted,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
Anilist.username ?: "",
|
||||||
|
Anilist.avatar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteComment(commentId: Int): Boolean {
|
||||||
|
val url = "$address/comments/$commentId"
|
||||||
|
val request = requestBuilder()
|
||||||
|
val json = request.delete(url)
|
||||||
|
val res = json.code == 200
|
||||||
|
if (!res) {
|
||||||
|
errorReason(json.code)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchAuthToken() {
|
||||||
|
val url = "$address/authenticate"
|
||||||
|
userId = generateUserId()
|
||||||
|
val user = User(userId ?: return, Anilist.username ?: "")
|
||||||
|
val body: FormBody = FormBody.Builder()
|
||||||
|
.add("user_id", user.id)
|
||||||
|
.add("username", user.username)
|
||||||
|
.add("profile_picture_url", Anilist.avatar ?: "")
|
||||||
|
.build()
|
||||||
|
val request = requestBuilder()
|
||||||
|
val json = request.post(url, requestBody = body)
|
||||||
|
if (!json.text.startsWith("{")) return
|
||||||
|
val parsed = try {
|
||||||
|
Json.decodeFromString<AuthResponse>(json.text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("comment: $e")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authToken = parsed.authToken
|
||||||
|
userId = parsed.user.id
|
||||||
|
isBanned = parsed.user.isBanned ?: false
|
||||||
|
isAdmin = parsed.user.isAdmin ?: false
|
||||||
|
isMod = parsed.user.isMod ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun headerBuilder(): Map<String, String> {
|
||||||
|
return if (authToken != null) {
|
||||||
|
mapOf(
|
||||||
|
"appauth" to appSecret,
|
||||||
|
"Authorization" to authToken!!
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mapOf(
|
||||||
|
"appauth" to appSecret,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestBuilder(): Requests {
|
||||||
|
return Requests(
|
||||||
|
Injekt.get<NetworkHelper>().client,
|
||||||
|
headerBuilder()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun errorReason(code: Int) {
|
||||||
|
val error = when (code) {
|
||||||
|
429 -> "Rate limited. :("
|
||||||
|
else -> "Failed to connect"
|
||||||
|
}
|
||||||
|
snackString("Error $code: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("GetInstance")
|
||||||
|
private fun generateUserId(): String? {
|
||||||
|
val anilistId = PrefManager.getVal(PrefName.AnilistUserId, null as String?) ?: return null
|
||||||
|
val userIdEncryptKey = BuildConfig.USER_ID_ENCRYPT_KEY
|
||||||
|
val keySpec = SecretKeySpec(userIdEncryptKey.toByteArray(), KeyProperties.KEY_ALGORITHM_AES)
|
||||||
|
val cipher = Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/ECB/${KeyProperties.ENCRYPTION_PADDING_PKCS7}")
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec)
|
||||||
|
val encrypted = cipher.doFinal(anilistId.toByteArray())
|
||||||
|
return encrypted.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthResponse(
|
||||||
|
@SerialName("authToken")
|
||||||
|
val authToken: String,
|
||||||
|
@SerialName("user")
|
||||||
|
val user: User
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class User(
|
||||||
|
@SerialName("user_id")
|
||||||
|
val id: String,
|
||||||
|
@SerialName("username")
|
||||||
|
val username: String,
|
||||||
|
@SerialName("profile_picture_url")
|
||||||
|
val profilePictureUrl: String? = null,
|
||||||
|
@SerialName("is_banned")
|
||||||
|
@Serializable(with = NumericBooleanSerializer::class)
|
||||||
|
val isBanned: Boolean? = null,
|
||||||
|
@SerialName("is_mod")
|
||||||
|
@Serializable(with = NumericBooleanSerializer::class)
|
||||||
|
val isAdmin: Boolean? = null,
|
||||||
|
@SerialName("is_admin")
|
||||||
|
@Serializable(with = NumericBooleanSerializer::class)
|
||||||
|
val isMod: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CommentResponse(
|
||||||
|
@SerialName("comments")
|
||||||
|
val comments: List<Comment>,
|
||||||
|
@SerialName("totalPages")
|
||||||
|
val totalPages: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Comment(
|
||||||
|
@SerialName("comment_id")
|
||||||
|
val commentId: Int,
|
||||||
|
@SerialName("user_id")
|
||||||
|
val userId: String,
|
||||||
|
@SerialName("media_id")
|
||||||
|
val mediaId: Int,
|
||||||
|
@SerialName("parent_comment_id")
|
||||||
|
val parentCommentId: Int?,
|
||||||
|
@SerialName("content")
|
||||||
|
val content: String,
|
||||||
|
@SerialName("timestamp")
|
||||||
|
val timestamp: String,
|
||||||
|
@SerialName("deleted")
|
||||||
|
@Serializable(with = NumericBooleanSerializer::class)
|
||||||
|
val deleted: Boolean?,
|
||||||
|
@SerialName("upvotes")
|
||||||
|
var upvotes: Int,
|
||||||
|
@SerialName("downvotes")
|
||||||
|
var downvotes: Int,
|
||||||
|
@SerialName("user_vote_type")
|
||||||
|
var userVoteType: Int?,
|
||||||
|
@SerialName("username")
|
||||||
|
val username: String,
|
||||||
|
@SerialName("profile_picture_url")
|
||||||
|
val profilePictureUrl: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReturnedComment(
|
||||||
|
@SerialName("id")
|
||||||
|
var id: Int,
|
||||||
|
@SerialName("comment_id")
|
||||||
|
var commentId: Int?,
|
||||||
|
@SerialName("user_id")
|
||||||
|
val userId: String,
|
||||||
|
@SerialName("media_id")
|
||||||
|
val mediaId: Int,
|
||||||
|
@SerialName("parent_comment_id")
|
||||||
|
val parentCommentId: Int? = null,
|
||||||
|
@SerialName("content")
|
||||||
|
val content: String,
|
||||||
|
@SerialName("timestamp")
|
||||||
|
val timestamp: String,
|
||||||
|
@SerialName("deleted")
|
||||||
|
@Serializable(with = NumericBooleanSerializer::class)
|
||||||
|
val deleted: Boolean?,
|
||||||
|
)
|
||||||
|
|
||||||
|
object NumericBooleanSerializer : KSerializer<Boolean> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Boolean) {
|
||||||
|
encoder.encodeInt(if (value) 1 else 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Boolean {
|
||||||
|
return decoder.decodeInt() != 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
package ani.dantotsu.media
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
|
||||||
import ani.dantotsu.databinding.FragmentCommentsBinding
|
|
||||||
import ani.dantotsu.loadImage
|
|
||||||
import ani.dantotsu.navBarHeight
|
|
||||||
import ani.dantotsu.snackString
|
|
||||||
import ani.dantotsu.statusBarHeight
|
|
||||||
import ani.dantotsu.themes.ThemeManager
|
|
||||||
|
|
||||||
class CommentsFragment : AppCompatActivity(){
|
|
||||||
lateinit var binding: FragmentCommentsBinding
|
|
||||||
//Comments
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
ThemeManager(this).applyTheme()
|
|
||||||
binding = FragmentCommentsBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
binding.commentsLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = statusBarHeight
|
|
||||||
bottomMargin = navBarHeight
|
|
||||||
}
|
|
||||||
binding.commentUserAvatar.loadImage(Anilist.avatar)
|
|
||||||
binding.commentTitle.text = "Work in progress"
|
|
||||||
binding.commentSend.setOnClickListener {
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||||
import ani.dantotsu.databinding.ItemChipBinding
|
import ani.dantotsu.databinding.ItemChipBinding
|
||||||
import ani.dantotsu.media.CommentsFragment
|
import ani.dantotsu.media.comments.CommentsFragment
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||||
|
@ -59,12 +59,13 @@ class AnimeWatchAdapter(
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
_binding = binding
|
_binding = binding
|
||||||
//Comments
|
//CommentsAPI
|
||||||
binding.animeComments.visibility = View.VISIBLE
|
binding.animeComments.visibility = View.VISIBLE
|
||||||
binding.animeComments.setOnClickListener {
|
binding.animeComments.setOnClickListener {
|
||||||
startActivity(
|
startActivity(
|
||||||
fragment.requireContext(),
|
fragment.requireContext(),
|
||||||
Intent(fragment.requireContext(), CommentsFragment::class.java),
|
Intent(fragment.requireContext(), CommentsFragment::class.java)
|
||||||
|
.putExtra("mediaId", media.id),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
140
app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
Normal file
140
app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package ani.dantotsu.media.comments
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.connections.comments.Comment
|
||||||
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
|
import ani.dantotsu.databinding.ItemCommentsBinding
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import com.xwray.groupie.GroupieAdapter
|
||||||
|
import com.xwray.groupie.Section
|
||||||
|
|
||||||
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
class CommentItem(private val comment: Comment,
|
||||||
|
private val markwon: Markwon,
|
||||||
|
private val section: Section
|
||||||
|
) : BindableItem<ItemCommentsBinding>() {
|
||||||
|
|
||||||
|
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
||||||
|
val isUserComment = CommentsAPI.userId == comment.userId
|
||||||
|
val node = markwon.parse(comment.content)
|
||||||
|
val spanned = markwon.render(node)
|
||||||
|
markwon.setParsedMarkdown(viewBinding.commentText, viewBinding.commentText.setSpoilerText(spanned, markwon))
|
||||||
|
viewBinding.commentDelete.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||||
|
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||||
|
viewBinding.commentReply.visibility = View.GONE //TODO: implement reply
|
||||||
|
viewBinding.commentTotalReplies.visibility = View.GONE //TODO: implement reply
|
||||||
|
viewBinding.commentReply.setOnClickListener {
|
||||||
|
|
||||||
|
}
|
||||||
|
viewBinding.commentDelete.setOnClickListener {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
scope.launch {
|
||||||
|
val success = CommentsAPI.deleteComment(comment.commentId)
|
||||||
|
if (success) {
|
||||||
|
snackString("Comment Deleted")
|
||||||
|
section.remove(this@CommentItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//fill the icon if the user has liked the comment
|
||||||
|
setVoteButtons(viewBinding)
|
||||||
|
viewBinding.commentUpVote.setOnClickListener {
|
||||||
|
val voteType = if (comment.userVoteType == 1) 0 else 1
|
||||||
|
val previousVoteType = comment.userVoteType
|
||||||
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
scope.launch {
|
||||||
|
val success = CommentsAPI.vote(comment.commentId, voteType)
|
||||||
|
if (success) {
|
||||||
|
comment.userVoteType = voteType
|
||||||
|
|
||||||
|
if (previousVoteType == -1) {
|
||||||
|
comment.downvotes -= 1
|
||||||
|
}
|
||||||
|
comment.upvotes += if (voteType == 1) 1 else -1
|
||||||
|
|
||||||
|
notifyChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewBinding.commentDownVote.setOnClickListener {
|
||||||
|
val voteType = if (comment.userVoteType == -1) 0 else -1
|
||||||
|
val previousVoteType = comment.userVoteType
|
||||||
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
scope.launch {
|
||||||
|
val success = CommentsAPI.vote(comment.commentId, voteType)
|
||||||
|
if (success) {
|
||||||
|
comment.userVoteType = voteType
|
||||||
|
|
||||||
|
if (previousVoteType == 1) {
|
||||||
|
comment.upvotes -= 1
|
||||||
|
}
|
||||||
|
comment.downvotes += if (voteType == -1) 1 else -1
|
||||||
|
|
||||||
|
notifyChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewBinding.commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
|
||||||
|
viewBinding.commentUserAvatar
|
||||||
|
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
|
||||||
|
viewBinding.commentUserName.text = comment.username
|
||||||
|
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLayout(): Int {
|
||||||
|
return R.layout.item_comments
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initializeViewBinding(view: View): ItemCommentsBinding {
|
||||||
|
return ItemCommentsBinding.bind(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setVoteButtons(viewBinding: ItemCommentsBinding) {
|
||||||
|
if (comment.userVoteType == 1) {
|
||||||
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
||||||
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
|
} else if (comment.userVoteType == -1) {
|
||||||
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
||||||
|
} else {
|
||||||
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatTimestamp(timestamp: String): String {
|
||||||
|
return try {
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||||
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
val parsedDate = dateFormat.parse(timestamp)
|
||||||
|
val currentDate = Date()
|
||||||
|
|
||||||
|
val diff = currentDate.time - (parsedDate?.time ?: 0)
|
||||||
|
|
||||||
|
val days = diff / (24 * 60 * 60 * 1000)
|
||||||
|
val hours = diff / (60 * 60 * 1000) % 24
|
||||||
|
val minutes = diff / (60 * 1000) % 60
|
||||||
|
|
||||||
|
return when {
|
||||||
|
days > 0 -> "$days days ago"
|
||||||
|
hours > 0 -> "$hours hours ago"
|
||||||
|
minutes > 0 -> "$minutes minutes ago"
|
||||||
|
else -> "just now"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"now"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
package ani.dantotsu.media.comments
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
|
import ani.dantotsu.connections.comments.Comment
|
||||||
|
import ani.dantotsu.connections.comments.CommentsAPI
|
||||||
|
import ani.dantotsu.databinding.FragmentCommentsBinding
|
||||||
|
import ani.dantotsu.databinding.ItemCommentsBinding
|
||||||
|
import ani.dantotsu.loadImage
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.statusBarHeight
|
||||||
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.RequestBuilder
|
||||||
|
import com.bumptech.glide.RequestManager
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.xwray.groupie.GroupAdapter
|
||||||
|
import com.xwray.groupie.GroupieAdapter
|
||||||
|
import com.xwray.groupie.Section
|
||||||
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
|
import io.noties.markwon.editor.MarkwonEditor
|
||||||
|
import io.noties.markwon.editor.MarkwonEditorTextWatcher
|
||||||
|
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
||||||
|
import io.noties.markwon.ext.tables.TablePlugin
|
||||||
|
import io.noties.markwon.ext.tasklist.TaskListPlugin
|
||||||
|
import io.noties.markwon.html.HtmlPlugin
|
||||||
|
import io.noties.markwon.image.AsyncDrawable
|
||||||
|
import io.noties.markwon.image.glide.GlideImagesPlugin
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class CommentsFragment : AppCompatActivity(){
|
||||||
|
lateinit var binding: FragmentCommentsBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeManager(this).applyTheme()
|
||||||
|
binding = FragmentCommentsBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
//get the media id from the intent
|
||||||
|
val mediaId = intent.getIntExtra("mediaId", -1)
|
||||||
|
if (mediaId == -1) {
|
||||||
|
snackString("Invalid Media ID")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = GroupieAdapter()
|
||||||
|
val section = Section()
|
||||||
|
val markwon = buildMarkwon()
|
||||||
|
|
||||||
|
binding.commentsLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = statusBarHeight
|
||||||
|
bottomMargin = navBarHeight
|
||||||
|
}
|
||||||
|
binding.commentUserAvatar.loadImage(Anilist.avatar)
|
||||||
|
binding.commentTitle.text = "Work in progress"
|
||||||
|
val markwonEditor = MarkwonEditor.create(markwon)
|
||||||
|
binding.commentInput.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(markwonEditor))
|
||||||
|
binding.commentSend.setOnClickListener {
|
||||||
|
if (CommentsAPI.isBanned) {
|
||||||
|
snackString("You are banned from commenting :(")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
binding.commentInput.text.toString().let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
binding.commentInput.text.clear()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val success = withContext(Dispatchers.IO) {
|
||||||
|
CommentsAPI.comment(mediaId, null, it)
|
||||||
|
}
|
||||||
|
if (success != null)
|
||||||
|
section.add(CommentItem(success, buildMarkwon(), section))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snackString("Comment cannot be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.commentInput.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: android.text.Editable?) {
|
||||||
|
if (binding.commentInput.text.length > 300) {
|
||||||
|
binding.commentInput.text.delete(300, binding.commentInput.text.length)
|
||||||
|
snackString("Comment cannot be longer than 300 characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.commentsList.adapter = adapter
|
||||||
|
binding.commentsList.layoutManager = LinearLayoutManager(this)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val comments = CommentsAPI.getCommentsForId(mediaId)
|
||||||
|
comments?.comments?.forEach {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
section.add(CommentItem(it, buildMarkwon(), section))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.add(section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMarkwon(): Markwon {
|
||||||
|
val markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
|
.usePlugin(StrikethroughPlugin.create())
|
||||||
|
.usePlugin(HtmlPlugin.create())
|
||||||
|
.usePlugin(TablePlugin.create(this))
|
||||||
|
.usePlugin(TaskListPlugin.create(this))
|
||||||
|
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
|
||||||
|
|
||||||
|
private val requestManager: RequestManager = Glide.with(this@CommentsFragment).apply {
|
||||||
|
addDefaultRequestListener(object : RequestListener<Any> {
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: Any,
|
||||||
|
model: Any,
|
||||||
|
target: Target<Any>,
|
||||||
|
dataSource: DataSource,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
if (resource is GifDrawable) {
|
||||||
|
resource.start()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Any>,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
|
||||||
|
return requestManager.load(drawable.destination)
|
||||||
|
}
|
||||||
|
override fun cancel(target: Target<*>) {
|
||||||
|
requestManager.clear(target)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.build()
|
||||||
|
return markwon
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package ani.dantotsu.media.comments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
|
||||||
|
class SpoilerTextView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : AppCompatTextView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val replaceWith = "▓"
|
||||||
|
private var originalSpanned: SpannableStringBuilder? = null
|
||||||
|
|
||||||
|
fun setSpoilerText(text: Spanned, markwon: Markwon) : Spanned {
|
||||||
|
val pattern = Regex("\\|\\|(.*?)\\|\\|")
|
||||||
|
val matcher = pattern.toPattern().matcher(text)
|
||||||
|
val spannableBuilder = SpannableStringBuilder(text)
|
||||||
|
//remove the "||" from the text
|
||||||
|
val originalBuilder = SpannableStringBuilder(text)
|
||||||
|
originalSpanned = originalBuilder
|
||||||
|
|
||||||
|
val map = mutableMapOf<Int, Int>()
|
||||||
|
while (matcher.find()) {
|
||||||
|
val start = matcher.start()
|
||||||
|
val end = matcher.end()
|
||||||
|
map[start] = end
|
||||||
|
}
|
||||||
|
|
||||||
|
map.forEach { (start, end) ->
|
||||||
|
val replacement = replaceWith.repeat(end - start)
|
||||||
|
spannableBuilder.replace(start, end, replacement)
|
||||||
|
}
|
||||||
|
val spannableString = SpannableString(spannableBuilder)
|
||||||
|
map.forEach { (start, end) ->
|
||||||
|
val clickableSpan = object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
markwon.setParsedMarkdown(this@SpoilerTextView, originalSpanned!!.delete(end - 2, end).delete(start, start + 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spannableString.setSpan(
|
||||||
|
clickableSpan,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
return spannableString
|
||||||
|
}
|
||||||
|
}
|
|
@ -160,6 +160,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
||||||
DiscordAvatar(Pref(Location.Protected, String::class, "")),
|
DiscordAvatar(Pref(Location.Protected, String::class, "")),
|
||||||
AnilistToken(Pref(Location.Protected, String::class, "")),
|
AnilistToken(Pref(Location.Protected, String::class, "")),
|
||||||
AnilistUserName(Pref(Location.Protected, String::class, "")),
|
AnilistUserName(Pref(Location.Protected, String::class, "")),
|
||||||
|
AnilistUserId(Pref(Location.Protected, String::class, "")),
|
||||||
MALCodeChallenge(Pref(Location.Protected, String::class, "")),
|
MALCodeChallenge(Pref(Location.Protected, String::class, "")),
|
||||||
MALToken(Pref(Location.Protected, MAL.ResponseToken::class, "")),
|
MALToken(Pref(Location.Protected, MAL.ResponseToken::class, "")),
|
||||||
}
|
}
|
|
@ -60,11 +60,13 @@
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:fontFamily="@font/poppins_semi_bold"
|
android:fontFamily="@font/poppins_semi_bold"
|
||||||
android:hint="Add a comment..."
|
android:hint="Add a comment..."
|
||||||
android:inputType="text"
|
android:inputType="textMultiLine"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:background="@drawable/card_outline"
|
android:background="@drawable/card_outline"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
|
android:maxLength="300"
|
||||||
|
android:maxLines="15"
|
||||||
tools:ignore="HardcodedText"
|
tools:ignore="HardcodedText"
|
||||||
android:autofillHints="The One Piece is real" />
|
android:autofillHints="The One Piece is real" />
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<ani.dantotsu.media.comments.SpoilerTextView
|
||||||
android:id="@+id/commentText"
|
android:id="@+id/commentText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -119,6 +119,7 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/commentUpVote"
|
android:id="@+id/commentUpVote"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue