refactor: translations (#78)

* ui changes

* fix more stuff

* fix: set ffmpeg preset to ultrafast

* refactor: ui tweaks
getIdentifier function

* feat: download manager filter

* add placeholder bitmoji

* change bitmoji blank color

* add settings page (not done)

* setting page impl

* confirmation dialog

* update theme

* remove snap context buttons

* fix: delete cache

* fix: bitmoji icon

* fix: french translation

* translation refactor

* translation: download manager

* refactor: get operator

* refactor: config properties

* add translation for download manager receiver

* fix: steak expire translation format

---------

Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
auth
2023-06-21 19:41:36 +02:00
committed by GitHub
parent 65cbf79dce
commit cc59f9d060
33 changed files with 426 additions and 320 deletions

View File

@@ -34,10 +34,10 @@
tools:ignore="ExportedService">
</service>
<receiver android:name=".download.MediaDownloadReceiver" android:exported="true"
<receiver android:name=".download.DownloadManagerReceiver" android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="me.rhunk.snapenhance.download.MediaDownloadReceiver.DOWNLOAD_ACTION" />
<action android:name="me.rhunk.snapenhance.download.DownloadManagerReceiver.DOWNLOAD_ACTION" />
</intent-filter>
</receiver>

View File

@@ -161,7 +161,7 @@
},
"conversation_preview": {
"streak_expiration": "expires in %s days %s hours %s minutes",
"streak_expiration": "expires in {day} days {hour} hours {minute} minutes",
"title": "Preview",
"unknown_user": "Unknown User"
},
@@ -199,5 +199,44 @@
"finished": "Done! You now can close this dialog.",
"no_messages_found": "No messages found!",
"exporting_message": "Exporting {conversation}..."
},
"download_manager_activity": {
"remove_all_title": "Remove all Downloads",
"remove_all_text": "Are you sure you want to do this?",
"remove_all": "Remove All",
"no_downloads": "No downloads",
"cancel": "Cancel",
"file_not_found_toast": "File does not exist!",
"category": {
"all_category": "All",
"pending_category": "Pending",
"snap_category": "Snaps",
"story_category": "Stories",
"spotlight_category": "Spotlight"
},
"settings": "Settings",
"button": {
"positive": "Yes",
"negative": "No",
"cancel": "Cancel",
"open": "Open"
},
"settings_page": {
"clear_file_title": "Clear {file_name} file",
"clear_file_confirmation": "Are you sure you want to clear the {file_name} file?",
"clear_cache_title": "Clear Cache",
"reset_all_title": "Reset all settings",
"reset_all_confirmation": "Are you sure you want to reset all settings?",
"success_toast": "Success!"
}
},
"download_manager_receiver": {
"saved_toast": "Saved to {path}",
"download_toast": "Downloading {path}...",
"processing_toast": "Processing {path}...",
"failed_generic_toast": "Failed to download",
"failed_processing_toast": "Failed to process {error}",
"failed_gallery_toast": "Failed to save to gallery {error}"
}
}

View File

@@ -159,7 +159,7 @@
"username": "Nom d'utilisateur",
"display_name": "Nom d'affichage",
"added_date": "Date d'ajout",
"birthday": "Anniversaire : {month} {day}"
"birthday": "Anniversaire : {day} {month}"
},
"auto_updater": {
"no_update_available": "Aucune mise à jour disponible !",

View File

@@ -10,7 +10,9 @@ import android.os.Process
import android.widget.Toast
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.asCoroutineDispatcher
import me.rhunk.snapenhance.bridge.AbstractBridgeClient
import me.rhunk.snapenhance.bridge.TranslationWrapper
import me.rhunk.snapenhance.data.MessageSender
import me.rhunk.snapenhance.database.DatabaseAccess
import me.rhunk.snapenhance.features.Feature
@@ -18,7 +20,6 @@ import me.rhunk.snapenhance.manager.impl.ActionManager
import me.rhunk.snapenhance.manager.impl.ConfigManager
import me.rhunk.snapenhance.manager.impl.FeatureManager
import me.rhunk.snapenhance.manager.impl.MappingManager
import me.rhunk.snapenhance.manager.impl.TranslationManager
import me.rhunk.snapenhance.util.download.DownloadServer
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -28,13 +29,17 @@ import kotlin.system.exitProcess
class ModContext {
private val executorService: ExecutorService = Executors.newCachedThreadPool()
val coroutineDispatcher by lazy {
executorService.asCoroutineDispatcher()
}
lateinit var androidContext: Context
var mainActivity: Activity? = null
lateinit var bridgeClient: AbstractBridgeClient
val gson: Gson = GsonBuilder().create()
val translation = TranslationManager(this)
val translation = TranslationWrapper()
val features = FeatureManager(this)
val mappings = MappingManager(this)
val config = ConfigManager(this)

View File

@@ -0,0 +1,26 @@
package me.rhunk.snapenhance
import android.content.Context
import me.rhunk.snapenhance.bridge.TranslationWrapper
import me.rhunk.snapenhance.download.DownloadTaskManager
/**
* Used to store objects between activities and receivers
*/
object SharedContext {
lateinit var downloadTaskManager: DownloadTaskManager
lateinit var translation: TranslationWrapper
fun ensureInitialized(context: Context) {
if (!this::downloadTaskManager.isInitialized) {
downloadTaskManager = DownloadTaskManager().apply {
init(context)
}
}
if (!this::translation.isInitialized) {
translation = TranslationWrapper().apply {
loadFromContext(context)
}
}
}
}

View File

@@ -5,6 +5,8 @@ import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Build
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.bridge.AbstractBridgeClient
import me.rhunk.snapenhance.bridge.client.RootBridgeClient
import me.rhunk.snapenhance.bridge.client.ServiceBridgeClient
@@ -39,7 +41,9 @@ class SnapEnhance {
return@start
}
runCatching {
init()
runBlocking {
init()
}
}.onFailure {
Logger.xposedLog("Failed to initialize", it)
}
@@ -67,10 +71,14 @@ class SnapEnhance {
}
@OptIn(ExperimentalTime::class)
private fun init() {
private suspend fun init() {
//load translations in a coroutine to speed up initialization
withContext(appContext.coroutineDispatcher) {
appContext.translation.loadFromBridge(appContext.bridgeClient)
}
measureTime {
with(appContext) {
translation.init()
config.init()
mappings.init()
//if mappings aren't loaded, we can't initialize features
@@ -82,10 +90,15 @@ class SnapEnhance {
}
}
@OptIn(ExperimentalTime::class)
private fun onActivityCreate() {
with(appContext) {
features.onActivityCreate()
actionManager.init()
measureTime {
with(appContext) {
features.onActivityCreate()
actionManager.init()
}
}.also { time ->
Logger.debug("onActivityCreate in $time")
}
}
}

View File

@@ -10,7 +10,7 @@ class CheckForUpdates : AbstractAction("action.check_for_updates", dependsOnProp
runCatching {
val latestVersion = context.feature(AutoUpdater::class).checkForUpdates()
if (latestVersion == null) {
context.longToast(context.translation.get("auto_updater.no_update_available"))
context.longToast(context.translation["auto_updater.no_update_available"])
}
}.onFailure {
context.longToast(it.message ?: "Failed to check for updates")

View File

@@ -62,7 +62,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
private suspend fun askExportType() = suspendCancellableCoroutine { cont ->
context.runOnUiThread {
AlertDialog.Builder(context.mainActivity)
.setTitle(context.translation.get("chat_export.select_export_format"))
.setTitle(context.translation["chat_export.select_export_format"])
.setItems(ExportFormat.values().map { it.name }.toTypedArray()) { _, which ->
cont.resumeWith(Result.success(ExportFormat.values()[which]))
}
@@ -83,7 +83,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
ContentType.STICKER
)
AlertDialog.Builder(context.mainActivity)
.setTitle(context.translation.get("chat_export.select_media_type"))
.setTitle(context.translation["chat_export.select_media_type"])
.setMultiChoiceItems(contentTypes.map { it.name }.toTypedArray(), BooleanArray(contentTypes.size) { false }) { _, which, isChecked ->
val media = contentTypes[which]
if (isChecked) {
@@ -111,7 +111,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
val selectedConversations = mutableListOf<FriendFeedInfo>()
AlertDialog.Builder(context.mainActivity)
.setTitle(context.translation.get("chat_export.select_conversation"))
.setTitle(context.translation["chat_export.select_conversation"])
.setMultiChoiceItems(
friendFeedEntries.map { it.feedDisplayName ?: it.friendDisplayName!!.split("|").firstOrNull() }.toTypedArray(),
BooleanArray(friendFeedEntries.size) { false }
@@ -122,13 +122,13 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
selectedConversations.remove(friendFeedEntries[which])
}
}
.setNegativeButton(context.translation.get("chat_export.dialog_negative_button")) { dialog, _ ->
.setNegativeButton(context.translation["chat_export.dialog_negative_button"]) { dialog, _ ->
dialog.dismiss()
}
.setNeutralButton(context.translation.get("chat_export.dialog_neutral_button")) { _, _ ->
.setNeutralButton(context.translation["chat_export.dialog_neutral_button"]) { _, _ ->
exportChatForConversations(friendFeedEntries)
}
.setPositiveButton(context.translation.get("chat_export.dialog_positive_button")) { _, _ ->
.setPositiveButton(context.translation["chat_export.dialog_positive_button"]) { _, _ ->
exportChatForConversations(selectedConversations)
}
.show()
@@ -188,11 +188,11 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
conversationAction(true, conversationId, if (friendFeedInfo.feedDisplayName != null) "USERCREATEDGROUP" else "ONEONONE")
logDialog(context.translation.get("chat_export.exporting_message").replace("{conversation}", conversationName))
logDialog(context.translation.format("chat_export.exporting_message", "conversation" to conversationName))
val foundMessages = fetchMessagesPaginated(conversationId, Long.MAX_VALUE).toMutableList()
var lastMessageId = foundMessages.firstOrNull()?.messageDescriptor?.messageId ?: run {
logDialog(context.translation.get("chat_export.no_messages_found"))
logDialog(context.translation["chat_export.no_messages_found"])
return
}
@@ -211,7 +211,7 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
"SnapEnhance/conversation_${conversationName}_${System.currentTimeMillis()}.${exportType!!.extension}"
).also { it.parentFile?.mkdirs() }
logDialog(context.translation.get("chat_export.writing_output"))
logDialog(context.translation["chat_export.writing_output"])
MessageExporter(
context = context,
friendFeedInfo = friendFeedInfo,
@@ -222,14 +222,16 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
runCatching {
it.readMessages(foundMessages)
}.onFailure {
logDialog(context.translation.get("chat_export.export_failed").replace("{conversation}", it.message.toString()))
logDialog(context.translation.format("chat_export.export_failed","conversation" to it.message.toString()))
Logger.error(it)
return
}
}.exportTo(exportType!!)
dialogLogs.clear()
logDialog("\n" + context.translation.get("chat_export.exported_to").replace("{path}", outputFile.absolutePath.toString()) + "\n")
logDialog("\n" + context.translation.format("chat_export.exported_to",
"path" to outputFile.absolutePath.toString()
) + "\n")
currentActionDialog?.setButton(DialogInterface.BUTTON_POSITIVE, "Open") { _, _ ->
val intent = Intent(Intent.ACTION_VIEW)
@@ -247,16 +249,16 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
val jobs = mutableListOf<Job>()
currentActionDialog = AlertDialog.Builder(context.mainActivity)
.setTitle(context.translation.get("chat_export.exporting_chats"))
.setTitle(context.translation["chat_export.exporting_chats"])
.setCancelable(false)
.setMessage("")
.setNegativeButton(context.translation.get("chat_export.dialog_negative_button")) { dialog, _ ->
.setNegativeButton(context.translation["chat_export.dialog_negative_button"]) { dialog, _ ->
jobs.forEach { it.cancel() }
dialog.dismiss()
}
.create()
val conversationSize = context.translation.get("chat_export.processing_chats").replace("{amount}", conversations.size.toString())
val conversationSize = context.translation.format("chat_export.processing_chats", "amount" to conversations.size.toString())
logDialog(conversationSize)
@@ -268,14 +270,14 @@ class ExportChatMessages : AbstractAction("action.export_chat_messages") {
runCatching {
exportFullConversation(conversation)
}.onFailure {
logDialog(context.translation.get("chat_export.export_fail").replace("{conversation}", conversation.key.toString()))
logDialog(context.translation.format("chat_export.export_fail", "conversation" to conversation.key.toString()))
logDialog(it.stackTraceToString())
Logger.xposedLog(it)
}
}.also { jobs.add(it) }
}
jobs.joinAll()
logDialog(context.translation.get("chat_export.finished"))
logDialog(context.translation["chat_export.finished"])
}
}
}

View File

@@ -0,0 +1,96 @@
package me.rhunk.snapenhance.bridge
import android.content.Context
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.data.LocalePair
import java.util.Locale
class TranslationWrapper {
companion object {
private const val DEFAULT_LOCALE = "en_US"
fun fetchLocales(context: Context): List<LocalePair> {
val deviceLocale = Locale.getDefault().toString()
val locales = mutableListOf<LocalePair>()
locales.add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() }))
if (deviceLocale == DEFAULT_LOCALE) return locales
val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(deviceLocale) }?.substring(0, 5) ?: return locales
context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream ->
locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() }))
}
return locales
}
}
private val translationMap = linkedMapOf<String, String>()
private lateinit var _locale: String
val locale by lazy {
Locale(_locale.substring(0, 2), _locale.substring(3, 5))
}
private fun load(localePair: LocalePair) {
if (!::_locale.isInitialized) {
_locale = localePair.locale
}
val translations = JsonParser.parseString(localePair.content).asJsonObject
if (translations == null || translations.isJsonNull) {
return
}
fun scanObject(jsonObject: JsonObject, prefix: String = "") {
jsonObject.entrySet().forEach {
if (it.value.isJsonPrimitive) {
val key = "$prefix${it.key}"
translationMap[key] = it.value.asString
}
if (!it.value.isJsonObject) return@forEach
scanObject(it.value.asJsonObject, "$prefix${it.key}.")
}
}
scanObject(translations)
}
fun loadFromBridge(bridgeClient: AbstractBridgeClient) {
bridgeClient.fetchTranslations().getLocales().forEach {
load(it)
}
}
fun loadFromContext(context: Context) {
fetchLocales(context).forEach {
load(it)
}
}
operator fun get(key: String): String {
return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") }
}
fun format(key: String, vararg args: Pair<String, String>): String {
return args.fold(get(key)) { acc, pair ->
acc.replace("{${pair.first}}", pair.second)
}
}
fun getCategory(key: String): TranslationWrapper {
return TranslationWrapper().apply {
translationMap.putAll(
this@TranslationWrapper.translationMap
.filterKeys { it.startsWith("$key.") }
.mapKeys { it.key.substring(key.length + 1) }
)
}
}
}

View File

@@ -110,7 +110,7 @@ class RootBridgeClient : AbstractBridgeClient() {
if (langJsonData != null) {
Logger.debug("Fetched translations for $locale")
return LocaleResult(locale, langJsonData)
return LocaleResult(arrayOf(locale), arrayOf(langJsonData.toString(Charsets.UTF_8)))
}
throw Throwable("Failed to fetch translations for $locale")

View File

@@ -2,18 +2,30 @@ package me.rhunk.snapenhance.bridge.common.impl.locale
import android.os.Bundle
import me.rhunk.snapenhance.bridge.common.BridgeMessage
import me.rhunk.snapenhance.data.LocalePair
class LocaleResult(
var locale: String? = null,
var content: ByteArray? = null
private var locales: Array<String>? = null,
private var localContentArray: Array<String>? = null
) : BridgeMessage(){
override fun write(bundle: Bundle) {
bundle.putString("locale", locale)
bundle.putByteArray("content", content)
fun getLocales(): List<LocalePair> {
val locales = locales ?: return emptyList()
val localContentArray = localContentArray ?: return emptyList()
return locales.mapIndexed { index, locale ->
LocalePair(locale, localContentArray[index])
}.asReversed()
}
override fun write(bundle: Bundle) {
bundle.putStringArray("locales", locales)
bundle.putSerializable("localContentArray", localContentArray)
}
@Suppress("UNCHECKED_CAST", "DEPRECATION")
override fun read(bundle: Bundle) {
locale = bundle.getString("locale")
content = bundle.getByteArray("content")
locales = bundle.getStringArray("locales")
localContentArray = bundle.getSerializable("localContentArray") as? Array<String>
}
}

View File

@@ -8,6 +8,7 @@ import android.net.Uri
import android.os.*
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.bridge.MessageLoggerWrapper
import me.rhunk.snapenhance.bridge.TranslationWrapper
import me.rhunk.snapenhance.bridge.common.BridgeMessageType
import me.rhunk.snapenhance.bridge.common.impl.*
import me.rhunk.snapenhance.bridge.common.impl.download.DownloadContentRequest
@@ -111,12 +112,15 @@ class BridgeService : Service() {
}
private fun handleLocaleRequest(reply: (Message) -> Unit) {
val deviceLocale = Locale.getDefault().toString()
val compatibleLocale = resources.assets.list("lang")?.find { it.startsWith(deviceLocale) }?.substring(0, 5) ?: "en_US"
val locales = sortedSetOf<String>()
val contentArray = sortedSetOf<String>()
resources.assets.open("lang/$compatibleLocale.json").use { inputStream ->
reply(LocaleResult(compatibleLocale, inputStream.readBytes()).toMessage(BridgeMessageType.LOCALE_RESULT.value))
TranslationWrapper.fetchLocales(context = this).forEach { pair ->
locales.add(pair.locale)
contentArray.add(pair.content)
}
reply(LocaleResult(locales.toTypedArray(), contentArray.toTypedArray()).toMessage(BridgeMessageType.LOCALE_RESULT.value))
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")

View File

@@ -1,17 +0,0 @@
package me.rhunk.snapenhance.bridge.service
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import me.rhunk.snapenhance.Constants
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
packageManager.getLaunchIntentForPackage(Constants.SNAPCHAT_PACKAGE_NAME)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(this)
}
finish()
}
}

View File

@@ -10,8 +10,7 @@ import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
import java.io.File
enum class ConfigProperty(
val nameKey: String,
val descriptionKey: String,
val translationKey: String,
val category: ConfigCategory,
val valueContainer: ConfigValue<*>,
val shouldAppearInSettings: Boolean = true,
@@ -19,26 +18,22 @@ enum class ConfigProperty(
) {
//SPYING AND PRIVACY
MESSAGE_LOGGER("property.message_logger",
"description.message_logger",
MESSAGE_LOGGER("message_logger",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
PREVENT_READ_RECEIPTS(
"property.prevent_read_receipts",
"description.prevent_read_receipts",
"prevent_read_receipts",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
HIDE_BITMOJI_PRESENCE(
"property.hide_bitmoji_presence",
"description.hide_bitmoji_presence",
"hide_bitmoji_presence",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
BETTER_NOTIFICATIONS(
"property.better_notifications",
"description.better_notifications",
"better_notifications",
ConfigCategory.SPYING_PRIVACY,
ConfigStateListValue(
listOf("snap", "chat", "reply_button"),
@@ -50,8 +45,7 @@ enum class ConfigProperty(
)
),
NOTIFICATION_BLACKLIST(
"property.notification_blacklist",
"description.notification_blacklist",
"notification_blacklist",
ConfigCategory.SPYING_PRIVACY,
ConfigStateListValue(
listOf("snap", "chat", "typing"),
@@ -62,56 +56,51 @@ enum class ConfigProperty(
)
)
),
DISABLE_METRICS("property.disable_metrics",
"description.disable_metrics",
DISABLE_METRICS("disable_metrics",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
BLOCK_ADS("property.block_ads",
"description.block_ads",
BLOCK_ADS("block_ads",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
UNLIMITED_SNAP_VIEW_TIME("property.unlimited_snap_view_time",
"description.unlimited_snap_view_time",
UNLIMITED_SNAP_VIEW_TIME("unlimited_snap_view_time",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
PREVENT_SCREENSHOT_NOTIFICATIONS(
"property.prevent_screenshot_notifications",
"description.prevent_screenshot_notifications",
"prevent_screenshot_notifications",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
PREVENT_STATUS_NOTIFICATIONS(
"property.prevent_status_notifications",
"description.prevent_status_notifications",
"prevent_status_notifications",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
ANONYMOUS_STORY_VIEW(
"property.anonymous_story_view",
"description.anonymous_story_view",
"anonymous_story_view",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
HIDE_TYPING_NOTIFICATION(
"property.hide_typing_notification",
"description.hide_typing_notification",
"hide_typing_notification",
ConfigCategory.SPYING_PRIVACY,
ConfigStateValue(false)
),
//MEDIA MANAGEMENT
SAVE_FOLDER(
"property.save_folder", "description.save_folder", ConfigCategory.MEDIA_MANAGEMENT,
"save_folder",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStringValue(File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat",
"SnapEnhance"
).absolutePath)
),
AUTO_DOWNLOAD_OPTIONS(
"property.auto_download_options", "description.auto_download_options", ConfigCategory.MEDIA_MANAGEMENT,
"auto_download_options",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("friend_snaps", "friend_stories", "public_stories", "spotlight"),
mutableMapOf(
@@ -123,7 +112,8 @@ enum class ConfigProperty(
)
),
DOWNLOAD_OPTIONS(
"property.download_options", "description.download_options", ConfigCategory.MEDIA_MANAGEMENT,
"download_options",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("format_user_folder", "format_hash", "format_date_time", "format_username", "merge_overlay"),
mutableMapOf(
@@ -136,22 +126,19 @@ enum class ConfigProperty(
)
),
CHAT_DOWNLOAD_CONTEXT_MENU(
"property.chat_download_context_menu",
"description.chat_download_context_menu",
"chat_download_context_menu",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateValue(false)
),
GALLERY_MEDIA_SEND_OVERRIDE(
"property.gallery_media_send_override",
"description.gallery_media_send_override",
"gallery_media_send_override",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateSelection(
listOf("OFF", "NOTE", "SNAP", "LIVE_SNAP"),
"OFF"
)
),
AUTO_SAVE_MESSAGES("property.auto_save_messages",
"description.auto_save_messages",
AUTO_SAVE_MESSAGES("auto_save_messages",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("CHAT", "SNAP", "NOTE", "EXTERNAL_MEDIA", "STICKER")
@@ -159,22 +146,19 @@ enum class ConfigProperty(
),
FORCE_MEDIA_SOURCE_QUALITY(
"property.force_media_source_quality",
"description.force_media_source_quality",
"force_media_source_quality",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateValue(false)
),
//UI AND TWEAKS
ENABLE_FRIEND_FEED_MENU_BAR(
"property.enable_friend_feed_menu_bar",
"description.enable_friend_feed_menu_bar",
"enable_friend_feed_menu_bar",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
FRIEND_FEED_MENU_BUTTONS(
"property.friend_feed_menu_buttons",
"description.friend_feed_menu_buttons",
"friend_feed_menu_buttons",
ConfigCategory.UI_TWEAKS,
ConfigStateListValue(
listOf("auto_download_blacklist", "anti_auto_save", "stealth_mode", "conversation_info"),
@@ -186,14 +170,12 @@ enum class ConfigProperty(
)
)
),
FRIEND_FEED_MENU_POSITION("property.friend_feed_menu_buttons_position",
"description.friend_feed_menu_buttons_position",
FRIEND_FEED_MENU_POSITION("friend_feed_menu_buttons_position",
ConfigCategory.UI_TWEAKS,
ConfigIntegerValue(1)
),
HIDE_UI_ELEMENTS(
"property.hide_ui_elements",
"description.hide_ui_elements",
"hide_ui_elements",
ConfigCategory.UI_TWEAKS,
ConfigStateListValue(
listOf("remove_voice_record_button", "remove_stickers_button", "remove_cognac_button", "remove_call_buttons", "remove_camera_borders"),
@@ -207,74 +189,62 @@ enum class ConfigProperty(
)
),
STREAK_EXPIRATION_INFO(
"property.streak_expiration_info",
"description.streakexpirationinfo",
"streak_expiration_info",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
DISABLE_SNAP_SPLITTING(
"property.disable_snap_splitting",
"description.disable_snap_splitting",
"disable_snap_splitting",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
DISABLE_VIDEO_LENGTH_RESTRICTION(
"property.disable_video_length_restriction",
"description.disable_video_length_restriction",
"disable_video_length_restriction",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
SNAPCHAT_PLUS("property.snapchat_plus",
"description.snapchat_plus",
SNAPCHAT_PLUS("snapchat_plus",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
NEW_MAP_UI("property.new_map_ui",
"description.new_map_ui",
NEW_MAP_UI("new_map_ui",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
LOCATION_SPOOF(
"property.location_spoof",
"description.location_spoof",
"location_spoof",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
LATITUDE(
"property.latitude_value",
"description.latitude_value",
"latitude_value",
ConfigCategory.UI_TWEAKS,
ConfigStringValue("0.0000"),
shouldAppearInSettings = false
),
LONGITUDE(
"property.longitude_value",
"description.longitude_value",
"longitude_value",
ConfigCategory.UI_TWEAKS,
ConfigStringValue("0.0000"),
shouldAppearInSettings = false
),
MESSAGE_PREVIEW_LENGTH(
"property.message_preview_length",
"description.message_preview_length",
"message_preview_length",
ConfigCategory.UI_TWEAKS,
ConfigIntegerValue(20)
),
UNLIMITED_CONVERSATION_PINNING(
"property.unlimited_conversation_pinning",
"description.unlimited_conversation_pinning",
"unlimited_conversation_pinning",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
DISABLE_SPOTLIGHT(
"property.disable_spotlight",
"description.disable_spotlight",
"disable_spotlight",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
ENABLE_APP_APPEARANCE(
"property.enable_app_appearance",
"description.enable_app_appearance",
"enable_app_appearance",
ConfigCategory.UI_TWEAKS,
ConfigStateValue(false)
),
@@ -282,20 +252,17 @@ enum class ConfigProperty(
//CAMERA
CAMERA_DISABLE(
"property.disable_camera",
"description.disable_camera",
"disable_camera",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
IMMERSIVE_CAMERA_PREVIEW(
"property.immersive_camera_preview",
"description.immersive_camera_preview",
"immersive_camera_preview",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
OVERRIDE_PREVIEW_RESOLUTION(
"property.preview_resolution",
"description.preview_resolution",
"preview_resolution",
ConfigCategory.CAMERA,
ConfigStateSelection(
CameraTweaks.resolutions,
@@ -304,8 +271,7 @@ enum class ConfigProperty(
disableValueLocalization = true
),
OVERRIDE_PICTURE_RESOLUTION(
"property.picture_resolution",
"description.picture_resolution",
"picture_resolution",
ConfigCategory.CAMERA,
ConfigStateSelection(
CameraTweaks.resolutions,
@@ -314,22 +280,19 @@ enum class ConfigProperty(
disableValueLocalization = true
),
FORCE_HIGHEST_FRAME_RATE(
"property.force_highest_frame_rate",
"description.force_highest_frame_rate",
"force_highest_frame_rate",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
FORCE_CAMERA_SOURCE_ENCODING(
"property.force_camera_source_encoding",
"description.force_camera_source_encoding",
"force_camera_source_encoding",
ConfigCategory.CAMERA,
ConfigStateValue(false)
),
// UPDATES
AUTO_UPDATER(
"property.auto_updater",
"description.auto_updater",
"auto_updater",
ConfigCategory.UPDATES,
ConfigStateSelection(
listOf("DISABLED", "EVERY_LAUNCH", "DAILY", "WEEKLY"),
@@ -339,32 +302,27 @@ enum class ConfigProperty(
// EXPERIMENTAL DEBUGGING
APP_PASSCODE(
"property.app_passcode",
"description.app_passcode",
"app_passcode",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStringValue("", isHidden = true)
),
APP_LOCK_ON_RESUME(
"property.app_lock_on_resume",
"description.app_lock_on_resume",
"app_lock_on_resume",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
INFINITE_STORY_BOOST(
"property.infinite_story_boost",
"description.infinite_story_boost",
"infinite_story_boost",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
MEO_PASSCODE_BYPASS(
"property.meo_passcode_bypass",
"description.meo_passcode_bypass",
"meo_passcode_bypass",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
),
AMOLED_DARK_MODE(
"property.amoled_dark_mode",
"description.amoled_dark_mode",
"amoled_dark_mode",
ConfigCategory.EXPERIMENTAL_DEBUGGING,
ConfigStateValue(false)
);

View File

@@ -0,0 +1,3 @@
package me.rhunk.snapenhance.data
data class LocalePair(val locale: String, val content: String)

View File

@@ -8,7 +8,7 @@ import me.rhunk.snapenhance.download.data.DownloadRequest
import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
import me.rhunk.snapenhance.download.enums.DownloadMediaType
class ClientDownloadManager (
class DownloadManagerClient (
private val context: ModContext,
private val outputPath: String,
private val mediaDisplaySource: String?,
@@ -17,8 +17,8 @@ class ClientDownloadManager (
) {
private fun sendToBroadcastReceiver(bundle: Bundle) {
val intent = Intent()
intent.setClassName(BuildConfig.APPLICATION_ID, MediaDownloadReceiver::class.java.name)
intent.action = MediaDownloadReceiver.DOWNLOAD_ACTION
intent.setClassName(BuildConfig.APPLICATION_ID, DownloadManagerReceiver::class.java.name)
intent.action = DownloadManagerReceiver.DOWNLOAD_ACTION
intent.putExtras(bundle)
context.androidContext.sendBroadcast(intent)
}

View File

@@ -23,6 +23,7 @@ import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
import me.rhunk.snapenhance.download.data.PendingDownload
import me.rhunk.snapenhance.download.enums.DownloadMediaType
import me.rhunk.snapenhance.download.enums.DownloadStage
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
import java.io.File
@@ -48,13 +49,16 @@ data class DownloadedFile(
)
/**
* MediaDownloadReceiver handles the download of media files
* DownloadManagerReceiver handles the download requests of the user
*/
@OptIn(ExperimentalEncodingApi::class)
class MediaDownloadReceiver : BroadcastReceiver() {
class DownloadManagerReceiver : BroadcastReceiver() {
companion object {
val downloadTaskManager = DownloadTaskManager()
const val DOWNLOAD_ACTION = "me.rhunk.snapenhance.download.MediaDownloadReceiver.DOWNLOAD_ACTION"
const val DOWNLOAD_ACTION = "me.rhunk.snapenhance.download.DownloadManagerReceiver.DOWNLOAD_ACTION"
}
private val translation by lazy {
SharedContext.translation.getCategory("download_manager_receiver")
}
private lateinit var context: Context
@@ -124,13 +128,15 @@ class MediaDownloadReceiver : BroadcastReceiver() {
if (!it.endsWith("/")) "$it/" else it
}
longToast("Saved media to ${outputFile.absolutePath.replace(parentName ?: "", "")}")
longToast(
translation.format("saved_toast", "path" to outputFile.absolutePath.replace(parentName ?: "", ""))
)
pendingDownload.outputFile = outputFile.absolutePath
pendingDownload.downloadStage = DownloadStage.SAVED
}.onFailure {
Logger.error("Failed to save media to gallery", it)
longToast("Failed to save media to gallery")
Logger.error(it)
longToast(translation.format("failed_gallery_toast", "error" to it.toString()))
pendingDownload.downloadStage = DownloadStage.FAILED
}
}
@@ -215,7 +221,7 @@ class MediaDownloadReceiver : BroadcastReceiver() {
val xmlData = dashPlaylistFile.outputStream()
TransformerFactory.newInstance().newTransformer().transform(DOMSource(playlistXml), StreamResult(xmlData))
longToast("Downloading dash media...")
longToast(translation.format("download_toast", "path" to dashPlaylistFile.nameWithoutExtension))
val outputFile = File.createTempFile("dash", ".mp4")
runCatching {
MediaDownloaderHelper.downloadDashChapterFile(
@@ -226,8 +232,8 @@ class MediaDownloadReceiver : BroadcastReceiver() {
saveMediaToGallery(outputFile, pendingDownloadObject)
}.onFailure {
if (coroutineContext.job.isCancelled) return@onFailure
Logger.error("failed to download dash media", it)
longToast("Failed to download dash media: ${it.message}")
Logger.error(it)
longToast(translation.format("failed_processing_toast", "error" to it.toString()))
pendingDownloadObject.downloadStage = DownloadStage.FAILED
}
@@ -247,14 +253,15 @@ class MediaDownloadReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != DOWNLOAD_ACTION) return
this.context = context
downloadTaskManager.init(context)
SharedContext.ensureInitialized(context)
val downloadRequest = DownloadRequest.fromBundle(intent.extras!!)
GlobalScope.launch(Dispatchers.IO) {
val pendingDownloadObject = PendingDownload.fromBundle(intent.extras!!)
downloadTaskManager.addTask(pendingDownloadObject)
SharedContext.downloadTaskManager.addTask(pendingDownloadObject)
pendingDownloadObject.apply {
job = coroutineContext.job
downloadStage = DownloadStage.DOWNLOADING
@@ -295,7 +302,7 @@ class MediaDownloadReceiver : BroadcastReceiver() {
val renamedOverlayMedia = renameFromFileType(overlayMedia.file, overlayMedia.fileType)
val mergedOverlay: File = File.createTempFile("merged", "." + media.fileType.fileExtension)
runCatching {
longToast("Merging overlay...")
longToast(translation.format("download_toast", "path" to media.file.nameWithoutExtension))
pendingDownloadObject.downloadStage = DownloadStage.MERGING
MediaDownloaderHelper.mergeOverlayFile(
@@ -307,8 +314,8 @@ class MediaDownloadReceiver : BroadcastReceiver() {
saveMediaToGallery(mergedOverlay, pendingDownloadObject)
}.onFailure {
if (coroutineContext.job.isCancelled) return@onFailure
Logger.error("failed to merge overlay", it)
longToast("Failed to merge overlay: ${it.message}")
Logger.error(it)
longToast(translation.format("failed_processing_toast", "error" to it.toString()))
pendingDownloadObject.downloadStage = DownloadStage.MERGE_FAILED
}
@@ -321,8 +328,8 @@ class MediaDownloadReceiver : BroadcastReceiver() {
downloadRemoteMedia(pendingDownloadObject, downloadedMedias, downloadRequest)
}.onFailure {
pendingDownloadObject.downloadStage = DownloadStage.FAILED
Logger.error("failed to download media", it)
longToast("Failed to download media: ${it.message}")
Logger.error(it)
longToast(translation["failed_generic_toast"])
}
}
}

View File

@@ -2,7 +2,7 @@ package me.rhunk.snapenhance.download.data
import android.os.Bundle
import kotlinx.coroutines.Job
import me.rhunk.snapenhance.download.MediaDownloadReceiver
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.download.enums.DownloadStage
data class PendingDownload(
@@ -35,7 +35,7 @@ data class PendingDownload(
set(value) = synchronized(this) {
changeListener(_stage, value)
_stage = value
MediaDownloadReceiver.downloadTaskManager.updateTask(this)
SharedContext.downloadTaskManager.updateTask(this)
}
fun isJobActive(): Boolean {

View File

@@ -62,21 +62,21 @@ class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVI
context.runOnUiThread {
AlertDialog.Builder(context.mainActivity)
.setTitle(context.translation.get("auto_updater.dialog_title"))
.setTitle(context.translation["auto_updater.dialog_title"])
.setMessage(
context.translation.get("auto_updater.dialog_message")
.replace("{version}", latestVersion)
.replace("{body}", releaseContentBody)
context.translation.format("auto_updater.dialog_message",
"version" to latestVersion,
"body" to releaseContentBody)
)
.setNegativeButton(context.translation.get("auto_updater.dialog_negative_button")) { dialog, _ ->
.setNegativeButton(context.translation["auto_updater.dialog_negative_button"]) { dialog, _ ->
dialog.dismiss()
}
.setPositiveButton(context.translation.get("auto_updater.dialog_positive_button")) { dialog, _ ->
.setPositiveButton(context.translation["auto_updater.dialog_positive_button"]) { dialog, _ ->
dialog.dismiss()
context.longToast(context.translation.get("auto_updater.downloading_toast"))
context.longToast(context.translation["auto_updater.downloading_toast"])
val request = DownloadManager.Request(Uri.parse(downloadEndpoint))
.setTitle(context.translation.get("auto_updater.download_manager_notification_title"))
.setTitle(context.translation["auto_updater.download_manager_notification_title"])
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "latest-snapenhance.apk")
.setMimeType("application/vnd.android.package-archive")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)

View File

@@ -19,7 +19,7 @@ import me.rhunk.snapenhance.data.wrapper.impl.media.dash.SnapPlaylistItem
import me.rhunk.snapenhance.data.wrapper.impl.media.opera.Layer
import me.rhunk.snapenhance.data.wrapper.impl.media.opera.ParamMap
import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.download.ClientDownloadManager
import me.rhunk.snapenhance.download.DownloadManagerClient
import me.rhunk.snapenhance.download.data.toKeyPair
import me.rhunk.snapenhance.download.enums.DownloadMediaType
import me.rhunk.snapenhance.features.Feature
@@ -59,7 +59,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
mediaDisplaySource: String? = null,
mediaDisplayType: String? = null,
friendInfo: FriendInfo? = null
): ClientDownloadManager {
): DownloadManagerClient {
val iconUrl = friendInfo?.takeIf {
it.bitmojiAvatarId != null && it.bitmojiSelfieId != null
}?.let {
@@ -71,7 +71,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
createNewFilePath(pathSuffix.hashCode(), pathSuffix)
).absolutePath
return ClientDownloadManager(
return DownloadManagerClient(
context = context,
mediaDisplaySource = mediaDisplaySource,
mediaDisplayType = mediaDisplayType,
@@ -143,7 +143,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
}
}
private fun downloadOperaMedia(clientDownloadManager: ClientDownloadManager, mediaInfoMap: Map<MediaType, MediaInfo>) {
private fun downloadOperaMedia(downloadManagerClient: DownloadManagerClient, mediaInfoMap: Map<MediaType, MediaInfo>) {
if (mediaInfoMap.isEmpty()) return
val originalMediaInfo = mediaInfoMap[MediaType.ORIGINAL]!!
@@ -153,7 +153,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val overlayReference = overlay?.let { handleLocalReferences(it.uri) }
overlay?.let {
clientDownloadManager.downloadMediaWithOverlay(
downloadManagerClient.downloadMediaWithOverlay(
originalMediaInfoReference,
overlayReference!!,
DownloadMediaType.fromUri(Uri.parse(originalMediaInfoReference)),
@@ -164,7 +164,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
return
}
clientDownloadManager.downloadMedia(
downloadManagerClient.downloadMedia(
originalMediaInfoReference,
DownloadMediaType.fromUri(Uri.parse(originalMediaInfoReference)),
originalMediaInfo.encryption?.toKeyPair()

View File

@@ -90,8 +90,7 @@ class FeatureManager(private val context: ModContext) : Manager {
}
private fun featureInitializer(isAsync: Boolean, param: Int, action: (Feature) -> Unit) {
features.forEach { feature ->
if (feature.loadParams and param == 0) return@forEach
features.filter { it.loadParams and param != 0 }.forEach { feature ->
val callback = {
runCatching {
action(feature)

View File

@@ -1,43 +0,0 @@
package me.rhunk.snapenhance.manager.impl
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.manager.Manager
import java.util.Locale
class TranslationManager(
private val context: ModContext
) : Manager {
private val translationMap = mutableMapOf<String, String>()
lateinit var locale: Locale
override fun init() {
val messageLocaleResult = context.bridgeClient.fetchTranslations()
locale = messageLocaleResult.locale?.split("_")?.let { Locale(it[0], it[1]) } ?: Locale.getDefault()
val translations = JsonParser.parseString(messageLocaleResult.content?.toString(Charsets.UTF_8)).asJsonObject
if (translations == null || translations.isJsonNull) {
context.crash("Failed to fetch translations")
return
}
fun scanObject(jsonObject: JsonObject, prefix: String = "") {
jsonObject.entrySet().forEach {
if (it.value.isJsonPrimitive) {
translationMap["$prefix${it.key}"] = it.value.asString
}
if (!it.value.isJsonObject) return@forEach
scanObject(it.value.asJsonObject, "$prefix${it.key}.")
}
}
scanObject(translations)
}
fun get(key: String): String {
return translationMap[key] ?: key.also { Logger.xposedLog("Missing translation for $key") }
}
}

View File

@@ -31,6 +31,7 @@ import java.net.URL
import kotlin.concurrent.thread
class DownloadListAdapter(
private val activity: DownloadManagerActivity,
private val downloadList: MutableList<PendingDownload>
): Adapter<DownloadListAdapter.ViewHolder>() {
private val previewJobs = mutableMapOf<Int, Job>()
@@ -56,15 +57,6 @@ class DownloadListAdapter(
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.download_manager_item, parent, false))
}
override fun onViewRecycled(holder: ViewHolder) {
val download = downloadList.getOrNull(holder.bindingAdapterPosition) ?: return
previewJobs[holder.hashCode()]?.let {
it.cancel()
previewJobs.remove(holder.hashCode())
}
}
override fun getItemCount(): Int {
return downloadList.size
}
@@ -99,6 +91,11 @@ class DownloadListAdapter(
}
}
private val openButtonText by lazy {
activity.translation["button.open"]
}
private fun updateViewHolder(download: PendingDownload, holder: ViewHolder) {
holder.status.text = download.downloadStage.toString()
holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background)
@@ -117,7 +114,10 @@ class DownloadListAdapter(
alpha = if (canInteract) 1f else 0.5f
background = context.getDrawable(if (isSaved) R.drawable.action_button_success else R.drawable.action_button_cancel)
setTextColor(context.getColor(if (isSaved) R.color.successColor else R.color.actionBarColor))
text = if (isSaved) "Open" else "Cancel"
text = if (isSaved)
activity.translation["button.open"]
else
activity.translation["button.cancel"]
}
}
@@ -171,7 +171,7 @@ class DownloadListAdapter(
pendingDownload.outputFile?.let {
val file = File(it)
if (!file.exists()) {
Toast.makeText(holder.view.context, "File does not exist", Toast.LENGTH_SHORT).show()
Toast.makeText(holder.view.context, activity.translation["file_not_found_toast"], Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val intent = Intent(Intent.ACTION_VIEW)

View File

@@ -10,7 +10,6 @@ import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
@@ -18,24 +17,24 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import me.rhunk.snapenhance.BuildConfig
import me.rhunk.snapenhance.R
import me.rhunk.snapenhance.download.MediaDownloadReceiver
import me.rhunk.snapenhance.bridge.TranslationWrapper
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.download.data.PendingDownload
class DownloadManagerActivity : Activity() {
lateinit var translation: TranslationWrapper
private val backCallbacks = mutableListOf<() -> Unit>()
private val fetchedDownloadTasks = mutableListOf<PendingDownload>()
private var listFilter = MediaFilter.NONE
private val downloadTaskManager by lazy {
MediaDownloadReceiver.downloadTaskManager.also { it.init(this) }
}
private val preferences by lazy {
getSharedPreferences("settings", Context.MODE_PRIVATE)
}
private fun updateNoDownloadText() {
findViewById<View>(R.id.no_download_title).let {
findViewById<TextView>(R.id.no_download_title).let {
it.text = translation["no_downloads"]
it.visibility = if (fetchedDownloadTasks.isEmpty()) View.VISIBLE else View.GONE
}
}
@@ -43,7 +42,7 @@ class DownloadManagerActivity : Activity() {
@SuppressLint("NotifyDataSetChanged")
private fun updateListContent() {
fetchedDownloadTasks.clear()
fetchedDownloadTasks.addAll(downloadTaskManager.queryAllTasks(filter = listFilter).values)
fetchedDownloadTasks.addAll(SharedContext.downloadTaskManager.queryAllTasks(filter = listFilter).values)
with(findViewById<RecyclerView>(R.id.download_list)) {
adapter?.notifyDataSetChanged()
@@ -68,6 +67,8 @@ class DownloadManagerActivity : Activity() {
@SuppressLint("BatteryLife", "NotifyDataSetChanged", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SharedContext.ensureInitialized(this)
translation = SharedContext.translation.getCategory("download_manager_activity")
setContentView(R.layout.download_manager_activity)
@@ -75,13 +76,13 @@ class DownloadManagerActivity : Activity() {
findViewById<TextView>(R.id.title).text = resources.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME
findViewById<ImageButton>(R.id.settings_button).setOnClickListener {
SettingLayoutInflater(this).inflate(findViewById<ViewGroup>(android.R.id.content))
SettingLayoutInflater(this).inflate(findViewById(android.R.id.content))
}
with(findViewById<RecyclerView>(R.id.download_list)) {
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this@DownloadManagerActivity)
adapter = DownloadListAdapter(fetchedDownloadTasks).apply {
adapter = DownloadListAdapter(this@DownloadManagerActivity, fetchedDownloadTasks).apply {
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
updateNoDownloadText()
@@ -113,7 +114,7 @@ class DownloadManagerActivity : Activity() {
@SuppressLint("NotifyDataSetChanged")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
fetchedDownloadTasks.removeAt(viewHolder.absoluteAdapterPosition).let {
downloadTaskManager.removeTask(it)
SharedContext.downloadTaskManager.removeTask(it)
}
adapter?.notifyItemRemoved(viewHolder.absoluteAdapterPosition)
}
@@ -133,7 +134,7 @@ class DownloadManagerActivity : Activity() {
if (lastVisibleItemPosition == fetchedDownloadTasks.size - 1 && !isLoading) {
isLoading = true
downloadTaskManager.queryTasks(fetchedDownloadTasks.last().id, filter = listFilter).forEach {
SharedContext.downloadTaskManager.queryTasks(fetchedDownloadTasks.last().id, filter = listFilter).forEach {
fetchedDownloadTasks.add(it.value)
adapter?.notifyItemInserted(fetchedDownloadTasks.size - 1)
}
@@ -151,7 +152,9 @@ class DownloadManagerActivity : Activity() {
Pair(R.id.spotlight_category, MediaFilter.SPOTLIGHT)
).let { categoryPairs ->
categoryPairs.forEach { pair ->
this@DownloadManagerActivity.findViewById<TextView>(pair.first).setOnClickListener { view ->
this@DownloadManagerActivity.findViewById<TextView>(pair.first).apply {
text = translation["category.${resources.getResourceEntryName(pair.first)}"]
}.setOnClickListener { view ->
listFilter = pair.second
updateListContent()
categoryPairs.map { this@DownloadManagerActivity.findViewById<TextView>(it.first) }.forEach {
@@ -162,12 +165,14 @@ class DownloadManagerActivity : Activity() {
}
}
this@DownloadManagerActivity.findViewById<Button>(R.id.remove_all_button).setOnClickListener {
this@DownloadManagerActivity.findViewById<Button>(R.id.remove_all_button).also {
it.text = translation["remove_all"]
}.setOnClickListener {
with(AlertDialog.Builder(this@DownloadManagerActivity)) {
setTitle(R.string.remove_all_title)
setMessage(R.string.remove_all_text)
setPositiveButton("Yes") { _, _ ->
downloadTaskManager.removeAllTasks()
setTitle(translation["remove_all_title"])
setMessage(translation["remove_all_text"])
setPositiveButton(translation["button.positive"]) { _, _ ->
SharedContext.downloadTaskManager.removeAllTasks()
fetchedDownloadTasks.removeIf {
if (it.isJobActive()) it.cancel()
true
@@ -175,7 +180,7 @@ class DownloadManagerActivity : Activity() {
adapter?.notifyDataSetChanged()
updateNoDownloadText()
}
setNegativeButton("Cancel") { dialog, _ ->
setNegativeButton(translation["button.negative"]) { dialog, _ ->
dialog.dismiss()
}
show()

View File

@@ -39,49 +39,56 @@ class SettingLayoutInflater(
AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { _, _ ->
.setPositiveButton(activity.translation["button.positive"]) { _, _ ->
action()
}
.setNegativeButton("No") { _, _ -> }
.setNegativeButton(activity.translation["button.negative"]) { _, _ -> }
.show()
}
}
private fun showSuccessToast() {
Toast.makeText(activity, "Success", Toast.LENGTH_SHORT).show()
}
fun inflate(parent: ViewGroup) {
val settingsView = activity.layoutInflater.inflate(R.layout.settings_page, parent, false)
val settingTranslation = activity.translation.getCategory("settings_page")
settingsView.findViewById<ImageButton>(R.id.settings_button).setOnClickListener {
parent.removeView(settingsView)
}
settingsView.findViewById<TextView>(R.id.title).text = activity.translation["settings"]
settingsView.findViewById<ListView>(R.id.setting_page_list).apply {
adapter = SettingAdapter(activity, R.layout.setting_item, mutableListOf<Pair<String, () -> Unit>>().apply {
add("Clear Cache" to {
add(settingTranslation["clear_cache_title"] to {
context.cacheDir.listFiles()?.forEach {
it.deleteRecursively()
}
Toast.makeText(context, "Cache cleared", Toast.LENGTH_SHORT).show()
showSuccessToast()
})
BridgeFileType.values().forEach { fileType ->
val actionName = "Clear ${fileType.displayName} File"
val actionName = settingTranslation.format("clear_file_title", "file_name" to fileType.displayName)
add(actionName to {
confirmAction(actionName, "Are you sure you want to clear ${fileType.displayName} file?") {
confirmAction(actionName, settingTranslation.format("clear_file_confirmation", "file_name" to fileType.displayName)) {
fileType.resolve(context).deleteRecursively()
Toast.makeText(context, "${fileType.displayName} file cleared", Toast.LENGTH_SHORT).show()
showSuccessToast()
}
})
}
add("Reset All" to {
confirmAction("Reset All", "Are you sure you want to reset all?") {
add(settingTranslation["reset_all_title"] to {
confirmAction(settingTranslation["reset_all_title"], settingTranslation["reset_all_confirmation"]) {
arrayOf(context.cacheDir, context.filesDir, File(context.dataDir, "databases"), File(context.dataDir, "shared_prefs")).forEach {
it.listFiles()?.forEach { file ->
file.deleteRecursively()
}
}
Toast.makeText(context, "Success!", Toast.LENGTH_SHORT).show()
showSuccessToast()
}
})
}.toTypedArray())

View File

@@ -57,7 +57,7 @@ class ChatActionMenu : AbstractMenu() {
if (context.config.bool(ConfigProperty.CHAT_DOWNLOAD_CONTEXT_MENU)) {
injectButton(Button(viewGroup.context).apply {
text = this@ChatActionMenu.context.translation.get("chat_action_menu.preview_button")
text = this@ChatActionMenu.context.translation["chat_action_menu.preview_button"]
setOnClickListener {
closeActionMenu()
this@ChatActionMenu.context.executeAsync { this@ChatActionMenu.context.feature(MediaDownloader::class).onMessageActionMenu(true) }
@@ -65,7 +65,7 @@ class ChatActionMenu : AbstractMenu() {
})
injectButton(Button(viewGroup.context).apply {
text = this@ChatActionMenu.context.translation.get("chat_action_menu.download_button")
text = this@ChatActionMenu.context.translation["chat_action_menu.download_button"]
setOnClickListener {
closeActionMenu()
this@ChatActionMenu.context.executeAsync {
@@ -80,7 +80,7 @@ class ChatActionMenu : AbstractMenu() {
//delete logged message button
if (context.config.bool(ConfigProperty.MESSAGE_LOGGER)) {
injectButton(Button(viewGroup.context).apply {
text = this@ChatActionMenu.context.translation.get("chat_action_menu.delete_logged_message_button")
text = this@ChatActionMenu.context.translation["chat_action_menu.delete_logged_message_button"]
setOnClickListener {
closeActionMenu()
this@ChatActionMenu.context.executeAsync {

View File

@@ -80,17 +80,18 @@ class FriendFeedInfoMenu : AbstractMenu() {
val birthday = Calendar.getInstance()
birthday[Calendar.MONTH] = (profile.birthday shr 32).toInt() - 1
val message: String = """
${context.translation.get("profile_info.username")}: ${profile.username}
${context.translation.get("profile_info.display_name")}: ${profile.displayName}
${context.translation.get("profile_info.added_date")}: ${formatDate(addedTimestamp)}
${context.translation["profile_info.username"]}: ${profile.username}
${context.translation["profile_info.display_name"]}: ${profile.displayName}
${context.translation["profile_info.added_date"]}: ${formatDate(addedTimestamp)}
${birthday.getDisplayName(
Calendar.MONTH,
Calendar.LONG,
context.translation.locale
)?.let {
context.translation.get("profile_info.birthday")
.replace("{month}", it)
.replace("{day}", profile.birthday.toInt().toString())
context.translation.format("profile_info.birthday",
"month" to it,
"day" to birthday[Calendar.DAY_OF_MONTH].toString()
)
}
}
""".trimIndent()
@@ -136,7 +137,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
}
}
var displayUsername = sender?.displayName ?: sender?.usernameForSorting?: context.translation.get("conversation_preview.unknown_user")
var displayUsername = sender?.displayName ?: sender?.usernameForSorting?: context.translation["conversation_preview.unknown_user"]
if (displayUsername.length > 12) {
displayUsername = displayUsername.substring(0, 13) + "... "
@@ -152,22 +153,23 @@ class FriendFeedInfoMenu : AbstractMenu() {
val timeSecondDiff = ((it - System.currentTimeMillis()) / 1000 / 60).toInt()
messageBuilder.append("\n\n")
.append("\uD83D\uDD25 ") //fire emoji
.append(context.translation.get("conversation_preview.streak_expiration").format(
timeSecondDiff / 60 / 24,
timeSecondDiff / 60 % 24,
timeSecondDiff % 60
.append(
context.translation.format("conversation_preview.streak_expiration",
"day" to (timeSecondDiff / 60 / 24).toString(),
"hour" to (timeSecondDiff / 60 % 24).toString(),
"minute" to (timeSecondDiff % 60).toString()
))
}
//alert dialog
val builder = AlertDialog.Builder(context.mainActivity)
builder.setTitle(context.translation.get("conversation_preview.title"))
builder.setTitle(context.translation["conversation_preview.title"])
builder.setMessage(messageBuilder.toString())
builder.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
targetPerson?.let {
builder.setNegativeButton(context.translation.get("modal_option.profile_info")) {_, _ ->
builder.setNegativeButton(context.translation["modal_option.profile_info"]) { _, _ ->
context.executeAsync {
showProfileInfo(it)
}
@@ -208,7 +210,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
private fun createToggleFeature(viewConsumer: ((View) -> Unit), text: String, isChecked: () -> Boolean, toggle: (Boolean) -> Unit) {
val switch = Switch(context.androidContext)
switch.text = context.translation.get(text)
switch.text = context.translation[text]
switch.isChecked = isChecked()
ViewAppearanceHelper.applyTheme(switch)
switch.setOnCheckedChangeListener { _: CompoundButton?, checked: Boolean ->
@@ -231,7 +233,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
if (!context.config.bool(ConfigProperty.ENABLE_FRIEND_FEED_MENU_BAR)) {
//preview button
val previewButton = Button(viewModel.context).apply {
text = modContext.translation.get("friend_menu_option.preview")
text = modContext.translation["friend_menu_option.preview"]
ViewAppearanceHelper.applyTheme(this, viewModel.width)
setOnClickListener {
showPreview(
@@ -244,7 +246,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
//stealth switch
val stealthSwitch = Switch(viewModel.context).apply {
text = modContext.translation.get("friend_menu_option.stealth_mode")
text = modContext.translation["friend_menu_option.stealth_mode"]
isChecked = modContext.feature(StealthMode::class).isStealth(conversationId)
ViewAppearanceHelper.applyTheme(this)
setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->

View File

@@ -70,7 +70,7 @@ class OperaContextActionMenu : AbstractMenu() {
ViewGroup.LayoutParams.MATCH_PARENT
)
val button = Button(childView.getContext())
button.text = context.translation.get("opera_context_menu.download")
button.text = context.translation["opera_context_menu.download"]
button.setOnClickListener { context.feature(MediaDownloader::class).downloadLastOperaMediaAsync() }
applyTheme(button)
linearLayout.addView(button)

View File

@@ -26,7 +26,7 @@ class SettingsMenu : AbstractMenu() {
@SuppressLint("ClickableViewAccessibility")
private fun createCategoryTitle(key: String): TextView {
val categoryText = TextView(context.androidContext)
categoryText.text = context.translation.get(key)
categoryText.text = context.translation[key]
ViewAppearanceHelper.applyTheme(categoryText)
categoryText.textSize = 20f
categoryText.typeface = categoryText.typeface?.let { Typeface.create(it, Typeface.BOLD) }
@@ -36,7 +36,7 @@ class SettingsMenu : AbstractMenu() {
@SuppressLint("SetTextI18n")
private fun createPropertyView(property: ConfigProperty): View {
val propertyName = context.translation.get(property.nameKey)
val propertyName = context.translation["property.${property.translationKey}"]
val updateButtonText: (TextView, String) -> Unit = { textView, text ->
textView.text = "$propertyName${if (text.isEmpty()) "" else ": $text"}"
}
@@ -50,7 +50,7 @@ class SettingsMenu : AbstractMenu() {
if (property.disableValueLocalization) {
it
} else {
context.translation.get("option." + property.nameKey + "." + it)
context.translation["option.property." + property.translationKey + "." + it]
}
}
})
@@ -129,7 +129,7 @@ class SettingsMenu : AbstractMenu() {
builder.setSingleChoiceItems(
property.valueContainer.keys().toTypedArray().map {
if (property.disableValueLocalization) it
else context.translation.get("option." + property.nameKey + "." + it)
else context.translation["option.property." + property.translationKey + "." + it]
}.toTypedArray(),
property.valueContainer.keys().indexOf(property.valueContainer.value())
) { _, which ->
@@ -158,7 +158,7 @@ class SettingsMenu : AbstractMenu() {
builder.setMultiChoiceItems(
sortedStates.toSortedMap().map {
if (property.disableValueLocalization) it.key
else context.translation.get("option." + property.nameKey + "." + it.key)
else context.translation["option.property." + property.translationKey + "." + it.key]
}.toTypedArray(),
sortedStates.map { it.value }.toBooleanArray()
) { _, which, isChecked ->
@@ -212,7 +212,7 @@ class SettingsMenu : AbstractMenu() {
val actions = context.actionManager.getActions().map {
Pair(it) {
val button = Button(viewModel.context)
button.text = context.translation.get(it.nameKey)
button.text = context.translation[it.nameKey]
button.setOnClickListener { _ ->
it.run()
}

View File

@@ -34,7 +34,7 @@
android:src="@drawable/settings_icon"
android:layout_gravity="center_vertical|end"
android:padding="8dp"
android:contentDescription="@string/settings" />
android:contentDescription="@null" />
</FrameLayout>
@@ -53,7 +53,7 @@
android:focusable="true"
android:fontFamily="@font/avenir_next_medium"
android:gravity="center"
android:text="@string/all_category"
android:text=""
android:textColor="@color/focusedCategoryColor" />
<TextView
@@ -65,7 +65,7 @@
android:focusable="true"
android:fontFamily="@font/avenir_next_medium"
android:gravity="center"
android:text="@string/pending_category"
android:text=""
android:textColor="@color/primaryText" />
<TextView
@@ -77,7 +77,7 @@
android:focusable="true"
android:fontFamily="@font/avenir_next_medium"
android:gravity="center"
android:text="@string/snap_category"
android:text=""
android:textColor="@color/primaryText" />
<TextView
@@ -89,7 +89,7 @@
android:focusable="true"
android:fontFamily="@font/avenir_next_medium"
android:gravity="center"
android:text="@string/story_category"
android:text=""
android:textColor="@color/primaryText" />
<TextView
@@ -101,7 +101,7 @@
android:focusable="true"
android:fontFamily="@font/avenir_next_medium"
android:gravity="center"
android:text="@string/spotlight_category"
android:text=""
android:textColor="@color/primaryText" />
</LinearLayout>
@@ -124,7 +124,7 @@
android:layout_centerInParent="true"
android:gravity="center"
android:paddingVertical="40dp"
android:text="@string/no_downloads"
android:text=""
android:textColor="@color/primaryText" />
</RelativeLayout>
@@ -141,7 +141,7 @@
android:layout_height="45dp"
android:background="@drawable/action_button_cancel"
android:padding="5dp"
android:text="@string/remove_all"
android:text=""
android:textColor="@color/darkText"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="5dp"/>

View File

@@ -71,7 +71,7 @@
android:background="@drawable/action_button_cancel"
android:padding="5dp"
android:layout_marginEnd="10dp"
android:text="@string/cancel"
android:text=""
android:textColor="@color/darkText"
tools:ignore="ButtonOrder" />
</LinearLayout>

View File

@@ -34,7 +34,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/settings"
android:text=""
android:textColor="@color/primaryText"
android:textSize="23sp"
android:fontFamily="@font/avenir_next_bold" />

View File

@@ -1,15 +1,3 @@
<resources>
<string name="app_name" translatable="false">SnapEnhance</string>
<string name="remove_all_title">Remove all Downloads</string>
<string name="remove_all_text">Are you sure you want to do this?</string>
<string name="remove_all">Remove All</string>
<string name="no_downloads">No downloads</string>
<string name="cancel">Cancel</string>
<string name="all_category">All</string>
<string name="pending_category">Pending</string>
<string name="snap_category">Snaps</string>
<string name="story_category">Stories</string>
<string name="spotlight_category">Spotlight</string>
<string name="settings">Settings</string>
<string name="refresh_mappings">Refresh Mappings</string>
</resources>