multiple fixes & improvements

This commit is contained in:
particle-box
2026-03-07 00:00:36 +05:30
parent 97797127bd
commit 89748cd261
21 changed files with 928 additions and 172 deletions

View File

@@ -45,7 +45,7 @@ body:
attributes:
label: "SnapEnhance Version"
description: "On which SnapEnhance version is this happening?"
placeholder: "ex. 1.2.5"
placeholder: "ex. 1.3.6"
validations:
required: true
- type: checkboxes

View File

@@ -4,6 +4,7 @@ import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.os.RemoteException
import kotlinx.coroutines.runBlocking
import me.eternal.purrfectsnap.RemoteSideContext
import me.eternal.purrfectsnap.SharedContextHolder
@@ -27,10 +28,26 @@ import kotlin.system.measureTimeMillis
class BridgeService : Service() {
private lateinit var remoteSideContext: RemoteSideContext
lateinit var syncCallback: SyncCallback
private var syncCallback: SyncCallback? = null
private var syncCallbackBinder: IBinder? = null
private val syncCallbackDeathRecipient = IBinder.DeathRecipient {
remoteSideContext.takeIf { ::remoteSideContext.isInitialized }?.log?.warn("Sync callback binder died")
clearSyncCallback()
}
var messagingBridge: MessagingBridge? = null
private fun clearSyncCallback() {
syncCallbackBinder?.let { binder ->
runCatching {
binder.unlinkToDeath(syncCallbackDeathRecipient, 0)
}
}
syncCallbackBinder = null
syncCallback = null
}
override fun onDestroy() {
clearSyncCallback()
if (::remoteSideContext.isInitialized) {
remoteSideContext.bridgeService = null
}
@@ -47,8 +64,10 @@ class BridgeService : Service() {
}
fun triggerScopeSync(scope: SocialScope, id: String, updateOnly: Boolean = false) {
val callback = syncCallback ?: return
runCatching {
if (!syncCallback.asBinder().pingBinder()) {
if (!callback.asBinder().pingBinder()) {
clearSyncCallback()
remoteSideContext.log.warn("Failed to sync $scope $id: Callback is dead")
return
}
@@ -57,11 +76,11 @@ class BridgeService : Service() {
val syncedObject = when (scope) {
SocialScope.FRIEND -> {
if (updateOnly && database.getFriendInfo(id) == null) return
syncCallback.syncFriend(id)
callback.syncFriend(id)
}
SocialScope.GROUP -> {
if (updateOnly && database.getGroupInfo(id) == null) return
syncCallback.syncGroup(id)
callback.syncGroup(id)
}
} ?: run {
remoteSideContext.log.warn("Failed to sync $scope $id")
@@ -77,6 +96,11 @@ class BridgeService : Service() {
}
}
}.onFailure {
if (it is RemoteException) {
clearSyncCallback()
remoteSideContext.log.warn("Failed to sync $scope $id: Callback is dead")
return@onFailure
}
remoteSideContext.log.error("Failed to sync $scope $id", it)
}
}
@@ -162,7 +186,16 @@ class BridgeService : Service() {
}
override fun sync(callback: SyncCallback) {
clearSyncCallback()
syncCallback = callback
syncCallbackBinder = callback.asBinder().also { binder ->
runCatching {
binder.linkToDeath(syncCallbackDeathRecipient, 0)
}.onFailure {
clearSyncCallback()
throw it
}
}
measureTimeMillis {
remoteSideContext.database.getFriends().map { it.userId } .forEach { friendId ->
triggerScopeSync(SocialScope.FRIEND, friendId, true)

View File

@@ -794,7 +794,9 @@ class Navigation(
} else {
navigation("main_" + route.routeInfo.id, route.routeInfo.id) {
composable("main_" + route.routeInfo.id) { route.content.invoke(it) }
children.forEach { child -> composable(child.routeInfo.id) { child.content.invoke(it) } }
children.forEach { child ->
composable(child.routeInfo.id) { child.content.invoke(it) }
}
route.customComposables.invoke(this)
}
}

View File

@@ -0,0 +1,51 @@
package me.eternal.purrfectsnap.ui.manager
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.ScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@Composable
fun rememberRouteScrollState(key: String): ScrollState {
val scrollState = remember(key) {
ScrollState(RouteStateCache.scrollOffsets[key] ?: 0)
}
LaunchedEffect(key, scrollState.value) {
RouteStateCache.scrollOffsets[key] = scrollState.value
}
DisposableEffect(key, scrollState) {
onDispose {
RouteStateCache.scrollOffsets[key] = scrollState.value
}
}
return scrollState
}
@Composable
fun rememberRouteLazyListState(key: String): LazyListState {
val savedState = RouteStateCache.lazyListOffsets[key]
val listState = remember(key) {
LazyListState(
firstVisibleItemIndex = savedState?.first ?: 0,
firstVisibleItemScrollOffset = savedState?.second ?: 0
)
}
LaunchedEffect(key, listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
RouteStateCache.lazyListOffsets[key] =
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}
DisposableEffect(key, listState) {
onDispose {
RouteStateCache.lazyListOffsets[key] =
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}
}
return listState
}
private object RouteStateCache {
val scrollOffsets = mutableMapOf<String, Int>()
val lazyListOffsets = mutableMapOf<String, Pair<Int, Int>>()
}

View File

@@ -301,12 +301,13 @@ class TasksRootSection : Routes.Route() {
)
Column {
Text(
text = context.translation["delete_files_option"],
text = context.translation["manager.sections.tasks.delete_files_option"],
color = Color.White,
fontWeight = FontWeight.SemiBold
)
Text(
text = context.translation["delete_files_option_hint"] ?: "Also remove downloaded files",
text = context.translation["manager.sections.tasks.delete_files_option_hint"]
?: "Also remove downloaded files",
color = PurrfectPalette.textSecondary,
style = MaterialTheme.typography.bodySmall
)

View File

@@ -91,6 +91,7 @@ import com.google.gson.reflect.TypeToken
import me.eternal.purrfectsnap.common.ui.TopBarActionButton
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList
import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.rememberRouteLazyListState
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.util.*
import org.json.JSONArray
@@ -254,7 +255,10 @@ class FeaturesRootSection : Routes.Route() {
}
override val content: @Composable (NavBackStackEntry) -> Unit = {
Container(context.config.root)
Container(
configContainer = context.config.root,
stateKey = "${routeInfo.id}:root"
)
}
override val customComposables: NavGraphBuilder.() -> Unit = {
@@ -277,6 +281,7 @@ class FeaturesRootSection : Routes.Route() {
val containerSubtitle = translation[it.key.propertyDescription()]
Container(
configContainer = it.value.get() as ConfigContainer,
stateKey = "${routeInfo.id}:container:$containerName",
sectionTitle = containerTitle,
sectionSubtitle = containerSubtitle,
onBack = { routes.navController.popBackStack() }
@@ -297,6 +302,7 @@ class FeaturesRootSection : Routes.Route() {
PropertiesView(
properties = properties,
stateKey = "${routeInfo.id}:search:$keyword",
isSearchResults = true,
searchKeyword = keyword,
enableGlobalSearch = true,
@@ -1568,6 +1574,7 @@ class FeaturesRootSection : Routes.Route() {
@Composable
private fun PropertiesView(
properties: List<PropertyPair<*>>,
stateKey: String,
isSearchResults: Boolean = false,
activeSectionTitle: String? = null,
activeSectionSubtitle: String? = null,
@@ -1577,7 +1584,7 @@ class FeaturesRootSection : Routes.Route() {
) {
val density = LocalDensity.current
var controlsHeight by remember { mutableStateOf(96.dp) }
val listState = rememberLazyListState()
val listState = rememberRouteLazyListState(stateKey)
val sharedSearchHistory = remember { mutableStateListOf<String>().apply { addAll(loadSearchHistory()) } }
var liveSearchQuery by rememberSaveable { mutableStateOf(searchKeyword.orEmpty()) }
val isActiveSearch = isSearchResults || liveSearchQuery.isNotBlank()
@@ -1682,6 +1689,7 @@ class FeaturesRootSection : Routes.Route() {
@Composable
private fun Container(
configContainer: ConfigContainer,
stateKey: String,
sectionTitle: String? = null,
sectionSubtitle: String? = null,
searchKeyword: String? = null,
@@ -1693,6 +1701,7 @@ class FeaturesRootSection : Routes.Route() {
!it.key.params.flags.contains(ConfigFlag.HIDDEN)
}
},
stateKey = stateKey,
activeSectionTitle = sectionTitle,
activeSectionSubtitle = sectionSubtitle,
searchKeyword = searchKeyword,

View File

@@ -38,6 +38,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.ui.manager.rememberRouteScrollState
import me.eternal.purrfectsnap.common.data.RuleState
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
import me.eternal.purrfectsnap.common.ui.rememberAsyncUpdateDispatcher
@@ -241,7 +242,7 @@ class ManageRuleFeature : Routes.Route() {
.fillMaxSize()
.padding(top = topBarHeight + 10.dp)
.padding(horizontal = 12.dp, vertical = 10.dp)
.verticalScroll(rememberScrollState()),
.verticalScroll(rememberRouteScrollState(routeInfo.id)),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val headerShape = RoundedCornerShape(22.dp)

View File

@@ -1,13 +1,20 @@
package me.eternal.purrfectsnap.ui.manager.pages.location
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
@@ -16,6 +23,7 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import me.eternal.purrfectsnap.bridge.location.LocationCoordinates
import me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.util.AlertDialogs
@@ -38,54 +46,105 @@ fun AddCoordinatesDialog(
alertDialogs.DefaultDialogCard {
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
val fieldColors = TextFieldDefaults.colors(
focusedContainerColor = Color.White.copy(alpha = 0.08f),
unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedLabelColor = PurrfectPalette.textSecondary,
unfocusedLabelColor = PurrfectPalette.textSecondary,
cursorColor = PurrfectPalette.glowSecondary,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White
)
Surface(
shape = RoundedCornerShape(24.dp),
color = Color.Transparent,
tonalElevation = 0.dp,
shadowElevation = 12.dp
) {
Text(translation["save_coordinates_dialog_title"], fontSize = 20.sp, fontWeight = FontWeight.Bold)
OutlinedTextField(
Column(
modifier = Modifier
.focusRequester(focusRequester),
value = savedName,
onValueChange = { savedName = it },
label = { Text(translation["saved_name_dialog_hint"]) }
)
LaunchedEffect(Unit) {
delay(200)
focusRequester.requestFocus()
}
OutlinedTextField(
value = savedLatitude,
onValueChange = { savedLatitude = it },
label = { Text(translation["latitude_dialog_hint"]) }
)
OutlinedTextField(
value = savedLongitude,
onValueChange = { savedLongitude = it },
label = { Text(translation["longitude_dialog_hint"]) }
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.End
.background(
Brush.linearGradient(
listOf(
PurrfectPalette.cardOverlayColor.copy(alpha = 0.98f),
Color(0xFF1A143A).copy(alpha = 0.94f)
)
),
RoundedCornerShape(24.dp)
)
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Button(
onClick = {
confirm(LocationCoordinates().apply {
this.name = savedName.text
this.latitude = savedLatitude.toDoubleOrNull() ?: 0.0
this.longitude = savedLongitude.toDoubleOrNull() ?: 0.0
})
},
enabled = savedName.text.isNotBlank() && savedLatitude.isNotBlank() && savedLongitude.isNotBlank()
Text(
text = translation["save_coordinates_dialog_title"],
fontSize = 20.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.White
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = savedName,
onValueChange = { savedName = it },
label = { Text(translation["saved_name_dialog_hint"]) },
colors = fieldColors,
shape = RoundedCornerShape(18.dp),
singleLine = true
)
LaunchedEffect(Unit) {
delay(200)
focusRequester.requestFocus()
}
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = savedLatitude,
onValueChange = { savedLatitude = it },
label = { Text(translation["latitude_dialog_hint"]) },
colors = fieldColors,
shape = RoundedCornerShape(18.dp),
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = savedLongitude,
onValueChange = { savedLongitude = it },
label = { Text(translation["longitude_dialog_hint"]) },
colors = fieldColors,
shape = RoundedCornerShape(18.dp),
singleLine = true
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
horizontalArrangement = Arrangement.End
) {
Text(translation["save_dialog_button"])
Button(
onClick = {
confirm(LocationCoordinates().apply {
this.name = savedName.text
this.latitude = savedLatitude.toDoubleOrNull() ?: 0.0
this.longitude = savedLongitude.toDoubleOrNull() ?: 0.0
})
},
enabled = savedName.text.isNotBlank() && savedLatitude.isNotBlank() && savedLongitude.isNotBlank(),
colors = ButtonDefaults.buttonColors(
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.3f),
contentColor = Color.White,
disabledContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.16f),
disabledContentColor = Color.White.copy(alpha = 0.6f)
)
) {
Text(translation["save_dialog_button"])
}
}
}
}
}
}
}

View File

@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -221,16 +220,21 @@ class BetterLocationRoot : Routes.Route() {
}
@Composable
private fun ThemedEditLocationButton(onClick: () -> Unit) {
private fun CoordinateActionButton(
icon: androidx.compose.ui.graphics.vector.ImageVector,
description: String,
accent: Color,
onClick: () -> Unit
) {
FilledIconButton(
modifier = Modifier.size(40.dp),
onClick = onClick,
modifier = Modifier.size(42.dp),
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.surface,
contentColor = if (isSystemInDarkTheme()) Color.White else Color(0xFF151A1A),
),
onClick = onClick
containerColor = accent.copy(alpha = 0.22f),
contentColor = Color.White
)
) {
Icon(Icons.Default.Edit, contentDescription = translation["edit_location_button_description"])
Icon(icon, contentDescription = description)
}
}
@@ -674,15 +678,19 @@ class BetterLocationRoot : Routes.Route() {
color = PurrfectPalette.textSecondary
)
}
FilledIconButton(onClick = {
CoordinateActionButton(
icon = Icons.Default.Edit,
description = translation["edit_icon_description"],
accent = PurrfectPalette.glowPrimary
) {
showEditDialog = true
}) {
Icon(Icons.Default.Edit, contentDescription = translation["edit_icon_description"])
}
FilledIconButton(onClick = {
CoordinateActionButton(
icon = Icons.Default.DeleteOutline,
description = translation["delete_icon_description"],
accent = PurrfectPalette.glowSecondary
) {
showDeleteDialog = true
}) {
Icon(Icons.Default.DeleteOutline, contentDescription = translation["delete_icon_description"])
}
}
}

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
}
// You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.3.5").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("275").get().toInt())
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.3.6").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("276").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate.
// Include version code so each release has a different hash; use random for uniqueness within same version.

View File

@@ -1,3 +1,12 @@
## v1.3.6
- Fix: Crash issues for some devices
- Fix: Scroll state
- New: Redesign profile picture downloader & spotlight comments username dialog
- New: Change the profile picture download button to an icon
- New: Redesign playback slider & saved coordinates buttons
- Fix: Add missing translations(en_US only for now, all languages to be supported in the next update)
- Fix: Deadosobjectexception
## v1.3.5
- New: Added support for Snap versions upto v13.81(server side update, requires v1.3.2 and above)
- New: Migration from libxposed API to YukiHook

View File

@@ -340,6 +340,7 @@
"remove_selected_tasks_title": "Are you sure you want to remove selected tasks?",
"remove_all_tasks_title": "Are you sure you want to remove all tasks?",
"delete_files_option": "Also delete files",
"delete_files_option_hint": "Permanently remove the original files from storage",
"remove_selected_tasks_confirm": "Remove {count} tasks?",
"remove_all_tasks_confirm": "Remove all tasks?"
},
@@ -2994,9 +2995,20 @@
"profile_picture_downloader": {
"button": "Download Profile Picture",
"title": "Profile Picture Downloader",
"subtitle": "Choose which profile image to save",
"empty_state": "No profile pictures available. Please wait for the profile to load.",
"download_hint": "Tap to download",
"avatar_option": "Avatar",
"background_option": "Background"
},
"spotlight_comments_username_dialog": {
"title": "User Information",
"subtitle": "Spotlight comment profile details",
"username_label": "Username",
"display_name_label": "Display Name",
"user_id_label": "User ID",
"not_available": "Not available"
},
"call_start_confirmation": {
"dialog_title": "Start Call",
"dialog_message": "Are you sure you want to start a call?"
@@ -3506,6 +3518,8 @@
"cancel": "Cancel",
"documentation": "Documentation"
},
"include_saved_locations": "Include Saved Locations",
"include_saved_locations_description": "Export your saved location coordinates",
"common": {
"cancel": "Cancel",
"close": "Close",

View File

@@ -1,14 +1,47 @@
package me.eternal.purrfectsnap.core.features.impl.downloader
import android.annotation.SuppressLint
import android.widget.Button
import android.util.TypedValue
import android.widget.Button as AndroidButton
import android.widget.RelativeLayout
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
import me.eternal.purrfectsnap.common.util.protobuf.ProtoReader
import me.eternal.purrfectsnap.core.event.events.impl.AddViewEvent
import me.eternal.purrfectsnap.core.event.events.impl.NetworkApiRequestEvent
import me.eternal.purrfectsnap.core.event.events.impl.UnaryCallEvent
import me.eternal.purrfectsnap.core.features.Feature
import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper
class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
@SuppressLint("SetTextI18n")
@@ -32,41 +65,78 @@ class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
val buttonText = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.button"]
if ((0 until event.parent.childCount).any {
val child = event.parent.getChildAt(it)
child is Button && (child as Button).text == buttonText
child is AndroidButton && child.contentDescription == buttonText
}) return@subscribe
event.parent.addView(Button(event.parent.context).apply {
text = buttonText
layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 200, 0, 0)
event.parent.addView(AndroidButton(event.parent.context).apply {
text = ""
contentDescription = buttonText
val density = resources.displayMetrics.density
val buttonHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics).toInt()
minWidth = 0
minimumWidth = 0
minHeight = buttonHeight
minimumHeight = buttonHeight
setPadding(
(6 * density).toInt(),
0,
0,
0
)
setCompoundDrawablesWithIntrinsicBounds(android.R.drawable.stat_sys_download, 0, 0, 0)
layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, buttonHeight).apply {
setMargins((8 * density).toInt(), 200, 0, 0)
}
setOnClickListener {
ViewAppearanceHelper.newAlertDialogBuilder(
this@ProfilePictureDownloader.context.mainActivity!!
).apply {
setTitle(this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"])
val choices = mutableMapOf<String, String>()
backgroundUrl?.let { choices["background_option"] = it }
avatarUrl?.let { choices["avatar_option"] = it }
if (choices.isEmpty()) {
setMessage("No profile pictures available. Please wait for the profile to load.")
setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
} else {
setItems(choices.keys.map {
this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$it"]
}.toTypedArray()) { _, which ->
runCatching {
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
choices.values.elementAt(which),
friendUsername ?: "unknown"
)
}.onFailure {
this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it)
}
}
val choices = buildList {
backgroundUrl?.let {
add(
ProfilePictureChoice(
key = "background_option",
url = it,
iconType = ProfilePictureChoiceIcon.BACKGROUND
)
)
}
}.show()
avatarUrl?.let {
add(
ProfilePictureChoice(
key = "avatar_option",
url = it,
iconType = ProfilePictureChoiceIcon.AVATAR
)
)
}
}
createComposeAlertDialog(
this@ProfilePictureDownloader.context.mainActivity!!,
content = { alertDialog ->
ProfilePictureDialog(
title = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.title"],
subtitle = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.subtitle"],
emptyText = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.empty_state"],
downloadHint = this@ProfilePictureDownloader.context.translation["profile_picture_downloader.download_hint"],
closeLabel = this@ProfilePictureDownloader.context.translation["common.close"],
choices = choices,
optionLabel = { key ->
this@ProfilePictureDownloader.context.translation["profile_picture_downloader.$key"]
},
onDownload = { selectedUrl ->
runCatching {
this@ProfilePictureDownloader.context.feature(MediaDownloader::class).downloadProfilePicture(
selectedUrl,
friendUsername ?: "unknown"
)
}.onFailure {
this@ProfilePictureDownloader.context.log.error("Failed to download profile picture", it)
}
alertDialog.dismiss()
},
onDismiss = { alertDialog.dismiss() }
)
}
).show()
}
})
}
@@ -99,4 +169,213 @@ class ProfilePictureDownloader : Feature("ProfilePictureDownloader") {
}
}
}
}
@Composable
private fun ProfilePictureDialog(
title: String,
subtitle: String,
emptyText: String,
downloadHint: String,
closeLabel: String,
choices: List<ProfilePictureChoice>,
optionLabel: (String) -> String,
onDownload: (String) -> Unit,
onDismiss: () -> Unit
) {
val shape = remember { RoundedCornerShape(24.dp) }
val overlayBrush = remember {
Brush.linearGradient(
listOf(
Color(0xFF2A2452).copy(alpha = 0.95f),
Color(0xFF1A143A).copy(alpha = 0.92f)
)
)
}
val accentBrush = remember {
Brush.linearGradient(
listOf(
Color(0xFF8C7BFF).copy(alpha = 0.42f),
Color(0xFF5FD8FF).copy(alpha = 0.34f)
)
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
shape = shape,
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f)),
colors = CardDefaults.cardColors(containerColor = Color(0xFF2A2452).copy(alpha = 0.95f))
) {
Box(
modifier = Modifier
.background(overlayBrush, shape)
.padding(20.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Box(
modifier = Modifier
.size(62.dp)
.background(accentBrush, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Download,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(30.dp)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.ExtraBold),
color = Color.White,
textAlign = TextAlign.Center
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFFD9D3FF),
textAlign = TextAlign.Center
)
}
if (choices.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
Color.White.copy(alpha = 0.06f),
RoundedCornerShape(18.dp)
)
.padding(horizontal = 18.dp, vertical = 20.dp),
contentAlignment = Alignment.Center
) {
Text(
text = emptyText,
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFFD9D3FF),
textAlign = TextAlign.Center
)
}
} else {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
choices.forEach { choice ->
ProfilePictureOptionCard(
title = optionLabel(choice.key),
iconType = choice.iconType,
downloadHint = downloadHint,
onClick = { onDownload(choice.url) }
)
}
}
}
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF8C7BFF).copy(alpha = 0.34f),
contentColor = Color.White
)
) {
Text(
text = closeLabel,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
}
@Composable
private fun ProfilePictureOptionCard(
title: String,
iconType: ProfilePictureChoiceIcon,
downloadHint: String,
onClick: () -> Unit
) {
val icon = when (iconType) {
ProfilePictureChoiceIcon.AVATAR -> Icons.Default.Person
ProfilePictureChoiceIcon.BACKGROUND -> Icons.Default.Image
}
Row(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.linearGradient(
listOf(
Color(0xFF8C7BFF).copy(alpha = 0.18f),
Color(0xFF5FD8FF).copy(alpha = 0.1f)
)
),
RoundedCornerShape(18.dp)
)
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(42.dp)
.background(Color.White.copy(alpha = 0.1f), CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = Color.White,
fontWeight = FontWeight.SemiBold
)
Text(
text = downloadHint,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFD9D3FF)
)
}
Icon(
imageVector = Icons.Default.Download,
contentDescription = null,
tint = Color(0xFF5FD8FF)
)
}
}
private data class ProfilePictureChoice(
val key: String,
val url: String,
val iconType: ProfilePictureChoiceIcon
)
private enum class ProfilePictureChoiceIcon {
AVATAR,
BACKGROUND
}
}

View File

@@ -1,29 +1,59 @@
package me.eternal.purrfectsnap.core.features.impl.ui
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color as ComposeColor
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.eternal.purrfectsnap.common.ui.createComposeAlertDialog
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.ViewAppearanceHelper
import me.eternal.purrfectsnap.core.ui.children
import me.eternal.purrfectsnap.core.util.EvictingMap
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername") {
private val usernameCache = EvictingMap<String, String>(150)
private val dialogTranslation by lazy { context.translation.getCategory("spotlight_comments_username_dialog") }
@SuppressLint("SetTextI18n")
override fun init() {
@@ -96,39 +126,182 @@ class SpotlightCommentsUsername : Feature("SpotlightCommentsUsername") {
}.getOrNull()
withContext(Dispatchers.Main) {
val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
builder.setTitle("User Information")
val userInfoText = buildString {
append("Username: $username\n")
userInfo?.let { info ->
append("User ID: ${userId}\n")
append("Display Name: ${info.displayName ?: "Not available"}\n")
} ?: append("Unable to retrieve additional information")
}
builder.setMessage(userInfoText)
builder.setPositiveButton("OK") { dialog: DialogInterface, _: Int ->
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
// Make text selectable
dialog.findViewById<TextView>(android.R.id.message)?.let { messageView ->
messageView.setTextIsSelectable(true)
messageView.typeface = Typeface.MONOSPACE
createComposeAlertDialog(
context.mainActivity!!,
content = { alertDialog ->
UserInfoDialog(
username = username,
userId = userId,
displayName = userInfo?.displayName,
title = dialogTranslation["title"],
subtitle = dialogTranslation["subtitle"],
usernameLabel = dialogTranslation["username_label"],
displayNameLabel = dialogTranslation["display_name_label"],
userIdLabel = dialogTranslation["user_id_label"],
unavailableLabel = dialogTranslation["not_available"],
closeLabel = context.translation["common.close"],
onDismiss = { alertDialog.dismiss() }
)
}
).show()
}
}
}
@Composable
private fun UserInfoDialog(
username: String,
userId: String,
displayName: String?,
title: String,
subtitle: String,
usernameLabel: String,
displayNameLabel: String,
userIdLabel: String,
unavailableLabel: String,
closeLabel: String,
onDismiss: () -> Unit
) {
val shape = remember { RoundedCornerShape(24.dp) }
val overlayBrush = remember {
Brush.linearGradient(
listOf(
ComposeColor(0xFF2A2452).copy(alpha = 0.95f),
ComposeColor(0xFF1A143A).copy(alpha = 0.92f)
)
)
}
val accentBrush = remember {
Brush.linearGradient(
listOf(
ComposeColor(0xFF8C7BFF).copy(alpha = 0.42f),
ComposeColor(0xFF5FD8FF).copy(alpha = 0.34f)
)
)
}
val rows = remember(username, userId, displayName) {
listOf(
usernameLabel to username,
displayNameLabel to (displayName?.takeIf { it.isNotBlank() } ?: unavailableLabel),
userIdLabel to userId
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
shape = shape,
border = BorderStroke(1.dp, ComposeColor.White.copy(alpha = 0.12f)),
colors = CardDefaults.cardColors(containerColor = ComposeColor(0xFF2A2452).copy(alpha = 0.95f))
) {
Box(
modifier = Modifier
.background(overlayBrush, shape)
.padding(20.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Box(
modifier = Modifier
.size(62.dp)
.background(accentBrush, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
tint = ComposeColor.White,
modifier = Modifier.size(30.dp)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.ExtraBold),
color = ComposeColor.White,
textAlign = TextAlign.Center
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = ComposeColor(0xFFD9D3FF),
textAlign = TextAlign.Center
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(
ComposeColor.White.copy(alpha = 0.06f),
RoundedCornerShape(18.dp)
)
.padding(horizontal = 14.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
rows.forEachIndexed { index, (label, value) ->
UserInfoRow(label = label, value = value)
if (index != rows.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 2.dp),
color = ComposeColor.White.copy(alpha = 0.08f)
)
}
}
}
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = ComposeColor(0xFF8C7BFF).copy(alpha = 0.34f),
contentColor = ComposeColor.White
)
) {
Text(
text = closeLabel,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
}
private fun formatDate(timestamp: Long): String {
return if (timestamp > 0) {
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
} else {
"Not available"
@Composable
private fun UserInfoRow(label: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Text(
text = label,
color = ComposeColor(0xFFD9D3FF),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.width(96.dp)
)
SelectionContainer(
modifier = Modifier.weight(1f)
) {
Text(
text = value,
color = ComposeColor.White,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.End
)
}
}
}
}

View File

@@ -8,17 +8,34 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SlowMotionVideo
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.res.use
@@ -137,30 +154,116 @@ class OperaContextActionMenu : AbstractMenu() {
val operaViewerParamsOverride = context.feature(OperaViewerParamsOverride::class)
linearLayout.addView(createComposeView(view.context) {
val glowPrimary = Color(0xFF8C7BFF)
val glowSecondary = Color(0xFF5FD8FF)
val cardShape = RoundedCornerShape(22.dp)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
var value by remember { mutableFloatStateOf(operaViewerParamsOverride.currentPlaybackRate) }
Slider(
value = value,
onValueChange = {
value = it
operaViewerParamsOverride.currentPlaybackRate = it
},
valueRange = 0.1F..4.0F,
steps = 0,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "x" + value.toString().take(4),
color = remember {
Color(context.userInterface.colorPrimary)
},
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Card(
shape = cardShape,
border = BorderStroke(
1.dp,
Brush.linearGradient(
listOf(
glowPrimary.copy(alpha = 0.45f),
glowSecondary.copy(alpha = 0.35f)
)
)
),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2A2452).copy(alpha = 0.94f)
)
) {
Column(
modifier = Modifier
.background(
Brush.linearGradient(
listOf(
Color(0xFF2A2452).copy(alpha = 0.95f),
Color(0xFF1A143A).copy(alpha = 0.92f)
)
),
cardShape
)
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.size(42.dp)
.background(
Brush.linearGradient(
listOf(
glowPrimary.copy(alpha = 0.35f),
glowSecondary.copy(alpha = 0.28f)
)
),
CircleShape
)
) {
Icon(
imageVector = Icons.Default.SlowMotionVideo,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.align(androidx.compose.ui.Alignment.Center)
.size(22.dp)
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Playback Rate",
color = Color.White,
fontWeight = FontWeight.ExtraBold
)
Text(
text = "x" + value.toString().take(4),
color = Color(0xFFD9D3FF),
textAlign = TextAlign.Start
)
}
}
Slider(
value = value,
onValueChange = {
value = it
operaViewerParamsOverride.currentPlaybackRate = it
},
valueRange = 0.1F..4.0F,
steps = 0,
colors = SliderDefaults.colors(
thumbColor = Color.White,
activeTrackColor = glowSecondary,
inactiveTrackColor = Color.White.copy(alpha = 0.16f),
activeTickColor = glowPrimary,
inactiveTickColor = Color.Transparent
),
modifier = Modifier.fillMaxWidth()
)
Row(modifier = Modifier.fillMaxWidth()) {
Text(
text = "0.1x",
color = Color(0xFFD9D3FF),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "4.0x",
color = Color(0xFFD9D3FF),
textAlign = TextAlign.End,
modifier = Modifier.weight(1f)
)
}
}
}
}
}.apply {
layoutParams = ViewGroup.LayoutParams(

View File

@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a
APP_VERSION_NAME=1.3.5
APP_VERSION_CODE=275
APP_VERSION_NAME=1.3.6
APP_VERSION_CODE=276
debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c

View File

@@ -15,7 +15,7 @@ pub static CLIENT_MODULE: Lazy<MappedLib> = Lazy::new(|| {
client_module = MappedLib::new("split_config.arm".into());
if let Err(error) = client_module.search() {
panic!("Unable to find split_config.arm: {}", error);
error!("Unable to find split_config.arm: {}", error);
}
}
@@ -46,4 +46,4 @@ pub fn attach_jni_env(block: impl FnOnce(&mut jni::JNIEnv)) {
let mut env: jni::AttachGuard = jvm.attach_current_thread().expect("Failed to attach to current thread");
block(&mut env);
}
}

View File

@@ -43,7 +43,7 @@ macro_rules! dobby_hook_sym {
crate::dobby_hook!(hook_symbol, $hook);
debug!("hooked symbol: {}", $sym);
} else {
panic!("Failed to resolve symbol: {}", $sym);
error!("Failed to resolve symbol: {}", $sym);
}
};
}

View File

@@ -155,9 +155,15 @@ fn setChecksums(mut env: JNIEnv, _class: JClass, checksums_json: JString) {
fn pre_init(_env: JNIEnv, _class: JObject) {
debug!("Pre init");
linker_hook::init();
custom_font_hook::init();
fstat_hook::init();
for (name, init) in [
("linker_hook", linker_hook::init as fn()),
("custom_font_hook", custom_font_hook::init as fn()),
("fstat_hook", fstat_hook::init as fn()),
] {
if let Err(error) = std::panic::catch_unwind(init) {
error!("{} init failed: {:?}", name, error);
}
}
}
fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
@@ -186,23 +192,29 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
let mut threads: Vec<std::thread::JoinHandle<()>> = Vec::new();
macro_rules! async_init {
($($f:expr),*) => {
($(($name:expr, $f:expr)),* $(,)?) => {
$(
threads.push(std::thread::spawn(move || {
$f;
if let Err(error) = std::panic::catch_unwind(|| { $f; }) {
error!("{} init failed: {:?}", $name, error);
}
}));
)*
};
}
async_init!(
duplex_hook::init(),
unary_call_hook::init(),
valdi_hook::init(),
sqlite_hook::init()
("duplex_hook", duplex_hook::init()),
("unary_call_hook", unary_call_hook::init()),
("valdi_hook", valdi_hook::init()),
("sqlite_hook", sqlite_hook::init())
);
threads.into_iter().for_each(|t| t.join().unwrap());
threads.into_iter().for_each(|t| {
if let Err(error) = t.join() {
error!("native init worker panicked: {:?}", error);
}
});
info!("native init took {:?}", start_time.elapsed());

View File

@@ -19,11 +19,13 @@ def_hook!(
let content = content.into_boxed_slice();
if libc::write(memfd, content.as_ptr() as *const c_void, content.len() as libc::size_t) == -1 {
panic!("failed to write to memfd");
error!("failed to write to memfd");
return linker_openat_original.unwrap()(dir_fd, pathname, flags, mode);
}
if libc::lseek(memfd, 0, libc::SEEK_SET) == -1 {
panic!("failed to seek memfd");
error!("failed to seek memfd");
return linker_openat_original.unwrap()(dir_fd, pathname, flags, mode);
}
std::mem::forget(content);