mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 20:40:13 +02:00
feat: streaks reminder
- add streak indicator in social - fix dialogs - fix container global state
This commit is contained in:
parent
7703d3f007
commit
3eb8c8f015
@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
@ -61,6 +62,8 @@
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true" />
|
||||
|
||||
<receiver android:name=".messaging.StreaksReminder" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import coil.ImageLoader
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import me.rhunk.snapenhance.bridge.BridgeService
|
||||
@ -17,6 +18,7 @@ import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper
|
||||
import me.rhunk.snapenhance.core.config.ModConfig
|
||||
import me.rhunk.snapenhance.download.DownloadTaskManager
|
||||
import me.rhunk.snapenhance.messaging.ModDatabase
|
||||
import me.rhunk.snapenhance.messaging.StreaksReminder
|
||||
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
||||
import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo
|
||||
import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo
|
||||
@ -39,6 +41,7 @@ class RemoteSideContext(
|
||||
val mappings = MappingsWrapper()
|
||||
val downloadTaskManager = DownloadTaskManager()
|
||||
val modDatabase = ModDatabase(this)
|
||||
val streaksReminder = StreaksReminder(this)
|
||||
|
||||
//used to load bitmoji selfies and download previews
|
||||
val imageLoader by lazy {
|
||||
@ -48,24 +51,30 @@ class RemoteSideContext(
|
||||
MemoryCache.Builder(androidContext)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
}.components { add(VideoFrameDecoder.Factory()) }.build()
|
||||
}
|
||||
|
||||
init {
|
||||
reload()
|
||||
}
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(androidContext.cacheDir.resolve("coil-disk-cache"))
|
||||
.maxSizeBytes(1024 * 1024 * 100) // 100MB
|
||||
.build()
|
||||
}
|
||||
.components { add(VideoFrameDecoder.Factory()) }.build()
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
runCatching {
|
||||
config.loadFromContext(androidContext)
|
||||
translation.userLocale = config.locale
|
||||
translation.loadFromContext(androidContext)
|
||||
translation.apply {
|
||||
userLocale = config.locale
|
||||
loadFromContext(androidContext)
|
||||
}
|
||||
mappings.apply {
|
||||
loadFromContext(androidContext)
|
||||
init(androidContext)
|
||||
}
|
||||
downloadTaskManager.init(androidContext)
|
||||
modDatabase.init()
|
||||
streaksReminder.init()
|
||||
}.onFailure {
|
||||
Logger.error("Failed to load RemoteSideContext", it)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ object SharedContextHolder {
|
||||
fun remote(context: Context): RemoteSideContext {
|
||||
if (!::_remoteSideContext.isInitialized || _remoteSideContext.get() == null) {
|
||||
_remoteSideContext = WeakReference(RemoteSideContext(context))
|
||||
_remoteSideContext.get()?.reload()
|
||||
}
|
||||
|
||||
return _remoteSideContext.get()!!
|
||||
|
@ -26,7 +26,9 @@ class BridgeService : Service() {
|
||||
remoteSideContext = SharedContextHolder.remote(this).apply {
|
||||
if (checkForRequirements()) return null
|
||||
}
|
||||
remoteSideContext.bridgeService = this
|
||||
remoteSideContext.apply {
|
||||
bridgeService = this@BridgeService
|
||||
}
|
||||
messageLoggerWrapper = MessageLoggerWrapper(getDatabasePath(BridgeFileType.MESSAGE_LOGGER_DATABASE.fileName)).also { it.init() }
|
||||
return BridgeBinder()
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package me.rhunk.snapenhance.bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import me.rhunk.snapenhance.SharedContextHolder
|
||||
|
||||
class ForceStartActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent.getBooleanExtra("streaks_notification_action", false)) {
|
||||
packageManager.getLaunchIntentForPackage("com.snapchat.android")?.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(this)
|
||||
}
|
||||
SharedContextHolder.remote(this).streaksReminder.dismissAllNotifications()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
@ -140,8 +140,11 @@ class ModDatabase(
|
||||
)
|
||||
//sync streaks
|
||||
if (friend.streakLength > 0) {
|
||||
database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, length) VALUES (?, ?, ?)", arrayOf(
|
||||
val streaks = getFriendStreaks(friend.userId!!)
|
||||
|
||||
database.execSQL("INSERT OR REPLACE INTO streaks (userId, notify, expirationTimestamp, length) VALUES (?, ?, ?, ?)", arrayOf(
|
||||
friend.userId,
|
||||
streaks?.notify ?: false,
|
||||
friend.streakExpirationTimestamp,
|
||||
friend.streakLength
|
||||
))
|
||||
@ -198,6 +201,7 @@ class ModDatabase(
|
||||
fun deleteFriend(userId: String) {
|
||||
executeAsync {
|
||||
database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(userId))
|
||||
database.execSQL("DELETE FROM streaks WHERE userId = ?", arrayOf(userId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,101 @@
|
||||
package me.rhunk.snapenhance.messaging
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.R
|
||||
import me.rhunk.snapenhance.RemoteSideContext
|
||||
import me.rhunk.snapenhance.SharedContextHolder
|
||||
import me.rhunk.snapenhance.bridge.ForceStartActivity
|
||||
import me.rhunk.snapenhance.ui.util.ImageRequestHelper
|
||||
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
|
||||
|
||||
class StreaksReminder(
|
||||
private val remoteSideContext: RemoteSideContext? = null
|
||||
): BroadcastReceiver() {
|
||||
companion object {
|
||||
private const val NOTIFICATION_CHANNEL_ID = "streaks"
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private fun getNotificationManager(context: Context) = context.getSystemService(NotificationManager::class.java).apply {
|
||||
createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
"Streaks",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
val remoteSideContext = this.remoteSideContext ?: SharedContextHolder.remote(ctx)
|
||||
if (remoteSideContext.config.root.streaksReminder.globalState != true) return
|
||||
|
||||
val notifyFriendList = remoteSideContext.modDatabase.getFriends()
|
||||
.associateBy { remoteSideContext.modDatabase.getFriendStreaks(it.userId) }
|
||||
.filter { (streaks, _) -> streaks != null && streaks.notify && streaks.isAboutToExpire() }
|
||||
|
||||
val notificationManager = getNotificationManager(ctx)
|
||||
|
||||
notifyFriendList.forEach { (streaks, friend) ->
|
||||
coroutineScope.launch {
|
||||
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
||||
val bitmojiImage = remoteSideContext.imageLoader.execute(
|
||||
ImageRequestHelper.newBitmojiImageRequest(ctx, bitmojiUrl)
|
||||
)
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("Streaks")
|
||||
.setContentText("You will lose streaks with ${friend.displayName} in ${streaks?.hoursLeft() ?: 0} hours")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(PendingIntent.getActivity(
|
||||
ctx,
|
||||
0,
|
||||
Intent(ctx, ForceStartActivity::class.java).apply {
|
||||
putExtra("streaks_notification_action", true)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
))
|
||||
.apply {
|
||||
bitmojiImage.drawable?.let {
|
||||
setLargeIcon(it.toBitmap())
|
||||
setSmallIcon(R.drawable.streak_icon)
|
||||
}
|
||||
}
|
||||
|
||||
notificationManager.notify(friend.userId.hashCode(), notificationBuilder.build().apply {
|
||||
flags = NotificationCompat.FLAG_ONLY_ALERT_ONCE
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: ask for notifications permission for a13+
|
||||
fun init() {
|
||||
if (remoteSideContext == null) throw IllegalStateException("RemoteSideContext is null")
|
||||
val reminderConfig = remoteSideContext.config.root.streaksReminder.also {
|
||||
if (it.globalState != true) return
|
||||
}
|
||||
|
||||
remoteSideContext.androidContext.getSystemService(AlarmManager::class.java).setRepeating(
|
||||
AlarmManager.RTC_WAKEUP, 5000, reminderConfig.interval.get().toLong() * 60 * 60 * 1000,
|
||||
PendingIntent.getBroadcast(remoteSideContext.androidContext, 0, Intent(remoteSideContext.androidContext, StreaksReminder::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE)
|
||||
)
|
||||
|
||||
onReceive(remoteSideContext.androidContext, Intent())
|
||||
}
|
||||
|
||||
fun dismissAllNotifications() = getNotificationManager(remoteSideContext!!.androidContext).cancelAll()
|
||||
}
|
@ -10,21 +10,23 @@ import androidx.compose.runtime.remember
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import me.rhunk.snapenhance.RemoteSideContext
|
||||
import me.rhunk.snapenhance.SharedContextHolder
|
||||
import me.rhunk.snapenhance.ui.AppMaterialTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var sections: Map<EnumSection, Section>
|
||||
private lateinit var navController: NavHostController
|
||||
private lateinit var managerContext: RemoteSideContext
|
||||
|
||||
override fun onPostResume() {
|
||||
super.onPostResume()
|
||||
sections.values.forEach { it.onResumed() }
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.getStringExtra("route")?.let { route ->
|
||||
intent.getStringExtra("route")?.let { route ->
|
||||
navController.popBackStack()
|
||||
navController.navigate(route) {
|
||||
popUpTo(navController.graph.findStartDestination().id){
|
||||
@ -38,7 +40,7 @@ class MainActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME
|
||||
val managerContext = SharedContextHolder.remote(this).apply {
|
||||
managerContext = SharedContextHolder.remote(this).apply {
|
||||
activity = this@MainActivity
|
||||
checkForRequirements()
|
||||
}
|
||||
|
@ -268,9 +268,9 @@ class FeaturesSection : Section() {
|
||||
navController.navigate(FEATURE_CONTAINER_ROUTE.replace("{name}", property.name))
|
||||
}
|
||||
|
||||
if (container.globalState == null) return
|
||||
if (!container.hasGlobalState) return
|
||||
|
||||
var state by remember { mutableStateOf(container.globalState!!) }
|
||||
var state by remember { mutableStateOf(container.globalState ?: false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -453,6 +453,22 @@ class FeaturesSection : Section() {
|
||||
if (showSearchBar) return
|
||||
|
||||
var showExportDropdownMenu by remember { mutableStateOf(false) }
|
||||
var showResetConfirmationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showResetConfirmationDialog) {
|
||||
Dialog(onDismissRequest = { showResetConfirmationDialog = false }) {
|
||||
alertDialogs.ConfirmDialog(
|
||||
title = "Reset config",
|
||||
message = "Are you sure you want to reset the config?",
|
||||
onConfirm = {
|
||||
context.config.reset()
|
||||
context.shortToast("Config successfully reset!")
|
||||
},
|
||||
onDismiss = { showResetConfirmationDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val actions = remember {
|
||||
mapOf(
|
||||
"Export" to {
|
||||
@ -477,10 +493,7 @@ class FeaturesSection : Section() {
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reset" to {
|
||||
context.config.reset()
|
||||
context.shortToast("Config successfully reset!")
|
||||
}
|
||||
"Reset" to { showResetConfirmationDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -29,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@ -53,31 +55,34 @@ class AddFriendDialog(
|
||||
private val section: SocialSection,
|
||||
) {
|
||||
@Composable
|
||||
private fun ListCardEntry(name: String, exists: Boolean, stateChanged: (state: Boolean) -> Unit = { }) {
|
||||
var state by remember { mutableStateOf(exists) }
|
||||
private fun ListCardEntry(name: String, currentState: () -> Boolean, onState: (Boolean) -> Unit = {}) {
|
||||
var currentState by remember { mutableStateOf(currentState()) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
state = !state
|
||||
stateChanged(state)
|
||||
currentState = !currentState
|
||||
onState(currentState)
|
||||
}
|
||||
.padding(4.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.onGloballyPositioned {
|
||||
currentState = currentState()
|
||||
}
|
||||
)
|
||||
|
||||
androidx.compose.material3.Checkbox(
|
||||
checked = state,
|
||||
Checkbox(
|
||||
checked = currentState,
|
||||
onCheckedChange = {
|
||||
state = it
|
||||
stateChanged(state)
|
||||
currentState = it
|
||||
onState(currentState)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -227,13 +232,12 @@ class AddFriendDialog(
|
||||
|
||||
items(filteredGroups.size) {
|
||||
val group = filteredGroups[it]
|
||||
|
||||
ListCardEntry(
|
||||
name = group.name,
|
||||
exists = remember { context.modDatabase.getGroupInfo(group.conversationId) != null }
|
||||
currentState = { context.modDatabase.getGroupInfo(group.conversationId) != null }
|
||||
) { state ->
|
||||
if (state) {
|
||||
context.bridgeService.triggerGroupSync(cachedGroups!![it].conversationId)
|
||||
context.bridgeService.triggerGroupSync(group.conversationId)
|
||||
} else {
|
||||
context.modDatabase.deleteGroup(group.conversationId)
|
||||
}
|
||||
@ -257,10 +261,10 @@ class AddFriendDialog(
|
||||
|
||||
ListCardEntry(
|
||||
name = friend.displayName?.takeIf { name -> name.isNotBlank() } ?: friend.mutableUsername,
|
||||
exists = remember { context.modDatabase.getFriendInfo(friend.userId) != null }
|
||||
currentState = { context.modDatabase.getFriendInfo(friend.userId) != null }
|
||||
) { state ->
|
||||
if (state) {
|
||||
context.bridgeService.triggerFriendSync(cachedFriends!![it].userId)
|
||||
context.bridgeService.triggerFriendSync(friend.userId)
|
||||
} else {
|
||||
context.modDatabase.deleteFriend(friend.userId)
|
||||
}
|
||||
|
@ -177,14 +177,11 @@ class ScopeContent(
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) ->
|
||||
if (selfieId == null || bitmojiId == null) return@let null
|
||||
BitmojiSelfie.getBitmojiSelfie(
|
||||
selfieId,
|
||||
bitmojiId,
|
||||
BitmojiSelfie.BitmojiSelfieType.THREE_D
|
||||
)
|
||||
}
|
||||
val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(
|
||||
friend.selfieId,
|
||||
friend.bitmojiId,
|
||||
BitmojiSelfie.BitmojiSelfieType.THREE_D
|
||||
)
|
||||
BitmojiImage(context = context, url = bitmojiUrl, size = 100)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
@ -218,13 +215,13 @@ class ScopeContent(
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(text = "Count: ${streaks.length}", maxLines = 1)
|
||||
Text(text = "Length: ${streaks.length}", maxLines = 1)
|
||||
Text(text = "Expires in: ${computeStreakETA(streaks.expirationTimestamp)}", maxLines = 1)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "Notify Expiration", maxLines = 1)
|
||||
Text(text = "Reminder", maxLines = 1, modifier = Modifier.padding(end = 10.dp))
|
||||
Switch(checked = shouldNotify, onCheckedChange = {
|
||||
context.modDatabase.setFriendStreaksNotify(id, it)
|
||||
shouldNotify = it
|
||||
|
@ -18,7 +18,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.DeleteForever
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -36,6 +35,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -45,6 +46,7 @@ import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.R
|
||||
import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
|
||||
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
|
||||
import me.rhunk.snapenhance.core.messaging.SocialScope
|
||||
@ -175,24 +177,41 @@ class SocialSection : Section() {
|
||||
}
|
||||
SocialScope.FRIEND -> {
|
||||
val friend = friendList[index]
|
||||
val streaks = remember { context.modDatabase.getFriendStreaks(friend.userId) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) ->
|
||||
if (selfieId == null || bitmojiId == null) return@let null
|
||||
BitmojiSelfie.getBitmojiSelfie(selfieId, bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
||||
}
|
||||
BitmojiImage(context = context, url = bitmojiUrl)
|
||||
BitmojiImage(
|
||||
context = context,
|
||||
url = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontWeight = FontWeight.Bold)
|
||||
Text(text = friend.userId, maxLines = 1, fontSize = 12.sp)
|
||||
Text(text = friend.mutableUsername, maxLines = 1, fontSize = 12.sp, fontWeight = FontWeight.Light)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (streaks != null && streaks.notify) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.streak_icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(40.dp),
|
||||
tint = if (streaks.isAboutToExpire())
|
||||
MaterialTheme.colorScheme.error
|
||||
else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(text = "${streaks.hoursLeft()}h", maxLines = 1, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,9 +28,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
|
||||
import me.rhunk.snapenhance.core.config.DataProcessors
|
||||
import me.rhunk.snapenhance.core.config.PropertyPair
|
||||
@ -57,21 +59,22 @@ class AlertDialogs(
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
title: String,
|
||||
data: String? = null,
|
||||
message: String? = null,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DefaultDialogCard {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
|
||||
)
|
||||
if (data != null) {
|
||||
if (message != null) {
|
||||
Text(
|
||||
text = data,
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
modifier = Modifier.padding(bottom = 15.dp)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
@ -91,20 +94,21 @@ class AlertDialogs(
|
||||
@Composable
|
||||
fun InfoDialog(
|
||||
title: String,
|
||||
data: String? = null,
|
||||
message: String? = null,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
DefaultDialogCard {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
|
||||
)
|
||||
if (data != null) {
|
||||
if (message != null) {
|
||||
Text(
|
||||
text = data,
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 10.dp)
|
||||
modifier = Modifier.padding(bottom = 15.dp)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
|
@ -36,18 +36,23 @@ fun BitmojiImage(context: RemoteSideContext, modifier: Modifier = Modifier, size
|
||||
)
|
||||
}
|
||||
|
||||
fun ImageRequest.Builder.cacheKey(key: String?) = apply {
|
||||
memoryCacheKey(key)
|
||||
diskCacheKey(key)
|
||||
}
|
||||
|
||||
object ImageRequestHelper {
|
||||
fun newBitmojiImageRequest(context: Context, url: String?) = ImageRequest.Builder(context)
|
||||
.data(url)
|
||||
.fallback(R.drawable.bitmoji_blank)
|
||||
.precision(Precision.INEXACT)
|
||||
.crossfade(true)
|
||||
.memoryCacheKey(url)
|
||||
.cacheKey(url)
|
||||
.build()
|
||||
|
||||
fun newDownloadPreviewImageRequest(context: Context, filePath: String?) = ImageRequest.Builder(context)
|
||||
.data(filePath)
|
||||
.memoryCacheKey(filePath)
|
||||
.cacheKey(filePath)
|
||||
.crossfade(true)
|
||||
.build()
|
||||
}
|
11
app/src/main/res/drawable/streak_icon.xml
Normal file
11
app/src/main/res/drawable/streak_icon.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M224.78,560q0,59.18 26.11,111.45 26.11,52.27 73.28,87.6 -4,-10.99 -6,-22.68 -2,-11.69 -2,-22.97 1.2,-30.8 13.31,-57.61 12.12,-26.8 34.4,-49.09L480,492.35l116.35,114.35q22.28,22.28 34.4,49.09 12.12,26.81 13.08,57.61 0,11.28 -2,22.97 -2,11.69 -5.76,22.68 46.7,-35.33 73.04,-87.6Q735.46,619.18 735.46,560q0,-52.09 -22.07,-102.06 -22.07,-49.97 -63.35,-91.96 -21,14.28 -43.84,22.42 -22.84,8.14 -45.21,8.14 -60.27,0 -100.63,-39.95Q420,316.65 417.37,254.61v-20q-44.36,32.26 -79.91,71.32 -35.55,39.05 -60.59,81.36 -25.04,42.3 -38.57,86.61 -13.52,44.31 -13.52,86.11ZM480,587.83l-68.31,67.56q-13.82,13.57 -20.96,29.92 -7.14,16.34 -7.14,35.64 0,39.5 28.09,66.89 28.09,27.38 68.37,27.38 40.28,0 68.33,-27.44 28.04,-27.44 28.04,-66.97 0,-19.05 -7.13,-35.4 -7.13,-16.35 -20.63,-29.93L480,587.83ZM483.59,114.26L483.59,252q0,32.48 22.45,54.44 22.45,21.97 54.94,21.97 17.21,0 32.04,-7.14 14.83,-7.14 26.35,-21.66l19.67,-24.39q76.21,43.41 120.38,119.74 44.17,76.33 44.17,164.97 0,135.53 -94.05,229.59 -94.05,94.06 -229.56,94.06 -135.51,0 -229.54,-94.04 -94.03,-94.04 -94.03,-229.55 0,-129.19 87.79,-249.61Q332,189.98 483.59,114.26Z"
|
||||
tools:ignore="VectorPath" />
|
||||
</vector>
|
@ -309,6 +309,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"streaks_reminder": {
|
||||
"name": "Streaks Reminder",
|
||||
"description": "Reminds you to keep your streaks",
|
||||
"properties": {
|
||||
"interval": {
|
||||
"name": "Interval",
|
||||
"description": "The interval between each reminder (in hours)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"name": "Experimental",
|
||||
"description": "Experimental features",
|
||||
|
@ -35,14 +35,14 @@ class BridgeClient(
|
||||
fun start(callback: (Boolean) -> Unit) {
|
||||
this.future = CompletableFuture()
|
||||
|
||||
//TODO: randomize package name
|
||||
with(context.androidContext) {
|
||||
//ensure the remote process is running
|
||||
startActivity(Intent()
|
||||
.setClassName(BuildConfig.APPLICATION_ID, ForceStartActivity::class.java.name)
|
||||
.setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.ForceStartActivity")
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
)
|
||||
|
||||
//TODO: randomize package name
|
||||
val intent = Intent()
|
||||
.setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.bridge.BridgeService")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
|
@ -1,11 +0,0 @@
|
||||
package me.rhunk.snapenhance.bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
|
||||
class ForceStartActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
finish()
|
||||
}
|
||||
}
|
@ -6,10 +6,11 @@ import kotlin.reflect.KProperty
|
||||
typealias ConfigParamsBuilder = ConfigParams.() -> Unit
|
||||
|
||||
open class ConfigContainer(
|
||||
var globalState: Boolean? = null
|
||||
val hasGlobalState: Boolean = false
|
||||
) {
|
||||
var parentContainerKey: PropertyKey<*>? = null
|
||||
val properties = mutableMapOf<PropertyKey<*>, PropertyValue<*>>()
|
||||
var globalState: Boolean? = null
|
||||
|
||||
private inline fun <T> registerProperty(
|
||||
key: String,
|
||||
|
@ -13,6 +13,8 @@ class Global : ConfigContainer() {
|
||||
val disableGooglePlayDialogs = boolean("disable_google_play_dialogs")
|
||||
val forceMediaSourceQuality = boolean("force_media_source_quality")
|
||||
val betterNotifications = multiple("better_notifications", "snap", "chat", "reply_button", "download_button")
|
||||
val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray())
|
||||
val notificationBlacklist = multiple("notification_blacklist", *NotificationType.getIncomingValues().map { it.key }.toTypedArray()) {
|
||||
customOptionTranslationPath = "features.options.notifications"
|
||||
}
|
||||
val disableSnapSplitting = boolean("disable_snap_splitting") { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) }
|
||||
}
|
@ -16,7 +16,9 @@ class MessagingTweaks : ConfigContainer() {
|
||||
"EXTERNAL_MEDIA",
|
||||
"STICKER"
|
||||
)
|
||||
val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray())
|
||||
val preventMessageSending = multiple("prevent_message_sending", *NotificationType.getOutgoingValues().map { it.key }.toTypedArray()) {
|
||||
customOptionTranslationPath = "features.options.notifications"
|
||||
}
|
||||
val messageLogger = boolean("message_logger") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) }
|
||||
val galleryMediaSendOverride = boolean("gallery_media_send_override")
|
||||
val messagePreviewLength = integer("message_preview_length", defaultValue = 20)
|
||||
|
@ -9,6 +9,7 @@ class RootConfig : ConfigContainer() {
|
||||
val global = container("global", Global()) { icon = "MiscellaneousServices" }
|
||||
val rules = container("rules", Rules()) { icon = "Rule" }
|
||||
val camera = container("camera", Camera()) { icon = "Camera"}
|
||||
val streaksReminder = container("streaks_reminder", StreaksReminderConfig()) { icon = "Alarm" }
|
||||
val experimental = container("experimental", Experimental()) { icon = "Science" }
|
||||
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" }
|
||||
}
|
@ -3,13 +3,13 @@ package me.rhunk.snapenhance.core.config.impl
|
||||
import me.rhunk.snapenhance.core.config.ConfigContainer
|
||||
|
||||
class Spoof : ConfigContainer() {
|
||||
inner class Location : ConfigContainer(globalState = false) {
|
||||
inner class Location : ConfigContainer(hasGlobalState = true) {
|
||||
val latitude = float("location_latitude")
|
||||
val longitude = float("location_longitude")
|
||||
}
|
||||
val location = container("location", Location())
|
||||
|
||||
inner class Device : ConfigContainer(globalState = false) {
|
||||
inner class Device : ConfigContainer(hasGlobalState = true) {
|
||||
val fingerprint = string("device_fingerprint")
|
||||
val androidId = string("device_android_id")
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package me.rhunk.snapenhance.core.config.impl
|
||||
|
||||
import me.rhunk.snapenhance.core.config.ConfigContainer
|
||||
|
||||
class StreaksReminderConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val interval = integer("interval", 2)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package me.rhunk.snapenhance.core.messaging
|
||||
|
||||
import me.rhunk.snapenhance.util.SerializableDataObject
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
|
||||
enum class RuleState(
|
||||
@ -45,8 +46,16 @@ data class FriendStreaks(
|
||||
val notify: Boolean,
|
||||
val expirationTimestamp: Long,
|
||||
val length: Int
|
||||
) : SerializableDataObject()
|
||||
) : SerializableDataObject() {
|
||||
companion object {
|
||||
//TODO: config
|
||||
val EXPIRE_THRESHOLD = 12.hours
|
||||
}
|
||||
|
||||
fun hoursLeft() = (expirationTimestamp - System.currentTimeMillis()) / 1000 / 60 / 60
|
||||
|
||||
fun isAboutToExpire() = expirationTimestamp - System.currentTimeMillis() < EXPIRE_THRESHOLD.inWholeMilliseconds
|
||||
}
|
||||
|
||||
data class MessagingGroupInfo(
|
||||
val conversationId: String,
|
||||
|
@ -61,11 +61,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
): DownloadManagerClient {
|
||||
val generatedHash = mediaIdentifier.hashCode().toString(16).replaceFirst("-", "")
|
||||
|
||||
val iconUrl = friendInfo?.takeIf {
|
||||
it.bitmojiAvatarId != null && it.bitmojiSelfieId != null
|
||||
}?.let {
|
||||
BitmojiSelfie.getBitmojiSelfie(it.bitmojiSelfieId!!, it.bitmojiAvatarId!!, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
||||
}
|
||||
val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
||||
|
||||
val downloadLogging by context.config.downloader.logging
|
||||
if (downloadLogging.contains("started")) {
|
||||
|
@ -50,7 +50,7 @@ object ViewAppearanceHelper {
|
||||
}
|
||||
}
|
||||
|
||||
val snapchatFontResId = resources.getIdentifier("avenir_next_medium", "font", "com.snapchat.android")
|
||||
val snapchatFontResId = resources.getIdentifier("avenir_next_medium", "font", Constants.SNAPCHAT_PACKAGE_NAME)
|
||||
val scalingFactor = resources.displayMetrics.densityDpi.toDouble() / 400
|
||||
|
||||
with(component) {
|
||||
|
@ -53,7 +53,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
||||
profile.bitmojiSelfieId.toString(),
|
||||
profile.bitmojiAvatarId.toString(),
|
||||
BitmojiSelfie.BitmojiSelfieType.THREE_D
|
||||
)
|
||||
)!!
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
@ -6,7 +6,10 @@ object BitmojiSelfie {
|
||||
THREE_D
|
||||
}
|
||||
|
||||
fun getBitmojiSelfie(selfieId: String, avatarId: String, type: BitmojiSelfieType): String {
|
||||
fun getBitmojiSelfie(selfieId: String?, avatarId: String?, type: BitmojiSelfieType): String? {
|
||||
if (selfieId.isNullOrEmpty() || avatarId.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return when (type) {
|
||||
BitmojiSelfieType.STANDARD -> "https://sdk.bitmoji.com/render/panel/$selfieId-$avatarId-v1.webp?transparent=1"
|
||||
BitmojiSelfieType.THREE_D -> "https://images.bitmoji.com/3d/render/$selfieId-$avatarId-v1.webp?trim=circle"
|
||||
|
Loading…
x
Reference in New Issue
Block a user