Merge pull request #4053 from k9mail/settings_export_fragment
Add settings export to settings screen
This commit is contained in:
commit
1e8b66e29b
22 changed files with 888 additions and 2 deletions
12
app/core/src/main/java/com/fsck/k9/helper/Timing.kt
Normal file
12
app/core/src/main/java/com/fsck/k9/helper/Timing.kt
Normal file
|
@ -0,0 +1,12 @@
|
|||
package com.fsck.k9.helper
|
||||
|
||||
import android.os.SystemClock
|
||||
|
||||
/**
|
||||
* Executes the given [block] and returns elapsed realtime in milliseconds.
|
||||
*/
|
||||
inline fun measureRealtimeMillis(block: () -> Unit): Long {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
block()
|
||||
return SystemClock.elapsedRealtime() - start
|
||||
}
|
|
@ -28,6 +28,7 @@ dependencies {
|
|||
implementation "androidx.lifecycle:lifecycle-extensions:${versions.androidxLifecycleExtensions}"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
|
||||
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
|
||||
implementation "de.cketti.library.changelog:ckchangelog:1.2.1"
|
||||
implementation "com.github.bumptech.glide:glide:3.6.1"
|
||||
implementation "com.splitwise:tokenautocomplete:2.0.7"
|
||||
|
@ -38,6 +39,7 @@ dependencies {
|
|||
implementation 'com.mikepenz:materialdrawer:6.1.1'
|
||||
implementation 'com.mikepenz:fontawesome-typeface:5.3.1.1@aar'
|
||||
implementation 'com.github.ByteHamster:SearchPreference:v1.1.4'
|
||||
implementation 'com.mikepenz:fastadapter:3.3.1'
|
||||
|
||||
implementation "commons-io:commons-io:${versions.commonsIo}"
|
||||
implementation "androidx.core:core-ktx:${versions.coreKtx}"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package com.fsck.k9.ui.helper
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class CoroutineScopeViewModel : ViewModel(), CoroutineScope {
|
||||
private val job = Job()
|
||||
|
||||
override val coroutineContext = Dispatchers.Main + job
|
||||
|
||||
override fun onCleared() {
|
||||
job.cancel()
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import com.fsck.k9.helper.NamedThreadFactory
|
|||
import com.fsck.k9.ui.account.AccountsLiveData
|
||||
import com.fsck.k9.ui.settings.account.AccountSettingsDataStoreFactory
|
||||
import com.fsck.k9.ui.settings.account.AccountSettingsViewModel
|
||||
import com.fsck.k9.ui.settings.export.SettingsExportViewModel
|
||||
import com.fsck.k9.ui.settings.general.GeneralSettingsDataStore
|
||||
import org.koin.android.architecture.ext.viewModel
|
||||
import org.koin.dsl.module.applicationContext
|
||||
|
@ -20,4 +21,6 @@ val settingsUiModule = applicationContext {
|
|||
|
||||
viewModel { AccountSettingsViewModel(get(), get()) }
|
||||
bean { AccountSettingsDataStoreFactory(get(), get(), get("SaveSettingsExecutorService")) }
|
||||
|
||||
viewModel { SettingsExportViewModel(get(), get()) }
|
||||
}
|
||||
|
|
|
@ -93,6 +93,17 @@ class SettingsListFragment : Fragment() {
|
|||
}
|
||||
accountSection.setHeader(SettingsDividerItem(getString(R.string.accounts_title)))
|
||||
settingsAdapter.add(accountSection)
|
||||
|
||||
val backupSection = Section().apply {
|
||||
val exportSettingsActionItem = SettingsActionItem(
|
||||
getString(R.string.settings_export_title),
|
||||
R.id.action_settingsListScreen_to_settingsExportScreen,
|
||||
R.attr.iconSettingsExport
|
||||
)
|
||||
add(exportSettingsActionItem)
|
||||
}
|
||||
backupSection.setHeader(SettingsDividerItem(getString(R.string.settings_list_backup_category)))
|
||||
settingsAdapter.add(backupSection)
|
||||
}
|
||||
|
||||
private fun handleItemClick(item: Item<*>) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.fsck.k9.ui.settings.export
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.fsck.k9.ui.R
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
import com.mikepenz.fastadapter.listeners.ClickEventHook
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
|
||||
abstract class CheckBoxItem(private val id: Long) : AbstractItem<CheckBoxItem, CheckBoxViewHolder>() {
|
||||
override fun getIdentifier(): Long = id
|
||||
|
||||
override fun getViewHolder(view: View): CheckBoxViewHolder = CheckBoxViewHolder(view)
|
||||
|
||||
override fun bindView(viewHolder: CheckBoxViewHolder, payloads: List<Any>) {
|
||||
super.bindView(viewHolder, payloads)
|
||||
viewHolder.checkBox.isChecked = isSelected
|
||||
viewHolder.itemView.isEnabled = isEnabled
|
||||
viewHolder.checkBox.isEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
class CheckBoxViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {
|
||||
val checkBox: CheckBox = itemView.findViewById(R.id.checkBox)
|
||||
|
||||
override val containerView = itemView
|
||||
}
|
||||
|
||||
class CheckBoxClickEvent(val action: (position: Int, isSelected: Boolean) -> Unit) : ClickEventHook<CheckBoxItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
|
||||
return if (viewHolder is CheckBoxViewHolder) viewHolder.checkBox else null
|
||||
}
|
||||
|
||||
override fun onClick(view: View, position: Int, fastAdapter: FastAdapter<CheckBoxItem>, item: CheckBoxItem) {
|
||||
action(position, !item.isSelected)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package com.fsck.k9.ui.settings.export
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.observeNotNull
|
||||
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_settings_export.*
|
||||
import org.koin.android.architecture.ext.viewModel
|
||||
|
||||
|
||||
class SettingsExportFragment : Fragment() {
|
||||
private val viewModel: SettingsExportViewModel by viewModel()
|
||||
|
||||
private lateinit var settingsExportAdapter: FastItemAdapter<CheckBoxItem>
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_settings_export, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState != null) {
|
||||
viewModel.initializeFromSavedState(savedInstanceState)
|
||||
}
|
||||
|
||||
initializeSettingsExportList(view)
|
||||
exportButton.setOnClickListener { viewModel.onExportButtonClicked() }
|
||||
shareButton.setOnClickListener { viewModel.onShareButtonClicked() }
|
||||
|
||||
viewModel.getUiModel().observeNotNull(this) { updateUi(it) }
|
||||
viewModel.getActionEvents().observeNotNull(this) { handleActionEvents(it) }
|
||||
}
|
||||
|
||||
private fun initializeSettingsExportList(view: View) {
|
||||
settingsExportAdapter = FastItemAdapter<CheckBoxItem>().apply {
|
||||
setHasStableIds(true)
|
||||
withOnClickListener { _, _, item: CheckBoxItem, position ->
|
||||
viewModel.onSettingsListItemSelected(position, !item.isSelected)
|
||||
true
|
||||
}
|
||||
withEventHook(CheckBoxClickEvent { position, isSelected ->
|
||||
viewModel.onSettingsListItemSelected(position, isSelected)
|
||||
})
|
||||
}
|
||||
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.settingsExportList)
|
||||
recyclerView.adapter = settingsExportAdapter
|
||||
}
|
||||
|
||||
private fun updateUi(model: SettingsExportUiModel) {
|
||||
when (model.exportButton) {
|
||||
ButtonState.DISABLED -> {
|
||||
exportButton.visibility = View.VISIBLE
|
||||
exportButton.isEnabled = false
|
||||
}
|
||||
ButtonState.ENABLED -> {
|
||||
exportButton.visibility = View.VISIBLE
|
||||
exportButton.isEnabled = true
|
||||
}
|
||||
ButtonState.INVISIBLE -> exportButton.visibility = View.INVISIBLE
|
||||
ButtonState.GONE -> exportButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
shareButton.visibility = if (model.isShareButtonVisible) View.VISIBLE else View.GONE
|
||||
progressBar.visibility = if (model.isProgressVisible) View.VISIBLE else View.GONE
|
||||
|
||||
when (model.statusText) {
|
||||
StatusText.HIDDEN -> statusText.visibility = View.GONE
|
||||
StatusText.EXPORT_SUCCESS -> {
|
||||
statusText.visibility = View.VISIBLE
|
||||
statusText.text = getString(R.string.settings_export_success_generic)
|
||||
}
|
||||
StatusText.PROGRESS -> {
|
||||
statusText.visibility = View.VISIBLE
|
||||
statusText.text = getString(R.string.settings_export_progress_text)
|
||||
}
|
||||
StatusText.EXPORT_FAILURE -> {
|
||||
statusText.visibility = View.VISIBLE
|
||||
statusText.text = getString(R.string.settings_export_failure)
|
||||
}
|
||||
}
|
||||
|
||||
setSettingsList(model.settingsList, model.isSettingsListEnabled)
|
||||
}
|
||||
|
||||
//TODO: Update list instead of replacing it completely
|
||||
private fun setSettingsList(items: List<SettingsListItem>, enable: Boolean) {
|
||||
val checkBoxItems = items.map { item ->
|
||||
val checkBoxItem = when (item) {
|
||||
is SettingsListItem.GeneralSettings -> GeneralSettingsItem()
|
||||
is SettingsListItem.Account -> AccountItem(item)
|
||||
}
|
||||
|
||||
checkBoxItem
|
||||
.withSetSelected(item.selected)
|
||||
.withEnabled(enable)
|
||||
}
|
||||
|
||||
settingsExportAdapter.set(checkBoxItems)
|
||||
|
||||
settingsExportList.isEnabled = enable
|
||||
}
|
||||
|
||||
private fun handleActionEvents(action: Action) {
|
||||
when (action) {
|
||||
is Action.PickDocument -> pickDocument(action.fileNameSuggestion, action.mimeType)
|
||||
is Action.ShareDocument -> shareDocument(action.contentUri, action.mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickDocument(fileNameSuggestion: String, mimeType: String) {
|
||||
val createDocumentIntent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
type = mimeType
|
||||
putExtra(Intent.EXTRA_TITLE, fileNameSuggestion)
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
startActivityForResult(createDocumentIntent, RESULT_PICK_DOCUMENT)
|
||||
}
|
||||
|
||||
private fun shareDocument(contentUri: Uri, mimeType: String) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = mimeType
|
||||
putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
viewModel.saveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == RESULT_PICK_DOCUMENT) {
|
||||
val contentUri = data?.data
|
||||
if (resultCode == Activity.RESULT_OK && contentUri != null) {
|
||||
viewModel.onDocumentPicked(contentUri)
|
||||
} else {
|
||||
viewModel.onDocumentPickCanceled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val RESULT_PICK_DOCUMENT = Activity.RESULT_FIRST_USER
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.fsck.k9.ui.settings.export
|
||||
|
||||
import com.fsck.k9.ui.R
|
||||
import kotlinx.android.synthetic.main.settings_export_account_list_item.*
|
||||
|
||||
|
||||
private const val GENERAL_SETTINGS_ID = 0L
|
||||
private const val ACCOUNT_ITEMS_ID_OFFSET = 1L
|
||||
|
||||
|
||||
class GeneralSettingsItem : CheckBoxItem(GENERAL_SETTINGS_ID) {
|
||||
override fun getType(): Int = R.id.settings_export_list_general_item
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.settings_export_general_list_item
|
||||
}
|
||||
|
||||
|
||||
class AccountItem(account: SettingsListItem.Account) : CheckBoxItem(account.accountNumber + ACCOUNT_ITEMS_ID_OFFSET) {
|
||||
private val displayName = account.displayName
|
||||
private val email = account.email
|
||||
|
||||
|
||||
override fun getType(): Int = R.id.settings_export_list_account_item
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.settings_export_account_list_item
|
||||
|
||||
override fun bindView(viewHolder: CheckBoxViewHolder, payloads: List<Any>) {
|
||||
super.bindView(viewHolder, payloads)
|
||||
viewHolder.accountDisplayName.text = displayName
|
||||
viewHolder.accountEmail.text = email
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package com.fsck.k9.ui.settings.export
|
||||
|
||||
import com.fsck.k9.Account as K9Account
|
||||
|
||||
|
||||
class SettingsExportUiModel {
|
||||
var settingsList: List<SettingsListItem> = emptyList()
|
||||
var isSettingsListEnabled = true
|
||||
var exportButton: ButtonState = ButtonState.DISABLED
|
||||
var isShareButtonVisible = false
|
||||
var isProgressVisible = false
|
||||
var statusText = StatusText.HIDDEN
|
||||
|
||||
|
||||
fun enableExportButton() {
|
||||
exportButton = ButtonState.ENABLED
|
||||
isShareButtonVisible = false
|
||||
isProgressVisible = false
|
||||
isSettingsListEnabled = true
|
||||
}
|
||||
|
||||
fun disableExportButton() {
|
||||
exportButton = ButtonState.DISABLED
|
||||
isShareButtonVisible = false
|
||||
isProgressVisible = false
|
||||
}
|
||||
|
||||
fun showProgress() {
|
||||
isProgressVisible = true
|
||||
exportButton = ButtonState.INVISIBLE
|
||||
isShareButtonVisible = false
|
||||
statusText = StatusText.PROGRESS
|
||||
isSettingsListEnabled = false
|
||||
}
|
||||
|
||||
fun showSuccessText() {
|
||||
exportButton = ButtonState.GONE
|
||||
isProgressVisible = false
|
||||
isShareButtonVisible = true
|
||||
isSettingsListEnabled = true
|
||||
statusText = StatusText.EXPORT_SUCCESS
|
||||
}
|
||||
|
||||
fun showFailureText() {
|
||||
exportButton = ButtonState.GONE
|
||||
isShareButtonVisible = false
|
||||
isProgressVisible = false
|
||||
isSettingsListEnabled = true
|
||||
statusText = StatusText.EXPORT_FAILURE
|
||||
}
|
||||
|
||||
fun initializeSettingsList(list: List<SettingsListItem>) {
|
||||
settingsList = list
|
||||
updateExportButtonFromSelection()
|
||||
}
|
||||
|
||||
fun setSettingsListItemSelection(position: Int, select: Boolean) {
|
||||
settingsList[position].selected = select
|
||||
statusText = StatusText.HIDDEN
|
||||
isShareButtonVisible = false
|
||||
updateExportButtonFromSelection()
|
||||
}
|
||||
|
||||
private fun updateExportButtonFromSelection() {
|
||||
if (isProgressVisible || isShareButtonVisible) return
|
||||
|
||||
val atLeastOnceSelected = settingsList.any { it.selected }
|
||||
if (atLeastOnceSelected) {
|
||||
enableExportButton()
|
||||
} else {
|
||||
disableExportButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SettingsListItem {
|
||||
var selected: Boolean = true
|
||||
|
||||
object GeneralSettings : SettingsListItem()
|
||||
data class Account(
|
||||
val accountNumber: Int,
|
||||
val displayName: String,
|
||||
val email: String
|
||||
) : SettingsListItem()
|
||||
}
|
||||
|
||||
enum class ButtonState {
|
||||
DISABLED,
|
||||
ENABLED,
|
||||
INVISIBLE,
|
||||
GONE
|
||||
}
|
||||
|
||||
enum class StatusText {
|
||||
HIDDEN,
|
||||
PROGRESS,
|
||||
EXPORT_SUCCESS,
|
||||
EXPORT_FAILURE
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package com.fsck.k9.ui.settings.export
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.helper.SingleLiveEvent
|
||||
import com.fsck.k9.helper.measureRealtimeMillis
|
||||
import com.fsck.k9.preferences.SettingsExporter
|
||||
import com.fsck.k9.ui.helper.CoroutineScopeViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private typealias AccountUuid = String
|
||||
private typealias AccountNumber = Int
|
||||
|
||||
class SettingsExportViewModel(val context: Context, val preferences: Preferences) : CoroutineScopeViewModel() {
|
||||
private val uiModelLiveData = MutableLiveData<SettingsExportUiModel>()
|
||||
private val actionLiveData = SingleLiveEvent<Action>()
|
||||
|
||||
private val uiModel = SettingsExportUiModel()
|
||||
private var accountsMap: Map<AccountNumber, AccountUuid> = emptyMap()
|
||||
private var savedSelection: SavedListItemSelection? = null
|
||||
private var contentUri: Uri? = null
|
||||
|
||||
private val includeGeneralSettings: Boolean
|
||||
get() {
|
||||
return savedSelection?.includeGeneralSettings
|
||||
?: uiModel.settingsList.first { it is SettingsListItem.GeneralSettings }.selected
|
||||
}
|
||||
|
||||
private val selectedAccounts: Set<AccountUuid>
|
||||
get() {
|
||||
return savedSelection?.selectedAccountUuids
|
||||
?: uiModel.settingsList.asSequence()
|
||||
.filterIsInstance<SettingsListItem.Account>()
|
||||
.filter { it.selected }
|
||||
.map {
|
||||
accountsMap[it.accountNumber] ?: error("Unknown account number: ${it.accountNumber}")
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
|
||||
|
||||
fun getActionEvents(): LiveData<Action> = actionLiveData
|
||||
|
||||
fun getUiModel(): LiveData<SettingsExportUiModel> {
|
||||
if (uiModelLiveData.value == null) {
|
||||
uiModelLiveData.value = uiModel
|
||||
|
||||
launch {
|
||||
val accounts = withContext(Dispatchers.IO) { preferences.accounts }
|
||||
|
||||
accountsMap = accounts.map { it.accountNumber to it.uuid }.toMap()
|
||||
|
||||
val listItems = savedSelection.let { savedState ->
|
||||
val generalSettings = SettingsListItem.GeneralSettings.apply {
|
||||
selected = savedState == null || savedState.includeGeneralSettings
|
||||
}
|
||||
|
||||
val accountListItems = accounts.map { account ->
|
||||
SettingsListItem.Account(account.accountNumber, account.displayName, account.email).apply {
|
||||
selected = savedState == null || account.uuid in savedState.selectedAccountUuids
|
||||
}
|
||||
}
|
||||
|
||||
listOf(generalSettings) + accountListItems
|
||||
}
|
||||
|
||||
updateUiModel {
|
||||
initializeSettingsList(listItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uiModelLiveData
|
||||
}
|
||||
|
||||
|
||||
fun initializeFromSavedState(savedInstanceState: Bundle) {
|
||||
savedSelection = SavedListItemSelection(
|
||||
includeGeneralSettings = savedInstanceState.getBoolean(STATE_INCLUDE_GENERAL_SETTINGS),
|
||||
selectedAccountUuids = savedInstanceState.getStringArray(STATE_SELECTED_ACCOUNTS)?.toSet() ?: emptySet()
|
||||
)
|
||||
|
||||
uiModel.apply {
|
||||
isSettingsListEnabled = savedInstanceState.getBoolean(STATE_SETTINGS_LIST_ENABLED)
|
||||
exportButton = ButtonState.valueOf(
|
||||
savedInstanceState.getString(STATE_EXPORT_BUTTON, ButtonState.DISABLED.name)
|
||||
)
|
||||
isShareButtonVisible = savedInstanceState.getBoolean(STATE_SHARE_BUTTON_VISIBLE)
|
||||
isProgressVisible = savedInstanceState.getBoolean(STATE_PROGRESS_VISIBLE)
|
||||
statusText = StatusText.valueOf(savedInstanceState.getString(STATE_STATUS_TEXT, StatusText.HIDDEN.name))
|
||||
}
|
||||
|
||||
contentUri = savedInstanceState.getParcelable(STATE_CONTENT_URI)
|
||||
}
|
||||
|
||||
fun onExportButtonClicked() {
|
||||
updateUiModel {
|
||||
disableExportButton()
|
||||
}
|
||||
|
||||
startExportSettings()
|
||||
}
|
||||
|
||||
fun onShareButtonClicked() {
|
||||
sendActionEvent(Action.ShareDocument(contentUri!!, SETTINGS_MIME_TYPE))
|
||||
}
|
||||
|
||||
private fun startExportSettings() {
|
||||
val exportFileName = SettingsExporter.generateDatedExportFileName()
|
||||
sendActionEvent(Action.PickDocument(exportFileName, SETTINGS_MIME_TYPE))
|
||||
}
|
||||
|
||||
fun onDocumentPicked(contentUri: Uri) {
|
||||
this.contentUri = contentUri
|
||||
|
||||
updateUiModel {
|
||||
showProgress()
|
||||
}
|
||||
|
||||
val includeGeneralSettings = this.includeGeneralSettings
|
||||
val selectedAccounts = this.selectedAccounts
|
||||
|
||||
launch {
|
||||
try {
|
||||
val elapsed = measureRealtimeMillis {
|
||||
withContext(Dispatchers.IO) {
|
||||
SettingsExporter.exportToUri(context, includeGeneralSettings, selectedAccounts, contentUri)
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsed < MIN_PROGRESS_DURATION) {
|
||||
delay(MIN_PROGRESS_DURATION - elapsed)
|
||||
}
|
||||
|
||||
updateUiModel {
|
||||
showSuccessText()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
updateUiModel {
|
||||
showFailureText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDocumentPickCanceled() {
|
||||
updateUiModel {
|
||||
enableExportButton()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(STATE_SETTINGS_LIST_ENABLED, uiModel.isSettingsListEnabled)
|
||||
outState.putString(STATE_EXPORT_BUTTON, uiModel.exportButton.name)
|
||||
outState.putBoolean(STATE_SHARE_BUTTON_VISIBLE, uiModel.isShareButtonVisible)
|
||||
outState.putBoolean(STATE_PROGRESS_VISIBLE, uiModel.isProgressVisible)
|
||||
outState.putString(STATE_STATUS_TEXT, uiModel.statusText.name)
|
||||
|
||||
outState.putBoolean(STATE_INCLUDE_GENERAL_SETTINGS, includeGeneralSettings)
|
||||
outState.putStringArray(STATE_SELECTED_ACCOUNTS, selectedAccounts.toTypedArray())
|
||||
|
||||
outState.putParcelable(STATE_CONTENT_URI, contentUri)
|
||||
}
|
||||
|
||||
fun onSettingsListItemSelected(position: Int, isSelected: Boolean) {
|
||||
savedSelection = null
|
||||
|
||||
updateUiModel {
|
||||
setSettingsListItemSelection(position, isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUiModel(block: SettingsExportUiModel.() -> Unit) {
|
||||
uiModel.block()
|
||||
uiModelLiveData.value = uiModel
|
||||
}
|
||||
|
||||
private fun sendActionEvent(action: Action) {
|
||||
actionLiveData.value = action
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val MIN_PROGRESS_DURATION = 1000L
|
||||
private const val SETTINGS_MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private const val STATE_SETTINGS_LIST_ENABLED = "settingsListEnabled"
|
||||
private const val STATE_EXPORT_BUTTON = "exportButton"
|
||||
private const val STATE_SHARE_BUTTON_VISIBLE = "shareButtonVisible"
|
||||
private const val STATE_PROGRESS_VISIBLE = "progressVisible"
|
||||
private const val STATE_STATUS_TEXT = "statusText"
|
||||
private const val STATE_INCLUDE_GENERAL_SETTINGS = "includeGeneralSettings"
|
||||
private const val STATE_SELECTED_ACCOUNTS = "selectedAccounts"
|
||||
private const val STATE_CONTENT_URI = "contentUri"
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Action {
|
||||
class PickDocument(val fileNameSuggestion: String, val mimeType: String) : Action()
|
||||
class ShareDocument(val contentUri: Uri, val mimeType: String) : Action()
|
||||
}
|
||||
|
||||
private data class SavedListItemSelection(
|
||||
val includeGeneralSettings: Boolean,
|
||||
val selectedAccountUuids: Set<AccountUuid>
|
||||
)
|
9
app/ui/src/main/res/drawable-anydpi/ic_export_dark.xml
Normal file
9
app/ui/src/main/res/drawable-anydpi/ic_export_dark.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="m8,7h3V16h2V7h3L12,3ZM4,19v2h16v-2z"/>
|
||||
</vector>
|
10
app/ui/src/main/res/drawable-anydpi/ic_export_light.xml
Normal file
10
app/ui/src/main/res/drawable-anydpi/ic_export_light.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:alpha="0.54"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m8,7h3V16h2V7h3L12,3ZM4,19v2h16v-2z"/>
|
||||
</vector>
|
94
app/ui/src/main/res/layout/fragment_settings_export.xml
Normal file
94
app/ui/src/main/res/layout/fragment_settings_export.xml
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/settingsExportList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottomBar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:listitem="@layout/settings_export_account_list_item" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottomBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/bottomBarBackground"
|
||||
android:elevation="8dp"
|
||||
android:minHeight="56dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<Button
|
||||
android:id="@+id/exportButton"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:enabled="false"
|
||||
android:text="@string/settings_export_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/shareButton"
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/settings_export_share_button"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/buttonBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="left"
|
||||
app:constraint_referenced_ids="exportButton,shareButton" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/exportButton"
|
||||
app:layout_constraintEnd_toEndOf="@+id/exportButton"
|
||||
app:layout_constraintStart_toStartOf="@+id/exportButton"
|
||||
app:layout_constraintTop_toTopOf="@+id/exportButton" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/buttonBarrier"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Exporting…"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountDisplayName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:layout_constraintBottom_toTopOf="@+id/accountEmail"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Account name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountEmail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/accountDisplayName"
|
||||
tools:text="user@domain.example" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="72dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:duplicateParentState="true"
|
||||
android:text="@string/general_settings_title"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="72dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -22,6 +22,9 @@
|
|||
<action
|
||||
android:id="@+id/action_settingsListScreen_to_addAccountScreen"
|
||||
app:destination="@id/addAccountScreen" />
|
||||
<action
|
||||
android:id="@+id/action_settingsListScreen_to_settingsExportScreen"
|
||||
app:destination="@id/settingsExportScreen" />
|
||||
</fragment>
|
||||
|
||||
<activity
|
||||
|
@ -41,4 +44,10 @@
|
|||
android:label="@string/account_setup_basics_title"
|
||||
tools:layout="@layout/account_setup_basics"/>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/settingsExportScreen"
|
||||
android:name="com.fsck.k9.ui.settings.export.SettingsExportFragment"
|
||||
android:label="@string/settings_export_title"
|
||||
tools:layout="@layout/fragment_settings_export"/>
|
||||
|
||||
</navigation>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<resources>
|
||||
|
||||
<declare-styleable name="K9Styles">
|
||||
<attr name="bottomBarBackground" format="reference|color" />
|
||||
<attr name="iconUnifiedInbox" format="reference" />
|
||||
<attr name="iconFolder" format="reference" />
|
||||
<attr name="iconFolderInbox" format="reference" />
|
||||
|
@ -63,6 +64,7 @@
|
|||
<attr name="iconSettingsGeneral" format="reference" />
|
||||
<attr name="iconSettingsAccount" format="reference" />
|
||||
<attr name="iconSettingsAccountAdd" format="reference" />
|
||||
<attr name="iconSettingsExport" format="reference" />
|
||||
<attr name="textColorPrimaryRecipientDropdown" format="reference" />
|
||||
<attr name="textColorSecondaryRecipientDropdown" format="reference" />
|
||||
<attr name="backgroundColorChooseAccountHeader" format="color" />
|
||||
|
|
|
@ -8,4 +8,7 @@
|
|||
<item type="id" name="dialog_attachment_progress"/>
|
||||
<item type="id" name="dialog_account_setup_error"/>
|
||||
|
||||
<item type="id" name="settings_export_list_general_item"/>
|
||||
<item type="id" name="settings_export_list_account_item"/>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -991,6 +991,15 @@ Please submit bug reports, contribute new features and ask questions at
|
|||
<string name="messagelist_sent_to_me_sigil">»</string>
|
||||
<string name="messagelist_sent_cc_me_sigil">›</string>
|
||||
|
||||
<string name="settings_list_backup_category">Backup</string>
|
||||
|
||||
<string name="settings_export_title">Export settings</string>
|
||||
<string name="settings_export_button">Export</string>
|
||||
<string name="settings_export_share_button">Share</string>
|
||||
<string name="settings_export_progress_text">Exporting settings…</string>
|
||||
<string name="settings_export_success_generic">Settings successfully exported</string>
|
||||
<string name="settings_export_failure">Failed to export settings</string>
|
||||
|
||||
<string name="import_export_action">Settings Import & Export</string>
|
||||
<string name="settings_export_account">Export account settings</string>
|
||||
<string name="settings_export_all">Export settings and accounts</string>
|
||||
|
@ -1002,7 +1011,6 @@ Please submit bug reports, contribute new features and ask questions at
|
|||
<string name="settings_exporting">Exporting settings…</string>
|
||||
<string name="settings_importing">Importing settings…</string>
|
||||
<string name="settings_import_scanning_file">Scanning file…</string>
|
||||
<string name="settings_export_success_generic">Settings successfully exported</string>
|
||||
<string name="settings_import_global_settings_success">Imported global settings</string>
|
||||
<string name="settings_import_success">Imported <xliff:g id="accounts">%s</xliff:g></string>
|
||||
<string name="settings_import_account_imported_as">Imported <xliff:g id="original_account_name">%s</xliff:g> as <xliff:g id="account_name_after_import">%s</xliff:g></string>
|
||||
|
@ -1010,7 +1018,6 @@ Please submit bug reports, contribute new features and ask questions at
|
|||
<item quantity="one">1 account</item>
|
||||
<item quantity="other"><xliff:g id="numAccounts">%s</xliff:g> accounts</item>
|
||||
</plurals>
|
||||
<string name="settings_export_failure">Failed to export settings</string>
|
||||
<string name="settings_import_failure">Failed to import any settings</string>
|
||||
<string name="settings_export_success_header">Export succeeded</string>
|
||||
<string name="settings_export_failed_header">Export failed</string>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
<item name="android:windowLightStatusBar" tools:targetApi="23">true</item>
|
||||
<item name="colorPrimary">@color/md_grey_100</item>
|
||||
<item name="colorPrimaryDark">@color/md_grey_100</item>
|
||||
<item name="bottomBarBackground">@color/md_grey_50</item>
|
||||
|
||||
<item name="material_drawer_background">@color/material_drawer_background</item>
|
||||
<item name="material_drawer_primary_text">@color/material_drawer_primary_text</item>
|
||||
|
@ -89,6 +90,7 @@
|
|||
<item name="iconSettingsGeneral">@drawable/ic_cog_light</item>
|
||||
<item name="iconSettingsAccount">@drawable/ic_account_light</item>
|
||||
<item name="iconSettingsAccountAdd">@drawable/ic_account_plus_light</item>
|
||||
<item name="iconSettingsExport">@drawable/ic_export_light</item>
|
||||
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_light</item>
|
||||
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_light</item>
|
||||
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
|
||||
|
@ -130,6 +132,7 @@
|
|||
<item name="android:windowLightStatusBar" tools:targetApi="23">false</item>
|
||||
<item name="colorPrimary">@color/md_grey_900</item>
|
||||
<item name="colorPrimaryDark">@color/md_grey_900</item>
|
||||
<item name="bottomBarBackground">@color/md_grey_900</item>
|
||||
|
||||
<item name="material_drawer_background">@color/material_drawer_dark_background</item>
|
||||
<item name="material_drawer_primary_text">@color/material_drawer_dark_primary_text</item>
|
||||
|
@ -205,6 +208,7 @@
|
|||
<item name="iconSettingsGeneral">@drawable/ic_cog_dark</item>
|
||||
<item name="iconSettingsAccount">@drawable/ic_account_dark</item>
|
||||
<item name="iconSettingsAccountAdd">@drawable/ic_account_plus_dark</item>
|
||||
<item name="iconSettingsExport">@drawable/ic_export_dark</item>
|
||||
<item name="textColorPrimaryRecipientDropdown">@android:color/primary_text_dark</item>
|
||||
<item name="textColorSecondaryRecipientDropdown">@android:color/secondary_text_dark</item>
|
||||
<item name="messageListSelectedBackgroundColor">#8038B8E2</item>
|
||||
|
|
|
@ -13,6 +13,7 @@ buildscript {
|
|||
'androidxLifecycleExtensions': '2.0.0',
|
||||
'androidxAnnotation': '1.0.1',
|
||||
'androidxNavigation': '2.0.0',
|
||||
'androidxConstraintLayout': '1.1.3',
|
||||
'coreKtx': '1.0.1',
|
||||
'preferencesFix': '1.0.0',
|
||||
'okio': '1.14.0',
|
||||
|
|
60
images/drawable-src/ic_export.svg
Normal file
60
images/drawable-src/ic_export.svg
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="ic_export.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1859"
|
||||
inkscape:window-height="1145"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="27.812866"
|
||||
inkscape:cx="18.87956"
|
||||
inkscape:cy="10.262807"
|
||||
inkscape:window-x="61"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path4" />
|
||||
<path
|
||||
id="path822"
|
||||
d="m 8,6.9999999 h 3 V 16 h 2 V 6.9999999 h 3 L 12,3.0000001 Z M 4,19 v 2 h 16 v -2 z"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccc" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
Loading…
Reference in a new issue