mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
@@ -3,7 +3,8 @@ KeePassDX(3.3.0)
|
||||
* Keep search context #1141
|
||||
* Add searchable groups #905 #1006
|
||||
* Search with regular expression #175
|
||||
* Fix styles
|
||||
* Merge from file and save as copy #1221 #1204 #840
|
||||
* New UI and fix styles
|
||||
|
||||
KeePassDX(3.2.0)
|
||||
* Manage data merge #840 #977
|
||||
|
||||
@@ -66,7 +66,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.changeControlColor
|
||||
@@ -370,7 +369,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
@@ -443,10 +441,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
@@ -36,18 +37,22 @@ import android.widget.*
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||
@@ -61,6 +66,7 @@ import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
@@ -68,14 +74,16 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.SettingsActivity
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.*
|
||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||
import org.joda.time.DateTime
|
||||
|
||||
|
||||
class GroupActivity : DatabaseLockActivity(),
|
||||
DatePickerDialog.OnDateSetListener,
|
||||
TimePickerDialog.OnTimeSetListener,
|
||||
@@ -83,9 +91,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
GroupFragment.NodesActionMenuListener,
|
||||
GroupFragment.OnScrollListener,
|
||||
GroupFragment.GroupRefreshedListener,
|
||||
SortDialogFragment.SortSelectionListener {
|
||||
SortDialogFragment.SortSelectionListener,
|
||||
AskMainCredentialDialogFragment.AskMainCredentialDialogListener {
|
||||
|
||||
// Views
|
||||
private var drawerLayout: DrawerLayout? = null
|
||||
private var databaseNavView: NavigationDatabaseView? = null
|
||||
private var rootContainerView: ViewGroup? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var lockView: View? = null
|
||||
@@ -116,6 +127,9 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
private var actionNodeMode: ActionMode? = null
|
||||
|
||||
// Manage merge
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
// Manage group
|
||||
private var mSearchState: SearchState? = null
|
||||
private var mMainGroupState: GroupState? = null // Group state, not a search
|
||||
@@ -215,6 +229,8 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||
|
||||
// Initialize views
|
||||
drawerLayout = findViewById(R.id.drawer_layout)
|
||||
databaseNavView = findViewById(R.id.database_nav_view)
|
||||
rootContainerView = findViewById(R.id.activity_group_container_view)
|
||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||
numberChildrenView = findViewById(R.id.group_numbers)
|
||||
@@ -236,6 +252,55 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
this, drawerLayout, toolbar,
|
||||
R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||
)
|
||||
drawerLayout?.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
|
||||
// Manage 'merge from" and "save to"
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
launchDialogToAskMainCredential(uri)
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { uri ->
|
||||
uri?.let {
|
||||
saveDatabaseTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Menu in drawer
|
||||
databaseNavView?.apply {
|
||||
inflateMenu(R.menu.settings)
|
||||
inflateMenu(R.menu.database_extra)
|
||||
inflateMenu(R.menu.about)
|
||||
setNavigationItemSelectedListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_app_settings -> {
|
||||
// To avoid flickering when launch settings in a LockingActivity
|
||||
SettingsActivity.launch(this@GroupActivity, true)
|
||||
}
|
||||
R.id.menu_merge_from -> {
|
||||
mExternalFileHelper?.openDocument()
|
||||
}
|
||||
R.id.menu_save_copy_to -> {
|
||||
mExternalFileHelper?.createDocument(
|
||||
getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_name_copy) +
|
||||
mDatabase?.defaultFileExtension)
|
||||
}
|
||||
R.id.menu_contribute -> {
|
||||
UriUtil.gotoUrl(this@GroupActivity, R.string.contribution_url)
|
||||
}
|
||||
R.id.menu_about -> {
|
||||
startActivity(Intent(this@GroupActivity, AboutActivity::class.java))
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
searchFiltersView?.closeAdvancedFilters()
|
||||
|
||||
@@ -512,8 +577,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
|
||||
// Search suggestion
|
||||
database?.let {
|
||||
databaseNameView?.text = if (it.name.isNotEmpty()) it.name else getString(R.string.database)
|
||||
val databaseName = it.name.ifEmpty { getString(R.string.database) }
|
||||
databaseNavView?.setDatabaseName(databaseName)
|
||||
databaseNameView?.text = databaseName
|
||||
databaseNavView?.setDatabaseVersion(it.version)
|
||||
val customColor = it.customColor
|
||||
databaseNavView?.setDatabaseColor(customColor)
|
||||
if (customColor != null) {
|
||||
databaseColorView?.visibility = View.VISIBLE
|
||||
databaseColorView?.setColorFilter(
|
||||
@@ -526,6 +595,12 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
mBreadcrumbAdapter?.iconDrawableFactory = it.iconDrawableFactory
|
||||
}
|
||||
|
||||
databaseNavView?.apply {
|
||||
if (!mMergeDataAllowed) {
|
||||
menu.findItem(R.id.menu_merge_from)?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
@@ -967,6 +1042,11 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
.show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
}
|
||||
|
||||
private fun launchDialogToAskMainCredential(uri: Uri?) {
|
||||
AskMainCredentialDialogFragment.getInstance(uri)
|
||||
.show(supportFragmentManager, AskMainCredentialDialogFragment.TAG_ASK_MAIN_CREDENTIAL)
|
||||
}
|
||||
|
||||
override fun onCopyMenuClick(
|
||||
database: Database,
|
||||
nodes: List<Node>
|
||||
@@ -1022,6 +1102,16 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?,
|
||||
mainCredential: MainCredential) {
|
||||
databaseUri?.let {
|
||||
mergeDatabaseFrom(it, mainCredential)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?,
|
||||
mainCredential: MainCredential) { }
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
@@ -1060,9 +1150,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
if (!mMergeDataAllowed) {
|
||||
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
} else {
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
@@ -1174,7 +1262,7 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
// TODO change database
|
||||
drawerLayout?.openDrawer(GravityCompat.START)
|
||||
return true
|
||||
}
|
||||
R.id.menu_search -> {
|
||||
@@ -1203,8 +1291,6 @@ class GroupActivity : DatabaseLockActivity(),
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
// Check the time lock before launching settings
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, true)
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
|
||||
class AskMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
|
||||
private var mListener: AskMainCredentialDialogListener? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
interface AskMainCredentialDialogListener {
|
||||
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AskMainCredentialDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
mainCredentialView = MainCredentialView(activity)
|
||||
databaseUri?.let {
|
||||
builder.setTitle(UriUtil.getFileData(requireContext(), it)?.name)
|
||||
}
|
||||
builder.setView(mainCredentialView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogPositiveClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogNegativeClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun retrieveMainCredential(): MainCredential {
|
||||
return mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
|
||||
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
|
||||
|
||||
fun getInstance(uri: Uri?): AskMainCredentialDialogFragment {
|
||||
val fragment = AskMainCredentialDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,8 +88,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) { fixDuplicateUuid ->
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(fixDuplicateUuid)
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
@@ -263,8 +263,16 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||
}
|
||||
|
||||
fun saveDatabaseTo(uri: Uri) {
|
||||
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(false)
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
}
|
||||
|
||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
|
||||
@@ -355,9 +355,15 @@ class DatabaseTaskProvider {
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseMerge(fixDuplicateUuid: Boolean) {
|
||||
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
if (fromDatabaseUri != null) {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||
}
|
||||
if (mainCredential != null) {
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
}
|
||||
, ACTION_DATABASE_MERGE_TASK)
|
||||
}
|
||||
@@ -692,9 +698,12 @@ class DatabaseTaskProvider {
|
||||
/**
|
||||
* Save Database without parameter
|
||||
*/
|
||||
fun startDatabaseSave(save: Boolean) {
|
||||
fun startDatabaseSave(save: Boolean, saveToUri: Uri? = null) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
if (saveToUri != null) {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||
}
|
||||
}
|
||||
, ACTION_DATABASE_SAVE)
|
||||
}
|
||||
|
||||
@@ -20,17 +20,19 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class MergeDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
@@ -41,11 +43,14 @@ class MergeDatabaseRunnable(private val context: Context,
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.mergeData(context.contentResolver,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater)
|
||||
mDatabase.mergeData(mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
context.contentResolver,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater
|
||||
)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private var saveDatabase: Boolean)
|
||||
private var saveDatabase: Boolean,
|
||||
private var databaseCopyUri: Uri? = null)
|
||||
: ActionRunnable() {
|
||||
|
||||
var mAfterSaveDatabase: ((Result) -> Unit)? = null
|
||||
@@ -37,7 +39,7 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
database.checkVersion()
|
||||
if (saveDatabase && result.isSuccess) {
|
||||
try {
|
||||
database.saveData(context.contentResolver)
|
||||
database.saveData(databaseCopyUri, context.contentResolver)
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -274,6 +274,9 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
val defaultFileExtension: String
|
||||
get() = mDatabaseKDB?.defaultFileExtension ?: mDatabaseKDBX?.defaultFileExtension ?: ".bin"
|
||||
|
||||
val type: Class<*>?
|
||||
get() = mDatabaseKDB?.javaClass ?: mDatabaseKDBX?.javaClass
|
||||
|
||||
@@ -634,9 +637,13 @@ class Database {
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater)
|
||||
progressTaskUpdater
|
||||
) {
|
||||
databaseKDB.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream
|
||||
)
|
||||
}
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
@@ -647,9 +654,12 @@ class Database {
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
progressTaskUpdater)
|
||||
progressTaskUpdater) {
|
||||
databaseKDBX.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
@@ -671,7 +681,9 @@ class Database {
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun mergeData(contentResolver: ContentResolver,
|
||||
fun mergeData(databaseToMergeUri: Uri?,
|
||||
databaseToMergeMainCredential: MainCredential?,
|
||||
contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
|
||||
@@ -681,30 +693,52 @@ class Database {
|
||||
|
||||
// New database instance to get new changes
|
||||
val databaseToMerge = Database()
|
||||
databaseToMerge.fileUri = this.fileUri
|
||||
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
try {
|
||||
databaseToMerge.fileUri?.let { databaseUri ->
|
||||
|
||||
val databaseKDB = DatabaseKDB()
|
||||
val databaseKDBX = DatabaseKDBX()
|
||||
val databaseUri = databaseToMerge.fileUri
|
||||
if (databaseUri != null) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
// Get keyFile inputStream
|
||||
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||
}
|
||||
}
|
||||
|
||||
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
databaseKDB
|
||||
val databaseToMergeKDB = DatabaseKDB()
|
||||
DatabaseInputKDB(databaseToMergeKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDB.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDB.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
databaseToMergeKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
val databaseToMergeKDBX = DatabaseKDBX()
|
||||
DatabaseInputKDBX(databaseToMergeKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDBX.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDBX.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
databaseToMergeKDBX
|
||||
}
|
||||
)
|
||||
|
||||
@@ -714,13 +748,15 @@ class Database {
|
||||
}
|
||||
databaseToMerge.mDatabaseKDB?.let { databaseKDBToMerge ->
|
||||
databaseMerger.merge(databaseKDBToMerge)
|
||||
this.dataModifiedSinceLastLoading = true
|
||||
}
|
||||
databaseToMerge.mDatabaseKDBX?.let { databaseKDBXToMerge ->
|
||||
databaseMerger.merge(databaseKDBXToMerge)
|
||||
this.dataModifiedSinceLastLoading = true
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||
} else {
|
||||
throw IODatabaseException("Database URI is null, database cannot be merged")
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
@@ -729,6 +765,7 @@ class Database {
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
databaseToMerge.clearAndClose()
|
||||
}
|
||||
}
|
||||
@@ -740,7 +777,8 @@ class Database {
|
||||
|
||||
// Retrieve the stream from the old database URI
|
||||
try {
|
||||
fileUri?.let { oldDatabaseUri ->
|
||||
val oldDatabaseUri = fileUri
|
||||
if (oldDatabaseUri != null) {
|
||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||
{ databaseInputStream ->
|
||||
val databaseKDB = DatabaseKDB()
|
||||
@@ -748,9 +786,9 @@ class Database {
|
||||
databaseKDB.binaryCache = it.binaryCache
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDB.masterKey = masterKey
|
||||
}
|
||||
databaseKDB
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
@@ -760,14 +798,14 @@ class Database {
|
||||
}
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
masterKey,
|
||||
progressTaskUpdater)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDBX.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
}
|
||||
)
|
||||
} ?: run {
|
||||
} else {
|
||||
throw IODatabaseException("Database URI is null, database cannot be reloaded")
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
@@ -867,10 +905,58 @@ class Database {
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun saveData(contentResolver: ContentResolver) {
|
||||
fun saveData(uri: Uri?, contentResolver: ContentResolver) {
|
||||
try {
|
||||
this.fileUri?.let {
|
||||
saveData(contentResolver, it)
|
||||
(uri ?: this.fileUri)?.let { saveUri ->
|
||||
if (saveUri.scheme == "file") {
|
||||
saveUri.path?.let { filename ->
|
||||
val tempFile = File("$filename.tmp")
|
||||
|
||||
var fileOutputStream: FileOutputStream? = null
|
||||
try {
|
||||
fileOutputStream = FileOutputStream(tempFile)
|
||||
val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) }
|
||||
pmo?.output()
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
fileOutputStream?.close()
|
||||
}
|
||||
|
||||
// Force data to disk before continuing
|
||||
try {
|
||||
fileOutputStream?.fd?.sync()
|
||||
} catch (e: SyncFailedException) {
|
||||
// Ignore if fsync fails. We tried.
|
||||
}
|
||||
|
||||
if (!tempFile.renameTo(File(filename))) {
|
||||
throw IOException()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = contentResolver.openOutputStream(saveUri, "rwt")
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput =
|
||||
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let {
|
||||
DatabaseOutputKDBX(
|
||||
it,
|
||||
definedOutputStream
|
||||
)
|
||||
}
|
||||
databaseOutput?.output()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
outputStream?.close()
|
||||
}
|
||||
}
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to save database", e)
|
||||
@@ -878,55 +964,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, DatabaseOutputException::class)
|
||||
private fun saveData(contentResolver: ContentResolver, uri: Uri) {
|
||||
|
||||
if (uri.scheme == "file") {
|
||||
uri.path?.let { filename ->
|
||||
val tempFile = File("$filename.tmp")
|
||||
|
||||
var fileOutputStream: FileOutputStream? = null
|
||||
try {
|
||||
fileOutputStream = FileOutputStream(tempFile)
|
||||
val pmo = mDatabaseKDB?.let { DatabaseOutputKDB(it, fileOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, fileOutputStream) }
|
||||
pmo?.output()
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
fileOutputStream?.close()
|
||||
}
|
||||
|
||||
// Force data to disk before continuing
|
||||
try {
|
||||
fileOutputStream?.fd?.sync()
|
||||
} catch (e: SyncFailedException) {
|
||||
// Ignore if fsync fails. We tried.
|
||||
}
|
||||
|
||||
if (!tempFile.renameTo(File(filename))) {
|
||||
throw IOException()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||
databaseOutput?.output()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
outputStream?.close()
|
||||
}
|
||||
}
|
||||
this.fileUri = uri
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
|
||||
fun clearIndexesAndBinaries(filesDirectory: File? = null) {
|
||||
this.mDatabaseKDB?.clearIndexes()
|
||||
this.mDatabaseKDBX?.clearIndexes()
|
||||
|
||||
@@ -64,6 +64,9 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
override val version: String
|
||||
get() = "V1"
|
||||
|
||||
override val defaultFileExtension: String
|
||||
get() = ".kdb"
|
||||
|
||||
init {
|
||||
// New manual root because KDB contains multiple root groups (here available with getRootGroups())
|
||||
rootGroup = createGroup().apply {
|
||||
|
||||
@@ -201,6 +201,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return "V2 - KDBX$kdbxStringVersion"
|
||||
}
|
||||
|
||||
override val defaultFileExtension: String
|
||||
get() = ".kdbx"
|
||||
|
||||
private open class NodeOperationHandler<T: NodeKDBXInterface> : NodeHandler<T>() {
|
||||
var containsCustomData = false
|
||||
override fun operate(node: T): Boolean {
|
||||
|
||||
@@ -62,6 +62,7 @@ abstract class DatabaseVersioned<
|
||||
protected set
|
||||
|
||||
abstract val version: String
|
||||
abstract val defaultFileExtension: String
|
||||
|
||||
/**
|
||||
* To manage binaries in faster way
|
||||
|
||||
@@ -43,15 +43,8 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var m
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
progressTaskUpdater: ProgressTaskUpdater?): D
|
||||
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
abstract fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
progressTaskUpdater: ProgressTaskUpdater?): D
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): D
|
||||
|
||||
protected fun startKeyTimer(progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
progressTaskUpdater?.updateMessage(R.string.retrieving_db_key)
|
||||
|
||||
@@ -39,7 +39,6 @@ import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
|
||||
/**
|
||||
@@ -50,27 +49,8 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDB {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDB {
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDB {
|
||||
|
||||
try {
|
||||
startKeyTimer(progressTaskUpdater)
|
||||
@@ -96,7 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
throw VersionDatabaseException()
|
||||
}
|
||||
|
||||
assignMasterKey?.invoke()
|
||||
assignMasterKey.invoke()
|
||||
|
||||
// Select algorithm
|
||||
when {
|
||||
|
||||
@@ -101,27 +101,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
password: String?,
|
||||
keyfileInputStream: InputStream?,
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.retrieveMasterKey(password, keyfileInputStream)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
masterKey: ByteArray,
|
||||
progressTaskUpdater: ProgressTaskUpdater?): DatabaseKDBX {
|
||||
return openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
mDatabase.masterKey = masterKey
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)? = null): DatabaseKDBX {
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDBX {
|
||||
try {
|
||||
startKeyTimer(progressTaskUpdater)
|
||||
|
||||
@@ -133,7 +114,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
hashOfHeader = headerAndHash.hash
|
||||
val pbHeader = headerAndHash.header
|
||||
|
||||
assignMasterKey?.invoke()
|
||||
assignMasterKey.invoke()
|
||||
mDatabase.makeFinalKey(header.masterSeed)
|
||||
|
||||
stopKeyTimer()
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.utils.readAllBytes
|
||||
import java.io.IOException
|
||||
@@ -43,7 +44,6 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
* Merge a KDB database in a KDBX database, by default all data are copied from the KDB
|
||||
*/
|
||||
fun merge(databaseToMerge: DatabaseKDB) {
|
||||
// TODO Test KDB merge
|
||||
val rootGroup = database.rootGroup
|
||||
val rootGroupId = rootGroup?.nodeId
|
||||
val rootGroupToMerge = databaseToMerge.rootGroup
|
||||
@@ -53,6 +53,11 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
throw IOException("Database is not open")
|
||||
}
|
||||
|
||||
// Replace the UUID of the KDB root group to init seed
|
||||
databaseToMerge.removeGroupIndex(rootGroupToMerge)
|
||||
rootGroupToMerge.nodeId = NodeIdInt(0)
|
||||
databaseToMerge.addGroupIndex(rootGroupToMerge)
|
||||
|
||||
// Merge children
|
||||
rootGroupToMerge.doForEachChild(
|
||||
object : NodeHandler<EntryKDB>() {
|
||||
@@ -87,31 +92,57 @@ class DatabaseKDBXMerger(private var database: DatabaseKDBX) {
|
||||
val entry = database.getEntryById(entryId)
|
||||
|
||||
databaseToMerge.getEntryById(entryId)?.let { srcEntryToMerge ->
|
||||
// Retrieve parent in current database
|
||||
var parentEntryToMerge: GroupKDBX? = null
|
||||
srcEntryToMerge.parent?.nodeId?.let {
|
||||
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
|
||||
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
|
||||
}
|
||||
val entryToMerge = EntryKDBX().apply {
|
||||
this.nodeId = srcEntryToMerge.nodeId
|
||||
this.icon = srcEntryToMerge.icon
|
||||
this.creationTime = DateInstant(srcEntryToMerge.creationTime)
|
||||
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
|
||||
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
|
||||
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
|
||||
this.expires = srcEntryToMerge.expires
|
||||
this.title = srcEntryToMerge.title
|
||||
this.username = srcEntryToMerge.username
|
||||
this.password = srcEntryToMerge.password
|
||||
this.url = srcEntryToMerge.url
|
||||
this.notes = srcEntryToMerge.notes
|
||||
// TODO attachment
|
||||
}
|
||||
if (entry != null) {
|
||||
entry.updateWith(entryToMerge, false)
|
||||
} else if (parentEntryToMerge != null) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
// Do not merge meta stream elements
|
||||
if (!srcEntryToMerge.isMetaStream()) {
|
||||
// Retrieve parent in current database
|
||||
var parentEntryToMerge: GroupKDBX? = null
|
||||
srcEntryToMerge.parent?.nodeId?.let {
|
||||
val parentGroupIdToMerge = getNodeIdUUIDFrom(seed, it)
|
||||
parentEntryToMerge = database.getGroupById(parentGroupIdToMerge)
|
||||
}
|
||||
// Copy attachment
|
||||
var newAttachment: Attachment? = null
|
||||
srcEntryToMerge.getAttachment(databaseToMerge.attachmentPool)?.let { attachment ->
|
||||
val binarySize = attachment.binaryData.getSize()
|
||||
val binaryData = database.buildNewBinaryAttachment(
|
||||
isRAMSufficient.invoke(binarySize),
|
||||
attachment.binaryData.isCompressed,
|
||||
attachment.binaryData.isProtected
|
||||
)
|
||||
attachment.binaryData.getInputDataStream(databaseToMerge.binaryCache)
|
||||
.use { inputStream ->
|
||||
binaryData.getOutputDataStream(database.binaryCache)
|
||||
.use { outputStream ->
|
||||
inputStream.readAllBytes { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
newAttachment = Attachment(attachment.name, binaryData)
|
||||
}
|
||||
// Create new entry format
|
||||
val entryToMerge = EntryKDBX().apply {
|
||||
this.nodeId = srcEntryToMerge.nodeId
|
||||
this.icon = srcEntryToMerge.icon
|
||||
this.creationTime = DateInstant(srcEntryToMerge.creationTime)
|
||||
this.lastModificationTime = DateInstant(srcEntryToMerge.lastModificationTime)
|
||||
this.lastAccessTime = DateInstant(srcEntryToMerge.lastAccessTime)
|
||||
this.expiryTime = DateInstant(srcEntryToMerge.expiryTime)
|
||||
this.expires = srcEntryToMerge.expires
|
||||
this.title = srcEntryToMerge.title
|
||||
this.username = srcEntryToMerge.username
|
||||
this.password = srcEntryToMerge.password
|
||||
this.url = srcEntryToMerge.url
|
||||
this.notes = srcEntryToMerge.notes
|
||||
newAttachment?.let {
|
||||
this.putAttachment(it, database.attachmentPool)
|
||||
}
|
||||
}
|
||||
if (entry != null) {
|
||||
entry.updateWith(entryToMerge, false)
|
||||
} else if (parentEntryToMerge != null) {
|
||||
database.addEntryTo(entryToMerge, parentEntryToMerge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
val actionRunnable: ActionRunnable? = when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database)
|
||||
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database)
|
||||
ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(database)
|
||||
ACTION_DATABASE_MERGE_TASK -> buildDatabaseMergeActionTask(intent, database)
|
||||
ACTION_DATABASE_RELOAD_TASK -> buildDatabaseReloadActionTask(database)
|
||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK -> buildDatabaseAssignPasswordActionTask(intent, database)
|
||||
ACTION_DATABASE_CREATE_GROUP_TASK -> buildDatabaseCreateGroupActionTask(intent, database)
|
||||
@@ -611,10 +611,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDatabaseMergeActionTask(database: Database): ActionRunnable {
|
||||
private fun buildDatabaseMergeActionTask(intent: Intent, database: Database): ActionRunnable {
|
||||
var databaseToMergeUri: Uri? = null
|
||||
var databaseToMergeMainCredential: MainCredential? = null
|
||||
if (intent.hasExtra(DATABASE_URI_KEY)) {
|
||||
databaseToMergeUri = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||
}
|
||||
if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) {
|
||||
databaseToMergeMainCredential = intent.getParcelableExtra(MAIN_CREDENTIAL_KEY)
|
||||
}
|
||||
|
||||
return MergeDatabaseRunnable(
|
||||
this,
|
||||
database,
|
||||
databaseToMergeUri,
|
||||
databaseToMergeMainCredential,
|
||||
this
|
||||
) { result ->
|
||||
// No need to add each info to reload database
|
||||
@@ -916,9 +927,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
*/
|
||||
private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? {
|
||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
|
||||
var databaseCopyUri: Uri? = null
|
||||
if (intent.hasExtra(DATABASE_URI_KEY)) {
|
||||
databaseCopyUri = intent.getParcelableExtra(DATABASE_URI_KEY)
|
||||
}
|
||||
|
||||
SaveDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false))
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
databaseCopyUri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.preference.*
|
||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.*
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetrieval {
|
||||
@@ -673,19 +672,21 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
return true
|
||||
true
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
reloadDatabase()
|
||||
return true
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
R.id.menu_app_settings -> {
|
||||
// Check the time lock before launching settings
|
||||
// TODO activity menu
|
||||
(activity as SettingsActivity?)?.let {
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(it, item, true)
|
||||
SettingsActivity.launch(it, true)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,11 @@ import com.kunzisoft.keepass.settings.SettingsActivity
|
||||
|
||||
object MenuUtil {
|
||||
|
||||
fun contributionMenuInflater(inflater: MenuInflater, menu: Menu) {
|
||||
if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
|
||||
inflater.inflate(R.menu.contribution, menu)
|
||||
}
|
||||
|
||||
fun defaultMenuInflater(inflater: MenuInflater, menu: Menu) {
|
||||
contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.default_menu, menu)
|
||||
}
|
||||
|
||||
fun onContributionItemSelected(context: Context) {
|
||||
UriUtil.gotoUrl(context, R.string.contribution_url)
|
||||
inflater.inflate(R.menu.settings, menu)
|
||||
inflater.inflate(R.menu.about, menu)
|
||||
if (!(BuildConfig.FULL_VERSION && BuildConfig.CLOSED_STORE))
|
||||
menu.findItem(R.id.menu_contribute)?.isVisible = false
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -54,7 +47,7 @@ object MenuUtil {
|
||||
timeoutEnable: Boolean = false) {
|
||||
when (item.itemId) {
|
||||
R.id.menu_contribute -> {
|
||||
onContributionItemSelected(activity)
|
||||
UriUtil.gotoUrl(activity, R.string.contribution_url)
|
||||
}
|
||||
R.id.menu_app_settings -> {
|
||||
// To avoid flickering when launch settings in a LockingActivity
|
||||
|
||||
@@ -30,13 +30,16 @@ import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
|
||||
class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@@ -55,7 +58,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_credentials, this)
|
||||
inflater?.inflate(R.layout.view_main_credentials, this)
|
||||
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class NavigationDatabaseView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: NavigationView(context, attrs, defStyle) {
|
||||
|
||||
private var databaseNavContainerView: View? = null
|
||||
private var databaseNavIconView: ImageView? = null
|
||||
private var databaseNavColorView: ImageView? = null
|
||||
private var databaseNavNameView: TextView? = null
|
||||
private var databaseNavVersionView: TextView? = null
|
||||
|
||||
init {
|
||||
inflateHeaderView(R.layout.nav_header_database)
|
||||
databaseNavIconView = databaseNavContainerView?.findViewById(R.id.nav_database_icon)
|
||||
databaseNavColorView = databaseNavContainerView?.findViewById(R.id.nav_database_color)
|
||||
databaseNavNameView = databaseNavContainerView?.findViewById(R.id.nav_database_name)
|
||||
databaseNavVersionView = databaseNavContainerView?.findViewById(R.id.nav_database_version)
|
||||
}
|
||||
|
||||
override fun inflateHeaderView(res: Int): View {
|
||||
val headerView = super.inflateHeaderView(res)
|
||||
databaseNavContainerView = headerView
|
||||
return headerView
|
||||
}
|
||||
|
||||
fun setDatabaseName(name: String) {
|
||||
databaseNavNameView?.text = name
|
||||
}
|
||||
|
||||
fun setDatabaseVersion(version: String) {
|
||||
databaseNavVersionView?.text = version
|
||||
}
|
||||
|
||||
fun setDatabaseColor(color: Int?) {
|
||||
if (color != null) {
|
||||
databaseNavColorView?.drawable?.colorFilter = BlendModeColorFilterCompat
|
||||
.createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_IN)
|
||||
databaseNavColorView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
databaseNavColorView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app/src/main/res/drawable/ic_database_white_48dp.xml
Normal file
11
app/src/main/res/drawable/ic_database_white_48dp.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="0.04285714"
|
||||
android:pathData="M 23.835938,5.7167969 C 13.70451,5.7167969 5,9.3383264 5,13.855469 c 0,4.512856 8.70451,8.167969 18.835938,8.167969 10.135713,0 18.835937,-3.655113 18.835937,-8.167969 0,-4.5171426 -8.700224,-8.1386721 -18.835937,-8.1386721 z M 42.671875,18.09375 c 0,4.517142 -8.700224,8.171875 -18.835937,8.171875 C 13.70451,26.265625 5,22.610513 5,18.097656 v 6.248047 c 0,4.512857 8.70451,8.167969 18.835938,8.167969 10.135713,0 18.835937,-3.655112 18.835937,-8.167969 z M 5,28.582031 v 6.25 C 5,39.344888 13.70451,43 23.835938,43 33.971651,43 42.671875,39.344888 42.671875,34.832031 v -6.25 c 0,4.517143 -8.700224,8.169922 -18.835937,8.169922 C 13.70451,36.751953 5,33.094888 5,28.582031 Z" />
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/ic_merge_from_white_24dp.xml
Normal file
13
app/src/main/res/drawable/ic_merge_from_white_24dp.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:strokeWidth="2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:pathData="M 6 3 C 5.446 3 5 3.446 5 4 L 5 21 C 5 21.554 5.4966699 22 6 22 L 19 22 C 19.554 22 20 21.554 20 21 L 20 8 L 15 3 L 14 3 L 7 3 L 6 3 z M 14 4 L 15 5 L 18 8 L 19 9 L 15.541016 9 L 16.955078 10.414062 L 14.183594 13.183594 L 12.769531 11.769531 L 15.541016 9 L 14 9 L 14 4 z M 9 9 L 12.955078 12.955078 L 12.910156 13 L 13 13 L 13 16 L 17 16 L 12 21 L 7 16 L 11 16 L 11 13.828125 L 7.5859375 10.414062 L 9 9 z" />
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml
Normal file
13
app/src/main/res/drawable/ic_save_copy_to_white_24dp.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:strokeWidth="2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:pathData="M 3 0 C 2.446 0 2 0.446 2 1 L 2 18 C 2 18.554 2.446 19 3 19 C 3.554 19 4 18.554 4 18 L 4 2 L 14 2 C 14.554 2 15 1.554 15 1 C 15 0.446 14.554 0 14 0 L 3 0 z M 7 4 C 6.446 4 6 4.446 6 5 L 6 22 C 6 22.554 6.49667 23 7 23 L 20 23 C 20.554 23 21 22.554 21 22 L 21 9 L 16 4 L 15 4 L 8 4 L 7 4 z M 15 5 L 16 6 L 19 9 L 20 10 L 15 10 L 15 5 z M 13 11 L 18 16 L 14 16 L 14 21 L 12 21 L 12 16 L 8 16 L 13 11 z" />
|
||||
</vector>
|
||||
@@ -17,139 +17,160 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<RelativeLayout
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/activity_group_container_view"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.kunzisoft.keepass.view.SpecialModeView
|
||||
android:id="@+id/special_mode_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="?attr/toolbarSpecialAppearance" />
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:title="@string/app_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_below="@+id/special_mode_view"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/toolbarAppearance" >
|
||||
<LinearLayout
|
||||
android:id="@+id/database_name_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<ImageView
|
||||
android:id="@+id/database_color"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:src="@drawable/background_rounded_square"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/content_description_database_color"/>
|
||||
<TextView
|
||||
android:id="@+id/database_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Database"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary" />
|
||||
</LinearLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/group_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/toolbar"
|
||||
android:layout_above="@+id/toolbar_action">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:targetApi="lollipop"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|snap|enterAlways">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar_breadcrumb"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
tools:targetApi="lollipop">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/breadcrumb_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:orientation="horizontal" />
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<com.kunzisoft.keepass.view.SearchFiltersView
|
||||
android:id="@+id/search_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/node_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_below="@+id/toolbar">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/nodes_list_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground" />
|
||||
</RelativeLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
android:id="@+id/add_node_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@+id/node_list_container"
|
||||
app:layout_anchorGravity="end|bottom" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.ToolbarAction
|
||||
android:id="@+id/toolbar_action"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:visibility="gone"
|
||||
android:theme="?attr/toolbarActionAppearance"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
<FrameLayout
|
||||
<RelativeLayout
|
||||
android:id="@+id/activity_group_container_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
<com.kunzisoft.keepass.view.SpecialModeView
|
||||
android:id="@+id/special_mode_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="?attr/toolbarSpecialAppearance" />
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:title="@string/app_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_below="@+id/special_mode_view"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/toolbarAppearance" >
|
||||
<FrameLayout
|
||||
android:id="@+id/database_name_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/database_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
tools:text="Database"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary" />
|
||||
</FrameLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/database_color"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:layout_marginStart="54dp"
|
||||
android:layout_marginLeft="54dp"
|
||||
android:layout_below="@+id/special_mode_view"
|
||||
android:src="@drawable/background_rounded_square"
|
||||
android:contentDescription="@string/content_description_database_color"/>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/group_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/toolbar"
|
||||
android:layout_above="@+id/toolbar_action">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:targetApi="lollipop"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|snap|enterAlways">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar_breadcrumb"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/toolbarAppearance"
|
||||
tools:targetApi="lollipop">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/breadcrumb_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:orientation="horizontal" />
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<com.kunzisoft.keepass.view.SearchFiltersView
|
||||
android:id="@+id/search_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/node_list_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_below="@+id/toolbar">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/nodes_list_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground" />
|
||||
</RelativeLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
android:id="@+id/add_node_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@+id/node_list_container"
|
||||
app:layout_anchorGravity="end|bottom" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.ToolbarAction
|
||||
android:id="@+id/toolbar_action"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:visibility="gone"
|
||||
android:theme="?attr/toolbarActionAppearance"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
</FrameLayout>
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
</FrameLayout>
|
||||
android:layout_alignParentBottom="true"/>
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
</RelativeLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.NavigationDatabaseView
|
||||
android:id="@+id/database_nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"/>
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
app:itemTextColor="?android:attr/textColor"
|
||||
app:subheaderColor="?attr/colorAccent"
|
||||
android:fitsSystemWindows="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
65
app/src/main/res/layout/nav_header_database.xml
Normal file
65
app/src/main/res/layout/nav_header_database.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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/colorPrimary"
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:elevation="4dp"
|
||||
android:paddingLeft="@dimen/default_margin"
|
||||
android:paddingTop="@dimen/default_margin"
|
||||
android:paddingRight="@dimen/default_margin"
|
||||
android:paddingBottom="@dimen/default_margin"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nav_database_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingRight="8dp"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Info"
|
||||
android:textSize="11sp"
|
||||
tools:text="version" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nav_database_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/content_description_nav_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nav_database_version"
|
||||
app:layout_constraintBottom_toTopOf="@+id/nav_database_name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:srcCompat="@drawable/ic_database_white_48dp"
|
||||
app:tint="?attr/textColorInverse"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/nav_database_color"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@string/content_description_database_color"
|
||||
android:src="@drawable/background_icon"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nav_database_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/nav_database_name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nav_database_name"
|
||||
style="@style/KeepassDXStyle.TextAppearance.Title.TextOnPrimary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/database"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/nav_database_color"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -13,10 +13,6 @@
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?android:attr/windowBackground"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:targetApi="lollipop">
|
||||
|
||||
<!-- Password Input -->
|
||||
@@ -19,16 +19,18 @@
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/menu_app_settings"
|
||||
android:icon="@drawable/ic_settings_white_24dp"
|
||||
android:title="@string/settings"
|
||||
android:orderInCategory="91"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item android:id="@+id/menu_about"
|
||||
android:icon="@drawable/ic_help_white_24dp"
|
||||
android:title="@string/about"
|
||||
android:orderInCategory="101"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
<group>
|
||||
<item android:id="@+id/menu_about"
|
||||
android:icon="@drawable/ic_help_white_24dp"
|
||||
android:title="@string/about"
|
||||
android:orderInCategory="92"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item android:id="@+id/menu_contribute"
|
||||
android:icon="@drawable/ic_heart_white_24dp"
|
||||
android:title="@string/contribute"
|
||||
android:orderInCategory="93"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
</group>
|
||||
</menu>
|
||||
36
app/src/main/res/menu/database_extra.xml
Normal file
36
app/src/main/res/menu/database_extra.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
KeePassDX is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
KeePassDX is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<group android:id="@+id/group_merge">
|
||||
<item android:id="@+id/menu_merge_from"
|
||||
android:icon="@drawable/ic_merge_from_white_24dp"
|
||||
android:title="@string/menu_merge_from"
|
||||
android:orderInCategory="80"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item android:id="@+id/menu_save_copy_to"
|
||||
android:icon="@drawable/ic_save_copy_to_white_24dp"
|
||||
android:title="@string/menu_save_copy_to"
|
||||
android:orderInCategory="81"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
</group>
|
||||
</menu>
|
||||
@@ -22,7 +22,7 @@
|
||||
<item android:id="@+id/menu_open_file_read_mode_key"
|
||||
android:icon="@drawable/ic_read_write_white_24dp"
|
||||
android:title="@string/menu_open_file_read_and_write"
|
||||
android:orderInCategory="85"
|
||||
android:orderInCategory="61"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017 Jeremy Jamet / Kunzisoft.
|
||||
|
||||
Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
KeePassDX is free software: you can redistribute it and/or modify
|
||||
@@ -19,10 +19,10 @@
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/menu_contribute"
|
||||
android:icon="@drawable/ic_heart_white_24dp"
|
||||
android:title="@string/contribute"
|
||||
android:orderInCategory="99"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
<item android:id="@+id/menu_app_settings"
|
||||
android:icon="@drawable/ic_settings_white_24dp"
|
||||
android:title="@string/settings"
|
||||
android:orderInCategory="71"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -46,6 +46,7 @@
|
||||
|
||||
<!-- File Path -->
|
||||
<string name="database_file_name_default" translatable="false">keepass</string>
|
||||
<string name="database_file_name_copy" translatable="false">_copy</string>
|
||||
<string name="database_file_extension_default" translatable="false">.kdbx</string>
|
||||
<string name="database_default_name" translatable="false">KeePassDX Database</string>
|
||||
<string name="app_properties_file_name" translatable="false">keepassdx_%1$s.properties</string>
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
<string name="content_description_database_color">Database color</string>
|
||||
<string name="content_description_entry_foreground_color">Entry foreground color</string>
|
||||
<string name="content_description_entry_background_color">Entry background color</string>
|
||||
<string name="content_description_nav_header">Navigation header</string>
|
||||
<string name="navigation_drawer_open">Navigation drawer open</string>
|
||||
<string name="navigation_drawer_close">Navigation drawer close</string>
|
||||
<string name="validate">Validate</string>
|
||||
<string name="discard_changes">Discard changes?</string>
|
||||
<string name="discard">Discard</string>
|
||||
@@ -242,6 +245,8 @@
|
||||
<string name="menu_save_database">Save data</string>
|
||||
<string name="menu_merge_database">Merge data</string>
|
||||
<string name="menu_reload_database">Reload data</string>
|
||||
<string name="menu_merge_from">Merge from …</string>
|
||||
<string name="menu_save_copy_to">Save a copy to …</string>
|
||||
<string name="menu_open">Open</string>
|
||||
<string name="menu_search">Search</string>
|
||||
<string name="menu_showpass">Show password</string>
|
||||
|
||||
102
art/ic_database.svg
Normal file
102
art/ic_database.svg
Normal file
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<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="48"
|
||||
height="48"
|
||||
id="svg4830"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
inkscape:export-filename="/home/joker/Project/Scratcheck/TestExport.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
sodipodi:docname="ic_database.svg">
|
||||
<defs
|
||||
id="defs4832" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#acacac"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9999997"
|
||||
inkscape:cx="25.017278"
|
||||
inkscape:cy="12.774208"
|
||||
inkscape:current-layer="g824"
|
||||
showgrid="true"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1">
|
||||
<sodipodi:guide
|
||||
position="0.99999471,22.999999"
|
||||
orientation="22,0"
|
||||
id="guide2987"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="0.99999471,0.99999888"
|
||||
orientation="0,22"
|
||||
id="guide2989"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="47,24"
|
||||
orientation="-22,0"
|
||||
id="guide2991"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="38,47"
|
||||
orientation="0,-22"
|
||||
id="guide2993"
|
||||
inkscape:locked="false" />
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2989" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata4835">
|
||||
<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></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
transform="translate(0,16)">
|
||||
<g
|
||||
id="g4770"
|
||||
transform="matrix(1.7777778,0,0,1.7777778,-205.48441,-31.997877)">
|
||||
<g
|
||||
id="Layer_1"
|
||||
transform="matrix(-0.00397893,0,0,0.00397893,125.58386,23.674135)" />
|
||||
<g
|
||||
id="g824"
|
||||
transform="matrix(0.02410714,0,0,0.02410714,116.939,11.007737)"
|
||||
style="fill:#ffffff;fill-opacity:1">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.04285714"
|
||||
d="M 23.835938,5.7167969 C 13.70451,5.7167969 5,9.3383264 5,13.855469 c 0,4.512856 8.70451,8.167969 18.835938,8.167969 10.135713,0 18.835937,-3.655113 18.835937,-8.167969 0,-4.5171426 -8.700224,-8.1386721 -18.835937,-8.1386721 z M 42.671875,18.09375 c 0,4.517142 -8.700224,8.171875 -18.835937,8.171875 C 13.70451,26.265625 5,22.610513 5,18.097656 v 6.248047 c 0,4.512857 8.70451,8.167969 18.835938,8.167969 10.135713,0 18.835937,-3.655112 18.835937,-8.167969 z M 5,28.582031 v 6.25 C 5,39.344888 13.70451,43 23.835938,43 33.971651,43 42.671875,39.344888 42.671875,34.832031 v -6.25 c 0,4.517143 -8.700224,8.169922 -18.835937,8.169922 C 13.70451,36.751953 5,33.094888 5,28.582031 Z"
|
||||
transform="matrix(23.333336,0,0,23.333336,-56.166796,-83.333456)"
|
||||
id="path822"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
59
art/merge_from.svg
Normal file
59
art/merge_from.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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="svg4"
|
||||
sodipodi:docname="merge_from.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<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="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#c8c8c8"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0.28235294"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview6"
|
||||
showgrid="true"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="15.998898"
|
||||
inkscape:cy="7.9607242"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid818" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6 3 C 5.446 3 5 3.446 5 4 L 5 21 C 5 21.554 5.4966699 22 6 22 L 19 22 C 19.554 22 20 21.554 20 21 L 20 8 L 15 3 L 14 3 L 7 3 L 6 3 z M 14 4 L 15 5 L 18 8 L 19 9 L 15.541016 9 L 16.955078 10.414062 L 14.183594 13.183594 L 12.769531 11.769531 L 15.541016 9 L 14 9 L 14 4 z M 9 9 L 12.955078 12.955078 L 12.910156 13 L 13 13 L 13 16 L 17 16 L 12 21 L 7 16 L 11 16 L 11 13.828125 L 7.5859375 10.414062 L 9 9 z "
|
||||
id="rect816-3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
59
art/save_copy_to.svg
Normal file
59
art/save_copy_to.svg
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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="svg4"
|
||||
sodipodi:docname="save_copy_to.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<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></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#c8c8c8"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0.28235294"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview6"
|
||||
showgrid="true"
|
||||
inkscape:zoom="22.627418"
|
||||
inkscape:cx="12.083672"
|
||||
inkscape:cy="15.914516"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid818" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 3 0 C 2.446 0 2 0.446 2 1 L 2 18 C 2 18.554 2.446 19 3 19 C 3.554 19 4 18.554 4 18 L 4 2 L 14 2 C 14.554 2 15 1.554 15 1 C 15 0.446 14.554 0 14 0 L 3 0 z M 7 4 C 6.446 4 6 4.446 6 5 L 6 22 C 6 22.554 6.49667 23 7 23 L 20 23 C 20.554 23 21 22.554 21 22 L 21 9 L 16 4 L 15 4 L 8 4 L 7 4 z M 15 5 L 16 6 L 19 9 L 20 10 L 15 10 L 15 5 z M 13 11 L 18 16 L 14 16 L 14 21 L 12 21 L 12 16 L 8 16 L 13 11 z "
|
||||
id="rect907" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -2,4 +2,5 @@
|
||||
* Keep search context #1141
|
||||
* Add searchable groups #905 #1006
|
||||
* Search with regular expression #175
|
||||
* Fix styles
|
||||
* Merge from file and save as copy #1221 #1204 #840
|
||||
* New UI and fix styles
|
||||
@@ -2,4 +2,5 @@
|
||||
* Garde le contexte de recherche #1141
|
||||
* Ajout des groupes cherchables #905 #1006
|
||||
* Recherche avec expression régulière #175
|
||||
* Correction des styles
|
||||
* Fusion depuis un fichier et sauvegarde de copie #1221 #1204 #840
|
||||
* Nouvelle interface utilisateur et correction des styles
|
||||
Reference in New Issue
Block a user