Auto Open stabilization
This commit is contained in:
@@ -573,8 +573,12 @@ class FeaturesRootSection : Routes.Route() {
|
||||
}
|
||||
DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> {
|
||||
val isMessageListProperty = property.key.name.endsWith("_messages")
|
||||
val isSleepWindowProperty = property.key.name.contains("sleep_window")
|
||||
|
||||
if (isMessageListProperty) {
|
||||
alertDialogs.MessageListPropertyDialog(property) { showDialog = false }
|
||||
} else if (isSleepWindowProperty) {
|
||||
alertDialogs.AutoOpenScheduleDialog(property as PropertyPair<String>) { showDialog = false }
|
||||
} else {
|
||||
alertDialogs.KeyboardInputDialog(property) { showDialog = false }
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -27,6 +28,7 @@ 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.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -80,6 +82,146 @@ import me.eternal.purrfectsnap.ui.util.Dialog as StandardDialog
|
||||
class AlertDialogs(
|
||||
private val translation: LocaleWrapper,
|
||||
){
|
||||
@Composable
|
||||
fun MessageListPropertyDialog(property: PropertyPair<*>, onDismiss: () -> Unit = {}) {
|
||||
val currentValue = property.value.getNullable()?.toString() ?: "[]"
|
||||
val propertyName = translation[property.key.propertyName()]
|
||||
|
||||
MessageListManagerDialog(
|
||||
title = propertyName ?: "",
|
||||
messageListJson = currentValue,
|
||||
onSave = { newValue: String ->
|
||||
property.value.setAny(newValue)
|
||||
},
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AutoOpenScheduleDialog(
|
||||
property: PropertyPair<String>,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val windowParts = (property.value.get() as String).split("-")
|
||||
val startTime = windowParts.getOrNull(0)?.split(":") ?: listOf("23", "00")
|
||||
val endTime = windowParts.getOrNull(1)?.split(":") ?: listOf("07", "00")
|
||||
|
||||
var isEditingEnd by remember { mutableStateOf(false) }
|
||||
|
||||
val startState = rememberTimePickerState(
|
||||
initialHour = startTime.getOrNull(0)?.toIntOrNull() ?: 23,
|
||||
initialMinute = startTime.getOrNull(1)?.toIntOrNull() ?: 0,
|
||||
is24Hour = true
|
||||
)
|
||||
val endState = rememberTimePickerState(
|
||||
initialHour = endTime.getOrNull(0)?.toIntOrNull() ?: 7,
|
||||
initialMinute = endTime.getOrNull(1)?.toIntOrNull() ?: 0,
|
||||
is24Hour = true
|
||||
)
|
||||
|
||||
DefaultDialogCard {
|
||||
Column(
|
||||
modifier = Modifier.padding(18.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = translation["auto_open_snaps.auto_open_schedule.title"] ?: "Auto Open Scheduler",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.White.copy(alpha = 0.05f))
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
val activeColor = PurrfectPalette.glowPrimary.copy(alpha = 0.25f)
|
||||
val inactiveColor = Color.Transparent
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (!isEditingEnd) activeColor else inactiveColor)
|
||||
.clickable { isEditingEnd = false }
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "${translation["auto_open_snaps.auto_open_schedule.start"] ?: "Start"}: ${String.format("%02d:%02d", startState.hour, startState.minute)}",
|
||||
color = if (!isEditingEnd) Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontWeight = if (!isEditingEnd) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (isEditingEnd) activeColor else inactiveColor)
|
||||
.clickable { isEditingEnd = true }
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "${translation["auto_open_snaps.auto_open_schedule.end"] ?: "End"}: ${String.format("%02d:%02d", endState.hour, endState.minute)}",
|
||||
color = if (isEditingEnd) Color.White else Color.White.copy(alpha = 0.6f),
|
||||
fontWeight = if (isEditingEnd) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TimePicker(
|
||||
state = if (isEditingEnd) endState else startState,
|
||||
colors = TimePickerDefaults.colors(
|
||||
clockDialColor = Color.White.copy(alpha = 0.05f),
|
||||
clockDialSelectedContentColor = Color.White,
|
||||
clockDialUnselectedContentColor = Color.White.copy(alpha = 0.7f),
|
||||
selectorColor = PurrfectPalette.glowPrimary,
|
||||
periodSelectorBorderColor = PurrfectPalette.glowPrimary,
|
||||
periodSelectorSelectedContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
||||
periodSelectorUnselectedContainerColor = Color.Transparent,
|
||||
periodSelectorSelectedContentColor = Color.White,
|
||||
periodSelectorUnselectedContentColor = Color.White.copy(alpha = 0.7f),
|
||||
timeSelectorSelectedContainerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.2f),
|
||||
timeSelectorUnselectedContainerColor = Color.White.copy(alpha = 0.05f),
|
||||
timeSelectorSelectedContentColor = Color.White,
|
||||
timeSelectorUnselectedContentColor = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End)
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = translation["button.negative"], color = Color.White)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
val startStr = String.format("%02d:%02d", startState.hour, startState.minute)
|
||||
val endStr = String.format("%02d:%02d", endState.hour, endState.minute)
|
||||
property.value.setAny("$startStr-$endStr")
|
||||
onDismiss()
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.35f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(text = translation["button.positive"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -1260,23 +1402,7 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageListPropertyDialog(property: PropertyPair<*>, onDismiss: () -> Unit = {}) {
|
||||
val currentValue = property.value.getNullable()?.toString() ?: "[]"
|
||||
val propertyName = translation[property.key.propertyName()]
|
||||
|
||||
MessageListManagerDialog(
|
||||
title = propertyName,
|
||||
messageListJson = currentValue,
|
||||
onSave = { newValue ->
|
||||
property.value.setAny(newValue)
|
||||
},
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageListManagerDialog(
|
||||
@@ -1313,7 +1439,6 @@ class AlertDialogs(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Message list
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1329,7 +1454,7 @@ class AlertDialogs(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = translation["auto_reply_messages.dialog.no_messages"],
|
||||
text = translation["bulk_messaging_action.no_messages_found"] ?: "No messages",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -1371,24 +1496,14 @@ class AlertDialogs(
|
||||
showAddDialog = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "Edit",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Icon(Icons.Default.Edit, contentDescription = translation["common.edit"] ?: "Edit", tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
messageList = messageList.toMutableList().apply {
|
||||
removeAt(index)
|
||||
}
|
||||
messageList = messageList.toMutableList().apply { removeAt(index) }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Icon(Icons.Default.Delete, contentDescription = translation["common.delete"] ?: "Delete", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1398,7 +1513,6 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
|
||||
// Add button
|
||||
Button(
|
||||
onClick = {
|
||||
editingIndex = -1
|
||||
@@ -1408,20 +1522,13 @@ class AlertDialogs(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = translation["auto_reply_messages.dialog.add_message"])
|
||||
Text(text = translation["common.add"] ?: "Add Message")
|
||||
}
|
||||
|
||||
// Dialog buttons
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1430,17 +1537,14 @@ class AlertDialogs(
|
||||
) {
|
||||
Button(
|
||||
onClick = { onDismiss() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Text(text = translation["button.cancel"])
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
val gson = com.google.gson.Gson()
|
||||
val jsonString = gson.toJson(messageList)
|
||||
onSave(jsonString)
|
||||
onSave(gson.toJson(messageList))
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
@@ -1450,7 +1554,6 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
|
||||
// Add/Edit message dialog
|
||||
if (showAddDialog) {
|
||||
StandardDialog(
|
||||
onDismissRequest = { showAddDialog = false },
|
||||
@@ -1460,7 +1563,7 @@ class AlertDialogs(
|
||||
) {
|
||||
DefaultDialogCard {
|
||||
Text(
|
||||
text = if (editingIndex == -1) translation["auto_reply_messages.dialog.add_message"] else translation["auto_reply_messages.dialog.edit_message"],
|
||||
text = if (editingIndex == -1) translation["common.add"] ?: "Add Message" else translation["common.edit"] ?: "Edit Message",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
@@ -1472,13 +1575,13 @@ class AlertDialogs(
|
||||
TextField(
|
||||
value = editingText,
|
||||
onValueChange = { editingText = it },
|
||||
label = { Text(translation["auto_reply_messages.dialog.message_label"]) },
|
||||
label = { Text(translation["common.message"] ?: "Message") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
minLines = 2,
|
||||
maxLines = 4,
|
||||
placeholder = { Text(translation["auto_reply_messages.dialog.message_placeholder"]) }
|
||||
placeholder = { Text(translation["common.type_message"] ?: "Type message...") }
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -1489,32 +1592,22 @@ class AlertDialogs(
|
||||
) {
|
||||
Button(
|
||||
onClick = { showAddDialog = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Text(text = translation["button.cancel"])
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (editingText.isNotBlank()) {
|
||||
if (editingIndex == -1) {
|
||||
// Add new message
|
||||
messageList = messageList.toMutableList().apply {
|
||||
add(editingText)
|
||||
}
|
||||
} else {
|
||||
// Edit existing message
|
||||
messageList = messageList.toMutableList().apply {
|
||||
set(editingIndex, editingText)
|
||||
}
|
||||
messageList = messageList.toMutableList().apply {
|
||||
if (editingIndex == -1) add(editingText) else set(editingIndex, editingText)
|
||||
}
|
||||
}
|
||||
showAddDialog = false
|
||||
},
|
||||
enabled = editingText.isNotBlank()
|
||||
) {
|
||||
Text(text = if (editingIndex == -1) translation["auto_reply_messages.dialog.add_message"] else translation["button.save"])
|
||||
Text(text = if (editingIndex == -1) translation["common.add"] ?: "Add" else translation["button.save"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1522,3 +1615,4 @@ class AlertDialogs(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"setup": {
|
||||
"activity": {
|
||||
"wrong_apk_title": "Wrong APK installed",
|
||||
@@ -1661,10 +1661,26 @@
|
||||
"name": "Auto Open Compact Notification",
|
||||
"description": "Use a smaller, single-line notification for status updates"
|
||||
},
|
||||
"show_progress_bar": {
|
||||
"name": "Show Progress Bar",
|
||||
"description": "Display a visual progress bar in the status notification"
|
||||
},
|
||||
"show_lifetime_stats": {
|
||||
"name": "Show Lifetime Statistics",
|
||||
"description": "Include the total number of snaps opened since installation in the notification"
|
||||
},
|
||||
"show_queue_preview": {
|
||||
"name": "Show Queue Preview",
|
||||
"description": "Show a list of the most recent snaps waiting in the queue (Expanded only)"
|
||||
},
|
||||
"only_on_wifi": { "name": "Auto Open only on Wi-Fi", "description": "Only process queue when connected to a Wi-Fi network to save mobile data" },
|
||||
"only_when_idle": {
|
||||
"name": "Auto Open only when Idle",
|
||||
"description": "Only process queue when the device is not in active use"
|
||||
"name": "Auto Open Schedule",
|
||||
"description": "Configure a specific time window where the engine will throttle its speed."
|
||||
},
|
||||
"sleep_window": {
|
||||
"name": "Auto Open Scheduler",
|
||||
"description": "Define the start and end times for scheduled throttled processing."
|
||||
},
|
||||
"pause_during_gaming": { "name": "Pause Auto Open During Gaming", "description": "Automatically slow down processing when a resource intensive app or a game is in the foreground" },
|
||||
"safe_processing": { "name": "Auto Open with stealth pace", "description": "Adds variable delays and natural breaks to remain undetected. Turn off for maximum speed." }
|
||||
@@ -2165,7 +2181,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_optimization": { "name": "Network Optimization", "description": "Optimizes network socket buffers for higher throughput" }, "better_transcript": {
|
||||
"network_optimization": { "name": "Improved Network Connectivity", "description": "Optimizes network socket buffers for maximum stability and high-speed upload/download performance" }, "better_transcript": {
|
||||
"name": "Better Transcript",
|
||||
"description": "Improves the voice note transcript",
|
||||
"properties": {
|
||||
@@ -3257,6 +3273,11 @@
|
||||
"forced_logout_toast": "Removed account due to forced logout"
|
||||
},
|
||||
"auto_open_snaps": { "title": "Auto Open Snaps", "processed_count": "Opened", "queue_size": "Queue", "action_reset": "Reset Statistics", "priority_title": "Auto Open Snaps (Priority)",
|
||||
"auto_open_schedule": {
|
||||
"title": "Auto Open Scheduler",
|
||||
"start": "Start",
|
||||
"end": "End"
|
||||
},
|
||||
"error_title": "Auto Open Snaps (Errors)",
|
||||
"channel_description": "Notifications for auto-opening snaps queue status",
|
||||
"priority_channel_description": "High priority notifications for auto-opening snaps",
|
||||
@@ -3277,6 +3298,16 @@
|
||||
"status_paused": "Paused",
|
||||
"status_monitoring": "Monitoring",
|
||||
"status_active": "Active",
|
||||
"status_failed": "Failed to open {sender}",
|
||||
"status_retrying": "Retrying in background...",
|
||||
"processing_speed_full": "Full Speed",
|
||||
"processing_speed": "Processing Speed",
|
||||
"speed_throttled": "Throttled",
|
||||
"estimated_time": "Estimated Time",
|
||||
"notification_statistics": "STATISTICS",
|
||||
"notification_total_opened": "Lifetime Opened",
|
||||
"notification_queue_preview": "QUEUE PREVIEW",
|
||||
"notification_no_snaps_queue": "Monitoring snaps in background...",
|
||||
"queue_cleared": "Queue cleared and statistics reset",
|
||||
"queue_cleared_title": "Queue cleared",
|
||||
"queue_cleared_reset": "Queue Cleared & Reset",
|
||||
@@ -3686,17 +3717,25 @@
|
||||
"include_saved_locations_description": "Export your saved location coordinates",
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"add": "Add",
|
||||
"ok": "OK",
|
||||
"quit": "Quit",
|
||||
"done": "Done",
|
||||
"back": "Back",
|
||||
"message": "Message",
|
||||
"type_message": "Type message...",
|
||||
"unknown": "Unknown",
|
||||
"unknown_error": "Unknown error",
|
||||
"not_available": "N/A",
|
||||
"added": "Added",
|
||||
"no_friends_found": "No friends found",
|
||||
"no_messages": "No messages",
|
||||
"message": "Message",
|
||||
"type_message": "Type message...",
|
||||
"exporting_memories": "Exporting memories... ({failed} failed)"
|
||||
},
|
||||
"clear_friend_feed": "Clear Friend Feed",
|
||||
|
||||
@@ -176,12 +176,19 @@ class MessagingTweaks : ConfigContainer() {
|
||||
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1000) != null }
|
||||
}
|
||||
val compactNotification = boolean("compact_notification", false)
|
||||
val showProgressBar = boolean("show_progress_bar", true)
|
||||
val showLifetimeStats = boolean("show_lifetime_stats", false)
|
||||
val showQueuePreview = boolean("show_queue_preview", true)
|
||||
|
||||
// Resource Intelligence: Smart triggers for battery and data safety
|
||||
val onlyOnWifi = boolean("only_on_wifi", false)
|
||||
val onlyWhenIdle = boolean("only_when_idle", false)
|
||||
val pauseDuringGaming = boolean("pause_during_gaming", false)
|
||||
val safeProcessing = boolean("safe_processing", true)
|
||||
val onlyWhenIdle = boolean("only_when_idle", false)
|
||||
val sleepWindow = string("sleep_window", defaultValue = "23:00-07:00") {
|
||||
addFlags(ConfigFlag.NO_DISABLE_KEY)
|
||||
inputCheck = { it.matches(Regex("^([01]\\d|2[0-3]):([0-5]\\d)-([01]\\d|2[0-3]):([0-5]\\d)$")) }
|
||||
}
|
||||
}
|
||||
|
||||
class AutoDeleteSentMessagesConfig : ConfigContainer(hasGlobalState = true) {
|
||||
@@ -324,4 +331,3 @@ class MessagingTweaks : ConfigContainer() {
|
||||
|
||||
val instantTranslation = container("instant_translation", InstantTranslationConfig()) { requireRestart() }
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ enum class FileType(
|
||||
JPG("jpg", "image/jpg",false, true, false),
|
||||
ZIP("zip", "application/zip", false, false, false),
|
||||
WEBP("webp", "image/webp", false, true, false),
|
||||
HEIC("heic", "image/heic", false, true, false),
|
||||
HEIF("heif", "image/heif", false, true, false),
|
||||
MPD("mpd", "text/xml", false, false, false),
|
||||
UNKNOWN("dat", "application/octet-stream", false, false, false);
|
||||
|
||||
@@ -64,6 +66,11 @@ enum class FileType(
|
||||
}
|
||||
|
||||
val majorBrand = String(array, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
|
||||
|
||||
// Explicitly exclude known IMAGE-only brands to prevent false positives (HEIC/HEIF)
|
||||
val imageBrands = setOf("heic", "heix", "hevc", "hevx", "mif1", "msf1")
|
||||
if (majorBrand in imageBrands) return false
|
||||
|
||||
return majorBrand in setOf(
|
||||
"mp41",
|
||||
"mp42",
|
||||
@@ -76,14 +83,13 @@ enum class FileType(
|
||||
"avc1",
|
||||
"dash",
|
||||
"cmfc",
|
||||
"mif1",
|
||||
"msnv",
|
||||
"3gp4",
|
||||
"3gp5",
|
||||
"3gp6",
|
||||
"3g2a",
|
||||
"3g2b"
|
||||
) || majorBrand.isNotEmpty() // FALLBACK: If it has the ftyp box, it's a video
|
||||
) || majorBrand.isNotEmpty() // FALLBACK: If it has the ftyp box and isn't a known image brand, it's a video
|
||||
}
|
||||
|
||||
fun fromFile(file: File): FileType {
|
||||
@@ -98,8 +104,24 @@ enum class FileType(
|
||||
val headerBytes = ByteArray(16)
|
||||
System.arraycopy(array, 0, headerBytes, 0, 16)
|
||||
val hex = bytesToHex(headerBytes)
|
||||
return fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value
|
||||
?: if (looksLikeIsoBmffVideo(headerBytes)) MP4 else UNKNOWN
|
||||
|
||||
// 1. Check strict signatures
|
||||
fileSignatures.entries.firstOrNull { hex.startsWith(it.key) }?.value?.let { return it }
|
||||
|
||||
// 2. Check ISO BMFF container type
|
||||
val majorBrand = if (headerBytes.size >= 12 &&
|
||||
headerBytes[4] == 'f'.code.toByte() && headerBytes[5] == 't'.code.toByte() &&
|
||||
headerBytes[6] == 'y'.code.toByte() && headerBytes[7] == 'p'.code.toByte()) {
|
||||
String(headerBytes, 8, 4, Charsets.US_ASCII).trim('\u0000').lowercase()
|
||||
} else null
|
||||
|
||||
if (majorBrand != null) {
|
||||
if (majorBrand in setOf("heic", "heix")) return HEIC
|
||||
if (majorBrand in setOf("mif1", "msf1")) return HEIF
|
||||
if (looksLikeIsoBmffVideo(headerBytes)) return MP4
|
||||
}
|
||||
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
fun fromInputStream(inputStream: InputStream): FileType {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user