forked from geode-sdk/android-launcher
initial profiles support
This commit is contained in:
11 files changed
+422
-15
No files matched your search
@@ -40,7 +40,7 @@ class UserDirectoryProvider : DocumentsProvider() {
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
val context = context ?: return false
|
||||
rootDir = LaunchUtils.getBaseDirectory(context)
|
||||
rootDir = LaunchUtils.getBaseDirectory(context, true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -174,6 +174,11 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_profiles)) {
|
||||
ProfileCreateCard()
|
||||
ProfileSelectCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -19,6 +21,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -32,9 +35,11 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -55,10 +60,13 @@ import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.LabelledText
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.geode.launcher.utils.Profile
|
||||
import com.geode.launcher.utils.ProfileManager
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||
@@ -556,10 +564,12 @@ internal val LocalSelectValue = compositionLocalOf<Any> { 0 }
|
||||
internal val LocalSelectSetValue = staticCompositionLocalOf<(Any) -> Unit> { {} }
|
||||
|
||||
@Composable
|
||||
fun <T> SelectOption(name: String, value: T) {
|
||||
fun <T> SelectOption(name: String, value: T, enabled: Boolean = true, leadingContent: @Composable (Boolean) -> Unit = {}) {
|
||||
val currentValue = LocalSelectValue.current
|
||||
val setValue = LocalSelectSetValue.current
|
||||
|
||||
val isSelected = currentValue.equals(value)
|
||||
|
||||
// do not give the row or column padding!! it messes up the selection effect
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -567,15 +577,221 @@ fun <T> SelectOption(name: String, value: T) {
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = { setValue(value as Any) },
|
||||
role = Role.RadioButton
|
||||
role = Role.RadioButton,
|
||||
enabled = enabled
|
||||
)
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = currentValue.equals(value),
|
||||
onClick = { setValue(value as Any) }
|
||||
)
|
||||
Text(name, style = Typography.bodyMedium)
|
||||
Row(modifier = Modifier.weight(1.0f, true), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = { setValue(value as Any) },
|
||||
enabled = enabled
|
||||
)
|
||||
Text(name, style = Typography.bodyMedium)
|
||||
}
|
||||
|
||||
leadingContent(isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
|
||||
var enteredValue by remember { mutableStateOf("") }
|
||||
|
||||
val filename = enteredValue.take(16)
|
||||
.lowercase()
|
||||
.map {
|
||||
if ("qwertyuiopasdfghjklzxcvbnm1234567890-_.".contains(it))
|
||||
it else '_'
|
||||
}
|
||||
.joinToString("")
|
||||
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val minimumReached = enteredValue.isEmpty() || filename.isEmpty()
|
||||
|
||||
val profileManager = ProfileManager.get(context)
|
||||
val currentProfiles = remember {
|
||||
profileManager.getProfiles().map { it.path }.toSet()
|
||||
}
|
||||
|
||||
val isDuplicate = currentProfiles.contains(filename)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(stringResource(R.string.preference_profiles_create))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1.0f, false)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = enteredValue,
|
||||
onValueChange = {
|
||||
enteredValue = it
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
label = {
|
||||
Text(stringResource(R.string.preference_profiles_create_name))
|
||||
},
|
||||
isError = isDuplicate
|
||||
)
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(stringResource(R.string.preference_profiles_create_duplicate))
|
||||
} else if (!minimumReached) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Text(stringResource(R.string.preference_profiles_create_info, filename))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
ProfileManager.get(context).storeProfile(Profile(filename, enteredValue))
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = !minimumReached && !isDuplicate
|
||||
) {
|
||||
Text(stringResource(R.string.preference_profiles_create_action))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileCreateCard() {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_profiles_create),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_person_add), contentDescription = null)
|
||||
}
|
||||
) {
|
||||
showDialog = true
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
ProfileCreateDialog(onDismissRequest = {
|
||||
showDialog = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileSelectCard() {
|
||||
val context = LocalContext.current
|
||||
val profileManager = ProfileManager.get(context)
|
||||
|
||||
val currentProfileId = remember {
|
||||
profileManager.getCurrentProfile()
|
||||
}
|
||||
|
||||
var currentProfileName = remember {
|
||||
val savedProfile = currentProfileId
|
||||
if (savedProfile == null) {
|
||||
"Default"
|
||||
} else {
|
||||
profileManager.getProfile(savedProfile)
|
||||
?.name ?: savedProfile
|
||||
}
|
||||
}
|
||||
|
||||
val clearedProfiles = remember { mutableStateListOf<String>() }
|
||||
|
||||
val profileList by profileManager.storedProfiles.collectAsState()
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_profiles_select),
|
||||
description = stringResource(R.string.preference_profiles_current, currentProfileName)
|
||||
) {
|
||||
showDialog = true
|
||||
}
|
||||
|
||||
val activity = LocalActivity.current
|
||||
if (showDialog) {
|
||||
SelectDialog(
|
||||
title = stringResource(R.string.preference_profiles_select),
|
||||
onDismissRequest = {
|
||||
showDialog = false
|
||||
},
|
||||
onSelect = {
|
||||
showDialog = false
|
||||
|
||||
val selectedProfileId = it.takeIf { it.isNotEmpty() }
|
||||
|
||||
if (clearedProfiles.isNotEmpty()) {
|
||||
profileManager.deleteProfiles(clearedProfiles)
|
||||
}
|
||||
|
||||
if (currentProfileId != selectedProfileId) {
|
||||
profileManager
|
||||
.setCurrentProfile(selectedProfileId)
|
||||
|
||||
activity?.run {
|
||||
packageManager.getLaunchIntentForPackage(packageName)?.also {
|
||||
val mainIntent = Intent.makeRestartActivityTask(it.component)
|
||||
startActivity(mainIntent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
initialValue = currentProfileId ?: "",
|
||||
) {
|
||||
SelectOption("Default", "")
|
||||
|
||||
profileList.forEach { profile ->
|
||||
val path = profile.path
|
||||
|
||||
SelectOption(
|
||||
stringResource(R.string.preference_profiles_select_value, profile.name, path),
|
||||
path,
|
||||
enabled = !clearedProfiles.contains(path),
|
||||
leadingContent = { selected ->
|
||||
IconButton(onClick = {
|
||||
if (clearedProfiles.contains(path)) {
|
||||
clearedProfiles.remove(path)
|
||||
} else {
|
||||
clearedProfiles.add(path)
|
||||
}
|
||||
}, enabled = !selected) {
|
||||
if (clearedProfiles.contains(path)) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_undo),
|
||||
contentDescription = stringResource(R.string.preference_profiles_delete_undo_alt)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ object GeodeUtils {
|
||||
return false
|
||||
}
|
||||
|
||||
return capabilityListener.get()?.onCapabilityAdded(capability) ?: false
|
||||
return capabilityListener.get()?.onCapabilityAdded(capability) == true
|
||||
}
|
||||
|
||||
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
|
||||
|
||||
@@ -47,7 +47,7 @@ object LaunchUtils {
|
||||
/**
|
||||
* Returns the directory that Geode/the game should base itself off of.
|
||||
*/
|
||||
fun getBaseDirectory(context: Context): File {
|
||||
fun getBaseDirectory(context: Context, ignoreProfile: Boolean = false): File {
|
||||
// deprecated, but seems to be the best choice of directory (i forced mat to test it)
|
||||
// also, is getting the first item the correct choice here?? what do they mean
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -58,6 +58,14 @@ object LaunchUtils {
|
||||
val noMediaPath = File(dir, ".nomedia")
|
||||
noMediaPath.createNewFile()
|
||||
|
||||
val currentProfile = ProfileManager.get(context).getCurrentProfile()
|
||||
if (currentProfile != null && !ignoreProfile) {
|
||||
val profile = File(dir, "profiles/$currentProfile/")
|
||||
profile.mkdirs()
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ import com.geode.launcher.BuildConfig
|
||||
*/
|
||||
class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
companion object {
|
||||
private const val FILE_KEY = "GeodeLauncherPreferencesFileKey"
|
||||
|
||||
@Composable
|
||||
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
|
||||
return usePreference(
|
||||
@@ -70,7 +68,9 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
preferenceSet: (PreferenceUtils, Key, T) -> Unit
|
||||
): MutableState<T> {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
|
||||
|
||||
val currentProfile = remember { ProfileManager.get(context).getCurrentFileKey() }
|
||||
val sharedPreferences = context.getSharedPreferences(currentProfile, Context.MODE_PRIVATE)
|
||||
|
||||
val preferences = get(sharedPreferences)
|
||||
|
||||
@@ -114,7 +114,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
}
|
||||
|
||||
fun get(context: Context): PreferenceUtils {
|
||||
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
|
||||
val currentFileKey = ProfileManager.get(context).getCurrentFileKey()
|
||||
val sharedPreferences = context.getSharedPreferences(currentFileKey, Context.MODE_PRIVATE)
|
||||
return get(sharedPreferences)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val FILE_KEY = "GeodeLauncherPreferencesFileKey"
|
||||
private const val PROFILES_FILE_KEY = "GeodeLauncherProfiles"
|
||||
|
||||
private const val PROFILE_LIST_SAVE_KEY = "profiles"
|
||||
private const val PROFILE_CURRENT_SAVE_KEY = "currentProfile"
|
||||
|
||||
@Serializable
|
||||
data class Profile(val path: String, val name: String)
|
||||
|
||||
class ProfileManager {
|
||||
private val applicationContext: Context
|
||||
private val preferences: SharedPreferences
|
||||
|
||||
companion object {
|
||||
private lateinit var managerInstance: ProfileManager
|
||||
|
||||
fun get(context: Context): ProfileManager {
|
||||
if (!Companion::managerInstance.isInitialized) {
|
||||
val applicationContext = context.applicationContext
|
||||
|
||||
managerInstance = ProfileManager(applicationContext)
|
||||
}
|
||||
|
||||
return managerInstance
|
||||
}
|
||||
}
|
||||
|
||||
constructor(applicationContext: Context) {
|
||||
this.applicationContext = applicationContext
|
||||
this.preferences = applicationContext.getSharedPreferences(PROFILES_FILE_KEY, Context.MODE_PRIVATE)
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
private val _storedProfiles = MutableStateFlow<List<Profile>>(emptyList())
|
||||
val storedProfiles = _storedProfiles.asStateFlow()
|
||||
|
||||
fun getCurrentProfile(): String? {
|
||||
return preferences.getString(PROFILE_CURRENT_SAVE_KEY, null)
|
||||
.takeUnless { it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
fun getCurrentFileKey(): String {
|
||||
val currentProfile = getCurrentProfile()
|
||||
?: return FILE_KEY
|
||||
|
||||
return "${FILE_KEY}_${currentProfile}"
|
||||
}
|
||||
|
||||
fun getProfile(path: String): Profile? {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null)
|
||||
return profilesSet?.firstNotNullOfOrNull {
|
||||
Json.decodeFromString<Profile>(it).takeIf { it.path == path }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a profile into the save, removing any duplicate profiles beforehand
|
||||
*/
|
||||
fun storeProfile(profile: Profile) {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
val removedList = profilesSet.filterTo(LinkedHashSet()) {
|
||||
val saveProfile = Json.decodeFromString<Profile>(it)
|
||||
saveProfile.path != profile.path
|
||||
}
|
||||
removedList.add(Json.encodeToString(profile))
|
||||
|
||||
preferences.edit {
|
||||
putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
|
||||
}
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
fun deleteProfile(path: String) {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
val removedList = profilesSet.filterTo(LinkedHashSet()) {
|
||||
val saveProfile = Json.decodeFromString<Profile>(it)
|
||||
saveProfile.path != path
|
||||
}
|
||||
|
||||
preferences.edit {
|
||||
putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
|
||||
}
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
fun deleteProfiles(paths: List<String>) {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
val removedList = profilesSet.filterTo(LinkedHashSet()) {
|
||||
val saveProfile = Json.decodeFromString<Profile>(it)
|
||||
!paths.contains(saveProfile.path)
|
||||
}
|
||||
|
||||
preferences.edit(commit = true) {
|
||||
putStringSet(PROFILE_LIST_SAVE_KEY, removedList)
|
||||
}
|
||||
|
||||
_storedProfiles.value = getProfiles()
|
||||
}
|
||||
|
||||
fun setCurrentProfile(profile: String?) {
|
||||
preferences.edit(commit = true) {
|
||||
if (profile.isNullOrEmpty()) {
|
||||
remove(PROFILE_CURRENT_SAVE_KEY)
|
||||
} else {
|
||||
putString(PROFILE_CURRENT_SAVE_KEY, profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfiles(): List<Profile> {
|
||||
val profilesSet = preferences.getStringSet(PROFILE_LIST_SAVE_KEY, null) ?: emptySet<String>()
|
||||
|
||||
return profilesSet.map {
|
||||
Json.decodeFromString<Profile>(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M720,560L720,440L600,440L600,360L720,360L720,240L800,240L800,360L920,360L920,440L800,440L800,560L720,560ZM360,480Q294,480 247,433Q200,386 200,320Q200,254 247,207Q294,160 360,160Q426,160 473,207Q520,254 520,320Q520,386 473,433Q426,480 360,480ZM40,800L40,688Q40,654 57.5,625.5Q75,597 104,582Q166,551 230,535.5Q294,520 360,520Q426,520 490,535.5Q554,551 616,582Q645,597 662.5,625.5Q680,654 680,688L680,800L40,800ZM120,720L600,720L600,688Q600,677 594.5,668Q589,659 580,654Q526,627 471,613.5Q416,600 360,600Q304,600 249,613.5Q194,627 140,654Q131,659 125.5,668Q120,677 120,688L120,720ZM360,400Q393,400 416.5,376.5Q440,353 440,320Q440,287 416.5,263.5Q393,240 360,240Q327,240 303.5,263.5Q280,287 280,320Q280,353 303.5,376.5Q327,400 360,400ZM360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320ZM360,720L360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720L360,720Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M280,760L280,680L564,680Q627,680 673.5,640Q720,600 720,540Q720,480 673.5,440Q627,400 564,400L312,400L416,504L360,560L160,360L360,160L416,216L312,320L564,320Q661,320 730.5,383Q800,446 800,540Q800,634 730.5,697Q661,760 564,760L280,760Z"/>
|
||||
</vector>
|
||||
@@ -219,6 +219,19 @@
|
||||
|
||||
<string name="preference_open_developer_options">Open developer options</string>
|
||||
|
||||
<string name="preference_profiles">Profiles</string>
|
||||
<string name="preference_profiles_create">Create profile</string>
|
||||
<string name="preference_profiles_name">Change profile</string>
|
||||
<string name="preference_profiles_select">Select profile</string>
|
||||
<string name="preference_profiles_create_name">Profile name</string>
|
||||
<string name="preference_profiles_create_action">Create</string>
|
||||
<string name="preference_profiles_select_value">%1$s</string>
|
||||
<string name="preference_profiles_current">Current profile: %1$s</string>
|
||||
<string name="preference_profiles_create_info">This profile will be created at %1$s.</string>
|
||||
<string name="preference_profiles_create_duplicate">A profile with this name already exists.</string>
|
||||
<string name="preference_profiles_delete_alt">Delete</string>
|
||||
<string name="preference_profiles_delete_undo_alt">Undo</string>
|
||||
|
||||
<!-- ApplicationLogsActivity strings -->
|
||||
<string name="title_activity_application_logs">Application Logs</string>
|
||||
<string name="application_logs_no_logs">No logs available.</string>
|
||||
|
||||
Reference in new issue
Block a user