Merge branch 'feature/Merge_from' into develop #1221 #1204 #840

This commit is contained in:
J-Jamet
2022-02-12 14:57:44 +01:00
38 changed files with 1082 additions and 371 deletions

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -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() {

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -62,6 +62,7 @@ abstract class DatabaseVersioned<
protected set
abstract val version: String
abstract val defaultFileExtension: String
/**
* To manage binaries in faster way

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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)
}
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}
}
}

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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 -->

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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

View File

@@ -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