Spotlight Creator Info
This commit is contained in:
@@ -957,6 +957,10 @@
|
|||||||
"name": "أيقونة اسم مستخدم تعليقات منصة الأضواء",
|
"name": "أيقونة اسم مستخدم تعليقات منصة الأضواء",
|
||||||
"description": "اختر الأيقونة التي يتم عرضها بجانب أسماء المستخدمين في تعليقات منصة الأضواء"
|
"description": "اختر الأيقونة التي يتم عرضها بجانب أسماء المستخدمين في تعليقات منصة الأضواء"
|
||||||
},
|
},
|
||||||
|
"spotlight_creator_info": {
|
||||||
|
"name": "معلومات منشئ منصة الأضواء",
|
||||||
|
"description": "عرض زر معلومات على سنابات منصة الأضواء/اكتشف لرؤية اسم العرض واسم المستخدم ومعرف المستخدم للمنشئ"
|
||||||
|
},
|
||||||
"bypass_video_length_restriction": {
|
"bypass_video_length_restriction": {
|
||||||
"name": "تجاوز قيود طول الفيديو",
|
"name": "تجاوز قيود طول الفيديو",
|
||||||
"description": "مفرد: يرسل فيديو واحد\nمقسم: يقسم الفيديو بعد التحرير"
|
"description": "مفرد: يرسل فيديو واحد\nمقسم: يقسم الفيديو بعد التحرير"
|
||||||
@@ -3128,6 +3132,25 @@
|
|||||||
"select_date_first": "يرجى تحديد تاريخ أولاً",
|
"select_date_first": "يرجى تحديد تاريخ أولاً",
|
||||||
"invalid_time": "يرجى تحديد وقت مستقبلي"
|
"invalid_time": "يرجى تحديد وقت مستقبلي"
|
||||||
},
|
},
|
||||||
|
"spotlight_creator_info": {
|
||||||
|
"title": "معلومات المنشئ",
|
||||||
|
"close": "إغلاق",
|
||||||
|
"creator_info": "معلومات المنشئ",
|
||||||
|
"display_name": "اسم العرض",
|
||||||
|
"username": "اسم المستخدم",
|
||||||
|
"user_id": "معرف المستخدم",
|
||||||
|
"posted_on": "نُشر في",
|
||||||
|
"loading_username": "جاري التحميل…",
|
||||||
|
"username_copied": "تم نسخ اسم المستخدم",
|
||||||
|
"user_id_copied": "تم نسخ معرف المستخدم",
|
||||||
|
"friend_status": "حالة الصداقة",
|
||||||
|
"mutual_friend": "صديق مشترك",
|
||||||
|
"following": "متابَع",
|
||||||
|
"friend_request_sent": "تم إرسال الطلب",
|
||||||
|
"friend_request_received": "تم استلام الطلب",
|
||||||
|
"blocked": "محظور",
|
||||||
|
"friend_removed": "تمت إزالته"
|
||||||
|
},
|
||||||
"auto_reply_messages": {
|
"auto_reply_messages": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"add_message": "إضافة رسالة",
|
"add_message": "إضافة رسالة",
|
||||||
|
|||||||
@@ -1796,6 +1796,10 @@
|
|||||||
"name": "Spotlight Comments Username Icon",
|
"name": "Spotlight Comments Username Icon",
|
||||||
"description": "Choose which icon is displayed next to usernames in Spotlight comments"
|
"description": "Choose which icon is displayed next to usernames in Spotlight comments"
|
||||||
},
|
},
|
||||||
|
"spotlight_creator_info": {
|
||||||
|
"name": "Spotlight Creator Info",
|
||||||
|
"description": "Show an info button on Spotlight/Discover snaps to view creator display name, username, and user ID"
|
||||||
|
},
|
||||||
"bypass_video_length_restriction": {
|
"bypass_video_length_restriction": {
|
||||||
"name": "Bypass Video Length Restrictions",
|
"name": "Bypass Video Length Restrictions",
|
||||||
"description": "Single: sends a single video\nSplit: split videos after editing"
|
"description": "Single: sends a single video\nSplit: split videos after editing"
|
||||||
@@ -3223,6 +3227,25 @@
|
|||||||
"select_date_first": "Please select a date",
|
"select_date_first": "Please select a date",
|
||||||
"invalid_time": "Please select a future time"
|
"invalid_time": "Please select a future time"
|
||||||
},
|
},
|
||||||
|
"spotlight_creator_info": {
|
||||||
|
"title": "Creator Info",
|
||||||
|
"close": "Close",
|
||||||
|
"creator_info": "Creator Info",
|
||||||
|
"display_name": "Display name",
|
||||||
|
"username": "Username",
|
||||||
|
"user_id": "User ID",
|
||||||
|
"posted_on": "Posted",
|
||||||
|
"loading_username": "Loading…",
|
||||||
|
"username_copied": "Username copied",
|
||||||
|
"user_id_copied": "User ID copied",
|
||||||
|
"friend_status": "Friend status",
|
||||||
|
"mutual_friend": "Mutual friend",
|
||||||
|
"following": "Following",
|
||||||
|
"friend_request_sent": "Request sent",
|
||||||
|
"friend_request_received": "Request received",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"friend_removed": "Removed"
|
||||||
|
},
|
||||||
"auto_reply_messages": {
|
"auto_reply_messages": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"add_message": "Add Message",
|
"add_message": "Add Message",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class Global : ConfigContainer() {
|
|||||||
val disableMemoriesSnapFeed = boolean("disable_memories_snap_feed")
|
val disableMemoriesSnapFeed = boolean("disable_memories_snap_feed")
|
||||||
val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() }
|
val spotlightCommentsUsername = boolean("spotlight_comments_username") { requireRestart() }
|
||||||
val spotlightCommentsUsernameIcon = string("spotlight_comments_username_icon", "[👤]") { requireRestart() }
|
val spotlightCommentsUsernameIcon = string("spotlight_comments_username_icon", "[👤]") { requireRestart() }
|
||||||
|
val spotlightCreatorInfo = boolean("spotlight_creator_info") { requireRestart() }
|
||||||
val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(
|
val bypassVideoLengthRestriction = unique("bypass_video_length_restriction", "split", "single") { addNotices(
|
||||||
FeatureNotice.BAN_RISK); requireRestart() }
|
FeatureNotice.BAN_RISK); requireRestart() }
|
||||||
val defaultVideoPlaybackRate = float("default_video_playback_rate", 1.0F) { requireRestart(); inputCheck = { (it.toFloatOrNull() ?: 1.0F) in 0.1F..4.0F} }
|
val defaultVideoPlaybackRate = float("default_video_playback_rate", 1.0F) { requireRestart(); inputCheck = { (it.toFloatOrNull() ?: 1.0F) in 0.1F..4.0F} }
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class FeatureManager(
|
|||||||
PreventForcedLogout(),
|
PreventForcedLogout(),
|
||||||
ConversationToolbox(),
|
ConversationToolbox(),
|
||||||
SpotlightCommentsUsername(),
|
SpotlightCommentsUsername(),
|
||||||
|
SpotlightCreatorInfo(),
|
||||||
OperaViewerParamsOverride(),
|
OperaViewerParamsOverride(),
|
||||||
StealthModeIndicator(),
|
StealthModeIndicator(),
|
||||||
DisablePermissionRequests(),
|
DisablePermissionRequests(),
|
||||||
|
|||||||
@@ -0,0 +1,503 @@
|
|||||||
|
package me.eternal.purrfectsnap.core.features.impl.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.eternal.purrfectsnap.common.data.FriendLinkType
|
||||||
|
import me.eternal.purrfectsnap.common.database.impl.FriendInfo
|
||||||
|
import me.eternal.purrfectsnap.common.util.ktx.copyToClipboard
|
||||||
|
import me.eternal.purrfectsnap.core.event.events.impl.BindViewEvent
|
||||||
|
import me.eternal.purrfectsnap.core.features.Feature
|
||||||
|
import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging
|
||||||
|
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayPalette
|
||||||
|
import me.eternal.purrfectsnap.core.ui.PurrfectOverlayTheme
|
||||||
|
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||||
|
import me.eternal.purrfectsnap.core.util.hook.hook
|
||||||
|
import me.eternal.purrfectsnap.core.util.ktx.getObjectField
|
||||||
|
import me.eternal.purrfectsnap.core.wrapper.impl.Snapchatter
|
||||||
|
import me.eternal.purrfectsnap.core.wrapper.impl.media.opera.Layer
|
||||||
|
import me.eternal.purrfectsnap.core.wrapper.impl.media.opera.ParamMap
|
||||||
|
import me.eternal.purrfectsnap.mapper.impl.OperaPageViewControllerMapper
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SpotlightCreatorInfo : Feature("SpotlightCreatorInfo") {
|
||||||
|
private data class CreatorInfo(
|
||||||
|
val snapId: String,
|
||||||
|
val creatorDisplayName: String,
|
||||||
|
val creatorUserId: String?,
|
||||||
|
val timestamp: Long?,
|
||||||
|
val friendLinkType: FriendLinkType? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var currentCreatorInfo: CreatorInfo? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isInSpotlight: Boolean = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var currentSnapId: String? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var lastSpotlightUpdateTime: Long = 0
|
||||||
|
|
||||||
|
private val userIdCache = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
override fun init() {
|
||||||
|
if (!context.config.global.spotlightCreatorInfo.get()) return
|
||||||
|
|
||||||
|
onNextActivityCreate {
|
||||||
|
context.event.subscribe(BindViewEvent::class) { event ->
|
||||||
|
val modelStr = event.prevModel.toString()
|
||||||
|
if (!modelStr.contains("Spotlight") && !modelStr.contains("SINGLE_SNAP_STORY")) return@subscribe
|
||||||
|
|
||||||
|
val userId = modelStr.takeIf { it.contains("userId=") }
|
||||||
|
?.substringAfter("userId=")
|
||||||
|
?.substringBefore(",")
|
||||||
|
?.substringBefore(")")
|
||||||
|
?.takeIf { it.isNotBlank() && it != "null" }
|
||||||
|
|
||||||
|
val snapId = modelStr.takeIf { it.contains("snapId=") || modelStr.contains("SNAP_ID") }
|
||||||
|
?.substringAfter("snapId=")
|
||||||
|
?.substringBefore(",")
|
||||||
|
?.substringBefore(")")
|
||||||
|
?: modelStr.takeIf { it.contains("SNAP_ID") }
|
||||||
|
?.substringAfter("SNAP_ID=")
|
||||||
|
?.substringBefore(",")
|
||||||
|
?.substringBefore(")")
|
||||||
|
|
||||||
|
if (userId != null && snapId != null) {
|
||||||
|
userIdCache[snapId] = userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.mappings.useMapper(OperaPageViewControllerMapper::class) {
|
||||||
|
arrayOf(onDisplayStateChange, onDisplayStateChangeGesture).forEach { methodName ->
|
||||||
|
classReference.get()?.hook(
|
||||||
|
methodName.get() ?: return@forEach,
|
||||||
|
HookStage.AFTER
|
||||||
|
) onCreatorInfoHook@{ param ->
|
||||||
|
val viewState = (param.thisObject<Any>()).getObjectField(viewStateField.get()!!).toString()
|
||||||
|
val operaLayerList = (param.thisObject<Any>()).getObjectField(layerListField.get()!!) as ArrayList<*>
|
||||||
|
|
||||||
|
if (operaLayerList.isEmpty()) {
|
||||||
|
if (isInSpotlight) {
|
||||||
|
isInSpotlight = false
|
||||||
|
currentCreatorInfo = null
|
||||||
|
currentSnapId = null
|
||||||
|
lastSpotlightUpdateTime = 0
|
||||||
|
}
|
||||||
|
return@onCreatorInfoHook
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaParamMap: ParamMap = operaLayerList.map { Layer(it) }.first().paramMap
|
||||||
|
val snapSource = mediaParamMap["SNAP_SOURCE"]?.toString()
|
||||||
|
|
||||||
|
if (snapSource != "SINGLE_SNAP_STORY") {
|
||||||
|
if (isInSpotlight) {
|
||||||
|
isInSpotlight = false
|
||||||
|
currentCreatorInfo = null
|
||||||
|
currentSnapId = null
|
||||||
|
lastSpotlightUpdateTime = 0
|
||||||
|
}
|
||||||
|
return@onCreatorInfoHook
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewState != "FULLY_DISPLAYED") return@onCreatorInfoHook
|
||||||
|
|
||||||
|
val snapId = mediaParamMap["SNAP_ID"]?.toString()
|
||||||
|
if (snapId == null || snapId == currentSnapId) return@onCreatorInfoHook
|
||||||
|
|
||||||
|
val creatorDisplayName = mediaParamMap["CREATOR_DISPLAY_NAME"]?.toString()
|
||||||
|
var creatorUserId = mediaParamMap["USER_ID"]?.toString()
|
||||||
|
?: mediaParamMap["CREATOR_USER_ID"]?.toString()
|
||||||
|
?: mediaParamMap["TOPIC_SNAP_CREATOR_USER_ID"]?.toString()
|
||||||
|
|
||||||
|
val playableStorySnapRecord = mediaParamMap["PLAYABLE_STORY_SNAP_RECORD"]?.toString()
|
||||||
|
if (creatorUserId == null && playableStorySnapRecord != null) {
|
||||||
|
val recordStr = playableStorySnapRecord.toString()
|
||||||
|
creatorUserId = recordStr.substringAfter("userId=", "")
|
||||||
|
.substringBefore(",")
|
||||||
|
.substringBefore(")")
|
||||||
|
.takeIf { it.isNotBlank() && it != "null" }
|
||||||
|
}
|
||||||
|
if (creatorUserId == null) creatorUserId = userIdCache[snapId]
|
||||||
|
|
||||||
|
val creationTimestamp = playableStorySnapRecord?.substringAfter("creationTimestamp=")
|
||||||
|
?.substringBefore(",")?.toLongOrNull()
|
||||||
|
val timestamp = creationTimestamp ?: mediaParamMap["SNAP_TIMESTAMP"]?.toString()?.toLongOrNull()
|
||||||
|
|
||||||
|
if (creatorDisplayName != null || creatorUserId != null) {
|
||||||
|
val friendInfo: FriendInfo? = creatorUserId?.let { context.database.getFriendInfo(it) }
|
||||||
|
val linkType = friendInfo?.let { FriendLinkType.fromValue(it.friendLinkType) }
|
||||||
|
|
||||||
|
currentSnapId = snapId
|
||||||
|
isInSpotlight = true
|
||||||
|
lastSpotlightUpdateTime = System.currentTimeMillis()
|
||||||
|
currentCreatorInfo = CreatorInfo(
|
||||||
|
snapId = snapId,
|
||||||
|
creatorDisplayName = creatorDisplayName ?: context.translation.getOrNull("common.unknown") ?: "Unknown",
|
||||||
|
creatorUserId = creatorUserId,
|
||||||
|
timestamp = timestamp,
|
||||||
|
friendLinkType = linkType
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (isInSpotlight) {
|
||||||
|
isInSpotlight = false
|
||||||
|
currentCreatorInfo = null
|
||||||
|
currentSnapId = null
|
||||||
|
lastSpotlightUpdateTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.inAppOverlay.addCustomComposable {
|
||||||
|
var info by remember { mutableStateOf<CreatorInfo?>(null) }
|
||||||
|
var inSpotlight by remember { mutableStateOf(false) }
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
val wasInSpotlight = inSpotlight
|
||||||
|
info = currentCreatorInfo
|
||||||
|
inSpotlight = isInSpotlight
|
||||||
|
if (wasInSpotlight && !inSpotlight) showDialog = false
|
||||||
|
if (inSpotlight && info != null && lastSpotlightUpdateTime > 0 && !showDialog) {
|
||||||
|
if (System.currentTimeMillis() - lastSpotlightUpdateTime > 4000) {
|
||||||
|
inSpotlight = false
|
||||||
|
info = null
|
||||||
|
showDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSpotlight && info != null) {
|
||||||
|
InfoIconOverlay(
|
||||||
|
onClick = {
|
||||||
|
showDialog = true
|
||||||
|
lastSpotlightUpdateTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDialog && info != null) {
|
||||||
|
CreatorInfoDialog(
|
||||||
|
creatorInfo = info!!,
|
||||||
|
onDismiss = { showDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoIconOverlay(onClick: () -> Unit) {
|
||||||
|
val translation = remember { context.translation.getCategory("spotlight_creator_info") }
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
shape = CircleShape,
|
||||||
|
color = Color.Transparent,
|
||||||
|
border = BorderStroke(1.dp, PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.6f)),
|
||||||
|
shadowElevation = 8.dp
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.radialGradient(
|
||||||
|
listOf(
|
||||||
|
PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.4f),
|
||||||
|
PurrfectOverlayPalette.cardOverlayColor
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
contentDescription = translation.getOrNull("creator_info") ?: "Creator Info",
|
||||||
|
tint = PurrfectOverlayPalette.textPrimary,
|
||||||
|
modifier = Modifier.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CreatorInfoDialog(creatorInfo: CreatorInfo, onDismiss: () -> Unit) {
|
||||||
|
var snapchatterInfo by remember { mutableStateOf<Snapchatter?>(null) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
val translation = remember { context.translation.getCategory("spotlight_creator_info") }
|
||||||
|
|
||||||
|
LaunchedEffect(creatorInfo.snapId) {
|
||||||
|
snapchatterInfo = null
|
||||||
|
isLoading = false
|
||||||
|
if (creatorInfo.creatorUserId != null) {
|
||||||
|
isLoading = true
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val info = runCatching {
|
||||||
|
context.feature(Messaging::class).fetchSnapchatterInfos(listOf(creatorInfo.creatorUserId!!)).firstOrNull()
|
||||||
|
}.onFailure {
|
||||||
|
context.log.error("Failed to fetch creator info for ${creatorInfo.creatorUserId}", it)
|
||||||
|
}.getOrNull()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
snapchatterInfo = info
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
PurrfectOverlayTheme {
|
||||||
|
val shape = RoundedCornerShape(20.dp)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = shape,
|
||||||
|
color = Color.Transparent,
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.55f),
|
||||||
|
PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.35f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shadowElevation = 0.dp,
|
||||||
|
tonalElevation = 0.dp
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(PurrfectOverlayPalette.cardOverlay, shape)
|
||||||
|
.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = translation.getOrNull("title") ?: "Creator Info",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = PurrfectOverlayPalette.textPrimary
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss, modifier = Modifier.size(32.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = translation.getOrNull("close") ?: "Close",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = PurrfectOverlayPalette.textSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(color = PurrfectOverlayPalette.textSecondary.copy(alpha = 0.2f))
|
||||||
|
|
||||||
|
creatorInfo.timestamp?.let { ts ->
|
||||||
|
InfoRow(
|
||||||
|
icon = Icons.Default.Schedule,
|
||||||
|
label = translation.getOrNull("posted_on") ?: "Posted",
|
||||||
|
value = formatDate(ts)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val displayName = snapchatterInfo?.displayName ?: creatorInfo.creatorDisplayName
|
||||||
|
InfoRow(
|
||||||
|
icon = Icons.Default.Person,
|
||||||
|
label = translation.getOrNull("display_name") ?: "Display name",
|
||||||
|
value = displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = PurrfectOverlayPalette.glowPrimary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
translation.getOrNull("loading_username") ?: "Loading…",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = PurrfectOverlayPalette.textSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val username = snapchatterInfo?.username
|
||||||
|
InfoRowWithCopy(
|
||||||
|
icon = Icons.Default.AccountCircle,
|
||||||
|
label = translation.getOrNull("username") ?: "Username",
|
||||||
|
value = username ?: (context.translation.getOrNull("common.unknown") ?: "—"),
|
||||||
|
canCopy = username != null,
|
||||||
|
onCopy = {
|
||||||
|
username?.let {
|
||||||
|
context.androidContext.copyToClipboard(it, translation.getOrNull("username") ?: "Username")
|
||||||
|
context.inAppOverlay.showStatusToast(Icons.Default.Check, translation.getOrNull("username_copied") ?: "Copied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoRowWithCopy(
|
||||||
|
icon = Icons.Default.Fingerprint,
|
||||||
|
label = translation.getOrNull("user_id") ?: "User ID",
|
||||||
|
value = creatorInfo.creatorUserId ?: (context.translation.getOrNull("common.unknown") ?: "—"),
|
||||||
|
canCopy = creatorInfo.creatorUserId != null,
|
||||||
|
onCopy = {
|
||||||
|
creatorInfo.creatorUserId?.let {
|
||||||
|
context.androidContext.copyToClipboard(it, translation.getOrNull("user_id") ?: "User ID")
|
||||||
|
context.inAppOverlay.showStatusToast(Icons.Default.Check, translation.getOrNull("user_id_copied") ?: "Copied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
creatorInfo.friendLinkType?.let { linkType ->
|
||||||
|
val statusText = when (linkType) {
|
||||||
|
FriendLinkType.MUTUAL -> translation.getOrNull("mutual_friend") ?: "Mutual friend"
|
||||||
|
FriendLinkType.FOLLOWING -> translation.getOrNull("following") ?: "Following"
|
||||||
|
FriendLinkType.OUTGOING -> translation.getOrNull("friend_request_sent") ?: "Request sent"
|
||||||
|
FriendLinkType.INCOMING -> translation.getOrNull("friend_request_received") ?: "Request received"
|
||||||
|
FriendLinkType.BLOCKED -> translation.getOrNull("blocked") ?: "Blocked"
|
||||||
|
FriendLinkType.DELETED -> translation.getOrNull("friend_removed") ?: "Removed"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (statusText != null) {
|
||||||
|
InfoRow(
|
||||||
|
icon = Icons.Default.Person,
|
||||||
|
label = translation.getOrNull("friend_status") ?: "Friend status",
|
||||||
|
value = statusText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRow(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = label,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.9f)
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = PurrfectOverlayPalette.textSecondary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = PurrfectOverlayPalette.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRowWithCopy(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
canCopy: Boolean,
|
||||||
|
onCopy: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = label,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.9f)
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = PurrfectOverlayPalette.textSecondary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = PurrfectOverlayPalette.textPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (canCopy) {
|
||||||
|
IconButton(onClick = onCopy, modifier = Modifier.size(32.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ContentCopy,
|
||||||
|
contentDescription = "Copy",
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = PurrfectOverlayPalette.glowSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDate(timestamp: Long): String {
|
||||||
|
val ts = if (timestamp < 10000000000L) timestamp * 1000 else timestamp
|
||||||
|
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(ts))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user