commit 37026afcb0505a70a3af1568aa6cc7f5980ae9b0 parent c6eb14fee0eb2ed6a4508696cfc487a30408ee0a Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:12:49 +0200 refactor: settings (#85) * refactor: settings * add: config property description * fix: category bug * fix translations and fine tune theme * feat: settings gear * remove unused view * fix dialog color, add missing translations * set switch track color * fix: colors - config textview color - alert dialog default theme * add more descriptions * fix: settings - action dependsOnProperty - property shouldAppearInSettings * update en_US * feat: hide story sections * fix: alert dialog edit text color * fix: discover block ads * fix: better immersive camera --------- Co-authored-by: auth <64337177+authorisation@users.noreply.github.com> Diffstat:
26 files changed, 786 insertions(+), 163 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml @@ -54,6 +54,11 @@ android:name=".ui.map.MapActivity" android:exported="true" android:excludeFromRecents="true" /> + <activity + android:name=".ui.config.ConfigActivity" + android:theme="@style/AppTheme" + android:excludeFromRecents="true" + android:exported="true" /> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/assets/lang/en_US.json b/app/src/main/assets/lang/en_US.json @@ -9,6 +9,7 @@ }, "action": { + "open_settings_button": "Open SnapEnhance Settings", "clean_cache": "Clean Cache", "clear_message_logger": "Clear Message Logger", "refresh_mappings": "Refresh Mappings", @@ -31,10 +32,11 @@ "auto_download_blacklist": "Auto Download Blacklist", "disable_metrics": "Disable Metrics", "prevent_screenshot_notifications": "Prevent Screenshot Notifications", - "prevent_status_notifications": "Prevent Status Notifications (Save to camera roll, etc)", + "prevent_status_notifications": "Prevent Status Notifications", "anonymous_story_view": "Anonymous Story View", "hide_typing_notification": "Hide Typing Notification", "message_preview_length": "Message Preview Length", + "unlimited_conversion_pinning": "Unlimited Conversation Pinning", "gallery_media_send_override": "Gallery Media Send Override", "auto_save_messages": "Auto Save Messages", "anti_auto_save": "Anti Auto Save Button", @@ -71,7 +73,68 @@ "friend_feed_menu_buttons": "Friend Feed Menu Buttons", "friend_feed_menu_buttons_position": "Friend Feed Buttons Position Index", "unlimited_conversation_pinning": "Unlimited Conversation Pinning", - "story_viewer_override": "Story Viewer Override" + "story_viewer_override": "Story Viewer Override", + "hide_story_section": "Hide story sections" + }, + + "description": { + "save_folder": "The directory where all media is saved", + "prevent_read_receipts": "Prevent anyone from knowing you've opened their Snaps", + "hide_bitmoji_presence": "Hides your Bitmoji presence from the chat", + "better_notifications": "Shows more information in notifications", + "notification_blacklist": "Hides selected notification type", + "message_logger": "Prevents messages from being deleted", + "unlimited_snap_view_time": "Removes the 10 second Snap view time limit", + "auto_download_options": "Select which medias to auto download", + "download_options": "Specify the file path format", + "chat_download_context_menu": "Enable the chat download context menu", + "auto_download_blacklist": "Prevents auto downloading from specified users", + "disable_metrics": "Disables metrics sent to Snapchat", + "prevent_screenshot_notifications": "Prevents anyone from knowing that you've taken a screenshot", + "prevent_status_notifications": "Prevents sending status notifications\ne.g. Saved to camera roll", + "anonymous_story_view": "Prevents anyone from knowing you've seen their story", + "hide_typing_notification": "Prevents typing notifications being sent", + "message_preview_length": "Specify the amount of messages to be previewed", + "unlimited_conversion_pinning": "Enables the ability to pin unlimited conversations", + "gallery_media_send_override": "Overrides media sent from the gallery", + "auto_save_messages": "Select which type of messages to auto save", + "anti_auto_save": "Prevents Auto saving of messages from the specified users", + "snapchat_plus": "Enables Snapchat Plus features", + "disable_snap_splitting": "Prevents Snaps from being split into multiple parts", + "disable_video_length_restriction": "Disables video length restrictions", + "force_media_source_quality": "Overrides the media source quality", + "remove_voice_record_button": "Removes the voice record button", + "remove_stickers_button": "Removes the stickers button", + "remove_cognac_button": "Removes the cognac button", + "remove_call_buttons": "Removes the call buttons", + "block_ads": "Prevents ads from being displayed", + "story_viewer_override": "Turns on certain features which Snapchat hid", + "streak_expiration_info": "Shows Streak expiration info next to streaks", + "new_map_ui": "Enables the new Map UI", + "app_passcode": "Sets a passcode to lock the app", + "app_lock_on_resume": "Locks the app when it's opened", + "meo_passcode_bypass": "Bypass the My Eyes Only passcode\nThis will only work if the passcode has been entered correctly before", + "location_spoof": "Spoofs your location on the Snapmap", + "latitude_value": "The latitude of your Spoofed location", + "longitude_value": "The longitude of your Spoofed location", + "hide_ui_elements": "Select which UI elements to hide", + "auto_updater": "The interval of checking for updates", + "disable_camera": "Prevents Snapchat from being able to use the camera", + "immersive_camera_preview": "Stops Snapchat from cropping the camera preview", + "infinite_story_boost": "Infinitely boosts your story", + "enable_app_appearance": "Enables the hidden app appearance settings", + "disable_spotlight": "Disables the Spotlight page", + "preview_resolution": "Overrides the preview resolution", + "picture_resolution": "Overrides the picture resolution", + "force_highest_frame_rate": "Forces the highest possible frame rate", + "amoled_dark_mode": "Enables AMOLED dark mode\nMake sure Snapchat's dark mode is enabled", + "friend_feed_menu_buttons_position": "The position of the Friend Feed Menu Buttons", + "friend_feed_menu_buttons": "Select which buttons to show in the Friend Feed Menu Bar", + "friend_feed_menu_bar": "Enables the new Friend Feed Menu Bar", + "enable_friend_feed_menu_bar": "Enables the new Friend Feed Menu Bar", + "unlimited_conversation_pinning": "Enables the ability to Pin unlimited conversations", + "force_camera_source_encoding": "Forces Camera Source Encoding", + "hide_story_section": "Hide certain UI Elements shown in the story section" }, "option": { @@ -136,6 +199,11 @@ "OFF": "Off", "DISCOVER_PLAYBACK_SEEKBAR": "Enable Discover Playback Seekbar", "VERTICAL_STORY_VIEWER": "Enable Vertical Story Viewer" + }, + "hide_story_section": { + "hide_friends": "Hide friends section", + "hide_following": "Hide following section", + "hide_for_you": "Hide For You section" } } }, @@ -208,6 +276,14 @@ "exporting_message": "Exporting {conversation}..." }, + "button": { + "ok": "OK", + "positive": "Yes", + "negative": "No", + "cancel": "Cancel", + "open": "Open" + }, + "download_manager_activity": { "remove_all_title": "Remove all Downloads", "remove_all_text": "Are you sure you want to do this?", @@ -223,12 +299,6 @@ "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?", @@ -247,5 +317,10 @@ "failed_generic_toast": "Failed to download", "failed_processing_toast": "Failed to process {error}", "failed_gallery_toast": "Failed to save to gallery {error}" + }, + "config_activity": { + "title": "SnapEnhance Settings", + "selected_text": "{count} selected", + "invalid_number_toast": "Invalid number!" } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -12,12 +12,12 @@ 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.ConfigWrapper import me.rhunk.snapenhance.bridge.TranslationWrapper import me.rhunk.snapenhance.data.MessageSender import me.rhunk.snapenhance.database.DatabaseAccess import me.rhunk.snapenhance.features.Feature 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.util.download.DownloadServer @@ -40,9 +40,9 @@ class ModContext { val gson: Gson = GsonBuilder().create() val translation = TranslationWrapper() + val config = ConfigWrapper() val features = FeatureManager(this) val mappings = MappingManager(this) - val config = ConfigManager(this) val actionManager = ActionManager(this) val database = DatabaseAccess(this) val downloadServer = DownloadServer() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -59,6 +59,23 @@ class SnapEnhance { if (isMainActivityNotNull || !appContext.mappings.areMappingsLoaded) return@hook onActivityCreate() } + + var activityWasResumed = false + + //we need to reload the config when the app is resumed + Hooker.hook(Activity::class.java, "onResume", HookStage.AFTER) { + val activity = it.thisObject() as Activity + + if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook + + if (!activityWasResumed) { + activityWasResumed = true + return@hook + } + + Logger.debug("Reloading config") + appContext.config.loadFromBridge(appContext.bridgeClient) + } } @SuppressLint("ObsoleteSdkInt") @@ -79,7 +96,7 @@ class SnapEnhance { measureTime { with(appContext) { - config.init() + config.loadFromBridge(bridgeClient) mappings.init() //if mappings aren't loaded, we can't initialize features if (!mappings.areMappingsLoaded) return diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ConfigWrapper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ConfigWrapper.kt @@ -0,0 +1,79 @@ +package me.rhunk.snapenhance.bridge + +import android.content.Context +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType +import me.rhunk.snapenhance.config.ConfigAccessor +import me.rhunk.snapenhance.config.ConfigProperty + +class ConfigWrapper: ConfigAccessor() { + companion object { + private val gson = GsonBuilder().setPrettyPrinting().create() + } + + private lateinit var isFileExistsAction: () -> Boolean + private lateinit var writeFileAction: (ByteArray) -> Unit + private lateinit var readFileAction: () -> ByteArray + + fun load() { + ConfigProperty.sortedByCategory().forEach { key -> + set(key, key.valueContainer) + } + + if (!isFileExistsAction()) { + writeConfig() + return + } + + runCatching { + loadConfig() + }.onFailure { + Logger.error("Failed to load config", it) + writeConfig() + } + } + + private fun loadConfig() { + val configContent = readFileAction() + + val configObject: JsonObject = gson.fromJson( + configContent.toString(Charsets.UTF_8), + JsonObject::class.java + ) + + entries().forEach { (key, value) -> + value.writeFrom(configObject.get(key.name)?.asString ?: value.read()) + } + } + + fun writeConfig() { + val configObject = JsonObject() + entries().forEach { (key, value) -> + configObject.addProperty(key.name, value.read()) + } + writeFileAction(gson.toJson(configObject).toByteArray(Charsets.UTF_8)) + } + + fun loadFromContext(context: Context) { + val configFile = BridgeFileType.CONFIG.resolve(context) + isFileExistsAction = { configFile.exists() } + readFileAction = { + if (!configFile.exists()) { + configFile.createNewFile() + configFile.writeBytes("{}".toByteArray(Charsets.UTF_8)) + } + configFile.readBytes() + } + writeFileAction = { configFile.writeBytes(it) } + load() + } + + fun loadFromBridge(bridgeClient: AbstractBridgeClient) { + isFileExistsAction = { bridgeClient.isFileExists(BridgeFileType.CONFIG) } + readFileAction = { bridgeClient.createAndReadFile(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) } + writeFileAction = { bridgeClient.writeFile(BridgeFileType.CONFIG, it) } + load() + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/ServiceBridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/client/ServiceBridgeClient.kt @@ -33,9 +33,7 @@ import me.rhunk.snapenhance.bridge.common.impl.messagelogger.MessageLoggerResult import me.rhunk.snapenhance.bridge.service.BridgeService import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock import kotlin.reflect.KClass import kotlin.system.exitProcess @@ -94,7 +92,10 @@ class ServiceBridgeClient: AbstractBridgeClient(), ServiceConnection { what = messageType.value replyTo = Messenger(object : Handler(handlerThread.looper) { override fun handleMessage(msg: Message) { - if (continuation.isCompleted) return + if (continuation.isCompleted) { + continuation.cancel(Throwable("Already completed")) + return + } continuation.resumeWith(Result.success(handleResponseMessage(msg) as T)) } }) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/service/BridgeService.kt @@ -35,7 +35,7 @@ class BridgeService : Service() { runCatching { this@BridgeService.handleMessage(msg) }.onFailure { - Logger.error("Failed to handle message", it) + Logger.error("Failed to handle message ${BridgeMessageType.fromValue(msg.what)}", it) } } }).binder @@ -97,6 +97,7 @@ class BridgeService : Service() { MessageLoggerRequest.Action.GET -> { val (state, messageData) = messageLoggerWrapper.getMessage(msg.conversationId!!, msg.index!!) reply(MessageLoggerResult(state, messageData).toMessage(BridgeMessageType.MESSAGE_LOGGER_RESULT.value)) + return } MessageLoggerRequest.Action.LIST_IDS -> { val messageIds = messageLoggerWrapper.getMessageIds(msg.conversationId!!, msg.index!!.toInt()) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt @@ -3,10 +3,10 @@ package me.rhunk.snapenhance.config enum class ConfigCategory( val key: String ) { - SPYING_PRIVACY("category.spying_privacy"), - MEDIA_MANAGEMENT("category.media_manager"), - UI_TWEAKS("category.ui_tweaks"), - UPDATES("category.updates"), - CAMERA("category.camera"), - EXPERIMENTAL_DEBUGGING("category.experimental_debugging"); + SPYING_PRIVACY("spying_privacy"), + MEDIA_MANAGEMENT("media_manager"), + UI_TWEAKS("ui_tweaks"), + UPDATES("updates"), + CAMERA("camera"), + EXPERIMENTAL_DEBUGGING("experimental_debugging"); } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt @@ -196,6 +196,18 @@ enum class ConfigProperty( ) ) ), + HIDE_STORY_SECTION( + "hide_story_section", + ConfigCategory.UI_TWEAKS, + ConfigStateListValue( + listOf("hide_friends", "hide_following", "hide_for_you"), + mutableMapOf( + "hide_friends" to false, + "hide_following" to false, + "hide_for_you" to false + ) + ) + ), STORY_VIEWER_OVERRIDE("story_viewer_override", ConfigCategory.UI_TWEAKS, ConfigStateSelection( diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt @@ -4,42 +4,55 @@ import android.annotation.SuppressLint import android.content.res.Resources import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.hook.hook class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { + private val identifierCache = mutableMapOf<String, Int>() + + @SuppressLint("DiscouragedApi") + fun getIdentifier(name: String, defType: String): Int { + return identifierCache.getOrPut("$name:$defType") { + context.resources.getIdentifier(name, defType, Constants.SNAPCHAT_PACKAGE_NAME) + } + } + + private fun hideStorySection(param: HookAdapter) { + val parent = param.thisObject() as ViewGroup + parent.visibility = View.GONE + val marginLayoutParams = parent.layoutParams as ViewGroup.MarginLayoutParams + marginLayoutParams.setMargins(-99999, -99999, -99999, -99999) + param.setResult(null) + } + @SuppressLint("DiscouragedApi") override fun onActivityCreate() { + val blockAds = context.config.bool(ConfigProperty.BLOCK_ADS) val hiddenElements = context.config.options(ConfigProperty.HIDE_UI_ELEMENTS) + val hideStorySection = context.config.options(ConfigProperty.HIDE_STORY_SECTION) val isImmersiveCamera = context.config.bool(ConfigProperty.IMMERSIVE_CAMERA_PREVIEW) - val resources = context.resources - - fun findIdentifier(name: String, defType: String) = resources.getIdentifier(name, defType, Constants.SNAPCHAT_PACKAGE_NAME) val displayMetrics = context.resources.displayMetrics - val capriViewfinderDefaultCornerRadius = findIdentifier("capri_viewfinder_default_corner_radius", "dimen") - val ngsHovaNavLargerCameraButtonSize = findIdentifier("ngs_hova_nav_larger_camera_button_size", "dimen") - val fullScreenSurfaceView = findIdentifier("full_screen_surface_view", "id") + val callButtonsStub = getIdentifier("call_buttons_stub", "id") + val callButton1 = getIdentifier("friend_action_button3", "id") + val callButton2 = getIdentifier("friend_action_button4", "id") - val callButtonsStub = findIdentifier("call_buttons_stub", "id") - val callButton1 = findIdentifier("friend_action_button3", "id") - val callButton2 = findIdentifier("friend_action_button4", "id") - - val chatNoteRecordButton = findIdentifier("chat_note_record_button", "id") - val chatInputBarSticker = findIdentifier("chat_input_bar_sticker", "id") - val chatInputBarCognac = findIdentifier("chat_input_bar_cognac", "id") + val chatNoteRecordButton = getIdentifier("chat_note_record_button", "id") Resources::class.java.methods.first { it.name == "getDimensionPixelSize"}.hook(HookStage.AFTER, { isImmersiveCamera } ) { param -> val id = param.arg<Int>(0) - if (id == capriViewfinderDefaultCornerRadius || id == ngsHovaNavLargerCameraButtonSize) { + if (id == getIdentifier("capri_viewfinder_default_corner_radius", "dimen") || + id == getIdentifier("ngs_hova_nav_larger_camera_button_size", "dimen")) { param.setResult(0) } } @@ -64,9 +77,33 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE val view: View = param.arg(0) val viewId = view.id - if (isImmersiveCamera && view.id == fullScreenSurfaceView) { - Hooker.hookObjectMethod(View::class.java, view, "layout", HookStage.BEFORE) { param -> - param.setArg(3, displayMetrics.heightPixels) + if (hideStorySection["hide_for_you"] == true) { + if (viewId == getIdentifier("df_large_story", "id") || + viewId == getIdentifier("df_promoted_story", "id")) { + hideStorySection(param) + return@hook + } + if (viewId == getIdentifier("stories_load_progress_layout", "id")) { + param.setResult(null) + } + } + + if (hideStorySection["hide_friends"] == true && viewId == getIdentifier("friend_card_frame", "id")) { + hideStorySection(param) + } + + if (hideStorySection["hide_following"] == true && (viewId == getIdentifier("df_small_story", "id")) + ) { + hideStorySection(param) + } + + if (blockAds && viewId == getIdentifier("df_promoted_story", "id")) { + hideStorySection(param) + } + + if (isImmersiveCamera && view.id == getIdentifier("full_screen_surface_view", "id")) { + Hooker.hookObjectMethod(View::class.java, view, "setLayoutParams", HookStage.BEFORE) { + it.setArg(0, FrameLayout.LayoutParams(displayMetrics.widthPixels, displayMetrics.heightPixels)) } } @@ -75,10 +112,10 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE view.setWillNotDraw(true) } - if (chatInputBarCognac == viewId && hiddenElements["remove_cognac_button"] == true) { + if (getIdentifier("chat_input_bar_cognac", "id") == viewId && hiddenElements["remove_cognac_button"] == true) { view.visibility = View.GONE } - if (chatInputBarSticker == viewId && hiddenElements["remove_stickers_button"] == true) { + if (getIdentifier("chat_input_bar_sticker", "id") == viewId && hiddenElements["remove_stickers_button"] == true) { view.visibility = View.GONE } if (viewId == callButton1 || viewId == callButton2) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt @@ -0,0 +1,252 @@ +package me.rhunk.snapenhance.ui.config + +import android.app.Activity +import android.app.AlertDialog +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageButton +import android.widget.Switch +import android.widget.TextView +import android.widget.Toast +import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.bridge.ConfigWrapper +import me.rhunk.snapenhance.config.ConfigCategory +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.config.impl.ConfigIntegerValue +import me.rhunk.snapenhance.config.impl.ConfigStateListValue +import me.rhunk.snapenhance.config.impl.ConfigStateSelection +import me.rhunk.snapenhance.config.impl.ConfigStateValue +import me.rhunk.snapenhance.config.impl.ConfigStringValue + +class ConfigActivity : Activity() { + private val config = ConfigWrapper() + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onBackPressed() { + super.onBackPressed() + finish() + } + + override fun onDestroy() { + super.onDestroy() + config.writeConfig() + } + + override fun onPause() { + super.onPause() + config.writeConfig() + } + + private val positiveButtonText by lazy { + SharedContext.translation["button.ok"] + } + + private val cancelButtonText by lazy { + SharedContext.translation["button.cancel"] + } + + private fun longToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + + private fun createTranslatedTextView(property: ConfigProperty, shouldTranslatePropertyValue: Boolean = true): TextView { + return object: TextView(this) { + override fun setText(text: CharSequence?, type: BufferType?) { + val newText = text?.takeIf { it.isNotEmpty() }?.let { + if (!shouldTranslatePropertyValue || property.disableValueLocalization) it + else SharedContext.translation["option.property." + property.translationKey + "." + it] + }?.let { + if (it.length > 20) { + it.substring(0, 20) + "..." + } else { + it + } + } ?: "" + super.setTextColor(getColor(R.color.tertiaryText)) + super.setText(newText, type) + } + } + } + + private fun askForValue(property: ConfigProperty, requestedInputType: Int, callback: (String) -> Unit) { + val editText = EditText(this).apply { + inputType = requestedInputType + setText(property.valueContainer.value().toString()) + } + AlertDialog.Builder(this) + .setTitle(SharedContext.translation["property.${property.translationKey}"]) + .setView(editText) + .setPositiveButton(positiveButtonText) { _, _ -> + callback(editText.text.toString()) + } + .setNegativeButton(cancelButtonText) { dialog, _ -> + dialog.cancel() + } + .show() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + config.loadFromContext(this) + SharedContext.ensureInitialized(this) + setContentView(R.layout.config_activity) + + findViewById<View>(R.id.title_bar).let { titleBar -> + titleBar.findViewById<TextView>(R.id.title).text = SharedContext.translation["config_activity.title"] + titleBar.findViewById<ImageButton>(R.id.back_button).visibility = View.GONE + } + + val propertyListLayout = findViewById<ViewGroup>(R.id.property_list) + + var currentCategory: ConfigCategory? = null + + config.entries().forEach { (property, value) -> + val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) + + fun addSeparator() { + //add separator + propertyListLayout.addView(View(this).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) + setBackgroundColor(getColor(R.color.tertiaryBackground)) + }) + } + + if (property.category != currentCategory) { + currentCategory = property.category + with(layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)) { + findViewById<TextView>(R.id.name).apply { + text = SharedContext.translation["category.${property.category.key}"] + textSize = 20f + typeface = typeface?.let { android.graphics.Typeface.create(it, android.graphics.Typeface.BOLD) } + } + propertyListLayout.addView(this) + } + addSeparator() + } + + if (!property.shouldAppearInSettings) return@forEach + + val propertyName = SharedContext.translation["property.${property.translationKey}"] + + configItem.findViewById<TextView>(R.id.name).text = propertyName + configItem.findViewById<TextView>(R.id.description).also { + it.text = SharedContext.translation["description.${property.translationKey}"] + it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE + } + + fun addValueView(view: View) { + configItem.findViewById<ViewGroup>(R.id.value).addView(view) + } + + when (value) { + is ConfigStateValue -> { + val switch = Switch(this) + switch.isChecked = value.value() + switch.trackTintList = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + switch.highlightColor, + getColor(R.color.tertiaryBackground) + ) + ) + switch.setOnCheckedChangeListener { _, isChecked -> + value.writeFrom(isChecked.toString()) + } + configItem.setOnClickListener { switch.toggle() } + addValueView(switch) + } + is ConfigStringValue, is ConfigIntegerValue -> { + val textView = createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { + it.text = value.value().toString() + } + configItem.setOnClickListener { + if (value is ConfigIntegerValue) { + askForValue(property, InputType.TYPE_CLASS_NUMBER) { + try { + value.writeFrom(it) + textView.text = value.value().toString() + } catch (e: NumberFormatException) { + longToast(SharedContext.translation["config_activity.invalid_number_toast"]) + } + } + return@setOnClickListener + } + askForValue(property, InputType.TYPE_CLASS_TEXT) { + value.writeFrom(it) + textView.text = value.value().toString() + } + } + addValueView(textView) + } + is ConfigStateListValue -> { + val textView = createTranslatedTextView(property, shouldTranslatePropertyValue = false) + val values = value.value() + + fun updateText() { + textView.text = SharedContext.translation.format("config_activity.selected_text", "count" to values.filter { it.value }.size.toString()) + } + + updateText() + + configItem.setOnClickListener { + AlertDialog.Builder(this) + .setTitle(propertyName) + .setPositiveButton(positiveButtonText) { _, _ -> + updateText() + } + .setMultiChoiceItems( + values.keys.map { + if (property.disableValueLocalization) it + else SharedContext.translation["option.property." + property.translationKey + "." + it] + }.toTypedArray(), + values.map { it.value }.toBooleanArray() + ) { _, which, isChecked -> + value.setKey(values.keys.elementAt(which), isChecked) + } + .show() + } + + addValueView(textView) + } + is ConfigStateSelection -> { + val textView = createTranslatedTextView(property, shouldTranslatePropertyValue = true) + textView.text = value.value() + + configItem.setOnClickListener { + val builder = AlertDialog.Builder(this) + builder.setTitle(propertyName) + + builder.setSingleChoiceItems( + value.keys().toTypedArray().map { + if (property.disableValueLocalization) it + else SharedContext.translation["option.property." + property.translationKey + "." + it] + }.toTypedArray(), + value.keys().indexOf(value.value()) + ) { _, which -> + value.writeFrom(value.keys()[which]) + } + + builder.setPositiveButton(positiveButtonText) { _, _ -> + textView.text = value.value() + } + + builder.show() + } + addValueView(textView) + } + } + + propertyListLayout.addView(configItem) + addSeparator() + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.job import kotlinx.coroutines.launch import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.download.data.PendingDownload import me.rhunk.snapenhance.download.enums.DownloadStage @@ -91,11 +92,6 @@ 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) @@ -115,9 +111,9 @@ class DownloadListAdapter( 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) - activity.translation["button.open"] + SharedContext.translation["button.open"] else - activity.translation["button.cancel"] + SharedContext.translation["button.cancel"] } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt @@ -71,8 +71,7 @@ class DownloadManagerActivity : Activity() { translation = SharedContext.translation.getCategory("download_manager_activity") setContentView(R.layout.download_manager_activity) - - window.navigationBarColor = getColor(R.color.primaryBackground) + findViewById<TextView>(R.id.title).text = resources.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME findViewById<ImageButton>(R.id.settings_button).setOnClickListener { @@ -171,7 +170,7 @@ class DownloadManagerActivity : Activity() { with(AlertDialog.Builder(this@DownloadManagerActivity)) { setTitle(translation["remove_all_title"]) setMessage(translation["remove_all_text"]) - setPositiveButton(translation["button.positive"]) { _, _ -> + setPositiveButton(SharedContext.translation["button.positive"]) { _, _ -> SharedContext.downloadTaskManager.removeAllTasks() fetchedDownloadTasks.removeIf { if (it.isJobActive()) it.cancel() @@ -180,7 +179,7 @@ class DownloadManagerActivity : Activity() { adapter?.notifyDataSetChanged() updateNoDownloadText() } - setNegativeButton(translation["button.negative"]) { dialog, _ -> + setNegativeButton(SharedContext.translation["button.negative"]) { dialog, _ -> dialog.dismiss() } show() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/SettingLayoutInflater.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/SettingLayoutInflater.kt @@ -9,6 +9,7 @@ import android.widget.ListView import android.widget.TextView import android.widget.Toast import me.rhunk.snapenhance.R +import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.bridge.common.impl.file.BridgeFileType import java.io.File @@ -39,10 +40,10 @@ class SettingLayoutInflater( AlertDialog.Builder(activity) .setTitle(title) .setMessage(message) - .setPositiveButton(activity.translation["button.positive"]) { _, _ -> + .setPositiveButton(SharedContext.translation["button.positive"]) { _, _ -> action() } - .setNegativeButton(activity.translation["button.negative"]) { _, _ -> } + .setNegativeButton(SharedContext.translation["button.negative"]) { _, _ -> } .show() } } @@ -52,18 +53,18 @@ class SettingLayoutInflater( } fun inflate(parent: ViewGroup) { - val settingsView = activity.layoutInflater.inflate(R.layout.settings_page, parent, false) + val settingsView = activity.layoutInflater.inflate(R.layout.debug_settings_page, parent, false) val settingTranslation = activity.translation.getCategory("settings_page") - settingsView.findViewById<ImageButton>(R.id.settings_button).setOnClickListener { + settingsView.findViewById<ImageButton>(R.id.back_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 { + adapter = SettingAdapter(activity, R.layout.debug_setting_item, mutableListOf<Pair<String, () -> Unit>>().apply { add(settingTranslation["clear_cache_title"] to { context.cacheDir.listFiles()?.forEach { it.deleteRecursively() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt @@ -22,6 +22,7 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar private val operaContextActionMenu = OperaContextActionMenu() private val chatActionMenu = ChatActionMenu() private val settingMenu = SettingsMenu() + private val settingsGearInjector = SettingsGearInjector() private val newChatString by lazy { context.resources.getString(context.resources.getIdentifier("new_chat", "string", Constants.SNAPCHAT_PACKAGE_NAME)) @@ -41,12 +42,15 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar operaContextActionMenu.context = context chatActionMenu.context = context settingMenu.context = context + settingsGearInjector.context = context val messaging = context.feature(Messaging::class) val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id", Constants.SNAPCHAT_PACKAGE_NAME) val actionSheetContainer = context.resources.getIdentifier("action_sheet_container", "id", Constants.SNAPCHAT_PACKAGE_NAME) val actionMenu = context.resources.getIdentifier("action_menu", "id", Constants.SNAPCHAT_PACKAGE_NAME) + val componentsHolder = context.resources.getIdentifier("components_holder", "id", Constants.SNAPCHAT_PACKAGE_NAME) + val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id", Constants.SNAPCHAT_PACKAGE_NAME) val addViewMethod = ViewGroup::class.java.getMethod( "addView", @@ -69,6 +73,11 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar val childView: View = param.arg(0) operaContextActionMenu.inject(viewGroup, childView) + if (viewGroup.id == componentsHolder && childView.id == feedNewChat) { + settingsGearInjector.inject(viewGroup, childView) + return@hook + } + //download in chat snaps and notes from the chat action menu if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) { if (viewGroup.parent == null || viewGroup.parent.parent == null) return@hook diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt @@ -0,0 +1,84 @@ +package me.rhunk.snapenhance.ui.menu.impl + +import android.annotation.SuppressLint +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import me.rhunk.snapenhance.BuildConfig +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.ui.config.ConfigActivity +import me.rhunk.snapenhance.ui.menu.AbstractMenu + + +@SuppressLint("DiscouragedApi") +class SettingsGearInjector : AbstractMenu() { + private val headerButtonOpaqueIconTint by lazy { + context.resources.getIdentifier("headerButtonOpaqueIconTint", "attr", Constants.SNAPCHAT_PACKAGE_NAME).let { + context.androidContext.theme.obtainStyledAttributes(intArrayOf(it)).getColorStateList(0) + } + } + + private val settingsSvg by lazy { + context.resources.getIdentifier("svg_settings_32x32", "drawable", Constants.SNAPCHAT_PACKAGE_NAME).let { + context.resources.getDrawable(it, context.androidContext.theme) + } + } + + private val ngsHovaHeaderSearchIconBackgroundMarginLeft by lazy { + context.resources.getIdentifier("ngs_hova_header_search_icon_background_margin_left", "dimen", Constants.SNAPCHAT_PACKAGE_NAME).let { + context.resources.getDimensionPixelSize(it) + } + } + + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") + fun inject(parent: ViewGroup, child: View) { + val firstView = (child as ViewGroup).getChildAt(0) + + child.clipChildren = false + child.addView(FrameLayout(parent.context).apply { + layoutParams = FrameLayout.LayoutParams(firstView.layoutParams.width, firstView.layoutParams.height).apply { + y = 0f + x = -(ngsHovaHeaderSearchIconBackgroundMarginLeft + firstView.layoutParams.width).toFloat() + } + + isClickable = true + + setOnClickListener { + val intent = Intent().apply { + setClassName(BuildConfig.APPLICATION_ID, ConfigActivity::class.java.name) + } + context.startActivity(intent) + } + + parent.setOnTouchListener { _, event -> + if (child.visibility == View.INVISIBLE || child.alpha == 0F) return@setOnTouchListener false + + val viewLocation = IntArray(2) + getLocationOnScreen(viewLocation) + + val x = event.rawX - viewLocation[0] + val y = event.rawY - viewLocation[1] + + if (x > 0 && x < width && y > 0 && y < height) { + performClick() + } + + false + } + backgroundTintList = firstView.backgroundTintList + background = firstView.background + + addView(ImageView(context).apply { + layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 17).apply { + gravity = android.view.Gravity.CENTER + } + setImageDrawable(settingsSvg) + headerButtonOpaqueIconTint?.let { + imageTintList = it + } + }) + }) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt @@ -11,8 +11,6 @@ import android.widget.EditText import android.widget.LinearLayout import android.widget.Switch import android.widget.TextView -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.config.impl.ConfigIntegerValue import me.rhunk.snapenhance.config.impl.ConfigStateListValue @@ -191,28 +189,14 @@ class SettingsMenu : AbstractMenu() { } @SuppressLint("SetTextI18n") - @Suppress("deprecation") fun inject(viewModel: View, addView: (View) -> Unit) { - val packageInfo = viewModel.context.packageManager.getPackageInfo(Constants.SNAPCHAT_PACKAGE_NAME, 0) - val versionTextBuilder = StringBuilder() - versionTextBuilder.append("SnapEnhance ").append(BuildConfig.VERSION_NAME) - .append(" by rhunk") - if (BuildConfig.DEBUG) { - versionTextBuilder.append("\n").append("Snapchat ").append(packageInfo.versionName) - .append(" (").append(packageInfo.longVersionCode).append(")") - } - val titleText = TextView(viewModel.context) - titleText.text = versionTextBuilder.toString() - ViewAppearanceHelper.applyTheme(titleText) - titleText.textSize = 18f - titleText.minHeight = 80 * versionTextBuilder.chars().filter { ch: Int -> ch == '\n'.code } - .count().coerceAtLeast(2).toInt() - addView(titleText) - val actions = context.actionManager.getActions().map { Pair(it) { val button = Button(viewModel.context) - button.text = context.translation[it.nameKey] + button.text = (it.dependsOnProperty?.let { property -> + "["+context.translation["property.${property.translationKey}"] + "] " + }?: "") + context.translation[it.nameKey] + button.setOnClickListener { _ -> it.run() } @@ -221,22 +205,8 @@ class SettingsMenu : AbstractMenu() { } } - context.config.entries().groupBy { - it.key.category - }.forEach { (category, value) -> - addView(createCategoryTitle(category.key)) - value.filter { it.key.shouldAppearInSettings }.forEach { (property, _) -> - addView(createPropertyView(property)) - actions.find { pair -> pair.first.dependsOnProperty == property}?.let { pair -> - addView(pair.second()) - } - } - } - - actions.filter { it.first.dependsOnProperty == null }.forEach { + actions.forEach { addView(it.second()) } - - addView(newSeparator(3, Color.parseColor("#f5f5f5"))) } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_default_header.xml b/app/src/main/res/layout/activity_default_header.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/title_bar" + android:layout_width="match_parent" + android:layout_height="50dp" + tools:ignore="UselessParent"> + + <ImageButton + android:id="@+id/back_button" + android:layout_width="45dp" + android:layout_height="match_parent" + android:background="@null" + android:src="@drawable/back_arrow" + android:layout_gravity="center_vertical|start" + android:padding="8dp" + tools:ignore="ContentDescription" /> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@color/borderColor" + android:layout_gravity="bottom" /> + + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:text="" + android:textColor="@color/primaryText" + android:textSize="23sp" + android:fontFamily="@font/avenir_next_bold" /> + +</FrameLayout>+ \ No newline at end of file diff --git a/app/src/main/res/layout/config_activity.xml b/app/src/main/res/layout/config_activity.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:background="@color/primaryBackground" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/title_bar" + layout="@layout/activity_default_header" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + + <LinearLayout + android:id="@+id/property_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + </LinearLayout> + + </ScrollView> + +</LinearLayout>+ \ No newline at end of file diff --git a/app/src/main/res/layout/config_activity_item.xml b/app/src/main/res/layout/config_activity_item.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="60dp" + android:orientation="horizontal" + android:padding="16dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="center_vertical" + android:layout_weight="1"> + + <TextView + android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:text="" + android:textColor="@color/primaryText" + android:textSize="16sp" /> + + <TextView + android:id="@+id/description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:text="" + android:textColor="@color/secondaryText" + android:textSize="12sp" /> + + </LinearLayout> + + + <LinearLayout + android:id="@+id/value" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:orientation="horizontal"> + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/debug_setting_item.xml b/app/src/main/res/layout/debug_setting_item.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="60dp" + android:orientation="horizontal" + android:padding="16dp"> + + <TextView + android:id="@+id/feature_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:text="" + android:textColor="@color/primaryText" + android:textSize="16sp" /> + +</LinearLayout>+ \ No newline at end of file diff --git a/app/src/main/res/layout/debug_settings_page.xml b/app/src/main/res/layout/debug_settings_page.xml @@ -0,0 +1,21 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/setting_page" + android:clickable="true" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/primaryBackground" + android:orientation="vertical"> + + <include + android:id="@+id/title_bar" + layout="@layout/activity_default_header" /> + + <ListView + android:id="@+id/setting_page_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:listSelector="@android:color/transparent" + android:orientation="vertical"> + </ListView> + +</LinearLayout> diff --git a/app/src/main/res/layout/setting_item.xml b/app/src/main/res/layout/setting_item.xml @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/preference_item" - android:layout_width="match_parent" - android:layout_height="60dp" - android:orientation="horizontal" - android:padding="16dp"> - - <TextView - android:id="@+id/feature_text" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:layout_weight="1" - android:text="" - android:textColor="@color/primaryText" - android:textSize="16sp" /> - -</LinearLayout>- \ No newline at end of file diff --git a/app/src/main/res/layout/settings_page.xml b/app/src/main/res/layout/settings_page.xml @@ -1,52 +0,0 @@ -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/setting_page" - android:clickable="true" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/primaryBackground" - android:orientation="vertical"> - - <FrameLayout - android:id="@+id/title_bar" - android:layout_width="match_parent" - android:layout_height="50dp" - tools:ignore="UselessParent"> - - <ImageButton - android:id="@+id/settings_button" - android:layout_width="45dp" - android:layout_height="match_parent" - android:background="@null" - android:src="@drawable/back_arrow" - android:layout_gravity="center_vertical|start" - android:padding="8dp" - tools:ignore="ContentDescription" /> - - <View - android:layout_width="match_parent" - android:layout_height="1dp" - android:background="@color/borderColor" - android:layout_gravity="bottom" /> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center" - android:text="" - android:textColor="@color/primaryText" - android:textSize="23sp" - android:fontFamily="@font/avenir_next_bold" /> - - </FrameLayout> - - <ListView - android:id="@+id/setting_page_list" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:listSelector="@android:color/transparent" - android:orientation="vertical"> - </ListView> - -</LinearLayout> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml @@ -7,6 +7,7 @@ <color name="darkText">#2B2B2B</color> <color name="primaryText">#DEDEDE</color> <color name="secondaryText">#999999</color> + <color name="tertiaryText">#666666</color> <color name="errorColor">#DF4C5C</color> <color name="successColor">#4FABF8</color> <color name="borderColor">#424242</color> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml @@ -4,5 +4,9 @@ <item name="android:fontFamily">@font/avenir_next_medium</item> <item name="android:windowNoTitle">true</item> <item name="android:windowContentOverlay">@null</item> + <item name="android:navigationBarColor">@color/primaryBackground</item> + <item name="android:textColor">@color/primaryText</item> + <item name="android:editTextColor">@color/primaryText</item> + <item name="android:alertDialogTheme">@android:style/Theme.DeviceDefault.Dialog.Alert</item> </style> </resources> \ No newline at end of file