Vault settings export/import, jump chain pro message fix, free vault import gate
- Add optional "Include settings" checkbox (unchecked by default) to both Save Vault Locally and Export Vault flows — exports 56 DataStore prefs (keyboard, display, QuickBar customization, hardware actions, etc.) - Import automatically restores settings when present in vault file - EXPORTABLE_*_KEYS lists in TerminalPrefsKeys define which prefs are backed up - Fix misleading "Jump Host" pro upgrade message — now says "Jump host chaining" so free users understand single jump hosts work, only chaining is pro-gated - Gate vault import for free users: can only import local vault saves (MODE_LOCAL), not pro-exported vaults (MODE_PASSWORD/MODE_QR) - All strings in EN/ES/SV/FR/DE Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c1440e80e
commit
ca4359a996
14 changed files with 255 additions and 13 deletions
|
|
@ -91,6 +91,34 @@ object TerminalPrefsKeys {
|
|||
val RECOVERY_USERNAME = stringPreferencesKey("recovery_username")
|
||||
val RECOVERY_CONNECT_TIME = longPreferencesKey("recovery_connect_time")
|
||||
val RECOVERY_PROTOCOL = stringPreferencesKey("recovery_protocol")
|
||||
|
||||
// Exportable key lists — used by vault save/export/import
|
||||
val EXPORTABLE_STRING_KEYS = listOf(
|
||||
FONT_FAMILY, THEME_NAME, KEYBOARD_LANGUAGE, TIME_FORMAT,
|
||||
NUMBER_ROW_MODE, QUICK_BAR_POSITION, KEY_COLOR_PRESET, KEY_COLOR_CUSTOM,
|
||||
QB_COLOR_PRESET, QB_COLOR_CUSTOM, KEYBOARD_TYPE,
|
||||
AQB_POSITION, AQB_COLOR_PRESET, AQB_COLOR_CUSTOM,
|
||||
CQB_CUSTOM_KEYS, CQB_CUSTOM_APPS, AQB_CUSTOM_KEYS, AQB_CUSTOM_APPS,
|
||||
APP_LANGUAGE, CURSOR_SPEED, SESSION_NAV_STYLE, SFTP_SORT_ORDER,
|
||||
ACTION_VOLUME_UP, ACTION_VOLUME_DOWN, ACTION_SHAKE,
|
||||
ACTION_VOLUME_UP_CUSTOM, ACTION_VOLUME_DOWN_CUSTOM, ACTION_SHAKE_CUSTOM,
|
||||
ACTION_VOLUME_UP_DOUBLE, ACTION_VOLUME_DOWN_DOUBLE,
|
||||
ACTION_VOLUME_UP_DOUBLE_CUSTOM, ACTION_VOLUME_DOWN_DOUBLE_CUSTOM
|
||||
)
|
||||
val EXPORTABLE_BOOLEAN_KEYS = listOf(
|
||||
HAPTIC_FEEDBACK, KEEP_SCREEN_ON, QUICK_BAR_VISIBLE, BIOMETRIC_LOCK,
|
||||
AUTO_RECONNECT, NOTIFY_ON_DISCONNECT, WIFI_LOCK_DEFAULT,
|
||||
KEYBOARD_SAME_SIZE_BOTH, SHOW_PAGE_INDICATORS, SHOW_KEY_HINTS,
|
||||
PROTECT_SCREEN_FULL_APP, PROTECT_SCREEN_VAULT, PROTECT_SCREEN_TERMINAL,
|
||||
CURSOR_BLINK, SHOW_SESSION_TAB_BAR
|
||||
)
|
||||
val EXPORTABLE_INT_KEYS = listOf(
|
||||
SCROLLBACK_LINES, QUICK_BAR_SIZE, AQB_SIZE,
|
||||
KEY_REPEAT_DELAY, LONG_PRESS_DELAY, ACTION_DOUBLE_PRESS_DELAY
|
||||
)
|
||||
val EXPORTABLE_FLOAT_KEYS = listOf(
|
||||
FONT_SIZE_SP, KEYBOARD_HEIGHT_PERCENT, KEYBOARD_HEIGHT_LANDSCAPE
|
||||
)
|
||||
}
|
||||
|
||||
class TerminalPreferences(private val dataStore: DataStore<Preferences>, private val isTablet: Boolean = false) {
|
||||
|
|
@ -634,4 +662,40 @@ class TerminalPreferences(private val dataStore: DataStore<Preferences>, private
|
|||
protocol = prefs[TerminalPrefsKeys.RECOVERY_PROTOCOL] ?: "ssh"
|
||||
)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Vault settings export / import
|
||||
// ========================================================================
|
||||
|
||||
/** Read all exportable preferences as a single snapshot. */
|
||||
suspend fun exportableSnapshot(): Preferences = dataStore.data.first()
|
||||
|
||||
/** Apply imported settings, overwriting current values in a single transaction. */
|
||||
suspend fun applyImportedSettings(settings: SettingsData) {
|
||||
dataStore.edit { prefs ->
|
||||
for ((name, value) in settings.stringValues) {
|
||||
val key = TerminalPrefsKeys.EXPORTABLE_STRING_KEYS.firstOrNull { it.name == name } ?: continue
|
||||
prefs[key] = value
|
||||
}
|
||||
for ((name, value) in settings.booleanValues) {
|
||||
val key = TerminalPrefsKeys.EXPORTABLE_BOOLEAN_KEYS.firstOrNull { it.name == name } ?: continue
|
||||
prefs[key] = value
|
||||
}
|
||||
for ((name, value) in settings.intValues) {
|
||||
val key = TerminalPrefsKeys.EXPORTABLE_INT_KEYS.firstOrNull { it.name == name } ?: continue
|
||||
prefs[key] = value
|
||||
}
|
||||
for ((name, value) in settings.floatValues) {
|
||||
val key = TerminalPrefsKeys.EXPORTABLE_FLOAT_KEYS.firstOrNull { it.name == name } ?: continue
|
||||
prefs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SettingsData(
|
||||
val stringValues: Map<String, String>,
|
||||
val booleanValues: Map<String, Boolean>,
|
||||
val intValues: Map<String, Int>,
|
||||
val floatValues: Map<String, Float>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -66,6 +65,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import com.roundingmobile.sshworkbench.R
|
||||
import com.roundingmobile.sshworkbench.ui.theme.AppColors
|
||||
import com.roundingmobile.sshworkbench.ui.theme.AppSwitch
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
|
@ -463,7 +463,7 @@ fun EditConnectionScreen(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
AppSwitch(
|
||||
checked = viewModel.agentForwarding,
|
||||
onCheckedChange = {
|
||||
if (proFeatures != null && !proFeatures.agentForwarding) {
|
||||
|
|
@ -577,7 +577,7 @@ fun EditConnectionScreen(
|
|||
if (wouldCycle) return@DropdownMenuItem
|
||||
if (hasOwnJump && proFeatures != null && proFeatures.maxJumpHops() <= 1) {
|
||||
jumpDropdownExpanded = false
|
||||
proFeatures.showUpgradePrompt(jumpContext, jumpContext.getString(R.string.jump_host))
|
||||
proFeatures.showUpgradePrompt(jumpContext, jumpContext.getString(R.string.jump_host_chain))
|
||||
} else {
|
||||
viewModel.jumpHostId = conn.id
|
||||
jumpDropdownExpanded = false
|
||||
|
|
@ -642,7 +642,7 @@ fun EditConnectionScreen(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
AppSwitch(
|
||||
checked = viewModel.wifiLock,
|
||||
onCheckedChange = { viewModel.wifiLock = it }
|
||||
)
|
||||
|
|
@ -685,7 +685,7 @@ fun EditConnectionScreen(
|
|||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
AppSwitch(
|
||||
checked = viewModel.autoReconnect,
|
||||
onCheckedChange = { viewModel.autoReconnect = it }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ fun VaultExportScreen(
|
|||
val selectedKeyIds by viewModel.selectedKeyIds.collectAsStateWithLifecycle()
|
||||
val selectedSnippetIds by viewModel.selectedSnippetIds.collectAsStateWithLifecycle()
|
||||
val includeCredentials by viewModel.includeCredentials.collectAsStateWithLifecycle()
|
||||
val includeSettings by viewModel.includeSettings.collectAsStateWithLifecycle()
|
||||
val exportMode by viewModel.exportMode.collectAsStateWithLifecycle()
|
||||
val password by viewModel.password.collectAsStateWithLifecycle()
|
||||
val confirmPassword by viewModel.confirmPassword.collectAsStateWithLifecycle()
|
||||
|
|
@ -203,6 +204,14 @@ fun VaultExportScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CheckboxRow(
|
||||
label = stringResource(R.string.vault_include_settings),
|
||||
checked = includeSettings,
|
||||
onToggle = { viewModel.toggleSettings() }
|
||||
)
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 12.dp))
|
||||
|
||||
// --- Mode selection ---
|
||||
|
|
@ -319,7 +328,7 @@ fun VaultExportScreen(
|
|||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// --- Export button ---
|
||||
val hasItems = selectedHostIds.isNotEmpty() || selectedKeyIds.isNotEmpty() || selectedSnippetIds.isNotEmpty()
|
||||
val hasItems = selectedHostIds.isNotEmpty() || selectedKeyIds.isNotEmpty() || selectedSnippetIds.isNotEmpty() || includeSettings
|
||||
val canExport = when (exportMode) {
|
||||
ExportMode.PASSWORD -> viewModel.passwordsMatch() && viewModel.passwordStrong()
|
||||
ExportMode.QR -> qrKeyBase64 != null && !qrGenerating && qrSavedOrShared
|
||||
|
|
|
|||
|
|
@ -241,12 +241,13 @@ fun VaultImportSheet(
|
|||
}
|
||||
|
||||
ImportState.SUCCESS -> {
|
||||
val parts = importSummary?.split("|") ?: listOf("0", "0")
|
||||
val parts = importSummary?.split("|") ?: listOf("0", "0", "false")
|
||||
val imported = parts.getOrElse(0) { "0" }.toIntOrNull() ?: 0
|
||||
val skippedCount = parts.getOrElse(1) { "0" }.toIntOrNull() ?: 0
|
||||
val settingsRestored = parts.getOrElse(2) { "false" } == "true"
|
||||
|
||||
Box(
|
||||
Modifier.fillMaxWidth().height(120.dp),
|
||||
Modifier.fillMaxWidth().height(if (settingsRestored) 150.dp else 120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
|
@ -259,6 +260,14 @@ fun VaultImportSheet(
|
|||
stringResource(R.string.vault_imported_summary, imported, skippedCount),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
if (settingsRestored) {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
stringResource(R.string.vault_settings_restored),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = {
|
||||
viewModel.resetState()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.roundingmobile.sshworkbench.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
|
|
@ -36,6 +37,7 @@ fun VaultLocalSaveScreen(
|
|||
val confirmPassword by viewModel.confirmPassword.collectAsStateWithLifecycle()
|
||||
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()
|
||||
val savedPath by viewModel.savedPath.collectAsStateWithLifecycle()
|
||||
val includeSettings by viewModel.includeSettings.collectAsStateWithLifecycle()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { viewModel.reset() }
|
||||
|
|
@ -136,6 +138,26 @@ fun VaultLocalSaveScreen(
|
|||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { viewModel.toggleSettings() }
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = includeSettings,
|
||||
onCheckedChange = { viewModel.toggleSettings() }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.vault_include_settings),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.save(context) },
|
||||
enabled = viewModel.passwordStrong() && viewModel.passwordsMatch() && confirmPassword.isNotEmpty(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.roundingmobile.sshworkbench.vault
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.roundingmobile.sshworkbench.data.SettingsData
|
||||
import com.roundingmobile.sshworkbench.data.TerminalPrefsKeys
|
||||
import com.roundingmobile.sshworkbench.data.local.PortForward
|
||||
import com.roundingmobile.sshworkbench.data.local.SavedConnection
|
||||
import com.roundingmobile.sshworkbench.data.local.Snippet
|
||||
|
|
@ -36,7 +39,8 @@ object VaultExportSerializer {
|
|||
val credentials: List<CredentialEntry>,
|
||||
val keys: List<KeyEntry>,
|
||||
val snippets: List<Snippet>,
|
||||
val portForwards: List<PortForward>
|
||||
val portForwards: List<PortForward>,
|
||||
val settings: SettingsData? = null
|
||||
)
|
||||
|
||||
data class CredentialEntry(
|
||||
|
|
@ -60,6 +64,7 @@ object VaultExportSerializer {
|
|||
put("keys", serializeKeys(data.keys))
|
||||
put("snippets", serializeSnippets(data.snippets))
|
||||
put("port_forwards", serializePortForwards(data.portForwards))
|
||||
if (data.settings != null) put("settings", serializeSettings(data.settings))
|
||||
}
|
||||
return json.toString().toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
|
|
@ -77,7 +82,8 @@ object VaultExportSerializer {
|
|||
credentials = deserializeCredentials(json.getJSONArray("credentials")),
|
||||
keys = deserializeKeys(json.getJSONArray("keys")),
|
||||
snippets = deserializeSnippets(json.getJSONArray("snippets")),
|
||||
portForwards = deserializePortForwards(json.optJSONArray("port_forwards") ?: JSONArray())
|
||||
portForwards = deserializePortForwards(json.optJSONArray("port_forwards") ?: JSONArray()),
|
||||
settings = json.optJSONObject("settings")?.let { deserializeSettings(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -334,4 +340,70 @@ object VaultExportSerializer {
|
|||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Settings serialization
|
||||
// ========================================================================
|
||||
|
||||
/** Build SettingsData from a DataStore preferences snapshot. */
|
||||
fun snapshotSettings(prefs: Preferences): SettingsData {
|
||||
val strings = mutableMapOf<String, String>()
|
||||
for (key in TerminalPrefsKeys.EXPORTABLE_STRING_KEYS) {
|
||||
prefs[key]?.let { strings[key.name] = it }
|
||||
}
|
||||
val booleans = mutableMapOf<String, Boolean>()
|
||||
for (key in TerminalPrefsKeys.EXPORTABLE_BOOLEAN_KEYS) {
|
||||
prefs[key]?.let { booleans[key.name] = it }
|
||||
}
|
||||
val ints = mutableMapOf<String, Int>()
|
||||
for (key in TerminalPrefsKeys.EXPORTABLE_INT_KEYS) {
|
||||
prefs[key]?.let { ints[key.name] = it }
|
||||
}
|
||||
val floats = mutableMapOf<String, Float>()
|
||||
for (key in TerminalPrefsKeys.EXPORTABLE_FLOAT_KEYS) {
|
||||
prefs[key]?.let { floats[key.name] = it }
|
||||
}
|
||||
return SettingsData(strings, booleans, ints, floats)
|
||||
}
|
||||
|
||||
private fun serializeSettings(settings: SettingsData): JSONObject {
|
||||
val obj = JSONObject()
|
||||
val strObj = JSONObject()
|
||||
for ((k, v) in settings.stringValues) strObj.put(k, v)
|
||||
obj.put("string", strObj)
|
||||
|
||||
val boolObj = JSONObject()
|
||||
for ((k, v) in settings.booleanValues) boolObj.put(k, v)
|
||||
obj.put("boolean", boolObj)
|
||||
|
||||
val intObj = JSONObject()
|
||||
for ((k, v) in settings.intValues) intObj.put(k, v)
|
||||
obj.put("int", intObj)
|
||||
|
||||
val floatObj = JSONObject()
|
||||
for ((k, v) in settings.floatValues) floatObj.put(k, v.toDouble())
|
||||
obj.put("float", floatObj)
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
private fun deserializeSettings(obj: JSONObject): SettingsData {
|
||||
val strings = mutableMapOf<String, String>()
|
||||
obj.optJSONObject("string")?.let { s ->
|
||||
for (key in s.keys()) strings[key] = s.getString(key)
|
||||
}
|
||||
val booleans = mutableMapOf<String, Boolean>()
|
||||
obj.optJSONObject("boolean")?.let { b ->
|
||||
for (key in b.keys()) booleans[key] = b.getBoolean(key)
|
||||
}
|
||||
val ints = mutableMapOf<String, Int>()
|
||||
obj.optJSONObject("int")?.let { i ->
|
||||
for (key in i.keys()) ints[key] = i.getInt(key)
|
||||
}
|
||||
val floats = mutableMapOf<String, Float>()
|
||||
obj.optJSONObject("float")?.let { f ->
|
||||
for (key in f.keys()) floats[key] = f.getDouble(key).toFloat()
|
||||
}
|
||||
return SettingsData(strings, booleans, ints, floats)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.net.Uri
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.roundingmobile.sshworkbench.data.CredentialStore
|
||||
import com.roundingmobile.sshworkbench.data.TerminalPreferences
|
||||
import com.roundingmobile.sshworkbench.data.local.PortForwardDao
|
||||
import com.roundingmobile.sshworkbench.data.local.SavedConnection
|
||||
import com.roundingmobile.sshworkbench.data.local.SavedConnectionDao
|
||||
|
|
@ -35,6 +36,7 @@ class VaultExportViewModel @Inject constructor(
|
|||
private val snippetDao: SnippetDao,
|
||||
private val portForwardDao: PortForwardDao,
|
||||
private val credentialStore: CredentialStore,
|
||||
private val terminalPreferences: TerminalPreferences,
|
||||
private val strings: com.roundingmobile.sshworkbench.di.StringResolver
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -61,6 +63,9 @@ class VaultExportViewModel @Inject constructor(
|
|||
private val _includeCredentials = MutableStateFlow(true)
|
||||
val includeCredentials: StateFlow<Boolean> = _includeCredentials
|
||||
|
||||
private val _includeSettings = MutableStateFlow(false)
|
||||
val includeSettings: StateFlow<Boolean> = _includeSettings
|
||||
|
||||
// Export settings
|
||||
private val _exportMode = MutableStateFlow(ExportMode.PASSWORD)
|
||||
val exportMode: StateFlow<ExportMode> = _exportMode
|
||||
|
|
@ -132,6 +137,10 @@ class VaultExportViewModel @Inject constructor(
|
|||
_includeCredentials.value = !_includeCredentials.value
|
||||
}
|
||||
|
||||
fun toggleSettings() {
|
||||
_includeSettings.value = !_includeSettings.value
|
||||
}
|
||||
|
||||
fun selectAllHosts() {
|
||||
_selectedHostIds.value = _hosts.value.map { it.id }.toSet()
|
||||
}
|
||||
|
|
@ -225,13 +234,20 @@ class VaultExportViewModel @Inject constructor(
|
|||
val selectedHostIdSet = _selectedHostIds.value
|
||||
val portForwards = allPortForwards.filter { it.connectionId in selectedHostIdSet }
|
||||
|
||||
// Settings snapshot (optional)
|
||||
val settings = if (_includeSettings.value) {
|
||||
val prefs = terminalPreferences.exportableSnapshot()
|
||||
VaultExportSerializer.snapshotSettings(prefs)
|
||||
} else null
|
||||
|
||||
// Serialize to JSON
|
||||
val vaultData = VaultExportSerializer.VaultData(
|
||||
hosts = selectedHosts,
|
||||
credentials = credentials,
|
||||
keys = keyEntries,
|
||||
snippets = selectedSnippets,
|
||||
portForwards = portForwards
|
||||
portForwards = portForwards,
|
||||
settings = settings
|
||||
)
|
||||
val jsonBytes = VaultExportSerializer.serialize(vaultData)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import com.google.zxing.MultiFormatReader
|
|||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.roundingmobile.sshworkbench.data.CredentialStore
|
||||
import com.roundingmobile.sshworkbench.data.TerminalPreferences
|
||||
import com.roundingmobile.sshworkbench.data.local.PortForwardDao
|
||||
import com.roundingmobile.sshworkbench.data.local.SavedConnectionDao
|
||||
import com.roundingmobile.sshworkbench.data.local.SnippetDao
|
||||
import com.roundingmobile.sshworkbench.data.local.SshKeyDao
|
||||
import com.roundingmobile.sshworkbench.pro.ProFeatures
|
||||
import com.roundingmobile.sshworkbench.R
|
||||
import com.roundingmobile.vaultcrypto.VaultCrypto
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -37,6 +39,8 @@ class VaultImportViewModel @Inject constructor(
|
|||
private val snippetDao: SnippetDao,
|
||||
private val portForwardDao: PortForwardDao,
|
||||
private val credentialStore: CredentialStore,
|
||||
private val proFeatures: ProFeatures,
|
||||
private val terminalPreferences: TerminalPreferences,
|
||||
private val strings: com.roundingmobile.sshworkbench.di.StringResolver
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -85,6 +89,13 @@ class VaultImportViewModel @Inject constructor(
|
|||
parsedSwb = swb
|
||||
_isLocalVault.value = swb.mode == VaultExportSerializer.MODE_LOCAL
|
||||
|
||||
// Free users can only import local vault saves (MODE_LOCAL)
|
||||
if (!proFeatures.isPro && swb.mode != VaultExportSerializer.MODE_LOCAL) {
|
||||
_errorMessage.value = strings.getString(R.string.vault_import_pro_only)
|
||||
_importState.value = ImportState.ERROR
|
||||
return@launch
|
||||
}
|
||||
|
||||
when (swb.mode) {
|
||||
VaultExportSerializer.MODE_PASSWORD,
|
||||
VaultExportSerializer.MODE_LOCAL -> {
|
||||
|
|
@ -376,7 +387,13 @@ class VaultImportViewModel @Inject constructor(
|
|||
imported++
|
||||
}
|
||||
|
||||
_importSummary.value = "$imported|$skipped"
|
||||
// Import settings (if present in vault)
|
||||
val settingsApplied = data.settings != null
|
||||
if (data.settings != null) {
|
||||
terminalPreferences.applyImportedSettings(data.settings)
|
||||
}
|
||||
|
||||
_importSummary.value = "$imported|$skipped|$settingsApplied"
|
||||
_importState.value = ImportState.SUCCESS
|
||||
} catch (e: Exception) {
|
||||
_errorMessage.value = e.message ?: strings.getString(R.string.vault_import_failed)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.os.Environment
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.roundingmobile.sshworkbench.data.CredentialStore
|
||||
import com.roundingmobile.sshworkbench.data.TerminalPreferences
|
||||
import com.roundingmobile.sshworkbench.data.local.PortForwardDao
|
||||
import com.roundingmobile.sshworkbench.data.local.SavedConnectionDao
|
||||
import com.roundingmobile.sshworkbench.data.local.SnippetDao
|
||||
|
|
@ -33,6 +34,7 @@ class VaultLocalSaveViewModel @Inject constructor(
|
|||
private val snippetDao: SnippetDao,
|
||||
private val portForwardDao: PortForwardDao,
|
||||
private val credentialStore: CredentialStore,
|
||||
private val terminalPreferences: TerminalPreferences,
|
||||
private val strings: com.roundingmobile.sshworkbench.di.StringResolver
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -51,6 +53,11 @@ class VaultLocalSaveViewModel @Inject constructor(
|
|||
private val _savedPath = MutableStateFlow<String?>(null)
|
||||
val savedPath: StateFlow<String?> = _savedPath
|
||||
|
||||
private val _includeSettings = MutableStateFlow(false)
|
||||
val includeSettings: StateFlow<Boolean> = _includeSettings
|
||||
|
||||
fun toggleSettings() { _includeSettings.value = !_includeSettings.value }
|
||||
|
||||
fun setPassword(pwd: String) {
|
||||
_password.value = pwd
|
||||
_errorMessage.value = null
|
||||
|
|
@ -100,7 +107,12 @@ class VaultLocalSaveViewModel @Inject constructor(
|
|||
}
|
||||
val portForwards = portForwardDao.getAllOnce()
|
||||
|
||||
val vaultData = VaultExportSerializer.VaultData(hosts, credentials, keyEntries, snippets, portForwards)
|
||||
val settings = if (_includeSettings.value) {
|
||||
val prefs = terminalPreferences.exportableSnapshot()
|
||||
VaultExportSerializer.snapshotSettings(prefs)
|
||||
} else null
|
||||
|
||||
val vaultData = VaultExportSerializer.VaultData(hosts, credentials, keyEntries, snippets, portForwards, settings)
|
||||
|
||||
// Serialize with device fingerprint
|
||||
val deviceFp = DeviceFingerprint.collect(context)
|
||||
|
|
@ -144,5 +156,6 @@ class VaultLocalSaveViewModel @Inject constructor(
|
|||
_confirmPassword.value = ""
|
||||
_errorMessage.value = null
|
||||
_savedPath.value = null
|
||||
_includeSettings.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@
|
|||
<string name="vault_local_passwords_dont_match">Passwörter stimmen nicht überein</string>
|
||||
<string name="vault_local_saved">Vault erfolgreich gespeichert</string>
|
||||
<string name="vault_local_wrong_device">Dieser Vault wurde auf einem anderen Gerät erstellt und kann hier nicht importiert werden</string>
|
||||
<string name="vault_import_pro_only">Dieser Vault wurde mit Vault exportieren erstellt, wofür ein Pro-Abonnement zum Importieren erforderlich ist. Gratisnutzer können nur lokale Vault-Sicherungen von diesem Gerät importieren.</string>
|
||||
<string name="vault_saving">Speichern…</string>
|
||||
<string name="vault_include_settings">Einstellungen einschließen (Tastatur, Anzeige, QuickBar-Anpassung)</string>
|
||||
<string name="vault_settings_restored">Einstellungen wiederhergestellt</string>
|
||||
<string name="vault_try_again">Erneut versuchen</string>
|
||||
<string name="new_connection">Neue Verbindung</string>
|
||||
<string name="state_connected">Verbunden</string>
|
||||
|
|
@ -83,6 +86,7 @@
|
|||
<string name="select_ssh_key">SSH-Schlüssel auswählen</string>
|
||||
<string name="no_keys_available">Keine Schlüssel verfügbar</string>
|
||||
<string name="jump_host">Sprunghost</string>
|
||||
<string name="jump_host_chain">Sprunghost-Verkettung</string>
|
||||
<string name="no_jump_host">Keiner (Direktverbindung)</string>
|
||||
<string name="via_jump_chain">über Sprungkette</string>
|
||||
<string name="cycle_detected">Zyklus erkannt — dies würde eine Endlosschleife erzeugen</string>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@
|
|||
<string name="vault_local_passwords_dont_match">Las contraseñas no coinciden</string>
|
||||
<string name="vault_local_saved">Vault guardado correctamente</string>
|
||||
<string name="vault_local_wrong_device">Este vault fue creado en otro dispositivo y no se puede importar aquí</string>
|
||||
<string name="vault_import_pro_only">Este vault fue creado con Exportar Vault, que requiere una suscripción Pro para importar. Los usuarios gratuitos solo pueden importar copias locales de este dispositivo.</string>
|
||||
<string name="vault_saving">Guardando…</string>
|
||||
<string name="vault_include_settings">Incluir ajustes (teclado, pantalla, personalización de QuickBar)</string>
|
||||
<string name="vault_settings_restored">Ajustes restaurados</string>
|
||||
<string name="vault_try_again">Intentar de nuevo</string>
|
||||
<string name="new_connection">Nueva conexión</string>
|
||||
<string name="state_connected">Conectado</string>
|
||||
|
|
@ -83,6 +86,7 @@
|
|||
<string name="select_ssh_key">Seleccionar clave SSH</string>
|
||||
<string name="no_keys_available">No hay claves disponibles</string>
|
||||
<string name="jump_host">Host de salto</string>
|
||||
<string name="jump_host_chain">Encadenar hosts de salto</string>
|
||||
<string name="no_jump_host">Ninguno (conexión directa)</string>
|
||||
<string name="via_jump_chain">a través de cadena de salto</string>
|
||||
<string name="cycle_detected">Ciclo detectado — esto crearía un bucle infinito</string>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@
|
|||
<string name="vault_local_passwords_dont_match">Les mots de passe ne correspondent pas</string>
|
||||
<string name="vault_local_saved">Vault enregistré avec succès</string>
|
||||
<string name="vault_local_wrong_device">Ce vault a été créé sur un autre appareil et ne peut pas être importé ici</string>
|
||||
<string name="vault_import_pro_only">Ce vault a été créé avec Exporter Vault, qui nécessite un abonnement Pro pour l\'importer. Les utilisateurs gratuits ne peuvent importer que les sauvegardes locales de cet appareil.</string>
|
||||
<string name="vault_saving">Enregistrement…</string>
|
||||
<string name="vault_include_settings">Inclure les paramètres (clavier, affichage, personnalisation de la QuickBar)</string>
|
||||
<string name="vault_settings_restored">Paramètres restaurés</string>
|
||||
<string name="vault_try_again">Réessayer</string>
|
||||
<string name="new_connection">Nouvelle connexion</string>
|
||||
<string name="state_connected">Connecté</string>
|
||||
|
|
@ -83,6 +86,7 @@
|
|||
<string name="select_ssh_key">Sélectionner une clé SSH</string>
|
||||
<string name="no_keys_available">Aucune clé disponible</string>
|
||||
<string name="jump_host">Hôte de rebond</string>
|
||||
<string name="jump_host_chain">Chaîne d\'hôtes de rebond</string>
|
||||
<string name="no_jump_host">Aucun (connexion directe)</string>
|
||||
<string name="via_jump_chain">via chaîne de rebond</string>
|
||||
<string name="cycle_detected">Cycle détecté — cela créerait une boucle infinie</string>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@
|
|||
<string name="vault_local_passwords_dont_match">Lösenorden matchar inte</string>
|
||||
<string name="vault_local_saved">Vault sparad</string>
|
||||
<string name="vault_local_wrong_device">Denna vault skapades på en annan enhet och kan inte importeras här</string>
|
||||
<string name="vault_import_pro_only">Denna vault skapades med Exportera Vault, som kräver en Pro-prenumeration för att importera. Gratisanvändare kan bara importera lokala vault-kopior från denna enhet.</string>
|
||||
<string name="vault_saving">Sparar…</string>
|
||||
<string name="vault_include_settings">Inkludera inställningar (tangentbord, visning, QuickBar-anpassning)</string>
|
||||
<string name="vault_settings_restored">Inställningar återställda</string>
|
||||
<string name="vault_try_again">Försök igen</string>
|
||||
<string name="new_connection">Ny anslutning</string>
|
||||
<string name="state_connected">Ansluten</string>
|
||||
|
|
@ -83,6 +86,7 @@
|
|||
<string name="select_ssh_key">Välj SSH-nyckel</string>
|
||||
<string name="no_keys_available">Inga nycklar tillgängliga</string>
|
||||
<string name="jump_host">Hoppvärd</string>
|
||||
<string name="jump_host_chain">Kedja av hoppvärdar</string>
|
||||
<string name="no_jump_host">Ingen (direktanslutning)</string>
|
||||
<string name="via_jump_chain">via hoppkedja</string>
|
||||
<string name="cycle_detected">Cykel upptäckt — detta skulle skapa en oändlig loop</string>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@
|
|||
<string name="vault_local_passwords_dont_match">Passwords do not match</string>
|
||||
<string name="vault_local_saved">Vault saved successfully</string>
|
||||
<string name="vault_local_wrong_device">This vault was created on a different device and cannot be imported here</string>
|
||||
<string name="vault_import_pro_only">This vault was created with Export Vault, which requires a Pro subscription to import. Free users can only import local vault saves from this device.</string>
|
||||
<string name="vault_saving">Saving…</string>
|
||||
<string name="vault_include_settings">Include settings (keyboard, display, QuickBar customization)</string>
|
||||
<string name="vault_settings_restored">Settings restored</string>
|
||||
<string name="vault_try_again">Try again</string>
|
||||
<string name="new_connection">New Connection</string>
|
||||
<string name="state_connected">Connected</string>
|
||||
|
|
@ -82,6 +85,7 @@
|
|||
<string name="select_ssh_key">Select SSH Key</string>
|
||||
<string name="no_keys_available">No keys available</string>
|
||||
<string name="jump_host">Jump Host</string>
|
||||
<string name="jump_host_chain">Jump host chaining</string>
|
||||
<string name="no_jump_host">None (direct connection)</string>
|
||||
<string name="via_jump_chain">via jump host chain</string>
|
||||
<string name="cycle_detected">Cycle detected — this would create an infinite loop</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue